mem100x
Version:
⚡ The FASTEST MCP memory server ever built - 66k+ entities/sec with intelligent context detection
507 lines • 21.6 kB
JavaScript
"use strict";
/**
* High-performance SQLite database implementation with better-sqlite3
* Optimized for speed with prepared statements, indexes, and WAL mode
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemoryDatabase = void 0;
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const fs_1 = require("fs");
const path_1 = require("path");
const cache_interface_js_1 = require("./utils/cache-interface.js");
const counting_bloom_filter_js_1 = require("./utils/counting-bloom-filter.js");
const compression_js_1 = require("./utils/compression.js");
const database_schema_js_1 = require("./database-schema.js");
const logger_js_1 = require("./utils/logger.js");
const fast_json_js_1 = require("./utils/fast-json.js");
const config_js_1 = require("./config.js");
class MemoryDatabase {
db;
dbPath;
// Performance optimizations
entityCache;
searchCache;
entityBloom;
compressionEnabled = config_js_1.config.performance.compressionEnabled;
relationQueryThreshold = config_js_1.config.performance.relationQueryThreshold;
// Transaction management
transactionDepth = 0;
isInTransaction = false;
// Prepared statements for maximum performance
statements = {};
constructor(dbPath) {
this.dbPath = dbPath;
// Initialize caches
this.entityCache = (0, cache_interface_js_1.createStringCache)(config_js_1.config.performance.cacheStrategy, config_js_1.config.performance.entityCacheSize);
this.searchCache = (0, cache_interface_js_1.createStringCache)(config_js_1.config.performance.cacheStrategy, config_js_1.config.performance.searchCacheSize);
// Initialize database
this.initDatabase();
// Initialize bloom filter
this.initializeBloomFilter();
}
initDatabase() {
const perf = new logger_js_1.PerformanceTracker('database_initialization', { dbPath: this.dbPath });
const dir = (0, path_1.dirname)(this.dbPath);
if (!(0, fs_1.existsSync)(dir)) {
(0, fs_1.mkdirSync)(dir, { recursive: true });
}
this.db = new better_sqlite3_1.default(this.dbPath);
(0, logger_js_1.logInfo)('Initializing database', { path: this.dbPath });
// Apply optimizations
const pragmas = (0, database_schema_js_1.getPragmas)();
for (const pragma of pragmas) {
this.db.exec(pragma);
}
// Create schema
this.db.exec((0, database_schema_js_1.getCompleteSchema)());
// Prepare statements for performance
this.prepareStatements();
perf.end({ status: 'success' });
}
prepareStatements() {
this.statements.createEntity = this.db.prepare(`
INSERT INTO entities (name, entity_type, observations)
VALUES (?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
entity_type = excluded.entity_type,
observations = excluded.observations,
updated_at = julianday('now')
`);
this.statements.getEntity = this.db.prepare('SELECT * FROM entities WHERE name = ?');
this.statements.searchEntities = this.db.prepare(`
SELECT e.* FROM entities e
JOIN entities_fts ON e.name = entities_fts.name
WHERE entities_fts MATCH ? ORDER BY rank LIMIT ?
`);
this.statements.searchEntitiesLike = this.db.prepare(`
SELECT * FROM entities
WHERE name LIKE ? OR entity_type LIKE ? OR observations LIKE ?
ORDER BY updated_at DESC LIMIT ?
`);
this.statements.createRelation = this.db.prepare(`
INSERT OR IGNORE INTO relations (from_entity, to_entity, relation_type)
VALUES (?, ?, ?)
`);
this.statements.deleteEntity = this.db.prepare('DELETE FROM entities WHERE name = ?');
this.statements.deleteRelation = this.db.prepare('DELETE FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?');
this.statements.updateObservations = this.db.prepare('UPDATE entities SET observations = ?, updated_at = julianday(\'now\') WHERE name = ?');
this.statements.getRelationsByEntity = this.db.prepare(`
SELECT * FROM relations
WHERE from_entity = ? OR to_entity = ?
`);
this.statements.getEntityStats = this.db.prepare('SELECT entity_type, COUNT(*) as count FROM entities GROUP BY entity_type');
}
initializeBloomFilter() {
const bloomPath = this.dbPath.replace('.db', '.cbloom');
const loadedFilter = counting_bloom_filter_js_1.CountingBloomFilter.loadFromFileSync(bloomPath);
if (loadedFilter) {
this.entityBloom = loadedFilter;
(0, logger_js_1.logInfo)('Counting Bloom filter loaded from disk', { path: bloomPath });
}
else {
this.entityBloom = new counting_bloom_filter_js_1.CountingBloomFilter(config_js_1.config.bloomFilter.expectedItems, config_js_1.config.bloomFilter.falsePositiveRate);
this.entityBloom.initSync();
(0, logger_js_1.logInfo)('New Counting Bloom filter created', {
expectedItems: config_js_1.config.bloomFilter.expectedItems,
falsePositiveRate: config_js_1.config.bloomFilter.falsePositiveRate
});
this.populateBloomFilter();
this.saveBloomFilter();
}
}
populateBloomFilter() {
if (!this.entityBloom)
return;
const rows = this.db.prepare('SELECT name FROM entities').all();
for (const row of rows) {
this.entityBloom.add(row.name.toLowerCase());
}
}
saveBloomFilter() {
if (!this.entityBloom)
return;
const bloomPath = this.dbPath.replace('.db', '.cbloom');
try {
this.entityBloom.saveToFileSync(bloomPath);
(0, logger_js_1.logDebug)('Counting Bloom filter saved to disk', { path: bloomPath });
}
catch (error) {
(0, logger_js_1.logError)('Failed to save counting bloom filter', error, { path: bloomPath });
}
}
// Transaction helpers
transaction(fn) {
return this.db.transaction(fn)();
}
createEntities(entities) {
const perf = new logger_js_1.PerformanceTracker('createEntities', { count: entities.length });
const created = this.transaction(() => {
const results = [];
for (const entity of entities) {
const observationsData = this.compressionEnabled
? compression_js_1.CompressionUtils.compressObservations(entity.observations)
: (0, fast_json_js_1.stringifyObservations)(entity.observations);
this.statements.createEntity.run(entity.name, entity.entityType, observationsData);
const result = {
type: 'entity',
name: entity.name,
entityType: entity.entityType,
observations: entity.observations
};
results.push(result);
// Update caches
this.entityCache.set(entity.name.toLowerCase(), result);
this.entityBloom.add(entity.name.toLowerCase());
}
return results;
});
// Clear search cache as new entities were added
this.searchCache.clear();
perf.end({ created: created.length });
return created;
}
createRelations(relations) {
const created = [];
this.transaction(() => {
for (const relation of relations) {
const result = this.statements.createRelation.run(relation.from, relation.to, relation.relationType);
if (result.changes > 0) {
created.push({
type: 'relation',
from: relation.from,
to: relation.to,
relationType: relation.relationType
});
}
}
});
return created;
}
getEntity(name) {
const lowerName = name.toLowerCase();
// Check cache first
const cached = this.entityCache.get(lowerName);
if (cached)
return cached;
// Check bloom filter
if (!this.entityBloom.contains(lowerName))
return undefined;
// Query database
const row = this.statements.getEntity.get(name);
if (!row)
return undefined;
const entity = {
type: 'entity',
name: row.name,
entityType: row.entity_type,
observations: this.compressionEnabled
? compression_js_1.CompressionUtils.decompressObservations(row.observations)
: (0, fast_json_js_1.parseObservations)(row.observations)
};
// Cache the result
this.entityCache.set(lowerName, entity);
return entity;
}
searchNodes(options) {
const { query, limit = 20 } = options;
const cacheKey = `search:${query}:${limit}`;
// Check cache
const cached = this.searchCache.get(cacheKey);
if (cached)
return cached;
// FTS search first
const ftsQuery = query.split(/\s+/)
.filter(term => term.length > 0)
.map(term => `"${term}"*`)
.join(' OR ');
let rows = this.statements.searchEntities.all(ftsQuery, limit);
// Fallback to LIKE search if no FTS results
if (rows.length === 0) {
const likePattern = `%${query}%`;
rows = this.statements.searchEntitiesLike.all(likePattern, likePattern, likePattern, limit);
}
// Convert rows to entities
const entities = rows.map(row => ({
type: 'entity',
name: row.name,
entityType: row.entity_type,
observations: this.compressionEnabled
? compression_js_1.CompressionUtils.decompressObservations(row.observations)
: (0, fast_json_js_1.parseObservations)(row.observations)
}));
// Update entity cache
entities.forEach(entity => this.entityCache.set(entity.name.toLowerCase(), entity));
// Get relations for found entities
const entityNames = entities.map(e => e.name);
const relations = entityNames.length > 0
? this.getRelationsForEntities(entityNames)
: [];
const result = { entities, relations };
this.searchCache.set(cacheKey, result);
return result;
}
readGraph(limit, offset = 0) {
const totalEntities = this.db.prepare('SELECT COUNT(*) as count FROM entities').get().count;
const totalRelations = this.db.prepare('SELECT COUNT(*) as count FROM relations').get().count;
const entityQuery = limit
? `SELECT * FROM entities ORDER BY updated_at DESC LIMIT ${limit} OFFSET ${offset}`
: 'SELECT * FROM entities ORDER BY updated_at DESC';
const entityRows = this.db.prepare(entityQuery).all();
const entities = entityRows.map(row => ({
type: 'entity',
name: row.name,
entityType: row.entity_type,
observations: this.compressionEnabled
? compression_js_1.CompressionUtils.decompressObservations(row.observations)
: (0, fast_json_js_1.parseObservations)(row.observations)
}));
const relations = entities.length > 0
? this.getRelationsForEntities(entities.map(e => e.name))
: [];
return {
entities,
relations,
pagination: limit ? {
totalEntities,
totalRelations,
offset,
limit,
hasMore: offset + entities.length < totalEntities
} : undefined
};
}
getRelationsForEntities(entityNames) {
if (entityNames.length === 0)
return [];
// Use temporary table for large queries
if (entityNames.length > this.relationQueryThreshold) {
return this.transaction(() => {
// Create temp table
this.db.exec('CREATE TEMP TABLE IF NOT EXISTS temp_entities (name TEXT PRIMARY KEY)');
const insertStmt = this.db.prepare('INSERT OR IGNORE INTO temp_entities VALUES (?)');
for (const name of entityNames) {
insertStmt.run(name);
}
// Query using temp table
const rows = this.db.prepare(`
SELECT DISTINCT r.* FROM relations r
WHERE r.from_entity IN (SELECT name FROM temp_entities)
OR r.to_entity IN (SELECT name FROM temp_entities)
`).all();
// Clean up
this.db.exec('DROP TABLE temp_entities');
return rows.map(row => ({
type: 'relation',
from: row.from_entity,
to: row.to_entity,
relationType: row.relation_type
}));
});
}
// Use IN clause for small queries
const placeholders = entityNames.map(() => '?').join(',');
const rows = this.db.prepare(`
SELECT * FROM relations
WHERE from_entity IN (${placeholders})
OR to_entity IN (${placeholders})
`).all(...entityNames, ...entityNames);
return rows.map(row => ({
type: 'relation',
from: row.from_entity,
to: row.to_entity,
relationType: row.relation_type
}));
}
addObservations(updates) {
if (updates.length === 0)
return;
this.transaction(() => {
for (const update of updates) {
const row = this.statements.getEntity.get(update.entityName);
if (row) {
const existing = this.compressionEnabled
? compression_js_1.CompressionUtils.decompressObservations(row.observations)
: (0, fast_json_js_1.parseObservations)(row.observations);
const combined = Array.from(new Set([...existing, ...update.contents]));
const observationsJson = this.compressionEnabled
? compression_js_1.CompressionUtils.compressObservations(combined)
: (0, fast_json_js_1.stringifyObservations)(combined);
this.statements.updateObservations.run(observationsJson, update.entityName);
this.entityCache.delete(update.entityName.toLowerCase());
}
}
});
this.searchCache.clear();
}
deleteEntities(entityNames) {
this.transaction(() => {
for (const name of entityNames) {
this.statements.deleteEntity.run(name);
this.entityBloom.remove(name.toLowerCase());
this.entityCache.delete(name.toLowerCase());
}
});
this.searchCache.clear();
}
deleteObservations(deletions) {
if (deletions.length === 0)
return;
this.transaction(() => {
for (const deletion of deletions) {
const row = this.statements.getEntity.get(deletion.entityName);
if (row) {
const existing = this.compressionEnabled
? compression_js_1.CompressionUtils.decompressObservations(row.observations)
: (0, fast_json_js_1.parseObservations)(row.observations);
const toRemove = new Set(deletion.observations);
const updated = existing.filter(obs => !toRemove.has(obs));
const observationsJson = this.compressionEnabled
? compression_js_1.CompressionUtils.compressObservations(updated)
: (0, fast_json_js_1.stringifyObservations)(updated);
this.statements.updateObservations.run(observationsJson, deletion.entityName);
this.entityCache.delete(deletion.entityName.toLowerCase());
}
}
});
this.searchCache.clear();
}
deleteRelations(relations) {
this.transaction(() => {
for (const relation of relations) {
this.statements.deleteRelation.run(relation.from, relation.to, relation.relationType);
}
});
}
openNodes(names) {
if (names.length === 0)
return { entities: [], relations: [] };
const entities = [];
const namesToQuery = [];
// Check cache and bloom filter
for (const name of names) {
const lowerName = name.toLowerCase();
if (!this.entityBloom.contains(lowerName))
continue;
const cached = this.entityCache.get(lowerName);
if (cached) {
entities.push(cached);
}
else {
namesToQuery.push(name);
}
}
// Query remaining entities
if (namesToQuery.length > 0) {
const placeholders = namesToQuery.map(() => '?').join(',');
const entityRows = this.db.prepare(`SELECT * FROM entities WHERE name IN (${placeholders})`).all(...namesToQuery);
for (const row of entityRows) {
const entity = {
type: 'entity',
name: row.name,
entityType: row.entity_type,
observations: this.compressionEnabled
? compression_js_1.CompressionUtils.decompressObservations(row.observations)
: (0, fast_json_js_1.parseObservations)(row.observations)
};
entities.push(entity);
this.entityCache.set(row.name.toLowerCase(), entity);
}
}
const foundNames = entities.map(e => e.name);
const relations = foundNames.length > 0
? this.getRelationsForEntities(foundNames)
: [];
return { entities, relations };
}
getStats() {
const entityCount = this.db.prepare('SELECT COUNT(*) as count FROM entities').get().count;
const relationCount = this.db.prepare('SELECT COUNT(*) as count FROM relations').get().count;
const typeRows = this.statements.getEntityStats.all();
const entityTypes = typeRows.reduce((acc, row) => {
acc[row.entity_type] = row.count;
return acc;
}, {});
const dbFileSize = (0, fs_1.statSync)(this.dbPath).size;
return {
totalEntities: entityCount,
totalRelations: relationCount,
entityTypes,
databaseSizeKb: Math.round(dbFileSize / 1024),
cacheStats: {
entity: this.entityCache.getStats(),
search: this.searchCache.getStats()
},
bloomStats: this.entityBloom.getStats()
};
}
// Utility method for tests that expect async initialization
async waitForBloomFilter() {
// Bloom filter is initialized synchronously in constructor,
// so this just returns immediately
return Promise.resolve();
}
// Transaction operations
beginTransaction() {
if (this.isInTransaction) {
throw new Error('A transaction is already active');
}
this.db.prepare('BEGIN').run();
this.isInTransaction = true;
this.transactionDepth = 1;
}
commitTransaction() {
if (!this.isInTransaction) {
throw new Error('No active transaction to commit');
}
this.db.prepare('COMMIT').run();
this.isInTransaction = false;
this.transactionDepth = 0;
// Clear caches after successful commit
this.entityCache.clear();
this.searchCache.clear();
}
rollbackTransaction() {
if (!this.isInTransaction) {
throw new Error('No active transaction to rollback');
}
this.db.prepare('ROLLBACK').run();
this.isInTransaction = false;
this.transactionDepth = 0;
// Clear caches after rollback
this.entityCache.clear();
this.searchCache.clear();
}
// Backup operations
backup(backupPath) {
// Save bloom filter before backup
this.saveBloomFilter();
// Checkpoint WAL to ensure all changes are in main database file
this.db.pragma('wal_checkpoint(TRUNCATE)');
(0, fs_1.copyFileSync)(this.dbPath, backupPath);
const bloomPath = this.dbPath.replace('.db', '.cbloom');
const backupBloomPath = backupPath.replace('.db', '.cbloom');
if ((0, fs_1.existsSync)(bloomPath)) {
(0, fs_1.copyFileSync)(bloomPath, backupBloomPath);
}
}
close() {
// Rollback any active transaction before closing
if (this.isInTransaction) {
try {
this.db.prepare('ROLLBACK').run();
}
catch (error) {
// Ignore errors during rollback on close
}
this.transactionDepth = 0;
this.isInTransaction = false;
}
this.saveBloomFilter();
this.db.close();
}
}
exports.MemoryDatabase = MemoryDatabase;
//# sourceMappingURL=database.js.map