Add comprehensive foreign key and simplified index support to the migration/schema system with a fluent API that allows defining FK constraints and indexes inline with column definitions.
The primary API for defining foreign keys is inline with column definitions:
// Basic FK with auto-generated name
t.column<int64?>("user_id")
.not_null()
.references("users", "id");
// Full FK with all options
t.column<int64?>("user_id")
.references("users", "id")
.name("fk_orders_users") // optional, auto-generated if omitted
.on_delete_cascade() // ON DELETE CASCADE
.on_delete_set_null() // ON DELETE SET NULL
.on_delete_set_default() // ON DELETE SET DEFAULT
.on_delete_no_action() // ON DELETE NO ACTION (default)
.on_delete_restrict() // ON DELETE RESTRICT
.on_update_cascade() // ON UPDATE CASCADE
.on_update_set_null() // etc.
.on_update_set_default()
.on_update_no_action() // default
.on_update_restrict()
.not_null(); // Can continue with column methods
For simple single-column indexes, use .indexed() directly on the column builder:
// Basic index with auto-generated name (idx_{table}_{column})
t.column<string>("email")
.not_null()
.indexed();
// Unique index - uniqueness inferred from column's unique() flag
t.column<string>("email")
.not_null()
.unique()
.indexed(); // Creates UNIQUE index
// Index with custom name
t.column<string>("email")
.indexed("idx_users_email_address");
Key Design Points:
indexed(string? name = null) returns MigrationColumnBuilder (same type, allows chaining).unique() flagt.index("name").on_column("col") API remains for:
The object returned by .references() must inherit from MigrationColumnBuilder so that:
.not_null() before or after .references())For .indexed(), it simply returns this (MigrationColumnBuilder) since no additional configuration is needed.
// Add column with FK
b.alter_table("orders", t => {
t.add_column<int64?>("product_id")
.references("products", "id")
.on_delete_restrict();
});
// Add column with index
b.alter_table("orders", t => {
t.add_column<string>("sku")
.unique()
.indexed(); // Creates UNIQUE index (uniqueness from column)
});
// Drop FK by constraint name
b.alter_table("orders", t => {
t.drop_foreign_key("fk_orders_users");
});
// Drop FK by column (useful for auto-generated names)
b.alter_table("orders", t => {
t.drop_foreign_key_on("user_id");
});
// Drop index by column (useful for auto-generated names)
b.alter_table("orders", t => {
t.drop_index_on("sku");
});
fk_{table}_{column} if not specifiedMigrationColumnBuilder
└── ForeignKeyBuilder (extends MigrationColumnBuilder)
- Stores FK configuration
- All column methods pass through to parent
- FK-specific methods return this
Note: indexed() does NOT need a separate builder - it returns this (MigrationColumnBuilder)
ForeignKeyBuilder (src/migrations/foreign-key-builder.vala)
DropForeignKeyOperation (in schema-operations.vala)
MigrationColumnBuilder
references(table, column) method returning ForeignKeyBuilderindexed(string? name = null) method returning thisTableBuilder
AlterTableBuilder
drop_foreign_key(name) methoddrop_foreign_key_on(column) methoddrop_index_on(column) methodTableConstraint
on_delete_action propertyon_update_action propertySqlDialect Interface
drop_foreign_key_sql(DropForeignKeyOperation op) methodSqliteDialect
build_constraint_sql() to include ON DELETE/UPDATEdrop_foreign_key_sql() with table recreationSQLite does not support ALTER TABLE DROP CONSTRAINT. The dialect must handle this transparently:
-- Generated SQL for dropping FK on SQLite:
BEGIN TRANSACTION;
CREATE TABLE _orders_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
-- ... other columns without FK constraint
);
INSERT INTO _orders_new SELECT * FROM orders;
DROP TABLE orders;
ALTER TABLE _orders_new RENAME TO orders;
-- Recreate indexes
CREATE INDEX idx_orders_user_id ON orders (user_id);
COMMIT;
drop_foreign_key_sql() returns a single string with multiple statements separated by semicolons and newlinespublic enum ReferentialAction {
NO_ACTION, // default
CASCADE,
SET_NULL,
SET_DEFAULT,
RESTRICT;
public string to_sql() {
switch (this) {
case CASCADE: return "CASCADE";
case SET_NULL: return "SET NULL";
case SET_DEFAULT: return "SET DEFAULT";
case RESTRICT: return "RESTRICT";
default: return "NO ACTION";
}
}
}
src/migrations/schema-operations.valato_sql() method for SQL generationon_delete_action property (ReferentialAction, default NO_ACTION)on_update_action property (ReferentialAction, default NO_ACTION)src/migrations/foreign-key-builder.valaname(), on_delete_*(), on_update_*()indexed(string? name = null) returning thisreferences(table, column) returning ForeignKeyBuilderdrop_foreign_key(string name) methoddrop_foreign_key_on(string column) methoddrop_index_on(string column) methodsrc/migrations/schema-operations.valadrop_foreign_key_sql(DropForeignKeyOperation op) abstract methodbuild_constraint_sql() to emit ON DELETE/UPDATE clausesdrop_foreign_key_sql() with table recreation logicv003-create-orders.vala to use new FK API and .indexed().indexed() with auto-generated name.indexed("custom_name") with custom name.unique().indexed() creates UNIQUE indexdrop_index_on()public class V003_CreateOrders : Migration {
public override int version { get { return 3; } }
public override string name { get { return "CreateOrders"; } }
public override void up(MigrationBuilder b) throws SqlError {
b.create_table("orders", t => {
t.column<int64?>("id")
.primary_key()
.auto_increment();
t.column<int64?>("user_id")
.not_null()
.references("users", "id")
.name("fk_orders_users")
.on_delete_cascade()
.indexed(); // Auto-generates idx_orders_user_id
t.column<int64?>("product_id")
.not_null()
.references("products", "id")
.on_delete_restrict()
.indexed(); // Auto-generates idx_orders_product_id
t.column<int64?>("quantity");
t.column<double?>("total");
t.column<string>("status")
.indexed(); // Auto-generates idx_orders_status
t.column<int64?>("created_at");
// Multi-column indexes still use the existing API:
// t.index("idx_composite").on_columns("col1", "col2");
});
}
public override void down(MigrationBuilder b) throws SqlError {
b.drop_table("orders");
}
}
public class V004_AddCategoryToProducts : Migration {
public override int version { get { return 4; } }
public override string name { get { return "AddCategoryToProducts"; } }
public override void up(MigrationBuilder b) throws SqlError {
// First create the categories table
b.create_table("categories", t => {
t.column<int64?>("id")
.primary_key()
.auto_increment();
t.column<string>("name")
.not_null();
});
// Then add the FK column to products
b.alter_table("products", t => {
t.add_column<int64?>("category_id")
.references("categories", "id")
.on_delete_set_null(); // Keep products when category deleted
});
}
public override void down(MigrationBuilder b) throws SqlError {
b.alter_table("products", t => {
t.drop_foreign_key_on("category_id");
t.drop_column("category_id");
});
b.drop_table("categories");
}
}
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER,
total REAL,
status TEXT,
created_at INTEGER,
CONSTRAINT fk_orders_users FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT fk_orders_products FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT
)
ALTER TABLE products ADD COLUMN category_id INTEGER REFERENCES categories (id) ON DELETE SET NULL
ALTER TABLE orders DROP CONSTRAINT fk_orders_users
BEGIN TRANSACTION;
CREATE TABLE _orders_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER,
total REAL,
status TEXT,
created_at INTEGER,
CONSTRAINT fk_orders_products FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT
);
INSERT INTO _orders_new SELECT id, user_id, product_id, quantity, total, status, created_at FROM orders;
DROP TABLE orders;
ALTER TABLE _orders_new RENAME TO orders;
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_product_id ON orders (product_id);
CREATE INDEX idx_orders_status ON orders (status);
COMMIT;
.name() for FKs, optional name parameter for .indexed(name); auto-generate as fk_{table}_{column} or idx_{table}_{column}drop_index_on().indexed(string? name = null) returning same builder; uniqueness inferred from column's .unique() flag