diff --git a/README.md b/README.md index 94c8a50..f3c6b63 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,30 @@ to set the version everywhere that it needs to be. Once that's done, the workflow will start, and you should see a release appear in the next few minutes. + +## Migration Procedure + +Because this application relies on a structured relational database schema, +changes to the schema must be handled with care to avoid destroying users' data. +Specifically, when changes are made to the schema, a *migration* must be defined +which provides instructions for Perfin to safely apply changes to an old schema. + +The database schema is versioned using whole-number versions (1, 2, 3, ...), and +a migration is defined for each transition from version to version, such that +any older version can be incrementally upgraded, step by step, to the latest +schema version. + +Perfin only supports the latest schema version, as defined by `JdbcDataSourceFactory.SCHEMA_VERSION`. +When the app loads a profile, it'll check that profile's schema version by +reading a `.jdbc-schema-version.txt` file in the profile's main directory. If +the profile's schema version is **less than** the current, Perfin will +ask the user if they want to upgrade. If the profile's schema version is +**greater than** the current, Perfin will tell the user that it can't load a +schema from a newer version, and will prompt the user to upgrade. + +### Writing a Migration + +1. Write your migration. This can be plain SQL (placed in `resources/sql/migration`), or Java code. +2. Add your migration to `com.andrewlalis.perfin.data.impl.migration.Migrations#getMigrations()`. +3. Increment the schema version defined in `JdbcDataSourceFactory`. +4. Test the migration yourself on a profile with data. diff --git a/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java b/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java index 9697e76..a006b25 100644 --- a/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java @@ -2,6 +2,9 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.perfin.data.TransactionCategoryRepository; +import com.andrewlalis.perfin.data.impl.JdbcDataSource; +import com.andrewlalis.perfin.data.impl.JdbcDataSourceFactory; +import com.andrewlalis.perfin.data.util.DbUtil; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.view.BindingUtil; import com.andrewlalis.perfin.view.component.CategoryTile; @@ -11,6 +14,9 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.layout.VBox; +import java.io.IOException; +import java.io.UncheckedIOException; + import static com.andrewlalis.perfin.PerfinApp.router; public class CategoriesViewController implements RouteSelectionListener { @@ -36,4 +42,22 @@ public class CategoriesViewController implements RouteSelectionListener { TransactionCategoryRepository::findTree ).thenAccept(nodes -> Platform.runLater(() -> categoryTreeNodes.setAll(nodes))); } + + @FXML public void addDefaultCategories() { + boolean confirm = Popups.confirm(categoriesVBox, "Are you sure you want to add all of Perfin's default categories to your profile? This might interfere with existing categories of the same name."); + if (!confirm) return; + JdbcDataSource ds = (JdbcDataSource) Profile.getCurrent().dataSource(); + try (var conn = ds.getConnection()) { + DbUtil.doTransaction(conn, () -> { + try { + new JdbcDataSourceFactory().insertDefaultCategories(conn); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + refreshCategories(); + } catch (Exception e) { + Popups.error(categoriesVBox, e); + } + } } diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java index d5320f4..bdad590 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSourceFactory.java @@ -100,7 +100,18 @@ public class JdbcDataSourceFactory implements DataSourceFactory { } } - private void insertDefaultData(Connection conn) throws IOException, SQLException { + /** + * Inserts all default data into the database, using static content found in + * various locations on the classpath. + * @param conn The connection to use to insert data. + * @throws IOException If resources couldn't be read. + * @throws SQLException If SQL fails. + */ + public void insertDefaultData(Connection conn) throws IOException, SQLException { + insertDefaultCategories(conn); + } + + public void insertDefaultCategories(Connection conn) throws IOException, SQLException { try ( var categoriesIn = JdbcDataSourceFactory.class.getResourceAsStream("/sql/data/default-categories.json"); var stmt = conn.prepareStatement( diff --git a/src/main/resources/categories-view.fxml b/src/main/resources/categories-view.fxml index 9b0641c..c34b38f 100644 --- a/src/main/resources/categories-view.fxml +++ b/src/main/resources/categories-view.fxml @@ -24,6 +24,7 @@