UNPKG

@fluxgraph/knowledge

Version:

A flexible, database-agnostic knowledge graph implementation for TypeScript

1,362 lines (1,357 loc) 52.2 kB
// src/adapters/base.ts var BaseAdapter = class { config; tablePrefix; constructor(config = {}) { this.config = config; this.tablePrefix = config.tablePrefix || ""; } getTableName(table) { return this.tablePrefix ? `${this.tablePrefix}_${table}` : table; } log(_message, ..._args) { } error(message, error) { console.error(`[KnowledgeGraph Error] ${message}`, error); } }; // src/adapters/sqlite.ts import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; import { eq, and, inArray, like } from "drizzle-orm"; // src/schema/index.ts import { sqliteTable, text, integer, index, primaryKey, real } from "drizzle-orm/sqlite-core"; var nodes = sqliteTable("kg_nodes", { id: text("id").primaryKey(), type: text("type").notNull(), label: text("label").notNull(), properties: text("properties").notNull(), // JSON string confidence: real("confidence").notNull().default(1), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), sourceSessionIds: text("source_session_ids") // JSON array of session IDs }, (table) => ({ typeIdx: index("idx_nodes_type").on(table.type), labelIdx: index("idx_nodes_label").on(table.label), createdAtIdx: index("idx_nodes_created_at").on(table.createdAt) })); var edges = sqliteTable("kg_edges", { id: text("id").primaryKey(), type: text("type").notNull(), fromNodeId: text("from_node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), toNodeId: text("to_node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), properties: text("properties").notNull().default("{}"), // JSON string confidence: real("confidence").notNull().default(1), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), sourceSessionIds: text("source_session_ids") // JSON array of session IDs }, (table) => ({ typeIdx: index("idx_edges_type").on(table.type), fromNodeIdx: index("idx_edges_from_node").on(table.fromNodeId), toNodeIdx: index("idx_edges_to_node").on(table.toNodeId), fromTypeIdx: index("idx_edges_from_type").on(table.fromNodeId, table.type), toTypeIdx: index("idx_edges_to_type").on(table.toNodeId, table.type) })); var nodeIndices = sqliteTable("kg_node_indices", { indexKey: text("index_key").notNull(), nodeId: text("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }, (table) => ({ pk: primaryKey({ columns: [table.indexKey, table.nodeId] }), keyIdx: index("idx_node_indices_key").on(table.indexKey), nodeIdx: index("idx_node_indices_node").on(table.nodeId) })); var edgeIndices = sqliteTable("kg_edge_indices", { indexKey: text("index_key").notNull(), edgeId: text("edge_id").notNull().references(() => edges.id, { onDelete: "cascade" }), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }, (table) => ({ pk: primaryKey({ columns: [table.indexKey, table.edgeId] }), keyIdx: index("idx_edge_indices_key").on(table.indexKey), edgeIdx: index("idx_edge_indices_edge").on(table.edgeId) })); var searchIndex = sqliteTable("kg_search_index", { term: text("term").notNull(), nodeId: text("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), field: text("field").notNull(), // 'label', 'property:key', etc. weight: real("weight").notNull().default(1) }, (table) => ({ pk: primaryKey({ columns: [table.term, table.nodeId, table.field] }), termIdx: index("idx_search_term").on(table.term), nodeIdx: index("idx_search_node").on(table.nodeId) })); var graphMetadata = sqliteTable("kg_graph_metadata", { key: text("key").primaryKey(), value: text("value").notNull(), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }); // src/adapters/sqlite.ts var SQLiteAdapter = class extends BaseAdapter { db = null; drizzle = null; constructor(config = {}) { super(config); } async initialize() { try { const dbPath = this.config.connection || ":memory:"; this.db = new Database(dbPath); this.drizzle = drizzle(this.db); this.db.exec("PRAGMA foreign_keys = ON"); if (this.config.autoCreate !== false) { await this.createTables(); } this.log("SQLite adapter initialized", { path: dbPath }); } catch (error) { this.error("Failed to initialize SQLite adapter", error); throw error; } } async createTables() { if (!this.db) throw new Error("Database not initialized"); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ); CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type); CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label); CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, to_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT ); CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type); CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id); CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id); CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type); CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id) ); CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key); CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL REFERENCES kg_edges(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id) ); CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key); CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field) ); CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term); CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); `); } async execute(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query); return stmt.all(...params); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async executeUpdate(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query); return stmt.run(...params); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.db) throw new Error("Database not initialized"); return new Promise((resolve, reject) => { try { if (!this.db) throw new Error("Database not initialized"); const result = this.db.transaction(async () => { const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("Transaction rollback"); } }; return await fn(tx); })(); resolve(result); } catch (error) { reject(error); } }); } // Node operations async insertNode(node) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(nodes).values(node).returning(); const insertedNode = result[0]; if (!insertedNode) throw new Error("Failed to create node"); return this.deserializeNode(insertedNode); } async updateNode(id, updates) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.update(nodes).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(nodes.id, id)).returning(); return result[0] ? this.deserializeNode(result[0]) : null; } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; const result = await this.executeUpdate(query, [id]); return result.changes > 0; } async getNode(id) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.select().from(nodes).where(eq(nodes.id, id)).limit(1); return result[0] ? this.deserializeNode(result[0]) : null; } async getNodes(ids) { if (!this.drizzle || ids.length === 0) return []; const result = await this.drizzle.select().from(nodes).where(inArray(nodes.id, ids)); return result.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { if (!this.drizzle) throw new Error("Database not initialized"); const whereConditions = Object.entries(conditions).map(([key, value]) => { const column = nodes[key]; return eq(column, value); }); const result = await this.drizzle.select().from(nodes).where(and(...whereConditions)).limit(limit).offset(offset); return result.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(edges).values(edge).returning(); const insertedEdge = result[0]; if (!insertedEdge) throw new Error("Failed to create edge"); return this.deserializeEdge(insertedEdge); } async updateEdge(id, updates) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.update(edges).set(updates).where(eq(edges.id, id)).returning(); return result[0] ? this.deserializeEdge(result[0]) : null; } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.select().from(edges).where(eq(edges.id, id)).limit(1); return result[0] ? this.deserializeEdge(result[0]) : null; } async getEdges(ids) { if (!this.drizzle || ids.length === 0) return []; const result = await this.drizzle.select().from(edges).where(inArray(edges.id, ids)); return result.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { if (!this.drizzle) throw new Error("Database not initialized"); const whereConditions = Object.entries(conditions).map(([key, value]) => { const fieldMap = { "from_node_id": "fromNodeId", "to_node_id": "toNodeId", "created_at": "createdAt", "updated_at": "updatedAt", "source_session_ids": "sourceSessionIds" }; const schemaKey = fieldMap[key] || key; const column = edges[schemaKey]; return eq(column, value); }); const result = await this.drizzle.select().from(edges).where(and(...whereConditions)).limit(limit).offset(offset); return result.map((e) => this.deserializeEdge(e)); } // Index operations async insertNodeIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(nodeIndices).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create node index"); return insertedIndex; } async deleteNodeIndex(indexKey, nodeId) { const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`; const params = nodeId ? [indexKey, nodeId] : [indexKey]; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(params); return info.changes; } async getNodeIndices(indexKey) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(nodeIndices).where(eq(nodeIndices.indexKey, indexKey)); } async insertEdgeIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(edgeIndices).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create edge index"); return insertedIndex; } async deleteEdgeIndex(indexKey, edgeId) { const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`; const params = edgeId ? [indexKey, edgeId] : [indexKey]; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(params); return info.changes; } async getEdgeIndices(indexKey) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(edgeIndices).where(eq(edgeIndices.indexKey, indexKey)); } // Search operations async insertSearchIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(searchIndex).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create search index"); return insertedIndex; } async deleteSearchIndex(nodeId) { const query = `DELETE FROM kg_search_index WHERE node_id = ?`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run([nodeId]); return info.changes; } async searchNodes(term, limit = 50) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(searchIndex).where(like(searchIndex.term, `%${term}%`)).limit(limit); } // Batch operations async batchInsertNodes(nodes2) { if (!this.drizzle || nodes2.length === 0) return []; const result = await this.drizzle.insert(nodes).values(nodes2).returning(); return result.map((n) => this.deserializeNode(n)); } async batchInsertEdges(edges2) { if (!this.drizzle || edges2.length === 0) return []; const result = await this.drizzle.insert(edges).values(edges2).returning(); return result.map((e) => this.deserializeEdge(e)); } async batchDeleteNodes(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(ids); return info.changes; } async batchDeleteEdges(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(ids); return info.changes; } // Maintenance operations async vacuum() { if (!this.db) throw new Error("Database not initialized"); this.db.exec("VACUUM"); this.log("Database vacuumed"); } async getStats() { if (!this.db) throw new Error("Database not initialized"); const nodeCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_nodes").get(); const edgeCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_edges").get(); const indexCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_node_indices").get(); return { nodeCount: nodeCount.count, edgeCount: edgeCount.count, indexCount: indexCount.count }; } async close() { if (this.db) { this.db.close(); this.db = null; this.drizzle = null; this.log("Database connection closed"); } } // Helper methods deserializeNode(node) { return { ...node, properties: JSON.parse(node.properties), sourceSessionIds: node.sourceSessionIds ? JSON.parse(node.sourceSessionIds) : void 0 }; } deserializeEdge(edge) { return { ...edge, properties: JSON.parse(edge.properties), sourceSessionIds: edge.sourceSessionIds ? JSON.parse(edge.sourceSessionIds) : void 0 }; } }; // src/adapters/d1.ts var D1Adapter = class extends BaseAdapter { db = null; constructor(config) { super(config); if (config.database) { this.db = config.database; } } setDatabase(db) { this.db = db; } async initialize() { if (!this.db) { throw new Error("D1 database not provided. Use setDatabase() or pass it in config."); } if (this.config.autoCreate !== false) { await this.createTables(); } this.log("D1 adapter initialized"); } async createTables() { if (!this.db) throw new Error("Database not initialized"); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL, to_node_id TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT, FOREIGN KEY (from_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE, FOREIGN KEY (to_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id), FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id), FOREIGN KEY (edge_id) REFERENCES kg_edges(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field), FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); `); } async execute(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query).bind(...params); const result = await stmt.all(); return result.results; } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.db) throw new Error("Database not initialized"); const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("Transaction rollback"); } }; try { return await fn(tx); } catch (error) { this.error("Transaction failed", error); throw error; } } // Node operations async insertNode(node) { const id = node.id || crypto.randomUUID(); const now = Date.now(); const query = ` INSERT INTO kg_nodes (id, type, label, properties, confidence, created_at, updated_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, node.type, node.label, JSON.stringify(node.properties || {}), node.confidence || 1, node.createdAt?.getTime() || now, node.updatedAt?.getTime() || now, node.sourceSessionIds ? JSON.stringify(node.sourceSessionIds) : null ]); return this.getNode(id); } async updateNode(id, updates) { const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.label !== void 0) { setClauses.push("label = ?"); params.push(updates.label); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } setClauses.push("updated_at = ?"); params.push(Date.now()); params.push(id); const query = `UPDATE kg_nodes SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getNode(id); } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; await this.execute(query, [id]); return true; } async getNode(id) { const query = `SELECT * FROM kg_nodes WHERE id = ? LIMIT 1`; const results = await this.execute(query, [id]); if (results.length === 0) return null; return this.deserializeNode(results[0]); } async getNodes(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_nodes WHERE id IN (${placeholders})`; const results = await this.execute(query, ids); return results.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; for (const [key, value] of Object.entries(conditions)) { whereClauses.push(`${key} = ?`); params.push(value); } params.push(limit, offset); const query = ` SELECT * FROM kg_nodes ${whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : ""} LIMIT ? OFFSET ? `; const results = await this.execute(query, params); return results.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { const id = edge.id || crypto.randomUUID(); const now = Date.now(); const query = ` INSERT INTO kg_edges (id, type, from_node_id, to_node_id, properties, confidence, created_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, edge.type, edge.fromNodeId, edge.toNodeId, JSON.stringify(edge.properties || {}), edge.confidence || 1, edge.createdAt?.getTime() || now, edge.sourceSessionIds ? JSON.stringify(edge.sourceSessionIds) : null ]); return this.getEdge(id); } async updateEdge(id, updates) { const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.fromNodeId !== void 0) { setClauses.push("from_node_id = ?"); params.push(updates.fromNodeId); } if (updates.toNodeId !== void 0) { setClauses.push("to_node_id = ?"); params.push(updates.toNodeId); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } params.push(id); const query = `UPDATE kg_edges SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getEdge(id); } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { const query = `SELECT * FROM kg_edges WHERE id = ? LIMIT 1`; const results = await this.execute(query, [id]); if (results.length === 0) return null; return this.deserializeEdge(results[0]); } async getEdges(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_edges WHERE id IN (${placeholders})`; const results = await this.execute(query, ids); return results.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; const fieldMap = { "fromNodeId": "from_node_id", "toNodeId": "to_node_id", "from_node_id": "from_node_id", "to_node_id": "to_node_id", "createdAt": "created_at", "updatedAt": "updated_at", "sourceSessionIds": "source_session_ids" }; for (const [key, value] of Object.entries(conditions)) { const dbKey = fieldMap[key] || key; whereClauses.push(`${dbKey} = ?`); params.push(value); } params.push(limit, offset); const query = ` SELECT * FROM kg_edges ${whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : ""} LIMIT ? OFFSET ? `; const results = await this.execute(query, params); return results.map((e) => this.deserializeEdge(e)); } // Index operations async insertNodeIndex(index2) { const query = ` INSERT INTO kg_node_indices (index_key, node_id, created_at) VALUES (?, ?, ?) `; await this.execute(query, [ index2.indexKey, index2.nodeId, index2.createdAt?.getTime() || Date.now() ]); return index2; } async deleteNodeIndex(indexKey, nodeId) { const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`; const params = nodeId ? [indexKey, nodeId] : [indexKey]; await this.execute(query, params); return 1; } async getNodeIndices(indexKey) { const query = `SELECT * FROM kg_node_indices WHERE index_key = ?`; const results = await this.execute(query, [indexKey]); return results.map((r) => this.deserializeNodeIndex(r)); } async insertEdgeIndex(index2) { const query = ` INSERT INTO kg_edge_indices (index_key, edge_id, created_at) VALUES (?, ?, ?) `; await this.execute(query, [ index2.indexKey, index2.edgeId, index2.createdAt?.getTime() || Date.now() ]); return index2; } async deleteEdgeIndex(indexKey, edgeId) { const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`; const params = edgeId ? [indexKey, edgeId] : [indexKey]; await this.execute(query, params); return 1; } async getEdgeIndices(indexKey) { const query = `SELECT * FROM kg_edge_indices WHERE index_key = ?`; const results = await this.execute(query, [indexKey]); return results.map((r) => this.deserializeEdgeIndex(r)); } // Search operations async insertSearchIndex(index2) { const query = ` INSERT OR REPLACE INTO kg_search_index (term, node_id, field, weight) VALUES (?, ?, ?, ?) `; await this.execute(query, [ index2.term, index2.nodeId, index2.field, index2.weight || 1 ]); return index2; } async deleteSearchIndex(nodeId) { const query = `DELETE FROM kg_search_index WHERE node_id = ?`; await this.execute(query, [nodeId]); return 1; } async searchNodes(term, limit = 50) { const query = ` SELECT * FROM kg_search_index WHERE term LIKE ? ORDER BY weight DESC LIMIT ? `; const results = await this.execute(query, [`%${term}%`, limit]); return results.map((r) => this.deserializeSearchIndex(r)); } // Batch operations async batchInsertNodes(nodes2) { const insertedNodes = []; for (const node of nodes2) { const inserted = await this.insertNode(node); insertedNodes.push(inserted); } return insertedNodes; } async batchInsertEdges(edges2) { const insertedEdges = []; for (const edge of edges2) { const inserted = await this.insertEdge(edge); insertedEdges.push(inserted); } return insertedEdges; } async batchDeleteNodes(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`; await this.execute(query, ids); return ids.length; } async batchDeleteEdges(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`; await this.execute(query, ids); return ids.length; } // Maintenance operations async vacuum() { this.log("Vacuum not needed for D1 (handled automatically)"); } async getStats() { const nodeCount = await this.execute("SELECT COUNT(*) as count FROM kg_nodes"); const edgeCount = await this.execute("SELECT COUNT(*) as count FROM kg_edges"); const indexCount = await this.execute("SELECT COUNT(*) as count FROM kg_node_indices"); return { nodeCount: nodeCount[0]?.count || 0, edgeCount: edgeCount[0]?.count || 0, indexCount: indexCount[0]?.count || 0 }; } async close() { this.db = null; this.log("D1 adapter closed"); } // Helper methods deserializeNode(row) { return { id: row.id, type: row.type, label: row.label, properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties || {}, confidence: row.confidence, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), sourceSessionIds: row.source_session_ids ? JSON.parse(row.source_session_ids) : void 0 }; } deserializeEdge(row) { return { id: row.id, type: row.type, fromNodeId: row.from_node_id, toNodeId: row.to_node_id, properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties || {}, confidence: row.confidence, createdAt: new Date(row.created_at), sourceSessionIds: row.source_session_ids ? JSON.parse(row.source_session_ids) : void 0 }; } deserializeNodeIndex(row) { return { indexKey: row.index_key, nodeId: row.node_id, createdAt: new Date(row.created_at) }; } deserializeEdgeIndex(row) { return { indexKey: row.index_key, edgeId: row.edge_id, createdAt: new Date(row.created_at) }; } deserializeSearchIndex(row) { return { term: row.term, nodeId: row.node_id, field: row.field, weight: row.weight }; } }; // src/adapters/sql-storage.ts var SqlStorageAdapter = class extends BaseAdapter { sql = null; constructor(config = {}) { super(config); } /** * Set the SqlStorage instance * Must be called before using the adapter */ setSqlStorage(sql) { this.sql = sql; } async initialize() { if (!this.sql) { throw new Error("SqlStorage not set. Call setSqlStorage() first."); } try { if (this.config.autoCreate !== false) { await this.createTables(); } this.log("SqlStorage adapter initialized"); } catch (error) { this.error("Failed to initialize SqlStorage adapter", error); throw error; } } async createTables() { if (!this.sql) throw new Error("SqlStorage not initialized"); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, to_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id) ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL REFERENCES kg_edges(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id) ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field) ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ) `); } async execute(query, params = []) { if (!this.sql) throw new Error("SqlStorage not initialized"); try { const result = this.sql.exec(query, ...params); return result.toArray(); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.sql) throw new Error("SqlStorage not initialized"); const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("SqlStorage does not support transaction rollback"); } }; try { return await fn(tx); } catch (error) { this.error("Transaction failed", error); throw error; } } // Node operations async insertNode(node) { const now = Date.now(); const id = node.id || this.generateId(); const properties = JSON.stringify(node.properties || {}); const sourceSessionIds = node.sourceSessionIds ? JSON.stringify(node.sourceSessionIds) : null; const query = ` INSERT INTO kg_nodes (id, type, label, properties, confidence, created_at, updated_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, node.type, node.label, properties, node.confidence || 1, node.createdAt?.getTime() || now, node.updatedAt?.getTime() || now, sourceSessionIds ]); const result = await this.getNode(id); if (!result) throw new Error("Failed to create node"); return result; } async updateNode(id, updates) { const current = await this.getNode(id); if (!current) return null; const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.label !== void 0) { setClauses.push("label = ?"); params.push(updates.label); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } setClauses.push("updated_at = ?"); params.push(Date.now()); params.push(id); const query = `UPDATE kg_nodes SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getNode(id); } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; const result = await this.execute(query, [id]); return Array.isArray(result) || result !== void 0; } async getNode(id) { const query = `SELECT * FROM kg_nodes WHERE id = ? LIMIT 1`; const result = await this.execute(query, [id]); if (!result || result.length === 0) return null; return this.deserializeNode(result[0]); } async getNodes(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_nodes WHERE id IN (${placeholders})`; const result = await this.execute(query, ids); return result.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; Object.entries(conditions).forEach(([key, value]) => { whereClauses.push(`${key} = ?`); params.push(value); }); params.push(limit, offset); const query = ` SELECT * FROM kg_nodes ${whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : ""} LIMIT ? OFFSET ? `; const result = await this.execute(query, params); return result.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { const now = Date.now(); const id = edge.id || this.generateId(); const properties = JSON.stringify(edge.properties || {}); const sourceSessionIds = edge.sourceSessionIds ? JSON.stringify(edge.sourceSessionIds) : null; const query = ` INSERT INTO kg_edges (id, type, from_node_id, to_node_id, properties, confidence, created_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, edge.type, edge.fromNodeId, edge.toNodeId, properties, edge.confidence || 1, edge.createdAt?.getTime() || now, sourceSessionIds ]); const result = await this.getEdge(id); if (!result) throw new Error("Failed to create edge"); return result; } async updateEdge(id, updates) { const current = await this.getEdge(id); if (!current) return null; const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.fromNodeId !== void 0) { setClauses.push("from_node_id = ?"); params.push(updates.fromNodeId); } if (updates.toNodeId !== void 0) { setClauses.push("to_node_id = ?"); params.push(updates.toNodeId); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } params.push(id); const query = `UPDATE kg_edges SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getEdge(id); } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { const query = `SELECT * FROM kg_edges WHERE id = ? LIMIT 1`; const result = await this.execute(query, [id]); if (!result || result.length === 0) return null; return this.deserializeEdge(result[0]); } async getEdges(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_edges WHERE id IN (${placeholders})`; const result = await this.execute(query, ids); return result.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; Object.entries(conditions).forEach(([key, value]) => { const dbKey = key === "fromNodeId" ? "from_node_id" : key === "toNodeId" ? "to_node_id" : key; whereClauses.push(`${dbKey} = ?`); params.push(value); }); params.push(limit, offset); const query = ` SELECT * FROM kg_edges ${whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : ""} LIMIT ? OFFSET ? `; const result = await this.execute(query, params); return result.map((e) => this.deserializeEdge(e)); } // Index operations async insertNodeIndex(index2) { const now = Date.now(); const query = ` INSERT INTO kg_node_indices (index_key, node_id, created_at) VALUES (?, ?, ?) `; await this.execute(query, [index2.indexKey, index2.nodeId, index2.createdAt?.getTime() || now]); return { ...index2, createdAt: index2.createdAt || new Date(now) }; } async deleteNodeIndex(indexKey, nodeId) { const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`; const params = nodeId ? [indexKey, nodeId] : [indexKey]; const result = await this.execute(query, params); return Array.isArray(result) ? result.length : 0; } async getNodeIndices(indexKey) { const query = `SELECT * FROM kg_node_indices WHERE index_key = ?`; const result = await this.execute(query, [indexKey]); return result.map((idx) => ({ indexKey: idx.index_key, nodeId: idx.node_id, createdAt: new Date(idx.created_at) })); } async insertEdgeIndex(index2) { const now = Date.now(); const query = ` INSERT INTO kg_edge_indices (index_key, edge_id, created_at) VALUES (?, ?, ?) `; await this.execute(query, [index2.indexKey, index2.edgeId, index2.createdAt?.getTime() || now]); return { ...index2, createdAt: index2.createdAt || new Date(now) }; } async deleteEdgeIndex(indexKey, edgeId) { const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`; const params = edgeId ? [indexKey, edgeId] : [indexKey]; const result = await this.execute(query, params); return Array.isArray(result) ? result.length : 0; } async getEdgeIndices(indexKey) { const query = `SELECT * FROM kg_edge_indices WHERE index_key = ?`; const result = await this.execute(query, [indexKey]); return result.map((idx) => ({ indexKey: idx.index_key, edgeId: idx.edge_id, createdAt: new Date(idx.created_at) })); } // Search operations async insertSearchIndex(index2) { const query = ` INSERT OR REPLACE INTO kg_search_index (term, node_id, field, weight) VALUES (?, ?, ?, ?) `; await this.execute(query, [index2.term, index2.nodeId, index2.field, index2.weight || 1]); return { ...index2, weight: index2.weight || 1 }; } async deleteSearchIndex(nodeId) { const query = `DELETE FROM kg_search_index WHERE node_id = ?`; const result = await this.execute(query, [nodeId]); return Array.isArray(result) ? result.length : 0; } async searchNodes(term, limit = 50) { const query = ` SELECT * FROM kg_search_index WHERE term LIKE ? LIMIT ? `; const result = await this.execute(query, [`%${term}%`, limit]); return result.map((idx) => ({ term: idx.term, nodeId: idx.node_id, field: idx.field, weight: idx.weight })); } // Batch operations async batchInsertNodes(nodes2) { const results = []; for (const node of nodes2) { const inserted = await this.insertNode(node); results.push(inserted); } return results; } async batchInsertEdges(edges2) { const results = []; for (const edge of edges2) { const inserted = await this.insertEdge(edge); results.push(inserted); } return results; } async batchDeleteNodes(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`; const result = await this.execute(query, ids); return Array.isArray(result) ? result.length : ids.length; } async batchDeleteEdges(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`; const result = await this.execute(query, ids); return Array.isArray(result) ? result.length : ids.length; } // Maintenance operations async vacuum() { if (!this.sql) throw new Error("SqlStorage not initialized"); try { this.sql.exec("VACUUM"); this.log("Database vacuumed"); } catch { this.log("VACUUM not supported by SqlStorage"); } } async getStats() { const nodeCountResult = await