Add Transaction Properties #15

Merged
andrewlalis merged 18 commits from transaction-properties into main 2024-02-04 04:31:04 +00:00
4 changed files with 64 additions and 1 deletions
Showing only changes of commit 396fd122a8 - Show all commits

View File

@ -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 Once that's done, the workflow will start, and you should see a release appear
in the next few minutes. 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.

View File

@ -2,6 +2,9 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.TransactionCategoryRepository; 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.model.Profile;
import com.andrewlalis.perfin.view.BindingUtil; import com.andrewlalis.perfin.view.BindingUtil;
import com.andrewlalis.perfin.view.component.CategoryTile; import com.andrewlalis.perfin.view.component.CategoryTile;
@ -11,6 +14,9 @@ import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import java.io.IOException;
import java.io.UncheckedIOException;
import static com.andrewlalis.perfin.PerfinApp.router; import static com.andrewlalis.perfin.PerfinApp.router;
public class CategoriesViewController implements RouteSelectionListener { public class CategoriesViewController implements RouteSelectionListener {
@ -36,4 +42,22 @@ public class CategoriesViewController implements RouteSelectionListener {
TransactionCategoryRepository::findTree TransactionCategoryRepository::findTree
).thenAccept(nodes -> Platform.runLater(() -> categoryTreeNodes.setAll(nodes))); ).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);
}
}
} }

View File

@ -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 ( try (
var categoriesIn = JdbcDataSourceFactory.class.getResourceAsStream("/sql/data/default-categories.json"); var categoriesIn = JdbcDataSourceFactory.class.getResourceAsStream("/sql/data/default-categories.json");
var stmt = conn.prepareStatement( var stmt = conn.prepareStatement(

View File

@ -24,6 +24,7 @@
</StyledText> </StyledText>
<HBox styleClass="std-padding, std-spacing" VBox.vgrow="NEVER"> <HBox styleClass="std-padding, std-spacing" VBox.vgrow="NEVER">
<Button text="Add Category" onAction="#addCategory"/> <Button text="Add Category" onAction="#addCategory"/>
<Button text="Add Default Categories" onAction="#addDefaultCategories"/>
</HBox> </HBox>
<ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS"> <ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS">
<VBox fx:id="categoriesVBox" styleClass="tile-container"/> <VBox fx:id="categoriesVBox" styleClass="tile-container"/>