UNPKG

wise-json-db

Version:

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

259 lines (232 loc) 11.9 kB
// wise-json/collection/indexes.js // const logger = require('../logger'); // --- УДАЛЕНО: Глобальный импорт больше не нужен. /** * Управляет индексами коллекции. */ class IndexManager { /** * @param {string} [collectionName='unknown'] - Имя коллекции для логирования. * @param {object} [logger] - Экземпляр логгера. Если не передан, будет использован логгер по умолчанию. */ constructor(collectionName = 'unknown', logger) { this.collectionName = collectionName; // +++ ИЗМЕНЕНИЕ: Сохраняем переданный логгер или используем фоллбэк. this.logger = logger || require('../logger'); 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') { this.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) { this.logger.warn(`[IndexManager] Индекс по полю '${fieldName}' (type: ${existingIndex.type}) для коллекции '${this.collectionName}' уже существует — создание пропускается.`); return; } else { this.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); this.logger.log(`[IndexManager] Индекс по полю '${fieldName}' (type: ${index.type}) для коллекции '${this.collectionName}' успешно создан.`); } /** * Удаляет индекс. * @param {string} fieldName */ dropIndex(fieldName) { if (!this.indexes.has(fieldName)) { this.logger.warn(`[IndexManager] Попытка удалить несуществующий индекс по полю '${fieldName}' для коллекции '${this.collectionName}'. Операция пропущена.`); return; } this.indexes.delete(fieldName); this.indexedFields.delete(fieldName); this.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) { for (const fieldName of this.indexedFields) { const def = this.indexes.get(fieldName); if (!def) { this.logger.warn(`[IndexManager] Определение индекса для поля '${fieldName}' не найдено при перестроении в коллекции '${this.collectionName}'.`); continue; } def.data.clear(); 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)) { this.logger.warn(`[IndexManager] Нарушение уникальности при перестроении индекса '${fieldName}' в коллекции '${this.collectionName}'. Значение '${value}' уже привязано к ID '${def.data.get(value)}', новый ID '${id}' будет проигнорирован для этого значения.`); } else { def.data.set(value, id); } } } else { // standard if (!def.data.has(value)) { def.data.set(value, new Set()); } def.data.get(value).add(id); } } } } /** * Обновляет индексы после вставки. * @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) { if (def.data.has(value) && def.data.get(value) !== doc._id) { this.logger.error(`[IndexManager] КРИТИЧЕСКАЯ ОШИБКА: Дубликат значения '${value}' в уникальном индексе '${fieldName}' (коллекция '${this.collectionName}') обнаружен ПОСЛЕ вставки документа ID '${doc._id}'. Этого не должно было произойти.`); } 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) { 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]; 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) { if (def.data.has(newVal) && def.data.get(newVal) !== newDoc._id) { this.logger.error(`[IndexManager] КРИТИЧЕСКАЯ ОШИБКА: Дубликат значения '${newVal}' в уникальном индексе '${fieldName}' (коллекция '${this.collectionName}') обнаружен ПОСЛЕ обновления документа ID '${newDoc._id}'.`); } def.data.set(newVal, newDoc._id); } } else { // standard if (newVal !== undefined || newVal === null) { 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') { return null; } return def.data.get(value) || 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') { return new Set(); } return def.data.get(value) || new Set(); } /** * Очистка всех данных индексов. */ clearAllData() { for (const def of this.indexes.values()) { def.data.clear(); } } } module.exports = IndexManager;