From b03dc9849fe423c599b9e8b9dc08fcfba0b34e08 Mon Sep 17 00:00:00 2001 From: pronchev Date: Wed, 1 Apr 2026 13:01:55 +0300 Subject: [PATCH] feat: smart index diffing on import Compare desired indexes against existing ones before applying changes: - skip indexes that are already up to date - drop and recreate if definition changed - handle key conflicts (same key, different name) - drop obsolete indexes not present in the JSON Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 6 +++- README.md | 6 +++- import-indexes.sh | 89 +++++++++++++++++++++++++++++++++++++---------- 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1199e1b..8b18a65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,5 +34,9 @@ ## Поведение импорта - Если коллекция не существует — создаётся автоматически -- Если в коллекции уже есть индексы (кроме `_id_`) — они дропаются перед созданием новых +- Индексы сравниваются с существующими перед применением: + - Совпадает по имени и определению — пропускается + - Совпадает по имени, но определение изменилось — дропается и пересоздаётся + - Совпадает по ключу, но имя другое — старый дропается, создаётся новый с нужным именем + - Есть в БД, но отсутствует в JSON — дропается как устаревший - `--dry-run` показывает все планируемые действия без их выполнения diff --git a/README.md b/README.md index c3b9572..facb40c 100644 --- a/README.md +++ b/README.md @@ -87,5 +87,9 @@ Bash-скрипты для экспорта и импорта схемы инд ## Поведение импорта - Коллекция не существует → создаётся автоматически -- В коллекции есть индексы → дропаются перед созданием новых +- Индексы сравниваются с существующими перед применением: + - Совпадает по имени и определению → пропускается + - Совпадает по имени, но определение изменилось → дропается и пересоздаётся + - Совпадает по ключу, но имя другое → старый дропается, создаётся с новым именем + - Есть в БД, но отсутствует в JSON → дропается как устаревший - Индекс `_id_` игнорируется при экспорте и импорте diff --git a/import-indexes.sh b/import-indexes.sh index 39150a9..725f7d1 100755 --- a/import-indexes.sh +++ b/import-indexes.sh @@ -43,46 +43,99 @@ const data = JSON.parse(process.env.INDEXES_JSON); const targetDb = db.getSiblingDB(dbName); const existingCollections = new Set(targetDb.getCollectionNames()); +function normalizeKey(key) { + return JSON.stringify(key, Object.keys(key).sort()); +} + +function normalizeIndex(idx) { + const { v, ns, background, ...rest } = idx; + return JSON.stringify(rest, Object.keys(rest).sort()); +} + +function indexesEqual(a, b) { + return normalizeIndex(a) === normalizeIndex(b); +} + if (dryRun) { print("[dry-run] No changes will be made.\n"); } for (const [collName, indexes] of Object.entries(data)) { + print(`Collection: ${collName}`); + + const coll = targetDb.getCollection(collName); + if (!existingCollections.has(collName)) { if (dryRun) { - print(`[dry-run] Would create collection: ${collName}`); + print(` [dry-run] Would create collection: ${collName}`); } else { targetDb.createCollection(collName); - print(`Created collection: ${collName}`); + print(` Created collection`); } } - const collectionExists = existingCollections.has(collName); - const existingIndexes = collectionExists - ? targetDb.getCollection(collName).getIndexes().filter(idx => idx.name !== "_id_") - : []; - - if (existingIndexes.length > 0) { - if (dryRun) { - const names = existingIndexes.map(i => i.name).join(", "); - print(`[dry-run] Would drop indexes on ${collName}: ${names}`); - } else { - targetDb.getCollection(collName).dropIndexes(); - print(`Dropped existing indexes on: ${collName}`); + const existingByName = {}; + const existingByKey = {}; + if (existingCollections.has(collName)) { + for (const idx of coll.getIndexes()) { + if (idx.name !== "_id_") { + existingByName[idx.name] = idx; + existingByKey[normalizeKey(idx.key)] = idx; + } } } + const consumedExistingNames = new Set(); + for (const idx of indexes) { if (idx.name === "_id_") continue; const { key, name, ...rest } = idx; const options = { name, ...rest }; - if (dryRun) { - print(`[dry-run] Would create index on ${collName}: ${JSON.stringify({ key, options })}`); + if (existingByName[name]) { + consumedExistingNames.add(name); + if (indexesEqual(existingByName[name], idx)) { + print(` Index "${name}" is up to date, skipping`); + } else { + if (dryRun) { + print(` [dry-run] Would drop and recreate index "${name}" (definition changed)`); + } else { + coll.dropIndex(name); + coll.createIndex(key, options); + print(` Dropped and recreated index "${name}" (definition changed)`); + } + } } else { - targetDb.getCollection(collName).createIndex(key, options); - print(`Created index "${name}" on: ${collName}`); + const conflicting = existingByKey[normalizeKey(key)]; + if (conflicting) { + consumedExistingNames.add(conflicting.name); + if (dryRun) { + print(` [dry-run] Would drop index "${conflicting.name}" and create "${name}" (same key, name changed)`); + } else { + coll.dropIndex(conflicting.name); + coll.createIndex(key, options); + print(` Dropped "${conflicting.name}" and created "${name}" (same key, name changed)`); + } + } else { + if (dryRun) { + print(` [dry-run] Would create index "${name}"`); + } else { + coll.createIndex(key, options); + print(` Created index "${name}"`); + } + } + } + } + + for (const name of Object.keys(existingByName)) { + if (!consumedExistingNames.has(name)) { + if (dryRun) { + print(` [dry-run] Would drop obsolete index "${name}"`); + } else { + coll.dropIndex(name); + print(` Dropped obsolete index "${name}"`); + } } } }