import isVectorField from "./is-vector-field"; import MariaDBQuoteGen from "./mariadb-quote-gen"; import runSchemaQuery, { querySchemaRows } from "./run-schema-query"; import schemaCondition from "./schema-condition"; import { grabDesiredUniqueConstraints } from "./sync-unique-constraints"; function isVectorIndexDef(index, table) { if (index.indexType === "VECTOR") 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"; } /** Normalize schema index type vs information_schema.INDEX_TYPE */ function indexTypesMatch(liveType, schemaIndex, table) { const live = (liveType || "").toUpperCase(); const schemaType = schemaIndex.indexType?.toUpperCase(); if (isVectorIndexDef(schemaIndex, table)) { // MariaDB may report VECTOR indexes as BTREE or VECTOR depending on version return live === "VECTOR" || live === "BTREE" || live === ""; } if (!schemaType || schemaType === "BTREE") { // Default / unspecified → BTREE return live === "BTREE" || live === ""; } if (schemaType === "FULLTEXT") { return live === "FULLTEXT"; } if (schemaType === "SPATIAL") { return live === "SPATIAL"; } if (schemaType === "HASH") { return live === "HASH"; } return live === schemaType; } async function createIndex({ table, index, config, }) { if (!index.indexName || !index.indexTableFields || index.indexTableFields.length === 0) { return; } 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, }); return; } 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, }); } 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)); for (const constraint of grabDesiredUniqueConstraints(table)) { protectedIndexNames.add(constraint.name); } 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, }); existingIndexesMap.delete(indexName); } 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]); const typeMatch = indexTypesMatch(details.type, schemaIndex, table); if (!columnsMatch || !typeMatch) { 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)) { await createIndex({ table, index, config }); } } }