bun-mariadb/dist/lib/schema/sync-indexes.js

132 lines
5.7 KiB
JavaScript

import isVectorField from "./is-vector-field";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
function isVectorIndexDef(index, table) {
if (index.indexType === "VECTOR")
return true;
if (table.isVector)
return true;
const firstFieldName = index.indexTableFields?.[0];
if (!firstFieldName)
return false;
const field = table.fields?.find((f) => f.fieldName === firstFieldName);
return isVectorField(field);
}
function vectorDistanceMetric(index) {
if (index.vectorDistanceMetric === "cosine")
return "cosine";
if (index.vectorDistanceMetric === "euclidean")
return "euclidean";
return "euclidean";
}
export default async function syncIndexes({ table, config, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
query: `SELECT INDEX_NAME, COLUMN_NAME, INDEX_TYPE FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY' ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
values: [...schemaCond.values, table.tableName],
config,
});
/**
* Indexes required by foreign keys / unique constraints cannot be dropped
* freely. Skip those when cleaning up schema indexes.
*/
const protectedConstraintRows = await querySchemaRows({
query: `SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_TYPE IN ('FOREIGN KEY', 'UNIQUE')`,
values: [...schemaCond.values, table.tableName],
config,
});
const protectedIndexNames = new Set(protectedConstraintRows.map((r) => r.CONSTRAINT_NAME));
// Column-level UNIQUE creates an index often named after the column
for (const field of table.fields || []) {
if (field.unique && field.fieldName) {
protectedIndexNames.add(field.fieldName);
}
}
for (const constraint of table.uniqueConstraints || []) {
if (constraint.constraintName) {
protectedIndexNames.add(constraint.constraintName);
}
}
const existingIndexesMap = new Map();
for (const row of rows) {
if (!existingIndexesMap.has(row.INDEX_NAME)) {
existingIndexesMap.set(row.INDEX_NAME, {
columns: [],
type: row.INDEX_TYPE,
});
}
existingIndexesMap.get(row.INDEX_NAME).columns.push(row.COLUMN_NAME);
}
for (const [indexName, details] of existingIndexesMap.entries()) {
if (protectedIndexNames.has(indexName)) {
continue;
}
const schemaIndex = table.indexes?.find((i) => i.indexName === indexName);
if (!schemaIndex) {
console.log(`Dropping index: ${indexName}`);
try {
await runSchemaQuery({
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
config,
});
}
catch (err) {
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
console.warn(`Skipping drop of index ${indexName}: required by a foreign key constraint`);
continue;
}
throw err;
}
}
else {
const schemaColumns = schemaIndex.indexTableFields || [];
const columnsMatch = details.columns.length === schemaColumns.length &&
details.columns.every((col, idx) => col === schemaColumns[idx]);
if (!columnsMatch) {
console.log(`Recreating changed index: ${indexName}`);
await runSchemaQuery({
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
config,
});
existingIndexesMap.delete(indexName);
}
}
}
for (const index of table.indexes || []) {
if (!index.indexName ||
!index.indexTableFields ||
index.indexTableFields.length === 0) {
continue;
}
if (!existingIndexesMap.has(index.indexName)) {
if (isVectorIndexDef(index, table)) {
console.log(`Creating Vector index: ${index.indexName}`);
const targetField = MariaDBQuoteGen(index.indexTableFields[0]);
const distanceMetric = vectorDistanceMetric(index);
await runSchemaQuery({
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
config,
});
}
else {
console.log(`Creating standard index: ${index.indexName}`);
const fields = index.indexTableFields
.map((field) => MariaDBQuoteGen(field))
.join(", ");
const typeUpper = index.indexType?.toUpperCase();
const isSpecialType = typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
const indexSuffix = !isSpecialType &&
(typeUpper === "BTREE" || typeUpper === "HASH")
? ` USING ${typeUpper}`
: "";
await runSchemaQuery({
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
config,
});
}
}
}
}