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 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 13:01:55 +03:00
parent b883cc3db3
commit b03dc9849f
3 changed files with 81 additions and 20 deletions

View File

@@ -34,5 +34,9 @@
## Поведение импорта
- Если коллекция не существует — создаётся автоматически
- Если в коллекции уже есть индексы (кроме `_id_`) — они дропаются перед созданием новых
- Индексы сравниваются с существующими перед применением:
- Совпадает по имени и определению — пропускается
- Совпадает по имени, но определение изменилось — дропается и пересоздаётся
- Совпадает по ключу, но имя другое — старый дропается, создаётся новый с нужным именем
- Есть в БД, но отсутствует в JSON — дропается как устаревший
- `--dry-run` показывает все планируемые действия без их выполнения

View File

@@ -87,5 +87,9 @@ Bash-скрипты для экспорта и импорта схемы инд
## Поведение импорта
- Коллекция не существует → создаётся автоматически
- В коллекции есть индексы → дропаются перед созданием новых
- Индексы сравниваются с существующими перед применением:
- Совпадает по имени и определению → пропускается
- Совпадает по имени, но определение изменилось → дропается и пересоздаётся
- Совпадает по ключу, но имя другое → старый дропается, создаётся с новым именем
- Есть в БД, но отсутствует в JSON → дропается как устаревший
- Индекс `_id_` игнорируется при экспорте и импорте

View File

@@ -43,34 +43,49 @@ 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_")
: [];
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;
}
}
}
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 consumedExistingNames = new Set();
for (const idx of indexes) {
if (idx.name === "_id_") continue;
@@ -78,11 +93,49 @@ for (const [collName, indexes] of Object.entries(data)) {
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 {
targetDb.getCollection(collName).createIndex(key, options);
print(`Created index "${name}" on: ${collName}`);
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 {
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}"`);
}
}
}
}