using Invercargill.DataStructures; using InvercargillSql; using InvercargillSql.Migrations; using InvercargillSql.Dialects; using InvercargillSql.Orm; /** * Test migrations for testing the migration system */ public class V001_CreateUsers : Migration { public override int version { get { return 1; } } public override string name { get { return "CreateUsers"; } } public override void up(MigrationBuilder b) throws SqlError { b.create_table("users", t => { // Type inferred from generic parameter - no redundant type_int() needed t.column("id") .primary_key() .auto_increment(); t.column("name") .not_null(); t.column("email") .unique(); // Index created within table builder using fluent API t.index("idx_users_email").on_column("email"); }); } public override void down(MigrationBuilder b) throws SqlError { b.drop_table("users"); // Indexes dropped automatically with table } } public class V002_AddAgeColumn : Migration { public override int version { get { return 2; } } public override string name { get { return "AddAgeColumn"; } } public override void up(MigrationBuilder b) throws SqlError { b.alter_table("users", t => { // Type inferred from generic parameter - no redundant type_int() needed t.add_column("age"); }); } public override void down(MigrationBuilder b) throws SqlError { b.alter_table("users", t => { t.drop_column("age"); }); } } /** * Migration demonstrating index operations in alter table */ public class V003_AddIndexOnName : Migration { public override int version { get { return 3; } } public override string name { get { return "AddIndexOnName"; } } public override void up(MigrationBuilder b) throws SqlError { b.alter_table("users", t => { // Create index using alter table builder t.create_index("idx_users_name").on_column("name"); }); } public override void down(MigrationBuilder b) throws SqlError { b.alter_table("users", t => { t.drop_index("idx_users_name"); }); } } /** * Migration tests for Invercargill-Sql library. */ public int main(string[] args) { print("=== Invercargill-Sql Migration Tests ===\n\n"); try { // SQL generation tests test_create_table_sql(); test_drop_table_sql(); test_create_index_sql(); test_create_unique_index_sql(); test_add_column_sql(); test_drop_column_sql(); test_rename_column_sql(); // MigrationBuilder tests test_migration_builder_create_table(); test_migration_builder_create_table_with_indexes(); test_migration_builder_alter_table(); test_migration_builder_alter_table_with_indexes(); // Index builder tests test_index_builder_single_column(); test_index_builder_composite(); test_index_builder_unique(); // Foreign Key tests test_fk_creation_with_auto_generated_name(); test_fk_creation_with_explicit_name(); test_fk_on_delete_actions(); test_fk_on_update_actions(); test_fk_in_alter_table_add_column(); // Indexed column tests test_indexed_with_auto_generated_name(); test_indexed_with_custom_name(); test_unique_indexed_creates_unique_index(); test_drop_index_on(); // MigrationRunner tests test_migration_runner_registration(); test_migration_runner_migrate_to_latest(); test_migration_runner_migrate_to_version(); test_migration_runner_rollback(); test_migration_runner_rollback_all(); print("\n=== All migration tests passed! ===\n"); return 0; } catch (Error e) { printerr("\n=== Test failed: %s ===\n", e.message); return 1; } } void test_create_table_sql() throws SqlError { print("Test: CREATE TABLE SQL generation... "); var dialect = new SqliteDialect(); var op = new CreateTableOperation() { table_name = "test" }; op.columns.add(new ColumnDefinition() { name = "id", column_type = ColumnType.INT_64, is_primary_key = true, auto_increment = true }); op.columns.add(new ColumnDefinition() { name = "name", column_type = ColumnType.TEXT, is_required = true }); var sql = dialect.create_table_sql(op); assert("CREATE TABLE test" in sql); assert("id" in sql); assert("INTEGER" in sql); assert("PRIMARY KEY" in sql); assert("name" in sql); assert("TEXT" in sql); assert("NOT NULL" in sql); print("PASSED\n"); } void test_drop_table_sql() throws SqlError { print("Test: DROP TABLE SQL generation... "); var dialect = new SqliteDialect(); var op = new DropTableOperation() { table_name = "test" }; var sql = dialect.drop_table_sql(op); assert(sql == "DROP TABLE IF EXISTS test"); print("PASSED\n"); } void test_create_index_sql() throws SqlError { print("Test: CREATE INDEX SQL generation... "); var dialect = new SqliteDialect(); var op = new CreateIndexOperation() { index_name = "idx_test", table_name = "test", is_unique = false }; op.columns.add("name"); var sql = dialect.create_index_sql(op); assert("CREATE INDEX idx_test ON test (name)" == sql); print("PASSED\n"); } void test_create_unique_index_sql() throws SqlError { print("Test: CREATE UNIQUE INDEX SQL generation... "); var dialect = new SqliteDialect(); var op = new CreateIndexOperation() { index_name = "idx_unique", table_name = "test", is_unique = true }; op.columns.add("email"); var sql = dialect.create_index_sql(op); assert("CREATE UNIQUE INDEX idx_unique ON test (email)" == sql); print("PASSED\n"); } void test_add_column_sql() throws SqlError { print("Test: ADD COLUMN SQL generation... "); var dialect = new SqliteDialect(); var op = new AddColumnOperation() { table_name = "users", column = new ColumnDefinition() { name = "age", column_type = ColumnType.INT_32 } }; var sql = dialect.add_column_sql(op); assert("ALTER TABLE users ADD COLUMN age" in sql); assert("INTEGER" in sql); print("PASSED\n"); } void test_drop_column_sql() throws SqlError { print("Test: DROP COLUMN SQL generation... "); var dialect = new SqliteDialect(); var op = new DropColumnOperation() { table_name = "users", column_name = "age" }; var sql = dialect.drop_column_sql(op); assert("ALTER TABLE users DROP COLUMN age" == sql); print("PASSED\n"); } void test_rename_column_sql() throws SqlError { print("Test: RENAME COLUMN SQL generation... "); var dialect = new SqliteDialect(); var op = new RenameColumnOperation() { table_name = "users", old_name = "old_name", new_name = "new_name" }; var sql = dialect.rename_column_sql(op); assert("ALTER TABLE users RENAME COLUMN old_name TO new_name" == sql); print("PASSED\n"); } void test_migration_builder_create_table() throws SqlError { print("Test: MigrationBuilder create_table... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); // Type inferred from generic parameter - cleaner API builder.create_table("users", t => { t.column("id").primary_key().auto_increment(); t.column("name").not_null(); }); var ops = builder.get_operations(); assert(ops.length == 1); var op = ops[0] as CreateTableOperation; assert(op != null); assert(op.table_name == "users"); assert(op.columns.length == 2); print("PASSED\n"); } void test_migration_builder_create_table_with_indexes() throws SqlError { print("Test: MigrationBuilder create_table with indexes... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); // Create table with indexes defined inline builder.create_table("users", t => { t.column("id").primary_key().auto_increment(); t.column("email").not_null(); t.column("name").not_null(); // Single column index t.index("idx_email").on_column("email"); // Composite unique index t.index("idx_email_name").on_columns("email", "name").unique(); }); var ops = builder.get_operations(); // Should have 1 create table + 2 create index operations assert(ops.length == 3); var table_op = ops[0] as CreateTableOperation; assert(table_op != null); assert(table_op.table_name == "users"); var idx1_op = ops[1] as CreateIndexOperation; assert(idx1_op != null); assert(idx1_op.index_name == "idx_email"); assert(idx1_op.is_unique == false); var idx2_op = ops[2] as CreateIndexOperation; assert(idx2_op != null); assert(idx2_op.index_name == "idx_email_name"); assert(idx2_op.is_unique == true); print("PASSED\n"); } void test_migration_builder_alter_table() throws SqlError { print("Test: MigrationBuilder alter_table... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); // Type inferred from generic parameter - cleaner API builder.alter_table("users", t => { t.add_column("email").not_null(); t.drop_column("old_column"); t.rename_column("old_name", "new_name"); }); var ops = builder.get_operations(); assert(ops.length == 3); assert(ops[0] is AddColumnOperation); assert(ops[1] is DropColumnOperation); assert(ops[2] is RenameColumnOperation); print("PASSED\n"); } void test_migration_builder_alter_table_with_indexes() throws SqlError { print("Test: MigrationBuilder alter_table with indexes... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.alter_table("users", t => { t.add_column("phone"); // Create index on new column t.create_index("idx_phone").on_column("phone"); // Drop old index t.drop_index("idx_old_email"); }); var ops = builder.get_operations(); assert(ops.length == 3); assert(ops[0] is AddColumnOperation); assert(ops[1] is CreateIndexOperation); assert(ops[2] is DropIndexOperation); print("PASSED\n"); } void test_index_builder_single_column() throws SqlError { print("Test: IndexBuilder single column... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("test", t => { t.column("name").not_null(); t.index("idx_name").on_column("name"); }); var ops = builder.get_operations(); var idx_op = ops[1] as CreateIndexOperation; assert(idx_op != null); assert(idx_op.index_name == "idx_name"); assert(idx_op.columns.length == 1); assert(idx_op.columns[0] == "name"); assert(idx_op.is_unique == false); print("PASSED\n"); } void test_index_builder_composite() throws SqlError { print("Test: IndexBuilder composite... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("test", t => { t.column("col1").not_null(); t.column("col2").not_null(); t.column("col3").not_null(); t.index("idx_composite").on_columns("col1", "col2", "col3"); }); var ops = builder.get_operations(); var idx_op = ops[1] as CreateIndexOperation; assert(idx_op != null); assert(idx_op.index_name == "idx_composite"); assert(idx_op.columns.length == 3); assert(idx_op.columns[0] == "col1"); assert(idx_op.columns[1] == "col2"); assert(idx_op.columns[2] == "col3"); print("PASSED\n"); } void test_index_builder_unique() throws SqlError { print("Test: IndexBuilder unique... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("test", t => { t.column("email").not_null(); t.index("uq_email").on_column("email").unique(); }); var ops = builder.get_operations(); var idx_op = ops[1] as CreateIndexOperation; assert(idx_op != null); assert(idx_op.index_name == "uq_email"); assert(idx_op.is_unique == true); print("PASSED\n"); } void test_migration_runner_registration() throws SqlError { print("Test: MigrationRunner registration... "); var conn = ConnectionFactory.create_and_open("sqlite::memory:"); var dialect = new SqliteDialect(); var runner = new MigrationRunner(conn, dialect); runner.register_migration(new V001_CreateUsers()); runner.register_migration(new V002_AddAgeColumn()); // Verify migrations are registered - current version should be 0 var current = runner.get_current_version(); assert(current == 0); // No migrations applied yet print("PASSED\n"); conn.close(); } void test_migration_runner_migrate_to_latest() throws SqlError { print("Test: MigrationRunner migrate_to_latest... "); var conn = ConnectionFactory.create_and_open("sqlite::memory:"); var dialect = new SqliteDialect(); var runner = new MigrationRunner(conn, dialect); runner.register_migration(new V001_CreateUsers()); runner.register_migration(new V002_AddAgeColumn()); runner.migrate_to_latest(); var current = runner.get_current_version(); assert(current == 2); // Verify table was created var cmd = conn.create_command("SELECT name FROM sqlite_master WHERE type='table' AND name='users'"); var results = cmd.execute_query(); assert(results.any()); print("PASSED\n"); conn.close(); } void test_migration_runner_migrate_to_version() throws SqlError { print("Test: MigrationRunner migrate_to_version... "); var conn = ConnectionFactory.create_and_open("sqlite::memory:"); var dialect = new SqliteDialect(); var runner = new MigrationRunner(conn, dialect); runner.register_migration(new V001_CreateUsers()); runner.register_migration(new V002_AddAgeColumn()); // Migrate to version 1 only runner.migrate_to(1); assert(runner.get_current_version() == 1); // Then migrate to version 2 runner.migrate_to(2); assert(runner.get_current_version() == 2); print("PASSED\n"); conn.close(); } void test_migration_runner_rollback() throws SqlError { print("Test: MigrationRunner rollback... "); var conn = ConnectionFactory.create_and_open("sqlite::memory:"); var dialect = new SqliteDialect(); var runner = new MigrationRunner(conn, dialect); runner.register_migration(new V001_CreateUsers()); runner.register_migration(new V002_AddAgeColumn()); runner.migrate_to_latest(); assert(runner.get_current_version() == 2); // Rollback one step runner.rollback(1); assert(runner.get_current_version() == 1); print("PASSED\n"); conn.close(); } void test_migration_runner_rollback_all() throws SqlError { print("Test: MigrationRunner rollback_all... "); var conn = ConnectionFactory.create_and_open("sqlite::memory:"); var dialect = new SqliteDialect(); var runner = new MigrationRunner(conn, dialect); runner.register_migration(new V001_CreateUsers()); runner.register_migration(new V002_AddAgeColumn()); runner.migrate_to_latest(); assert(runner.get_current_version() == 2); // Rollback all runner.rollback_all(); assert(runner.get_current_version() == 0); print("PASSED\n"); conn.close(); } // ========== Foreign Key Tests ========== void test_fk_creation_with_auto_generated_name() throws SqlError { print("Test: FK creation with auto-generated name... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("id").primary_key().auto_increment(); t.column("user_id") .not_null() .references("users", "id"); }); var ops = builder.get_operations(); assert(ops.length == 1); var table_op = ops[0] as CreateTableOperation; assert(table_op != null); // Debug: print constraint count if (table_op.constraints.length != 1) { print("\n DEBUG: constraints.length = %u (expected 1)\n", table_op.constraints.length); } assert(table_op.constraints.length == 1); var constraint = table_op.constraints.get(0); assert(constraint.constraint_type == "FOREIGN KEY"); assert(constraint.name == "fk_orders_user_id"); // Auto-generated name assert(constraint.reference_table == "users"); assert(constraint.reference_columns.get(0) == "id"); assert(constraint.columns.get(0) == "user_id"); // Verify SQL contains proper FOREIGN KEY clause var sql = dialect.create_table_sql(table_op); assert("FOREIGN KEY" in sql); assert("REFERENCES users (id)" in sql); assert("fk_orders_user_id" in sql); print("PASSED\n"); } void test_fk_creation_with_explicit_name() throws SqlError { print("Test: FK creation with explicit name... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("id").primary_key().auto_increment(); t.column("user_id") .not_null() .references("users", "id") .name("custom_fk_orders_users"); }); var ops = builder.get_operations(); var table_op = ops[0] as CreateTableOperation; assert(table_op != null); var constraint = table_op.constraints.get(0); assert(constraint.name == "custom_fk_orders_users"); // Custom name var sql = dialect.create_table_sql(table_op); assert("custom_fk_orders_users" in sql); print("PASSED\n"); } void test_fk_on_delete_actions() throws SqlError { print("Test: FK ON DELETE actions... "); var dialect = new SqliteDialect(); // Test CASCADE var builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_delete_cascade(); }); var ops = builder.get_operations(); var table_op = ops[0] as CreateTableOperation; var constraint = table_op.constraints.get(0); assert(constraint.on_delete_action == ReferentialAction.CASCADE); var sql = dialect.create_table_sql(table_op); assert("ON DELETE CASCADE" in sql); // Test SET NULL builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_delete_set_null(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_delete_action == ReferentialAction.SET_NULL); sql = dialect.create_table_sql(table_op); assert("ON DELETE SET NULL" in sql); // Test SET DEFAULT builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_delete_set_default(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_delete_action == ReferentialAction.SET_DEFAULT); sql = dialect.create_table_sql(table_op); assert("ON DELETE SET DEFAULT" in sql); // Test NO ACTION builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_delete_no_action(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_delete_action == ReferentialAction.NO_ACTION); // Test RESTRICT builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_delete_restrict(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_delete_action == ReferentialAction.RESTRICT); sql = dialect.create_table_sql(table_op); assert("ON DELETE RESTRICT" in sql); print("PASSED\n"); } void test_fk_on_update_actions() throws SqlError { print("Test: FK ON UPDATE actions... "); var dialect = new SqliteDialect(); // Test CASCADE var builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_update_cascade(); }); var ops = builder.get_operations(); var table_op = ops[0] as CreateTableOperation; var constraint = table_op.constraints.get(0); assert(constraint.on_update_action == ReferentialAction.CASCADE); var sql = dialect.create_table_sql(table_op); assert("ON UPDATE CASCADE" in sql); // Test SET NULL builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_update_set_null(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_update_action == ReferentialAction.SET_NULL); sql = dialect.create_table_sql(table_op); assert("ON UPDATE SET NULL" in sql); // Test SET DEFAULT builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_update_set_default(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_update_action == ReferentialAction.SET_DEFAULT); sql = dialect.create_table_sql(table_op); assert("ON UPDATE SET DEFAULT" in sql); // Test NO ACTION builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_update_no_action(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_update_action == ReferentialAction.NO_ACTION); // Test RESTRICT builder = new MigrationBuilder(dialect); builder.create_table("orders", t => { t.column("user_id") .references("users", "id") .on_update_restrict(); }); ops = builder.get_operations(); table_op = ops[0] as CreateTableOperation; constraint = table_op.constraints.get(0); assert(constraint.on_update_action == ReferentialAction.RESTRICT); sql = dialect.create_table_sql(table_op); assert("ON UPDATE RESTRICT" in sql); print("PASSED\n"); } void test_fk_in_alter_table_add_column() throws SqlError { print("Test: FK in ALTER TABLE add column... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.alter_table("orders", t => { t.add_column("product_id") .not_null() .references("products", "id") .on_delete_restrict(); }); var ops = builder.get_operations(); assert(ops.length == 1); var add_col_op = ops[0] as AddColumnOperation; assert(add_col_op != null); assert(add_col_op.foreign_key_constraint != null); var fk = add_col_op.foreign_key_constraint; assert(fk.constraint_type == "FOREIGN KEY"); assert(fk.reference_table == "products"); assert(fk.reference_columns.get(0) == "id"); assert(fk.on_delete_action == ReferentialAction.RESTRICT); // Verify SQL contains inline REFERENCES clause var sql = dialect.add_column_sql(add_col_op); assert("REFERENCES products (id)" in sql); assert("ON DELETE RESTRICT" in sql); print("PASSED\n"); } // ========== Indexed Column Tests ========== void test_indexed_with_auto_generated_name() throws SqlError { print("Test: indexed() with auto-generated name... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("users", t => { t.column("id").primary_key().auto_increment(); t.column("email") .not_null() .indexed(); }); var ops = builder.get_operations(); // Should have 1 create table + 1 create index operation assert(ops.length == 2); var table_op = ops[0] as CreateTableOperation; assert(table_op != null); var idx_op = ops[1] as CreateIndexOperation; assert(idx_op != null); assert(idx_op.index_name == "idx_users_email"); // Auto-generated name assert(idx_op.table_name == "users"); assert(idx_op.columns.length == 1); assert(idx_op.columns.get(0) == "email"); assert(idx_op.is_unique == false); print("PASSED\n"); } void test_indexed_with_custom_name() throws SqlError { print("Test: indexed() with custom name... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("users", t => { t.column("id").primary_key().auto_increment(); t.column("email") .not_null() .indexed("idx_users_email_address"); }); var ops = builder.get_operations(); assert(ops.length == 2); var idx_op = ops[1] as CreateIndexOperation; assert(idx_op != null); assert(idx_op.index_name == "idx_users_email_address"); // Custom name print("PASSED\n"); } void test_unique_indexed_creates_unique_index() throws SqlError { print("Test: unique().indexed() creates UNIQUE index... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.create_table("users", t => { t.column("id").primary_key().auto_increment(); t.column("email") .not_null() .unique() .indexed(); }); var ops = builder.get_operations(); assert(ops.length == 2); var idx_op = ops[1] as CreateIndexOperation; assert(idx_op != null); assert(idx_op.is_unique == true); // Uniqueness inferred from column's unique() flag var sql = dialect.create_index_sql(idx_op); assert("CREATE UNIQUE INDEX" in sql); print("PASSED\n"); } void test_drop_index_on() throws SqlError { print("Test: drop_index_on() generates correct DropIndexOperation... "); var dialect = new SqliteDialect(); var builder = new MigrationBuilder(dialect); builder.alter_table("users", t => { t.drop_index_on("email"); }); var ops = builder.get_operations(); assert(ops.length == 1); var drop_op = ops[0] as DropIndexOperation; assert(drop_op != null); assert(drop_op.index_name == "idx_users_email"); // Auto-generated name assert(drop_op.table_name == "users"); print("PASSED\n"); }