UNPKG

wise-json-db

Version:

Blazing fast, crash-proof embedded JSON database for Node.js with batch operations, TTL, indexes, and segmented checkpointing.

282 lines (256 loc) 16.1 kB
// wise-json/collection/indexes.js const logger = require('../logger'); // Убедитесь, что logger импортирован /** * Управляет индексами коллекции. */ class IndexManager { constructor(collectionName = 'unknown') { this.collectionName = collectionName; this.indexes = new Map(); // fieldName -> { type, data, fieldName } this.indexedFields = new Set(); } /** * Создаёт индекс. * @param {string} fieldName * @param {{unique?: boolean}} [options] */ createIndex(fieldName, options = {}) { if (!fieldName || typeof fieldName !== 'string') { // Оставляем throw Error для невалидных входных данных, но логируем через logger logger.error(`[IndexManager] fieldName должен быть строкой для коллекции '${this.collectionName}', получено: ${typeof fieldName} ('${fieldName}')`); throw new Error(`IndexManager: fieldName должен быть непустой строкой`); } if (this.indexes.has(fieldName)) { // ИЗМЕНЕНИЕ ЗДЕСЬ: Вместо ошибки - предупреждение и выход const existingIndex = this.indexes.get(fieldName); const newIsUnique = options.unique === true; const existingIsUnique = existingIndex.type === 'unique'; if (newIsUnique === existingIsUnique) { logger.warn(`[IndexManager] Индекс по полю '${fieldName}' (type: ${existingIndex.type}) для коллекции '${this.collectionName}' уже существует — создание пропускается.`); return; // Просто выходим, не создавая заново и не бросая ошибку } else { // Попытка изменить тип существующего индекса (например, standard на unique) // Это более сложный случай. Текущая логика - бросить ошибку. // Можно было бы сначала удалить старый, потом создать новый, но это потенциально длительная операция. logger.error(`[IndexManager] Попытка изменить тип существующего индекса для поля '${fieldName}' в коллекции '${this.collectionName}'. Существующий: ${existingIndex.type}, Новый: ${newIsUnique ? 'unique' : 'standard'}. Удалите старый индекс перед созданием нового с другим типом.`); throw new Error(`IndexManager: индекс по полю '${fieldName}' уже существует с другим типом. Удалите его перед повторным созданием.`); } } const isUnique = options.unique === true; const index = { type: isUnique ? 'unique' : 'standard', data: isUnique ? new Map() : new Map(), // value -> ID или Set<ID> fieldName, }; this.indexes.set(fieldName, index); this.indexedFields.add(fieldName); logger.log(`[IndexManager] Индекс по полю '${fieldName}' (type: ${index.type}) для коллекции '${this.collectionName}' успешно создан.`); } /** * Удаляет индекс. * @param {string} fieldName */ dropIndex(fieldName) { if (!this.indexes.has(fieldName)) { logger.warn(`[IndexManager] Попытка удалить несуществующий индекс по полю '${fieldName}' для коллекции '${this.collectionName}'. Операция пропущена.`); return; } this.indexes.delete(fieldName); this.indexedFields.delete(fieldName); logger.log(`[IndexManager] Индекс по полю '${fieldName}' для коллекции '${this.collectionName}' успешно удален.`); } /** * Возвращает мета-информацию об индексах. * @returns {Array<{fieldName: string, type: string}>} */ getIndexesMeta() { return Array.from(this.indexes.values()).map(index => ({ fieldName: index.fieldName, type: index.type, })); } /** * Восстанавливает индексы из данных. * @param {Map<string, object>} documents */ rebuildIndexesFromData(documents) { // logger.debug(`[IndexManager] Начало перестроения индексов для коллекции '${this.collectionName}'... Полей для индексации: ${this.indexedFields.size}`); for (const fieldName of this.indexedFields) { const def = this.indexes.get(fieldName); if (!def) { // На всякий случай, если indexedFields и indexes рассинхронизированы logger.warn(`[IndexManager] Определение индекса для поля '${fieldName}' не найдено при перестроении в коллекции '${this.collectionName}'.`); continue; } def.data.clear(); // Очищаем старые данные индекса // logger.debug(`[IndexManager] Перестроение индекса '${fieldName}' (type: ${def.type}) для коллекции '${this.collectionName}'...`); let count = 0; for (const [id, doc] of documents.entries()) { // Убедимся, что документ является объектом if (typeof doc !== 'object' || doc === null) continue; const value = doc[fieldName]; if (def.type === 'unique') { if (value !== undefined && value !== null) { if (def.data.has(value)) { // Это предупреждение важно, но не должно останавливать процесс перестроения. // Оно указывает на нарушение уникальности в самих данных. logger.warn(`[IndexManager] Нарушение уникальности при перестроении индекса '${fieldName}' в коллекции '${this.collectionName}'. Значение '${value}' уже привязано к ID '${def.data.get(value)}', новый ID '${id}' будет проигнорирован для этого значения.`); } else { def.data.set(value, id); count++; } } } else { // standard // Для неуникальных индексов значение может быть любым, включая undefined/null // Ключом в Map будет само значение (undefined, null, строка, число и т.д.) if (!def.data.has(value)) { def.data.set(value, new Set()); } def.data.get(value).add(id); count++; } } // logger.debug(`[IndexManager] Индекс '${fieldName}' для коллекции '${this.collectionName}' перестроен. Записей в индексе: ${count} (реальный размер Map: ${def.data.size}).`); } // logger.log(`[IndexManager] Все индексы для коллекции '${this.collectionName}' перестроены.`); } /** * Обновляет индексы после вставки. * @param {object} doc */ afterInsert(doc) { if (typeof doc !== 'object' || doc === null) return; // Защита for (const fieldName of this.indexedFields) { const def = this.indexes.get(fieldName); if (!def) continue; const value = doc[fieldName]; if (def.type === 'unique') { if (value !== undefined && value !== null) { // Проверка на дубликат должна была быть выполнена *до* записи в WAL и применения в памяти, // но на всякий случай можно оставить проверку или просто доверять предварительной. // Если тут возникает ошибка - это серьезная проблема в логике предварительных проверок. if (def.data.has(value) && def.data.get(value) !== doc._id) { logger.error(`[IndexManager] КРИТИЧЕСКАЯ ОШИБКА: Дубликат значения '${value}' в уникальном индексе '${fieldName}' (коллекция '${this.collectionName}') обнаружен ПОСЛЕ вставки документа ID '${doc._id}'. Этого не должно было произойти.`); // Бросать ошибку здесь может быть опасно, т.к. данные уже в WAL/памяти. // Лучше залоггировать как критическую и, возможно, пометить индекс как "неконсистентный". } def.data.set(value, doc._id); } } else { // standard if (!def.data.has(value)) def.data.set(value, new Set()); def.data.get(value).add(doc._id); } } } /** * Обновляет индексы после удаления. * @param {object} doc */ afterRemove(doc) { if (typeof doc !== 'object' || doc === null) return; // Защита for (const fieldName of this.indexedFields) { const def = this.indexes.get(fieldName); if (!def) continue; const value = doc[fieldName]; if (def.type === 'unique') { if (value !== undefined && value !== null) { // Только если значение было, и оно не null/undefined if (def.data.get(value) === doc._id) { def.data.delete(value); } } } else { // standard const set = def.data.get(value); if (set) { set.delete(doc._id); if (set.size === 0) def.data.delete(value); } } } } /** * Обновляет индексы после обновления. * @param {object} oldDoc * @param {object} newDoc */ afterUpdate(oldDoc, newDoc) { if (typeof oldDoc !== 'object' || oldDoc === null || typeof newDoc !== 'object' || newDoc === null) return; // Защита for (const fieldName of this.indexedFields) { const def = this.indexes.get(fieldName); if (!def) continue; const oldVal = oldDoc[fieldName]; const newVal = newDoc[fieldName]; // Обновляем индекс только если значение поля изменилось // или если это поле является индексируемым и могло быть undefined в старом документе // и появилось в новом (или наоборот). if (oldVal !== newVal || (newDoc.hasOwnProperty(fieldName) && oldDoc[fieldName] === undefined) || (oldDoc.hasOwnProperty(fieldName) && newDoc[fieldName] === undefined)) { // Удаляем старое значение из индекса if (def.type === 'unique') { if (oldVal !== undefined && oldVal !== null) { if (def.data.get(oldVal) === oldDoc._id) { def.data.delete(oldVal); } } } else { // standard const oldSet = def.data.get(oldVal); if (oldSet) { oldSet.delete(oldDoc._id); if (oldSet.size === 0) def.data.delete(oldVal); } } // Добавляем новое значение в индекс if (def.type === 'unique') { if (newVal !== undefined && newVal !== null) { // Опять же, проверка на дубликат должна была быть до WAL. if (def.data.has(newVal) && def.data.get(newVal) !== newDoc._id) { logger.error(`[IndexManager] КРИТИЧЕСКАЯ ОШИБКА: Дубликат значения '${newVal}' в уникальном индексе '${fieldName}' (коллекция '${this.collectionName}') обнаружен ПОСЛЕ обновления документа ID '${newDoc._id}'.`); } def.data.set(newVal, newDoc._id); } } else { // standard if (newVal !== undefined || newVal === null) { // Индексируем null, но не undefined (если только он не был ключом) if (!def.data.has(newVal)) def.data.set(newVal, new Set()); def.data.get(newVal).add(newDoc._id); } } } } } /** * Поиск по индексу (уникальному). * @param {string} fieldName * @param {any} value * @returns {string|null} - ID или null */ findOneIdByIndex(fieldName, value) { const def = this.indexes.get(fieldName); if (!def || def.type !== 'unique') { // logger.debug(`[IndexManager] Попытка найти по уникальному индексу '${fieldName}', но индекс не найден или не уникален для коллекции '${this.collectionName}'.`); return null; } return def.data.get(value) || null; // Возвращает ID или undefined, которое преобразуется в null } /** * Поиск по индексу (стандартному). * @param {string} fieldName * @param {any} value * @returns {Set<string>} - множество ID (может быть пустым) */ findIdsByIndex(fieldName, value) { const def = this.indexes.get(fieldName); if (!def || def.type !== 'standard') { // logger.debug(`[IndexManager] Попытка найти по стандартному индексу '${fieldName}', но индекс не найден или не стандартный для коллекции '${this.collectionName}'.`); return new Set(); } return def.data.get(value) || new Set(); // Возвращает Set ID или пустой Set } /** * Очистка всех данных индексов. * Вызывается, например, при collection.clear(). */ clearAllData() { // logger.debug(`[IndexManager] Очистка всех данных для всех индексов коллекции '${this.collectionName}'.`); for (const def of this.indexes.values()) { def.data.clear(); } } } module.exports = IndexManager;