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 @@
+