Repository: RIP-Comm/sossoldi
Pull request #291: feat: Implement database migration system
Author: login: xDefcon
Created: 2025-03-15T12:50:19Z | Merged: 2025-03-22T16:26:09Z

### Changes Made
- Added database migration system
Implemented a structured migration system to handle database schema evolution systematically.
- Initial Schema (_createDB) is now handled as a migration
- Added docs to the database migration system

### How It Works
The migration system automatically handles schema changes through versioned migration classes:  
1. Each migration has a version number and description
2. The MigrationManager tracks database versions and applies migrations in sequence
3. New migrations are registered in migration_registry.dart
4. When upgrading the database, only migrations with newer version numbers are applied in the specified order

This approach provides a reliable way to evolve the database schema while maintaining backward compatibility with existing data.


### Additional Considerations
- **Migration Rollback**: Rollback functionality is not implemented by purpose (database versions can only go up). To modify a previous migration, create a new one with the corrective changes.  
- **Logging**: Currently using `print()` statements wrapped with `kDebugMode` checks for development visibility. A future enhancement should implement a proper logging system (using the `logger` package or a custom logging service which is not the purpose of this PR)
- **Migration Testing**: Each migration should be tested independently to ensure it works correctly both on fresh installs and when upgrading from previous versions. Maybe add some tests related to this?



