import isVectorField from "./is-vector-field"; import MariaDBQuoteGen from "./mariadb-quote-gen"; import runSchemaQuery, { querySchemaRows } from "./run-schema-query"; function columnsKey(columns) { return columns.join("\0"); } function columnsEqual(a, b) { return a.length === b.length && a.every((col, i) => col === b[i]); } function defaultConstraintName(columns) { return `unique_${columns.join("_")}`; } export function grabDesiredUniqueConstraints(table) { const desired = []; const seenColumnSets = new Set(); for (const constraint of table.uniqueConstraints || []) { const columns = (constraint.constraintTableFields || []) .map((field) => field.value) .filter((value) => Boolean(value)); if (columns.length === 0) { continue; } const key = columnsKey(columns); if (seenColumnSets.has(key)) { continue; } seenColumnSets.add(key); desired.push({ name: constraint.constraintName || defaultConstraintName(columns), columns, }); } for (const field of table.fields || []) { if (!field.fieldName || !field.unique || field.primaryKey || isVectorField(field)) { continue; } const columns = [field.fieldName]; const key = columnsKey(columns); if (seenColumnSets.has(key)) { continue; } seenColumnSets.add(key); // Matches MariaDB's default name for column-level UNIQUE desired.push({ name: field.fieldName, columns, }); } return desired; } async function grabLiveUniqueConstraints({ tableName, config, }) { const databaseName = config?.db_name || global.CONFIG?.db_name; const schemaWhere = databaseName ? "tc.TABLE_SCHEMA = ?" : "tc.TABLE_SCHEMA = DATABASE()"; const schemaValues = databaseName ? [databaseName] : []; const rows = await querySchemaRows({ query: ` SELECT tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION FROM information_schema.TABLE_CONSTRAINTS tc INNER JOIN information_schema.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME WHERE ${schemaWhere} AND tc.TABLE_NAME = ? AND tc.CONSTRAINT_TYPE = 'UNIQUE' ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION `, values: [...schemaValues, tableName], config, }); const map = new Map(); for (const row of rows) { if (!map.has(row.CONSTRAINT_NAME)) { map.set(row.CONSTRAINT_NAME, []); } map.get(row.CONSTRAINT_NAME).push(row.COLUMN_NAME); } return [...map.entries()].map(([name, columns]) => ({ name, columns })); } export default async function syncUniqueConstraints({ table, config, }) { const desired = grabDesiredUniqueConstraints(table); const live = await grabLiveUniqueConstraints({ tableName: table.tableName, config, }); const matchedLiveNames = new Set(); for (const want of desired) { const byColumns = live.find((existing) => !matchedLiveNames.has(existing.name) && columnsEqual(existing.columns, want.columns)); if (byColumns) { matchedLiveNames.add(byColumns.name); continue; } const byName = live.find((existing) => existing.name === want.name); if (byName && !matchedLiveNames.has(byName.name)) { console.log(`Recreating changed unique constraint: ${want.name} on ${table.tableName}`); await runSchemaQuery({ query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(byName.name)}`, config, }); matchedLiveNames.add(byName.name); } const fields = want.columns.map((col) => MariaDBQuoteGen(col)).join(", "); console.log(`Creating unique constraint: ${want.name} (${want.columns.join(", ")}) on ${table.tableName}`); await runSchemaQuery({ query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD CONSTRAINT ${MariaDBQuoteGen(want.name)} UNIQUE (${fields})`, config, }); } for (const existing of live) { if (matchedLiveNames.has(existing.name)) { continue; } const stillDesired = desired.some((want) => columnsEqual(want.columns, existing.columns)); if (stillDesired) { continue; } console.log(`Dropping unique constraint: ${existing.name} on ${table.tableName}`); try { await runSchemaQuery({ query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(existing.name)}`, config, }); } catch (err) { if (String(err?.message || "").includes("needed in a foreign key constraint")) { console.warn(`Skipping drop of unique constraint ${existing.name}: required by a foreign key constraint`); continue; } throw err; } } }