import { BUN_MARIADB_ForeignKeyRules, type BUN_MARIADB_FieldSchemaType, type BUN_MARIADB_ForeignKeyRule, } from "../../types"; import MariaDBQuoteGen from "./mariadb-quote-gen"; export function defaultForeignKeyName( tableName: string, fieldName: string, ): string { return `fk_${tableName}_${fieldName}`; } export function resolveForeignKeyName( field: BUN_MARIADB_FieldSchemaType, tableName: string, ): string { const fieldName = field.fieldName || "column"; return field.foreignKey?.foreignKeyName || defaultForeignKeyName(tableName, fieldName); } /** * Normalize a referential action as MariaDB reports it in * `information_schema.REFERENTIAL_CONSTRAINTS`. InnoDB treats `NO ACTION` * exactly like `RESTRICT`, so both compare as `RESTRICT`. */ export function normalizeForeignKeyRule( rule: string | undefined | null, ): BUN_MARIADB_ForeignKeyRule { const normalized = String(rule || "RESTRICT") .trim() .toUpperCase() .replace(/[_\s]+/g, " "); if (normalized === "NO ACTION") return "RESTRICT"; if (!(BUN_MARIADB_ForeignKeyRules as readonly string[]).includes(normalized)) { throw new Error( `Invalid foreign key rule \`${rule}\`. Expected one of: ${BUN_MARIADB_ForeignKeyRules.join(", ")}`, ); } return normalized as BUN_MARIADB_ForeignKeyRule; } /** * Resolve the ON DELETE / ON UPDATE actions for a field's foreign key. * Precedence: `foreignKey.onDelete` > field `onDeleteLiteral` / `onDelete` * (legacy) > `cascadeDelete` > `RESTRICT`. Same for update, except the * field-level `onUpdate*` keys are column `ON UPDATE` clauses, not FK rules. */ export function resolveForeignKeyRules( field: BUN_MARIADB_FieldSchemaType, tableName?: string, ): { onDelete: BUN_MARIADB_ForeignKeyRule; onUpdate: BUN_MARIADB_ForeignKeyRule } { const fk = field.foreignKey || {}; const onDelete = normalizeForeignKeyRule( fk.onDelete || field.onDeleteLiteral || field.onDelete || (fk.cascadeDelete ? "CASCADE" : "RESTRICT"), ); const onUpdate = normalizeForeignKeyRule( fk.onUpdate || (fk.cascadeUpdate ? "CASCADE" : "RESTRICT"), ); if ( (onDelete === "SET NULL" || onUpdate === "SET NULL") && (field.notNullValue || field.primaryKey) ) { throw new Error( `Foreign key on \`${tableName ? `${tableName}.` : ""}${field.fieldName}\` uses SET NULL but the column is NOT NULL`, ); } return { onDelete, onUpdate }; } export default function buildForeignKeyConstraint( field: BUN_MARIADB_FieldSchemaType, tableName: string, ): string { const fk = field.foreignKey!; const constraintName = resolveForeignKeyName(field, tableName); const { onDelete, onUpdate } = resolveForeignKeyRules(field, tableName); let constraint = `CONSTRAINT ${MariaDBQuoteGen(constraintName)} FOREIGN KEY (${MariaDBQuoteGen(field.fieldName!)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName!)}(${MariaDBQuoteGen(fk.destinationTableColumnName!)})`; if (onDelete !== "RESTRICT") { constraint += ` ON DELETE ${onDelete}`; } if (onUpdate !== "RESTRICT") { constraint += ` ON UPDATE ${onUpdate}`; } return constraint; }