Diff:
diff --git a/lib/database/migration_base.dart b/lib/database/migration_base.dart
new file mode 100644
index 00000000..08c4209e
--- /dev/null
+++ b/lib/database/migration_base.dart
@@ -0,0 +1,48 @@
+import 'package:sqflite/sqflite.dart';
+
+/// Represents a database migration that can be applied to evolve the schema.
+///
+/// Each migration has a database [version] number and a [description] that
+/// explains what the migration does.
+/// Migrations are collected in the migration registry and executed in order based
+/// on their version numbers when the database needs to be created or upgraded.
+///
+/// To create a new migration:
+/// 1. Extend this class with a concrete implementation
+/// 2. Implement the [up] method with the schema changes
+/// 3. (optional) Implement the [down] method to revert those changes
+/// 4. Add your migration to the registry in `migration_registry.dart`
+///
+/// Example:
+/// ```dart
+/// class AddUserAvatarMigration extends Migration {
+///   AddUserAvatarMigration()
+///     : super(
+///         version: 3,
+///         description: 'Add avatar column to user table'
+///       );
+///
+///   @override
+///   Future<void> up(Database db) async {
+///     await db.execute('ALTER TABLE users ADD COLUMN avatar TEXT');
+///   }
+///
+///   @override
+///   Future<void> down(Database db) async {
+///     // reserved for future use
+///   }
+/// }
+/// ```
+abstract class Migration {
+  /// The database version number of this migration.
+  final int version;
+
+  /// A description of what this migration does.
+  final String description;
+
+  Migration({required this.version, required this.description});
+
+  /// Applies this migration to upgrade the database schema.
+  Future<void> up(Database db);
+
+}
diff --git a/lib/database/migration_manager.dart b/lib/database/migration_manager.dart
new file mode 100644
index 00000000..af92c59c
--- /dev/null
+++ b/lib/database/migration_manager.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/foundation.dart';
+import 'package:sqflite/sqflite.dart';
+import 'migration_base.dart';
+import 'migrations/migration_registry.dart';
+
+class MigrationManager {
+  final List<Migration> _migrations = getMigrations();
+
+  /// Get highest migration version
+  int get latestVersion => getLatestVersion();
+
+  Future<void> migrate(Database db, int oldVersion, int newVersion) async {
+    if (kDebugMode) {
+      print('[MigrationManager] Migrating database from $oldVersion to $newVersion');
+    }
+
+    for (var migration in _migrations) {
+      if (migration.version > oldVersion && migration.version <= newVersion) {
+        if (kDebugMode) {
+          print('[MigrationManager] Running migration ${migration.version}: ${migration.description}');
+        }
+        await migration.up(db);
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart
new file mode 100644
index 00000000..74e71b97
--- /dev/null
+++ b/lib/database/migrations/0001_initial_schema.dart
@@ -0,0 +1,124 @@
+import 'package:sqflite/sqflite.dart';
+import '../migration_base.dart';
+
+// Models
+import '/model/bank_account.dart';
+import '/model/budget.dart';
+import '/model/category_transaction.dart';
+import '/model/currency.dart';
+import '/model/recurring_transaction.dart';
+import '/model/transaction.dart';
+
+class InitialSchema extends Migration {
+  InitialSchema() : super(
+      version: 1,
+      description: 'Initial database schema creation'
+  );
+
+  @override
+  Future<void> up(Database db) async {
+    const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT';
+    const integerNotNull = 'INTEGER NOT NULL';
+    const integer = 'INTEGER';
+    const realNotNull = 'REAL NOT NULL';
+    const textNotNull = 'TEXT NOT NULL';
+    const text = 'TEXT';
+
+    // Bank accounts Table
+    await db.execute('''
+      CREATE TABLE `$bankAccountTable`(
+        `${BankAccountFields.id}` $integerPrimaryKeyAutoincrement,
+        `${BankAccountFields.name}` $textNotNull,
+        `${BankAccountFields.symbol}` $textNotNull,
+        `${BankAccountFields.color}` $integerNotNull,
+        `${BankAccountFields.startingValue}` $realNotNull,
+        `${BankAccountFields.active}` $integerNotNull CHECK (${BankAccountFields.active} IN (0, 1)),
+        `${BankAccountFields.mainAccount}` $integerNotNull CHECK (${BankAccountFields.mainAccount} IN (0, 1)),
+        `${BankAccountFields.createdAt}` $textNotNull,
+        `${BankAccountFields.updatedAt}` $textNotNull
+      )
+      ''');
+
+    // Transactions Table
+    await db.execute('''
+      CREATE TABLE `$transactionTable`(
+        `${TransactionFields.id}` $integerPrimaryKeyAutoincrement,
+        `${TransactionFields.date}` $text,
+        `${TransactionFields.amount}` $realNotNull,
+        `${TransactionFields.type}` $integerNotNull,
+        `${TransactionFields.note}` $text,
+        `${TransactionFields.idCategory}` $integer,
+        `${TransactionFields.idBankAccount}` $integerNotNull,
+        `${TransactionFields.idBankAccountTransfer}` $integer,
+        `${TransactionFields.recurring}` $integerNotNull CHECK (${TransactionFields.recurring} IN (0, 1)),
+        `${TransactionFields.idRecurringTransaction}` $integer,
+        `${TransactionFields.createdAt}` $textNotNull,
+        `${TransactionFields.updatedAt}` $textNotNull
+      )
+    ''');
+
+    // Recurring Transactions Amount Table
+    await db.execute('''
+      CREATE TABLE `$recurringTransactionTable`(
+        `${RecurringTransactionFields.id}` $integerPrimaryKeyAutoincrement,
+        `${RecurringTransactionFields.fromDate}` $textNotNull,
+        `${RecurringTransactionFields.toDate}` $text,
+        `${RecurringTransactionFields.amount}` $realNotNull,
+        `${RecurringTransactionFields.note}` $textNotNull,
+        `${RecurringTransactionFields.recurrency}` $textNotNull,
+        `${RecurringTransactionFields.idCategory}` $integerNotNull,
+        `${RecurringTransactionFields.idBankAccount}` $integerNotNull,
+        `${RecurringTransactionFields.lastInsertion}` $text,
+        `${RecurringTransactionFields.createdAt}` $textNotNull,
+        `${RecurringTransactionFields.updatedAt}` $textNotNull
+      )
+    ''');
+
+    // Category Transaction Table
+    await db.execute('''
+      CREATE TABLE `$categoryTransactionTable`(
+        `${CategoryTransactionFields.id}` $integerPrimaryKeyAutoincrement,
+        `${CategoryTransactionFields.name}` $textNotNull,
+        `${CategoryTransactionFields.type}` $textNotNull,
+        `${CategoryTransactionFields.symbol}` $textNotNull,
+        `${CategoryTransactionFields.color}` $integerNotNull,
+        `${CategoryTransactionFields.note}` $text,
+        `${CategoryTransactionFields.parent}` $integer,
+        `${CategoryTransactionFields.createdAt}` $textNotNull,
+        `${CategoryTransactionFields.updatedAt}` $textNotNull
+      )
+    ''');
+
+    // Budget Table
+    await db.execute('''
+      CREATE TABLE `$budgetTable`(
+        `${BudgetFields.id}` $integerPrimaryKeyAutoincrement,
+        `${BudgetFields.idCategory}` $integerNotNull,
+        `${BudgetFields.name}` $textNotNull,
+        `${BudgetFields.amountLimit}` $realNotNull,
+        `${BudgetFields.active}` $integerNotNull  CHECK (${BudgetFields.active} IN (0, 1)),
+        `${BudgetFields.createdAt}` $textNotNull,
+        `${BudgetFields.updatedAt}` $textNotNull
+      )
+    ''');
+
+    // Currencies Table
+    await db.execute('''
+      CREATE TABLE `$currencyTable`(
+        `${CurrencyFields.id}` $integerPrimaryKeyAutoincrement,
+        `${CurrencyFields.symbol}` $textNotNull,
+        `${CurrencyFields.code}` $textNotNull,
+        `${CurrencyFields.name}` $textNotNull,
+        `${CurrencyFields.mainCurrency}` $integerNotNull CHECK (${CurrencyFields.mainCurrency} IN (0, 1))
+      )
+      ''');
+
+    await db.execute('''
+      INSERT INTO `$currencyTable`(`${CurrencyFields.symbol}`, `${CurrencyFields.code}`, `${CurrencyFields.name}`, `${CurrencyFields.mainCurrency}`) VALUES
+        ("€", "EUR", "Euro", 1),
+        ("\$", "USD", "United States Dollar", 0),
+        ("CHF", "CHF", "Switzerland Franc", 0),
+        ("£", "GBP", "United Kingdom Pound", 0);
+    ''');
+  }
+}
diff --git a/lib/database/migrations/migration_registry.dart b/lib/database/migrations/migration_registry.dart
new file mode 100644
index 00000000..699ad439
--- /dev/null
+++ b/lib/database/migrations/migration_registry.dart
@@ -0,0 +1,44 @@
+/// Manages database migrations for the application.
+///
+/// This registry maintains the list of all database migrations in the order they should be executed.
+/// When adding new migrations to this list, please follow these guidelines:
+///
+/// 1. Add migrations in ascending version order
+/// 2. When two migrations share the same version number, their order in this list
+///    determines execution order (first in the list = executed first)
+/// 3. Use descriptive file names that include the version number (e.g., 0002_add_transaction_indexes.dart)
+///
+/// The MigrationManager will execute migrations in the exact order defined here.
+library;
+
+
+import '0001_initial_schema.dart';
+import '../migration_base.dart';
+
+
+/// Returns all available migrations in execution order.
+///
+/// IMPORTANT: The order of migrations in this list is critical!
+/// Migrations are executed in the order they appear here, not necessarily by their version number.
+/// When multiple migrations share the same version number, their position in this list
+/// determines which runs first.
+List<Migration> getMigrations() {
+  return [
+    InitialSchema()
+    // Add future migrations here
+  ];
+}
+
+/// Returns the highest migration version number across all migrations.
+/// Used to determine the current database schema version.
+///
+/// NOTE: This should return the maximum version number found in any migration,
+/// not just the version of the last migration in the list. If migrations aren't
+/// added in strict version order, make sure this function still returns the highest version.
+int getLatestVersion() {
+  final migrations = getMigrations();
+  if (migrations.isEmpty) return 1;
+
+  return migrations.fold<int>(1, (max, migration) =>
+  migration.version > max ? migration.version : max);
+}
\ No newline at end of file
diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart
index 7357a87b..8dea1534 100644
--- a/lib/database/sossoldi_database.dart
+++ b/lib/database/sossoldi_database.dart
@@ -12,12 +12,15 @@ import '../model/category_transaction.dart';
 import '../model/currency.dart';
 import '../model/recurring_transaction.dart';
 import '../model/transaction.dart';
+import 'migration_manager.dart';
 
 class SossoldiDatabase {
   static final SossoldiDatabase instance = SossoldiDatabase._init();
+  final MigrationManager _migrationManager = MigrationManager();
   static Database? _database;
   static String dbName = 'sossoldi.db';
 
+
   // Zero args constructor needed to extend this class
   SossoldiDatabase({String? dbName}) {
     dbName = dbName ?? 'sossoldi.db';
@@ -35,114 +38,21 @@ class SossoldiDatabase {
   Future<Database> _initDB(String filePath) async {
     final databasePath = await getDatabasesPath();
     final path = join(databasePath, filePath);
-    return await openDatabase(path, version: 1, onCreate: _createDB);
+    return await openDatabase(
+        path,
+        version: _migrationManager.latestVersion,
+        onCreate: _createDB,
+        onUpgrade: _upgradeDB);
   }
 
   static Future _createDB(Database database, int version) async {
-    const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT';
-    const integerNotNull = 'INTEGER NOT NULL';
-    const integer = 'INTEGER';
-    const realNotNull = 'REAL NOT NULL';
-    const textNotNull = 'TEXT NOT NULL';
-    const text = 'TEXT';
-
-    // Bank accounts Table
-    await database.execute('''
-      CREATE TABLE `$bankAccountTable`(
-        `${BankAccountFields.id}` $integerPrimaryKeyAutoincrement,
-        `${BankAccountFields.name}` $textNotNull,
-        `${BankAccountFields.symbol}` $textNotNull,
-        `${BankAccountFields.color}` $integerNotNull,
-        `${BankAccountFields.startingValue}` $realNotNull,
-        `${BankAccountFields.active}` $integerNotNull CHECK (${BankAccountFields.active} IN (0, 1)),
-        `${BankAccountFields.mainAccount}` $integerNotNull CHECK (${BankAccountFields.mainAccount} IN (0, 1)),
-        `${BankAccountFields.createdAt}` $textNotNull,
-        `${BankAccountFields.updatedAt}` $textNotNull
-      )
-      ''');
-
-    // Transactions Table
-    await database.execute('''
-      CREATE TABLE `$transactionTable`(
-        `${TransactionFields.id}` $integerPrimaryKeyAutoincrement,
-        `${TransactionFields.date}` $text,
-        `${TransactionFields.amount}` $realNotNull,
-        `${TransactionFields.type}` $integerNotNull,
-        `${TransactionFields.note}` $text,
-        `${TransactionFields.idCategory}` $integer,
-        `${TransactionFields.idBankAccount}` $integerNotNull,
-        `${TransactionFields.idBankAccountTransfer}` $integer,
-        `${TransactionFields.recurring}` $integerNotNull CHECK (${TransactionFields.recurring} IN (0, 1)),
-        `${TransactionFields.idRecurringTransaction}` $integer,
-        `${TransactionFields.createdAt}` $textNotNull,
-        `${TransactionFields.updatedAt}` $textNotNull
-      )
-    ''');
-
-    // Recurring Transactions Amount Table
-    await database.execute('''
-      CREATE TABLE `$recurringTransactionTable`(
-        `${RecurringTransactionFields.id}` $integerPrimaryKeyAutoincrement,
-        `${RecurringTransactionFields.fromDate}` $textNotNull,
-        `${RecurringTransactionFields.toDate}` $text,
-        `${RecurringTransactionFields.amount}` $realNotNull,
-        `${RecurringTransactionFields.note}` $textNotNull,
-        `${RecurringTransactionFields.recurrency}` $textNotNull,
-        `${RecurringTransactionFields.idCategory}` $integerNotNull,
-        `${RecurringTransactionFields.idBankAccount}` $integerNotNull,
-        `${RecurringTransactionFields.lastInsertion}` $text,
-        `${RecurringTransactionFields.createdAt}` $textNotNull,
-        `${RecurringTransactionFields.updatedAt}` $textNotNull
-      )
-    ''');
-
-    // Category Transaction Table
-    await database.execute('''
-      CREATE TABLE `$categoryTransactionTable`(
-        `${CategoryTransactionFields.id}` $integerPrimaryKeyAutoincrement,
-        `${CategoryTransactionFields.name}` $textNotNull,
-        `${CategoryTransactionFields.type}` $textNotNull,
-        `${CategoryTransactionFields.symbol}` $textNotNull,
-        `${CategoryTransactionFields.color}` $integerNotNull,
-        `${CategoryTransactionFields.note}` $text,
-        `${CategoryTransactionFields.parent}` $integer,
-        `${CategoryTransactionFields.createdAt}` $textNotNull,
-        `${CategoryTransactionFields.updatedAt}` $textNotNull
-      )
-    ''');
-
-    // Budget Table
-    await database.execute('''
-      CREATE TABLE `$budgetTable`(
-        `${BudgetFields.id}` $integerPrimaryKeyAutoincrement,
-        `${BudgetFields.idCategory}` $integerNotNull,
-        `${BudgetFields.name}` $textNotNull,
-        `${BudgetFields.amountLimit}` $realNotNull,
-        `${BudgetFields.active}` $integerNotNull  CHECK (${BudgetFields.active} IN (0, 1)),
-        `${BudgetFields.createdAt}` $textNotNull,
-        `${BudgetFields.updatedAt}` $textNotNull
-      )
-    ''');
-
-    // Currencies Table
-    await database.execute('''
-      CREATE TABLE `$currencyTable`(
-        `${CurrencyFields.id}` $integerPrimaryKeyAutoincrement,
-        `${CurrencyFields.symbol}` $textNotNull,
-        `${CurrencyFields.code}` $textNotNull,
-        `${CurrencyFields.name}` $textNotNull,
-        `${CurrencyFields.mainCurrency}` $integerNotNull CHECK (${CurrencyFields.mainCurrency} IN (0, 1))
-      )
-      ''');
-
-    await database.execute('''
-      INSERT INTO `$currencyTable`(`${CurrencyFields.symbol}`, `${CurrencyFields.code}`, `${CurrencyFields.name}`, `${CurrencyFields.mainCurrency}`) VALUES
-        ("€", "EUR", "Euro", 1),
-        ("\$", "USD", "United States Dollar", 0),
-        ("CHF", "CHF", "Switzerland Franc", 0),
-        ("£", "GBP", "United Kingdom Pound", 0);
-    ''');
+    // Use the migration manager to apply all migrations from version 0
+    // This will run the InitialSchema migration (version 1) first
+    await instance._migrationManager.migrate(database, 0, version);
+  }
 
+  static Future _upgradeDB(Database database, int oldVersion, int newVersion) async {
+    await instance._migrationManager.migrate(database, oldVersion, newVersion);
   }
 
   Future<String> exportToCSV() async {
@@ -180,7 +90,8 @@ class SossoldiDatabase {
         final List<Map<String, dynamic>> rows = await db.query(tableName);
 
         for (var row in rows) {
-          List<dynamic> csvRow = List.filled(headers.length, ''); // Initialize with empty strings
+          List<dynamic> csvRow =
+              List.filled(headers.length, ''); // Initialize with empty strings
           csvRow[0] = tableName; // Set table name
 
           // Fill in values for existing columns
@@ -215,7 +126,8 @@ class SossoldiDatabase {
       }
 
       final String csvData = await file.readAsString();
-      final List<List<dynamic>> rows = const CsvToListConverter().convert(csvData);
+      final List<List<dynamic>> rows =
+          const CsvToListConverter().convert(csvData);
 
       if (rows.isEmpty) {
         throw Exception('CSV file is empty');
@@ -251,7 +163,8 @@ class SossoldiDatabase {
             for (int i = 1; i < tableRows.length; i++) {
               final Map<String, dynamic> row = {};
               for (int j = 0; j < headers.length; j++) {
-                if (j != tableNameIndex) { // Skip the table_name column
+                if (j != tableNameIndex) {
+                  // Skip the table_name column
                   final String header = headers[j];
                   final dynamic value = tableRows[i][j];
 


Reviews:
user: login: mikev-cw
body: Ok the overall approach is very simple, effective and safe. 
Developers options are kept operational.

I don't like so much that rollbacks are not implemented, they would be useful for developing, and while we could just add a new migration for that, this could add a lot of migration files, and slow down the whole process in the long run. 

However, I think this could be acceptable at this stage: I don't think we'll need so much database edits under normal conditions.

Just a question for the constant on top of the first migration, and you're done. Good job!

Can you also add an example test if you have time to?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T21:48:40Z
user: login: xDefcon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T22:08:04Z
user: login: mikev-cw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T22:30:17Z
user: login: xDefcon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:29:26Z
user: login: mikev-cw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T08:03:12Z

Review comments:
diff_hunk: @@ -0,0 +1,124 @@
+import 'package:sqflite/sqflite.dart';
+import '../migration_base.dart';
+
+// Models
+import '/model/bank_account.dart';
+import '/model/budget.dart';
+import '/model/category_transaction.dart';
+import '/model/currency.dart';
+import '/model/recurring_transaction.dart';
+import '/model/transaction.dart';
+
+class InitialSchema extends Migration {
+  InitialSchema() : super(
+      version: 1,
+      description: 'Initial database schema creation'
+  );
+
+  @override
+  Future<void> up(Database db) async {
+    const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT';
+    const integerNotNull = 'INTEGER NOT NULL';
+    const integer = 'INTEGER';
+    const realNotNull = 'REAL NOT NULL';
+    const textNotNull = 'TEXT NOT NULL';
+    const text = 'TEXT';
path: lib/database/migrations/0001_initial_schema.dart
user: login: mikev-cw
body: Do you think that these constants could be added somewhere else for future use? Maybe in the `Migration()` class?
I think they could be useful in future migrations, to avoid duplicate code.
created_at: 2025-03-17T21:46:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,124 @@
+import 'package:sqflite/sqflite.dart';
+import '../migration_base.dart';
+
+// Models
+import '/model/bank_account.dart';
+import '/model/budget.dart';
+import '/model/category_transaction.dart';
+import '/model/currency.dart';
+import '/model/recurring_transaction.dart';
+import '/model/transaction.dart';
+
+class InitialSchema extends Migration {
+  InitialSchema() : super(
+      version: 1,
+      description: 'Initial database schema creation'
+  );
+
+  @override
+  Future<void> up(Database db) async {
+    const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT';
+    const integerNotNull = 'INTEGER NOT NULL';
+    const integer = 'INTEGER';
+    const realNotNull = 'REAL NOT NULL';
+    const textNotNull = 'TEXT NOT NULL';
+    const text = 'TEXT';
path: lib/database/migrations/0001_initial_schema.dart
user: login: xDefcon
body: Thanks for the input, @mikev-cw!

On rollbacks, I decided not to implement them primarily due to the added complexity of needing a tool like a CLI or specific methods to manage them. For now, new migrations feel sufficient, but we can revisit if needed. 

For the constants, I’d prefer keeping them local to each migration or even removing them entirely. They don’t add much benefit since migrations are self-contained, we’re not even saving many characters, and inlining the types keeps things straightforward for one-off cases like this.
Let me know what you think.


created_at: 2025-03-17T22:08:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,124 @@
+import 'package:sqflite/sqflite.dart';
+import '../migration_base.dart';
+
+// Models
+import '/model/bank_account.dart';
+import '/model/budget.dart';
+import '/model/category_transaction.dart';
+import '/model/currency.dart';
+import '/model/recurring_transaction.dart';
+import '/model/transaction.dart';
+
+class InitialSchema extends Migration {
+  InitialSchema() : super(
+      version: 1,
+      description: 'Initial database schema creation'
+  );
+
+  @override
+  Future<void> up(Database db) async {
+    const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT';
+    const integerNotNull = 'INTEGER NOT NULL';
+    const integer = 'INTEGER';
+    const realNotNull = 'REAL NOT NULL';
+    const textNotNull = 'TEXT NOT NULL';
+    const text = 'TEXT';
path: lib/database/migrations/0001_initial_schema.dart
user: login: mikev-cw
body: Well, yes. Actually we could just get rid of them in the future. You can leve these here if you want, since they're already there!

I was thinking in extreme cases: what would happen if the syntax of those commands changed with new versions of SQLite? Older migrations would have different syntax and would fail.

In reality, besides being an almost impossible scenario, we still have full control over it: we just need to properly lock the version of `sqflite` and run a million tests in case of major updates.

Btw, I'm not sure if you saw the last edit to my message (I don't think GH notifies about edits): could you set up a test example for migration 1?
created_at: 2025-03-17T22:30:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,124 @@
+import 'package:sqflite/sqflite.dart';
+import '../migration_base.dart';
+
+// Models
+import '/model/bank_account.dart';
+import '/model/budget.dart';
+import '/model/category_transaction.dart';
+import '/model/currency.dart';
+import '/model/recurring_transaction.dart';
+import '/model/transaction.dart';
+
+class InitialSchema extends Migration {
+  InitialSchema() : super(
+      version: 1,
+      description: 'Initial database schema creation'
+  );
+
+  @override
+  Future<void> up(Database db) async {
+    const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT';
+    const integerNotNull = 'INTEGER NOT NULL';
+    const integer = 'INTEGER';
+    const realNotNull = 'REAL NOT NULL';
+    const textNotNull = 'TEXT NOT NULL';
+    const text = 'TEXT';
path: lib/database/migrations/0001_initial_schema.dart
user: login: xDefcon
body: Thanks for the feedback, @mikev-cw! I’ll add some basic tests for migration 1 as you suggested. 

I’ll keep them simple (something like verifying the initial schema creation and maybe a basic upgrade scenario) since creating a full, comprehensive test suite for the entire migration system would require incredible effort and might not be worth it in the current state. 

For example, I could test that the tables are created with the right columns on a fresh install, but mocking out every possible upgrade path would be overkill for now.

Let me know if that will work for you!

created_at: 2025-03-18T18:29:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,124 @@
+import 'package:sqflite/sqflite.dart';
+import '../migration_base.dart';
+
+// Models
+import '/model/bank_account.dart';
+import '/model/budget.dart';
+import '/model/category_transaction.dart';
+import '/model/currency.dart';
+import '/model/recurring_transaction.dart';
+import '/model/transaction.dart';
+
+class InitialSchema extends Migration {
+  InitialSchema() : super(
+      version: 1,
+      description: 'Initial database schema creation'
+  );
+
+  @override
+  Future<void> up(Database db) async {
+    const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT';
+    const integerNotNull = 'INTEGER NOT NULL';
+    const integer = 'INTEGER';
+    const realNotNull = 'REAL NOT NULL';
+    const textNotNull = 'TEXT NOT NULL';
+    const text = 'TEXT';
path: lib/database/migrations/0001_initial_schema.dart
user: login: mikev-cw
body: Yes, I agreed 👍🏻
Also tests system is about to be refactored, so there's no need to overkill
created_at: 2025-03-19T08:03:12Z
author_association: COLLABORATOR

PR comments:
user: login: xDefcon
created_at: 2025-03-15T12:53:29Z
body: Related to issue #261 

@mikev-cw Could you review this? I'd also appreciate your feedback on the additional considerations mentioned above. Thanks!

This PR requires testing, possibly including automated tests.

author_association: CONTRIBUTOR
user: login: mikev-cw
created_at: 2025-03-15T17:22:32Z
body: Super, of course I will. I think this could be a valuable addition and i want to go super deep this contribution though! :D Give me some time.
@xDefcon are you on our Discord, in case i need some clarifications?
author_association: COLLABORATOR
user: login: xDefcon
created_at: 2025-03-15T18:57:55Z
body: @mikev-cw Yes I am, with the same username.
author_association: CONTRIBUTOR
user: login: mikev-cw
created_at: 2025-03-22T16:25:32Z
body: @xDefcon I'm merging this, also without tests. We need to do a release next days and we want to have this feature ready for that. Please, if you can, create an issue for tests. Thank you so much!
author_association: COLLABORATOR