139 lines
5.2 KiB
JavaScript
139 lines
5.2 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|