184 lines
6.6 KiB
JavaScript
184 lines
6.6 KiB
JavaScript
import buildForeignKeyConstraint, { resolveForeignKeyName, } from "./build-foreign-key-constraint";
|
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
|
function normalizeRule(rule) {
|
|
return String(rule || "RESTRICT").toUpperCase().replace(/_/g, " ");
|
|
}
|
|
function ruleIsCascade(rule) {
|
|
return normalizeRule(rule) === "CASCADE";
|
|
}
|
|
function fkIdentityKey(fk) {
|
|
return `${fk.column}\0${fk.refTable}\0${fk.refColumn}`;
|
|
}
|
|
function rulesMatch(live, desired) {
|
|
return (ruleIsCascade(live.deleteRule) === desired.cascadeDelete &&
|
|
ruleIsCascade(live.updateRule) === desired.cascadeUpdate);
|
|
}
|
|
export function grabDesiredForeignKeys(table) {
|
|
if (table.isVector) {
|
|
return [];
|
|
}
|
|
const desired = [];
|
|
for (const field of table.fields || []) {
|
|
const fk = field.foreignKey;
|
|
if (!field.fieldName ||
|
|
!fk?.destinationTableName ||
|
|
!fk.destinationTableColumnName) {
|
|
continue;
|
|
}
|
|
desired.push({
|
|
name: resolveForeignKeyName(field, table.tableName),
|
|
column: field.fieldName,
|
|
refTable: fk.destinationTableName,
|
|
refColumn: fk.destinationTableColumnName,
|
|
cascadeDelete: Boolean(fk.cascadeDelete),
|
|
cascadeUpdate: Boolean(fk.cascadeUpdate),
|
|
field,
|
|
});
|
|
}
|
|
return desired;
|
|
}
|
|
export async function grabLiveForeignKeys({ tableName, config, }) {
|
|
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
|
const schemaWhere = databaseName
|
|
? "rc.CONSTRAINT_SCHEMA = ?"
|
|
: "rc.CONSTRAINT_SCHEMA = DATABASE()";
|
|
const schemaValues = databaseName ? [databaseName] : [];
|
|
const rows = await querySchemaRows({
|
|
query: `
|
|
SELECT
|
|
rc.CONSTRAINT_NAME,
|
|
kcu.COLUMN_NAME,
|
|
kcu.REFERENCED_TABLE_NAME,
|
|
kcu.REFERENCED_COLUMN_NAME,
|
|
rc.DELETE_RULE,
|
|
rc.UPDATE_RULE
|
|
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
INNER JOIN information_schema.KEY_COLUMN_USAGE kcu
|
|
ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
|
AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
|
AND rc.TABLE_NAME = kcu.TABLE_NAME
|
|
WHERE ${schemaWhere}
|
|
AND rc.TABLE_NAME = ?
|
|
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
ORDER BY rc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
|
|
`,
|
|
values: [...schemaValues, tableName],
|
|
config,
|
|
});
|
|
// Single-column FKs only (schema model is one FK per field)
|
|
const byName = new Map();
|
|
for (const row of rows) {
|
|
if (byName.has(row.CONSTRAINT_NAME)) {
|
|
continue;
|
|
}
|
|
byName.set(row.CONSTRAINT_NAME, {
|
|
name: row.CONSTRAINT_NAME,
|
|
column: row.COLUMN_NAME,
|
|
refTable: row.REFERENCED_TABLE_NAME,
|
|
refColumn: row.REFERENCED_COLUMN_NAME,
|
|
deleteRule: row.DELETE_RULE,
|
|
updateRule: row.UPDATE_RULE,
|
|
});
|
|
}
|
|
return [...byName.values()];
|
|
}
|
|
export async function dropForeignKey({ tableName, constraintName, config, }) {
|
|
console.log(`Dropping foreign key: ${constraintName} on ${tableName}`);
|
|
await runSchemaQuery({
|
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} DROP FOREIGN KEY ${MariaDBQuoteGen(constraintName)}`,
|
|
config,
|
|
});
|
|
}
|
|
export async function dropForeignKeysOnColumns({ tableName, columns, config, }) {
|
|
if (columns.length === 0)
|
|
return;
|
|
const live = await grabLiveForeignKeys({ tableName, config });
|
|
const colSet = new Set(columns);
|
|
for (const fk of live) {
|
|
if (colSet.has(fk.column)) {
|
|
await dropForeignKey({
|
|
tableName,
|
|
constraintName: fk.name,
|
|
config,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Drop foreign keys that are removed from schema or need recreation
|
|
* (name/cascade/target changed). Run before index cleanup.
|
|
*/
|
|
export async function dropObsoleteForeignKeys({ table, config, }) {
|
|
const desired = grabDesiredForeignKeys(table);
|
|
const live = await grabLiveForeignKeys({
|
|
tableName: table.tableName,
|
|
config,
|
|
});
|
|
const keepNames = new Set();
|
|
const droppedNames = new Set();
|
|
const dropOnce = async (name) => {
|
|
if (droppedNames.has(name))
|
|
return;
|
|
droppedNames.add(name);
|
|
await dropForeignKey({
|
|
tableName: table.tableName,
|
|
constraintName: name,
|
|
config,
|
|
});
|
|
};
|
|
for (const want of desired) {
|
|
const byIdentity = live.find((existing) => !keepNames.has(existing.name) &&
|
|
!droppedNames.has(existing.name) &&
|
|
fkIdentityKey(existing) === fkIdentityKey(want));
|
|
if (byIdentity &&
|
|
rulesMatch(byIdentity, want) &&
|
|
byIdentity.name === want.name) {
|
|
keepNames.add(byIdentity.name);
|
|
continue;
|
|
}
|
|
// Changed relationship → drop so ensureForeignKeys can recreate
|
|
if (byIdentity) {
|
|
await dropOnce(byIdentity.name);
|
|
}
|
|
const byName = live.find((existing) => existing.name === want.name);
|
|
if (byName && byName !== byIdentity) {
|
|
await dropOnce(byName.name);
|
|
}
|
|
}
|
|
for (const existing of live) {
|
|
if (keepNames.has(existing.name) || droppedNames.has(existing.name)) {
|
|
continue;
|
|
}
|
|
// Not in schema anymore
|
|
await dropOnce(existing.name);
|
|
}
|
|
}
|
|
/**
|
|
* Ensure all desired foreign keys exist. Run after indexes/uniques.
|
|
*/
|
|
export async function ensureForeignKeys({ table, config, }) {
|
|
const desired = grabDesiredForeignKeys(table);
|
|
const live = await grabLiveForeignKeys({
|
|
tableName: table.tableName,
|
|
config,
|
|
});
|
|
for (const want of desired) {
|
|
const exists = live.some((existing) => fkIdentityKey(existing) === fkIdentityKey(want) &&
|
|
rulesMatch(existing, want) &&
|
|
existing.name === want.name);
|
|
if (exists) {
|
|
continue;
|
|
}
|
|
console.log(`Creating foreign key: ${want.name} (${want.column} → ${want.refTable}.${want.refColumn}) on ${table.tableName}`);
|
|
await runSchemaQuery({
|
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD ${buildForeignKeyConstraint(want.field, table.tableName)}`,
|
|
config,
|
|
});
|
|
}
|
|
}
|
|
export default async function syncForeignKeys({ table, config, }) {
|
|
await dropObsoleteForeignKeys({ table, config });
|
|
await ensureForeignKeys({ table, config });
|
|
}
|