|
@@ -342,6 +342,52 @@ public class Test_Missing_Serial : Migration {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// --- FK + indexed regression test migrations ---
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Chained FK + indexed: the exact shape from the bug report.
|
|
|
|
|
+ * The index must actually be created on the FK column.
|
|
|
|
|
+ */
|
|
|
|
|
+public class Test_FkIndexed_V1 : Migration {
|
|
|
|
|
+ public override string migration_namespace { get { return "fkidx"; } }
|
|
|
|
|
+ public override uint64 serial { get { return 1; } }
|
|
|
|
|
+ public override string name { get { return "CreateRepos"; } }
|
|
|
|
|
+
|
|
|
|
|
+ public override void up(MigrationBuilder b) throws SqlError {
|
|
|
|
|
+ b.create_table("repos", t => {
|
|
|
|
|
+ t.column<int64?>("id").primary_key().auto_increment();
|
|
|
|
|
+ t.column<string>("name").not_null();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public override void down(MigrationBuilder b) throws SqlError {
|
|
|
|
|
+ b.drop_table("repos");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+public class Test_FkIndexed_V2 : Migration {
|
|
|
|
|
+ public override string migration_namespace { get { return "fkidx"; } }
|
|
|
|
|
+ public override uint64 serial { get { return 2; } }
|
|
|
|
|
+ public override string name { get { return "CreateOrgRepos"; } }
|
|
|
|
|
+ public override string[] dependencies { owned get { return {"fkidx:1"}; } }
|
|
|
|
|
+
|
|
|
|
|
+ public override void up(MigrationBuilder b) throws SqlError {
|
|
|
|
|
+ b.create_table("org_repos", t => {
|
|
|
|
|
+ t.column<int64?>("id").primary_key().auto_increment();
|
|
|
|
|
+ t.column<int64?>("repo_id")
|
|
|
|
|
+ .not_null()
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .on_delete_cascade()
|
|
|
|
|
+ .indexed();
|
|
|
|
|
+ t.column<string>("slug").not_null();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public override void down(MigrationBuilder b) throws SqlError {
|
|
|
|
|
+ b.drop_table("org_repos");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
|
// TEST MAIN
|
|
// TEST MAIN
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
@@ -441,6 +487,18 @@ public int main(string[] args) {
|
|
|
Test.add_func("/migrations/indexed/unique", test_unique_indexed_creates_unique_index);
|
|
Test.add_func("/migrations/indexed/unique", test_unique_indexed_creates_unique_index);
|
|
|
Test.add_func("/migrations/indexed/drop", test_drop_index_on);
|
|
Test.add_func("/migrations/indexed/drop", test_drop_index_on);
|
|
|
|
|
|
|
|
|
|
+ // ========================================
|
|
|
|
|
+ // 11. FK + Indexed Regression Tests
|
|
|
|
|
+ // ========================================
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/e2e_index_created", test_fk_indexed_creates_index_in_database);
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/operation_model", test_fk_indexed_emits_index_operation);
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/custom_name", test_fk_indexed_with_custom_name);
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/not_null_propagates", test_fk_chain_not_null_propagates);
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/unique_propagates", test_fk_chain_unique_propagates);
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/type_overrides_propagate", test_fk_chain_type_overrides_propagate);
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/alter_table_index_created", test_fk_indexed_in_alter_table_add_column);
|
|
|
|
|
+ Test.add_func("/migrations/fk_indexed/double_fk_not_required", test_fk_chain_double_references_returns_fk_builder);
|
|
|
|
|
+
|
|
|
return Test.run();
|
|
return Test.run();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1643,3 +1701,254 @@ void test_drop_index_on() {
|
|
|
assert(drop_op.index_name == "idx_users_email");
|
|
assert(drop_op.index_name == "idx_users_email");
|
|
|
assert(drop_op.table_name == "users");
|
|
assert(drop_op.table_name == "users");
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// 11. FK + INDEXED REGRESSION TESTS
|
|
|
|
|
+//
|
|
|
|
|
+// Regression guard for the dropped-flag bug class: ForeignKeyBuilder wraps a
|
|
|
|
|
+// MigrationColumnBuilder, and any column method called after .references()
|
|
|
|
|
+// (indexed(), not_null(), unique(), type_*(), ...) used to mutate the
|
|
|
|
|
+// wrapper's own throwaway state instead of the wrapped column builder that
|
|
|
|
|
+// TableBuilder/AlterTableBuilder read at build time.
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * End-to-end: the chained FK+indexed form must produce a real index in an
|
|
|
|
|
+ * actual SQLite database file, verified via sqlite_master (what
|
|
|
|
|
+ * `sqlite3 db .schema` reads).
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_indexed_creates_index_in_database() {
|
|
|
|
|
+ string db_path = GLib.Path.build_filename(
|
|
|
|
|
+ GLib.Environment.get_tmp_dir(),
|
|
|
|
|
+ "invercargill-sql-fk-indexed-test.db"
|
|
|
|
|
+ );
|
|
|
|
|
+ GLib.FileUtils.remove(db_path); // ignore missing
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ var conn = ConnectionFactory.create_and_open(@"sqlite://$db_path");
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var runner = new MigrationRunner(conn, dialect);
|
|
|
|
|
+
|
|
|
|
|
+ runner.register_migration(new Test_FkIndexed_V1());
|
|
|
|
|
+ runner.register_migration(new Test_FkIndexed_V2());
|
|
|
|
|
+ runner.migrate_to_latest();
|
|
|
|
|
+
|
|
|
|
|
+ assert(runner.get_current_serial("fkidx") == 2);
|
|
|
|
|
+
|
|
|
|
|
+ // The FK constraint itself must still be there
|
|
|
|
|
+ var fk_cmd = conn.create_command(
|
|
|
|
|
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='org_repos'"
|
|
|
|
|
+ );
|
|
|
|
|
+ var create_sql = fk_cmd.execute_scalar();
|
|
|
|
|
+ assert(create_sql != null);
|
|
|
|
|
+ string table_sql = create_sql.as_string_or_null() ?? "";
|
|
|
|
|
+ assert("FOREIGN KEY" in table_sql);
|
|
|
|
|
+ assert("REFERENCES repos (id)" in table_sql);
|
|
|
|
|
+ assert("ON DELETE CASCADE" in table_sql);
|
|
|
|
|
+
|
|
|
|
|
+ // THE regression: a CREATE INDEX for the FK column must exist
|
|
|
|
|
+ var idx_cmd = conn.create_command(
|
|
|
|
|
+ "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND tbl_name='org_repos' AND name='idx_org_repos_repo_id'"
|
|
|
|
|
+ );
|
|
|
|
|
+ var count_elem = idx_cmd.execute_scalar();
|
|
|
|
|
+ assert(count_elem != null);
|
|
|
|
|
+ assert(count_elem.as_int64_or_null() == 1);
|
|
|
|
|
+
|
|
|
|
|
+ conn.close();
|
|
|
|
|
+ } catch (SqlError e) {
|
|
|
|
|
+ stderr.printf("ERROR: %s\n", e.message);
|
|
|
|
|
+ assert_not_reached();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ GLib.FileUtils.remove(db_path);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Unit level: the chained form must emit a CreateIndexOperation alongside
|
|
|
|
|
+ * the CreateTableOperation, with the auto-generated idx_{table}_{column} name.
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_indexed_emits_index_operation() {
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var builder = new MigrationBuilder(dialect);
|
|
|
|
|
+
|
|
|
|
|
+ builder.create_table("org_repos", t => {
|
|
|
|
|
+ t.column<int64?>("id").primary_key().auto_increment();
|
|
|
|
|
+ t.column<int64?>("repo_id")
|
|
|
|
|
+ .not_null()
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .on_delete_cascade()
|
|
|
|
|
+ .indexed();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var ops = builder.get_operations();
|
|
|
|
|
+ assert(ops.length == 2);
|
|
|
|
|
+
|
|
|
|
|
+ var table_op = ops[0] as CreateTableOperation;
|
|
|
|
|
+ assert(table_op != null);
|
|
|
|
|
+
|
|
|
|
|
+ // FK constraint still collected from the inner builder chain
|
|
|
|
|
+ assert(table_op.constraints.length == 1);
|
|
|
|
|
+ var constraint = table_op.constraints.get(0);
|
|
|
|
|
+ assert(constraint.constraint_type == "FOREIGN KEY");
|
|
|
|
|
+ assert(constraint.name == "fk_org_repos_repo_id");
|
|
|
|
|
+ assert(constraint.reference_table == "repos");
|
|
|
|
|
+ assert(constraint.on_delete_action == ReferentialAction.CASCADE);
|
|
|
|
|
+
|
|
|
|
|
+ // The index operation must ALSO be emitted
|
|
|
|
|
+ var idx_op = ops[1] as CreateIndexOperation;
|
|
|
|
|
+ assert(idx_op != null);
|
|
|
|
|
+ assert(idx_op.index_name == "idx_org_repos_repo_id");
|
|
|
|
|
+ assert(idx_op.table_name == "org_repos");
|
|
|
|
|
+ assert(idx_op.columns.length == 1);
|
|
|
|
|
+ assert(idx_op.columns.get(0) == "repo_id");
|
|
|
|
|
+ assert(idx_op.is_unique == false);
|
|
|
|
|
+
|
|
|
|
|
+ var sql = dialect.create_index_sql(idx_op);
|
|
|
|
|
+ assert("CREATE INDEX idx_org_repos_repo_id ON org_repos (repo_id)" == sql);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Custom index names given after .references() must survive the wrapper.
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_indexed_with_custom_name() {
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var builder = new MigrationBuilder(dialect);
|
|
|
|
|
+
|
|
|
|
|
+ builder.create_table("org_repos", t => {
|
|
|
|
|
+ t.column<int64?>("repo_id")
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .indexed("idx_org_repos_repo_fk");
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ 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_org_repos_repo_fk");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * not_null() called after .references() must reach the real column.
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_chain_not_null_propagates() {
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var builder = new MigrationBuilder(dialect);
|
|
|
|
|
+
|
|
|
|
|
+ builder.create_table("org_repos", t => {
|
|
|
|
|
+ t.column<int64?>("repo_id")
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .not_null();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var ops = builder.get_operations();
|
|
|
|
|
+ var table_op = ops[0] as CreateTableOperation;
|
|
|
|
|
+ assert(table_op != null);
|
|
|
|
|
+ assert(table_op.columns.length == 1);
|
|
|
|
|
+ assert(table_op.columns.get(0).is_required == true);
|
|
|
|
|
+
|
|
|
|
|
+ var sql = dialect.create_table_sql(table_op);
|
|
|
|
|
+ assert("NOT NULL" in sql);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * unique() called after .references() must reach the real column.
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_chain_unique_propagates() {
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var builder = new MigrationBuilder(dialect);
|
|
|
|
|
+
|
|
|
|
|
+ builder.create_table("org_repos", t => {
|
|
|
|
|
+ t.column<int64?>("repo_id")
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .unique()
|
|
|
|
|
+ .indexed();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var ops = builder.get_operations();
|
|
|
|
|
+ assert(ops.length == 2);
|
|
|
|
|
+
|
|
|
|
|
+ var table_op = ops[0] as CreateTableOperation;
|
|
|
|
|
+ assert(table_op.columns.get(0).is_unique == true);
|
|
|
|
|
+
|
|
|
|
|
+ var idx_op = ops[1] as CreateIndexOperation;
|
|
|
|
|
+ assert(idx_op != null);
|
|
|
|
|
+ assert(idx_op.is_unique == true);
|
|
|
|
|
+ var sql = dialect.create_index_sql(idx_op);
|
|
|
|
|
+ assert("CREATE UNIQUE INDEX" in sql);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Column type overrides called after .references() must reach the real column
|
|
|
|
|
+ * (type_text() overriding the generic int64 inference).
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_chain_type_overrides_propagate() {
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var builder = new MigrationBuilder(dialect);
|
|
|
|
|
+
|
|
|
|
|
+ builder.create_table("org_repos", t => {
|
|
|
|
|
+ t.column<int64?>("repo_id")
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .type_text();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var ops = builder.get_operations();
|
|
|
|
|
+ var table_op = ops[0] as CreateTableOperation;
|
|
|
|
|
+ assert(table_op.columns.get(0).column_type == ColumnType.TEXT);
|
|
|
|
|
+
|
|
|
|
|
+ var sql = dialect.create_table_sql(table_op);
|
|
|
|
|
+ assert("TEXT" in sql);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * ALTER TABLE form: add_column with the chained FK+indexed must emit the
|
|
|
|
|
+ * index operation too.
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_indexed_in_alter_table_add_column() {
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var builder = new MigrationBuilder(dialect);
|
|
|
|
|
+
|
|
|
|
|
+ builder.alter_table("org_repos", t => {
|
|
|
|
|
+ t.add_column<int64?>("repo_id")
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .indexed();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var ops = builder.get_operations();
|
|
|
|
|
+ assert(ops.length == 2);
|
|
|
|
|
+
|
|
|
|
|
+ var add_col_op = ops[0] as AddColumnOperation;
|
|
|
|
|
+ assert(add_col_op != null);
|
|
|
|
|
+ assert(add_col_op.foreign_key_constraint != null);
|
|
|
|
|
+ assert(add_col_op.foreign_key_constraint.reference_table == "repos");
|
|
|
|
|
+
|
|
|
|
|
+ var idx_op = ops[1] as CreateIndexOperation;
|
|
|
|
|
+ assert(idx_op != null);
|
|
|
|
|
+ assert(idx_op.index_name == "idx_org_repos_repo_id");
|
|
|
|
|
+ assert(idx_op.columns.get(0) == "repo_id");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Chaining a second .references() after FK configuration must still return a
|
|
|
|
|
+ * ForeignKeyBuilder (documented behaviour) and register on the inner builder.
|
|
|
|
|
+ */
|
|
|
|
|
+void test_fk_chain_double_references_returns_fk_builder() {
|
|
|
|
|
+ var dialect = new SqliteDialect();
|
|
|
|
|
+ var builder = new MigrationBuilder(dialect);
|
|
|
|
|
+
|
|
|
|
|
+ builder.create_table("org_repos", t => {
|
|
|
|
|
+ var fk = t.column<int64?>("repo_id")
|
|
|
|
|
+ .references("repos", "id")
|
|
|
|
|
+ .on_delete_cascade()
|
|
|
|
|
+ .references("other_table", "id");
|
|
|
|
|
+ assert(fk is ForeignKeyBuilder);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // The inner column builder must expose the LAST fk configured
|
|
|
|
|
+ var ops = builder.get_operations();
|
|
|
|
|
+ var table_op = ops[0] as CreateTableOperation;
|
|
|
|
|
+ assert(table_op != null);
|
|
|
|
|
+ assert(table_op.constraints.length == 1);
|
|
|
|
|
+ assert(table_op.constraints.get(0).reference_table == "other_table");
|
|
|
|
|
+}
|
|
|
|
|
+
|