bigbasealpha
Version:
Professional Grade Custom Database System - A sophisticated, dependency-free database with encryption, caching, indexing, and web dashboard
585 lines (475 loc) • 14.8 kB
JavaScript
import { promises as fs, existsSync } from 'fs';
import { join } from 'path';
/**
* Index Manager for BigBaseAlpha
* Handles indexing for fast data retrieval
*/
export class IndexManager {
constructor(config) {
this.config = config;
this.enabled = config.indexing !== false;
this.basePath = config.path;
this.indexes = new Map(); // Collection -> Field -> Index
this.indexPath = join(this.basePath, 'indexes');
}
async init() {
if (!this.enabled) {
return;
}
// Create indexes directory
if (!existsSync(this.indexPath)) {
await fs.mkdir(this.indexPath, { recursive: true });
}
// Load existing indexes
await this._loadIndexes();
}
/**
* Create indexes for a collection based on schema
*/
async createIndexes(collectionName, schema) {
if (!this.enabled || !schema) {
return;
}
const collectionIndexes = new Map();
// Create indexes for specified fields
for (const [fieldName, fieldConfig] of Object.entries(schema)) {
if (fieldConfig.index || fieldConfig.unique) {
const indexType = fieldConfig.unique ? 'unique' : 'standard';
const index = new FieldIndex(fieldName, indexType);
collectionIndexes.set(fieldName, index);
await this._saveIndex(collectionName, fieldName, index);
}
}
// Always create an index for _id field
const idIndex = new FieldIndex('_id', 'unique');
collectionIndexes.set('_id', idIndex);
await this._saveIndex(collectionName, '_id', idIndex);
this.indexes.set(collectionName, collectionIndexes);
}
/**
* Add document to relevant indexes
*/
async addToIndex(collectionName, document) {
if (!this.enabled) {
return;
}
const collectionIndexes = this.indexes.get(collectionName);
if (!collectionIndexes) {
return;
}
for (const [fieldName, index] of collectionIndexes) {
const fieldValue = this._getFieldValue(document, fieldName);
if (fieldValue !== undefined) {
index.add(fieldValue, document._id);
await this._saveIndex(collectionName, fieldName, index);
}
}
}
/**
* Remove document from indexes
*/
async removeFromIndex(collectionName, document) {
if (!this.enabled) {
return;
}
const collectionIndexes = this.indexes.get(collectionName);
if (!collectionIndexes) {
return;
}
for (const [fieldName, index] of collectionIndexes) {
const fieldValue = this._getFieldValue(document, fieldName);
if (fieldValue !== undefined) {
index.remove(fieldValue, document._id);
await this._saveIndex(collectionName, fieldName, index);
}
}
}
/**
* Update indexes when document is modified
*/
async updateIndex(collectionName, oldDocument, newDocument) {
if (!this.enabled) {
return;
}
// Remove old values and add new values
await this.removeFromIndex(collectionName, oldDocument);
await this.addToIndex(collectionName, newDocument);
}
/**
* Query using indexes
*/
async query(collectionName, whereClause) {
if (!this.enabled) {
return [];
}
const collectionIndexes = this.indexes.get(collectionName);
if (!collectionIndexes) {
return [];
}
// Find the best index to use
const indexQuery = this._planIndexQuery(whereClause, collectionIndexes);
if (!indexQuery) {
return [];
}
const { fieldName, operation, value } = indexQuery;
const index = collectionIndexes.get(fieldName);
if (!index) {
return [];
}
return this._executeIndexQuery(index, operation, value);
}
/**
* Get index statistics
*/
getIndexStats(collectionName) {
if (!this.enabled) {
return {};
}
const collectionIndexes = this.indexes.get(collectionName);
if (!collectionIndexes) {
return {};
}
const stats = {};
for (const [fieldName, index] of collectionIndexes) {
stats[fieldName] = {
type: index.type,
size: index.size(),
uniqueValues: index.getUniqueValueCount()
};
}
return stats;
}
/**
* Rebuild all indexes for a collection
*/
async rebuildIndexes(collectionName, documents) {
if (!this.enabled) {
return;
}
const collectionIndexes = this.indexes.get(collectionName);
if (!collectionIndexes) {
return;
}
// Clear existing indexes
for (const [fieldName, index] of collectionIndexes) {
index.clear();
}
// Rebuild with all documents
for (const document of documents) {
await this.addToIndex(collectionName, document);
}
}
/**
* Create a custom index
*/
async createCustomIndex(collectionName, fieldName, indexType = 'standard') {
if (!this.enabled) {
return;
}
let collectionIndexes = this.indexes.get(collectionName);
if (!collectionIndexes) {
collectionIndexes = new Map();
this.indexes.set(collectionName, collectionIndexes);
}
const index = new FieldIndex(fieldName, indexType);
collectionIndexes.set(fieldName, index);
await this._saveIndex(collectionName, fieldName, index);
return index;
}
/**
* Drop an index
*/
async dropIndex(collectionName, fieldName) {
if (!this.enabled) {
return;
}
const collectionIndexes = this.indexes.get(collectionName);
if (!collectionIndexes || !collectionIndexes.has(fieldName)) {
return false;
}
collectionIndexes.delete(fieldName);
// Remove index file
const indexFilePath = this._getIndexFilePath(collectionName, fieldName);
if (existsSync(indexFilePath)) {
await fs.unlink(indexFilePath);
}
return true;
}
async close() {
if (!this.enabled) {
return;
}
// Save all indexes before closing
for (const [collectionName, collectionIndexes] of this.indexes) {
for (const [fieldName, index] of collectionIndexes) {
await this._saveIndex(collectionName, fieldName, index);
}
}
this.indexes.clear();
}
// Private methods
async _loadIndexes() {
if (!existsSync(this.indexPath)) {
return;
}
try {
const collections = await fs.readdir(this.indexPath, { withFileTypes: true });
for (const collection of collections) {
if (!collection.isDirectory()) continue;
const collectionName = collection.name;
const collectionIndexes = new Map();
const collectionIndexPath = join(this.indexPath, collectionName);
const indexFiles = await fs.readdir(collectionIndexPath);
for (const indexFile of indexFiles) {
if (!indexFile.endsWith('.idx')) continue;
const fieldName = indexFile.replace('.idx', '');
const index = await this._loadIndex(collectionName, fieldName);
if (index) {
collectionIndexes.set(fieldName, index);
}
}
this.indexes.set(collectionName, collectionIndexes);
}
} catch (error) {
console.error('Error loading indexes:', error);
}
}
async _loadIndex(collectionName, fieldName) {
const indexFilePath = this._getIndexFilePath(collectionName, fieldName);
if (!existsSync(indexFilePath)) {
return null;
}
try {
const indexData = await fs.readFile(indexFilePath, 'utf8');
const parsed = JSON.parse(indexData);
const index = new FieldIndex(fieldName, parsed.type);
index.fromJSON(parsed);
return index;
} catch (error) {
console.error(`Error loading index ${collectionName}.${fieldName}:`, error);
return null;
}
}
async _saveIndex(collectionName, fieldName, index) {
const collectionIndexPath = join(this.indexPath, collectionName);
if (!existsSync(collectionIndexPath)) {
await fs.mkdir(collectionIndexPath, { recursive: true });
}
const indexFilePath = this._getIndexFilePath(collectionName, fieldName);
const indexData = JSON.stringify(index.toJSON(), null, 2);
await fs.writeFile(indexFilePath, indexData);
}
_getIndexFilePath(collectionName, fieldName) {
return join(this.indexPath, collectionName, `${fieldName}.idx`);
}
_getFieldValue(document, fieldPath) {
// Support nested field paths like 'user.profile.name'
const parts = fieldPath.split('.');
let value = document;
for (const part of parts) {
if (value && typeof value === 'object' && part in value) {
value = value[part];
} else {
return undefined;
}
}
return value;
}
_planIndexQuery(whereClause, indexes) {
// Find the best index for the query
for (const [fieldName, condition] of Object.entries(whereClause)) {
if (indexes.has(fieldName)) {
if (typeof condition === 'object' && condition !== null) {
// Handle operators
for (const [operator, value] of Object.entries(condition)) {
return {
fieldName,
operation: operator,
value
};
}
} else {
// Direct equality
return {
fieldName,
operation: '$eq',
value: condition
};
}
}
}
return null;
}
_executeIndexQuery(index, operation, value) {
switch (operation) {
case '$eq':
return index.find(value);
case '$gt':
return index.findRange(value, null, false, true);
case '$gte':
return index.findRange(value, null, true, true);
case '$lt':
return index.findRange(null, value, true, false);
case '$lte':
return index.findRange(null, value, true, true);
case '$in':
if (Array.isArray(value)) {
const results = [];
for (const v of value) {
results.push(...index.find(v));
}
return [...new Set(results)]; // Remove duplicates
}
return [];
default:
return [];
}
}
}
/**
* Field Index implementation
*/
class FieldIndex {
constructor(fieldName, type = 'standard') {
this.fieldName = fieldName;
this.type = type; // 'standard', 'unique'
this.data = new Map(); // value -> Set of document IDs
this.sortedKeys = []; // For range queries
this.sorted = false;
}
add(value, documentId) {
const key = this._normalizeValue(value);
if (this.type === 'unique' && this.data.has(key)) {
const existingIds = this.data.get(key);
if (existingIds.size > 0 && !existingIds.has(documentId)) {
throw new Error(`Unique constraint violation: duplicate value '${value}' for field '${this.fieldName}'`);
}
}
if (!this.data.has(key)) {
this.data.set(key, new Set());
this.sorted = false;
}
this.data.get(key).add(documentId);
}
remove(value, documentId) {
const key = this._normalizeValue(value);
if (this.data.has(key)) {
const ids = this.data.get(key);
ids.delete(documentId);
if (ids.size === 0) {
this.data.delete(key);
this.sorted = false;
}
}
}
find(value) {
const key = this._normalizeValue(value);
const ids = this.data.get(key);
return ids ? Array.from(ids) : [];
}
findRange(minValue, maxValue, includeMin = true, includeMax = true) {
this._ensureSorted();
const results = [];
for (const key of this.sortedKeys) {
const keyValue = this._denormalizeValue(key);
// Check min bound
if (minValue !== null) {
if (includeMin ? keyValue < minValue : keyValue <= minValue) {
continue;
}
}
// Check max bound
if (maxValue !== null) {
if (includeMax ? keyValue > maxValue : keyValue >= maxValue) {
break;
}
}
const ids = this.data.get(key);
if (ids) {
results.push(...Array.from(ids));
}
}
return results;
}
clear() {
this.data.clear();
this.sortedKeys = [];
this.sorted = false;
}
size() {
return this.data.size;
}
getUniqueValueCount() {
return this.data.size;
}
toJSON() {
const dataObj = {};
for (const [key, ids] of this.data) {
dataObj[key] = Array.from(ids);
}
return {
fieldName: this.fieldName,
type: this.type,
data: dataObj
};
}
fromJSON(json) {
this.fieldName = json.fieldName;
this.type = json.type;
this.data.clear();
for (const [key, ids] of Object.entries(json.data)) {
this.data.set(key, new Set(ids));
}
this.sorted = false;
}
_normalizeValue(value) {
if (value === null || value === undefined) {
return '__NULL__';
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return `__NUM__${value}`;
}
if (typeof value === 'boolean') {
return `__BOOL__${value}`;
}
if (value instanceof Date) {
return `__DATE__${value.toISOString()}`;
}
// For complex objects, use JSON representation
return `__OBJ__${JSON.stringify(value)}`;
}
_denormalizeValue(key) {
if (key === '__NULL__') {
return null;
}
if (key.startsWith('__NUM__')) {
return parseFloat(key.substring(7));
}
if (key.startsWith('__BOOL__')) {
return key.substring(8) === 'true';
}
if (key.startsWith('__DATE__')) {
return new Date(key.substring(8));
}
if (key.startsWith('__OBJ__')) {
return JSON.parse(key.substring(7));
}
return key;
}
_ensureSorted() {
if (!this.sorted) {
this.sortedKeys = Array.from(this.data.keys()).sort((a, b) => {
const aVal = this._denormalizeValue(a);
const bVal = this._denormalizeValue(b);
if (aVal < bVal) return -1;
if (aVal > bVal) return 1;
return 0;
});
this.sorted = true;
}
}
}
export default IndexManager;