102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
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;
|
|
}
|