UNPKG

@fluxgraph/knowledge

Version:

A flexible, database-agnostic knowledge graph implementation for TypeScript

1 lines 104 kB
{"version":3,"sources":["../../src/adapters/base.ts","../../src/adapters/sqlite.ts","../../src/schema/index.ts","../../src/adapters/d1.ts","../../src/adapters/sql-storage.ts","../../src/adapters/index.ts"],"sourcesContent":["import type { Node, Edge, NodeIndex, EdgeIndex, SearchIndex, NewNode, NewEdge, NewNodeIndex, NewEdgeIndex, NewSearchIndex } from '../schema';\n\n/**\n * Base adapter interface for database operations\n * All adapters must implement this interface\n */\nexport interface DatabaseAdapter {\n /**\n * Initialize the database schema\n */\n initialize(): Promise<void>;\n\n /**\n * Execute a raw SQL query\n */\n execute<T = unknown>(query: string, params?: unknown[]): Promise<T[]>;\n\n /**\n * Begin a transaction\n */\n transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T>;\n\n /**\n * Node operations\n */\n insertNode(node: NewNode): Promise<Node>;\n updateNode(id: string, updates: Partial<NewNode>): Promise<Node | null>;\n deleteNode(id: string): Promise<boolean>;\n getNode(id: string): Promise<Node | null>;\n getNodes(ids: string[]): Promise<Node[]>;\n queryNodes(conditions: Record<string, unknown>, limit?: number, offset?: number): Promise<Node[]>;\n\n /**\n * Edge operations\n */\n insertEdge(edge: NewEdge): Promise<Edge>;\n updateEdge(id: string, updates: Partial<NewEdge>): Promise<Edge | null>;\n deleteEdge(id: string): Promise<boolean>;\n getEdge(id: string): Promise<Edge | null>;\n getEdges(ids: string[]): Promise<Edge[]>;\n queryEdges(conditions: Record<string, unknown>, limit?: number, offset?: number): Promise<Edge[]>;\n\n /**\n * Index operations\n */\n insertNodeIndex(index: NewNodeIndex): Promise<NodeIndex>;\n deleteNodeIndex(indexKey: string, nodeId?: string): Promise<number>;\n getNodeIndices(indexKey: string): Promise<NodeIndex[]>;\n\n insertEdgeIndex(index: NewEdgeIndex): Promise<EdgeIndex>;\n deleteEdgeIndex(indexKey: string, edgeId?: string): Promise<number>;\n getEdgeIndices(indexKey: string): Promise<EdgeIndex[]>;\n\n /**\n * Search operations\n */\n insertSearchIndex(index: NewSearchIndex): Promise<SearchIndex>;\n deleteSearchIndex(nodeId: string): Promise<number>;\n searchNodes(term: string, limit?: number): Promise<SearchIndex[]>;\n\n /**\n * Batch operations\n */\n batchInsertNodes(nodes: NewNode[]): Promise<Node[]>;\n batchInsertEdges(edges: NewEdge[]): Promise<Edge[]>;\n batchDeleteNodes(ids: string[]): Promise<number>;\n batchDeleteEdges(ids: string[]): Promise<number>;\n\n /**\n * Cleanup and maintenance\n */\n vacuum(): Promise<void>;\n getStats(): Promise<DatabaseStats>;\n close(): Promise<void>;\n}\n\n/**\n * Transaction context for atomic operations\n */\nexport interface TransactionContext {\n execute<T = unknown>(query: string, params?: unknown[]): Promise<T[]>;\n rollback(): Promise<void>;\n}\n\n/**\n * Database statistics\n */\nexport interface DatabaseStats {\n nodeCount: number;\n edgeCount: number;\n indexCount: number;\n sizeInBytes?: number;\n lastVacuum?: Date;\n}\n\n/**\n * Adapter configuration\n */\nexport interface AdapterConfig {\n /**\n * Database connection string or configuration\n */\n connection?: string | Record<string, unknown>;\n\n /**\n * Enable debug logging\n */\n debug?: boolean;\n\n /**\n * Custom table prefix\n */\n tablePrefix?: string;\n\n /**\n * Auto-create tables if they don't exist\n */\n autoCreate?: boolean;\n\n /**\n * Additional adapter-specific options\n */\n options?: Record<string, unknown>;\n}\n\n/**\n * Base adapter class with common functionality\n */\nexport abstract class BaseAdapter implements DatabaseAdapter {\n protected config: AdapterConfig;\n protected tablePrefix: string;\n\n constructor(config: AdapterConfig = {}) {\n this.config = config;\n this.tablePrefix = config.tablePrefix || '';\n }\n\n protected getTableName(table: string): string {\n return this.tablePrefix ? `${this.tablePrefix}_${table}` : table;\n }\n\n\n protected log(_message: string, ..._args: unknown[]): void {\n // Logging disabled for now to avoid console warnings\n // if (this.config.debug) {\n // console.log(`[KnowledgeGraph] ${message}`, ...args);\n // }\n }\n\n protected error(message: string, error?: unknown): void {\n // Use console.error which is allowed by linter\n console.error(`[KnowledgeGraph Error] ${message}`, error);\n }\n\n // Abstract methods that must be implemented by subclasses\n abstract initialize(): Promise<void>;\n abstract execute<T = unknown>(query: string, params?: any[]): Promise<T[]>;\n abstract transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T>;\n\n abstract insertNode(node: NewNode): Promise<Node>;\n abstract updateNode(id: string, updates: Partial<NewNode>): Promise<Node | null>;\n abstract deleteNode(id: string): Promise<boolean>;\n abstract getNode(id: string): Promise<Node | null>;\n abstract getNodes(ids: string[]): Promise<Node[]>;\n abstract queryNodes(conditions: Record<string, any>, limit?: number, offset?: number): Promise<Node[]>;\n\n abstract insertEdge(edge: NewEdge): Promise<Edge>;\n abstract updateEdge(id: string, updates: Partial<NewEdge>): Promise<Edge | null>;\n abstract deleteEdge(id: string): Promise<boolean>;\n abstract getEdge(id: string): Promise<Edge | null>;\n abstract getEdges(ids: string[]): Promise<Edge[]>;\n abstract queryEdges(conditions: Record<string, any>, limit?: number, offset?: number): Promise<Edge[]>;\n\n abstract insertNodeIndex(index: NewNodeIndex): Promise<NodeIndex>;\n abstract deleteNodeIndex(indexKey: string, nodeId?: string): Promise<number>;\n abstract getNodeIndices(indexKey: string): Promise<NodeIndex[]>;\n\n abstract insertEdgeIndex(index: NewEdgeIndex): Promise<EdgeIndex>;\n abstract deleteEdgeIndex(indexKey: string, edgeId?: string): Promise<number>;\n abstract getEdgeIndices(indexKey: string): Promise<EdgeIndex[]>;\n\n abstract insertSearchIndex(index: NewSearchIndex): Promise<SearchIndex>;\n abstract deleteSearchIndex(nodeId: string): Promise<number>;\n abstract searchNodes(term: string, limit?: number): Promise<SearchIndex[]>;\n\n abstract batchInsertNodes(nodes: NewNode[]): Promise<Node[]>;\n abstract batchInsertEdges(edges: NewEdge[]): Promise<Edge[]>;\n abstract batchDeleteNodes(ids: string[]): Promise<number>;\n abstract batchDeleteEdges(ids: string[]): Promise<number>;\n\n abstract vacuum(): Promise<void>;\n abstract getStats(): Promise<DatabaseStats>;\n abstract close(): Promise<void>;\n}\n","import Database from 'better-sqlite3';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport { eq, and, inArray, like } from 'drizzle-orm';\nimport { BaseAdapter, TransactionContext, DatabaseStats, AdapterConfig } from './base';\nimport * as schema from '../schema';\nimport type { Node, Edge, NodeIndex, EdgeIndex, SearchIndex, NewNode, NewEdge, NewNodeIndex, NewEdgeIndex, NewSearchIndex } from '../schema';\n\n/**\n * SQLite adapter implementation using better-sqlite3\n */\nexport class SQLiteAdapter extends BaseAdapter {\n private db: Database.Database | null = null;\n private drizzle: ReturnType<typeof drizzle> | null = null;\n\n constructor(config: AdapterConfig = {}) {\n super(config);\n }\n\n async initialize(): Promise<void> {\n try {\n const dbPath = (this.config.connection as string) || ':memory:';\n this.db = new Database(dbPath);\n this.drizzle = drizzle(this.db);\n\n // Enable foreign keys\n this.db.exec('PRAGMA foreign_keys = ON');\n\n if (this.config.autoCreate !== false) {\n await this.createTables();\n }\n\n this.log('SQLite adapter initialized', { path: dbPath });\n } catch (error) {\n this.error('Failed to initialize SQLite adapter', error);\n throw error;\n }\n }\n\n private async createTables(): Promise<void> {\n if (!this.db) throw new Error('Database not initialized');\n\n // Create nodes table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_nodes (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n label TEXT NOT NULL,\n properties TEXT NOT NULL DEFAULT '{}',\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n source_session_ids TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type);\n CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label);\n CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at);\n `);\n\n // Create edges table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_edges (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n from_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n to_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n properties TEXT NOT NULL DEFAULT '{}',\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n source_session_ids TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type);\n CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id);\n CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id);\n CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type);\n CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type);\n `);\n\n // Create node indices table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_node_indices (\n index_key TEXT NOT NULL,\n node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (index_key, node_id)\n );\n CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key);\n CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id);\n `);\n\n // Create edge indices table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_edge_indices (\n index_key TEXT NOT NULL,\n edge_id TEXT NOT NULL REFERENCES kg_edges(id) ON DELETE CASCADE,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (index_key, edge_id)\n );\n CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key);\n CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id);\n `);\n\n // Create search index table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_search_index (\n term TEXT NOT NULL,\n node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n field TEXT NOT NULL,\n weight REAL NOT NULL DEFAULT 1.0,\n PRIMARY KEY (term, node_id, field)\n );\n CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term);\n CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id);\n `);\n\n // Create graph metadata table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_graph_metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n );\n `);\n }\n\n async execute<T = unknown>(query: string, params: unknown[] = []): Promise<T[]> {\n if (!this.db) throw new Error('Database not initialized');\n\n try {\n const stmt = this.db.prepare(query);\n return stmt.all(...params) as T[];\n } catch (error) {\n this.error('Query execution failed', { query, params, error });\n throw error;\n }\n }\n\n async executeUpdate(query: string, params: unknown[] = []): Promise<{ changes: number }> {\n if (!this.db) throw new Error('Database not initialized');\n\n try {\n const stmt = this.db.prepare(query);\n return stmt.run(...params);\n } catch (error) {\n this.error('Query execution failed', { query, params, error });\n throw error;\n }\n }\n\n async transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T> {\n if (!this.db) throw new Error('Database not initialized');\n\n return new Promise((resolve, reject) => {\n try {\n if (!this.db) throw new Error('Database not initialized');\n const result = this.db.transaction(async () => {\n const tx: TransactionContext = {\n execute: async <U = unknown>(query: string, params: unknown[] = []): Promise<U[]> => {\n return this.execute<U>(query, params);\n },\n rollback: async () => {\n throw new Error('Transaction rollback');\n },\n };\n\n return await fn(tx);\n })();\n\n resolve(result);\n } catch (error) {\n reject(error);\n }\n });\n }\n\n // Node operations\n async insertNode(node: NewNode): Promise<Node> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.nodes).values(node).returning();\n const insertedNode = result[0];\n if (!insertedNode) throw new Error('Failed to create node');\n return this.deserializeNode(insertedNode);\n }\n\n async updateNode(id: string, updates: Partial<NewNode>): Promise<Node | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle\n .update(schema.nodes)\n .set({ ...updates, updatedAt: new Date() })\n .where(eq(schema.nodes.id, id))\n .returning();\n\n return result[0] ? this.deserializeNode(result[0]) : null;\n }\n\n async deleteNode(id: string): Promise<boolean> {\n const query = `DELETE FROM kg_nodes WHERE id = ?`;\n const result = await this.executeUpdate(query, [id]);\n return result.changes > 0;\n }\n\n async getNode(id: string): Promise<Node | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.select().from(schema.nodes).where(eq(schema.nodes.id, id)).limit(1);\n\n return result[0] ? this.deserializeNode(result[0]) : null;\n }\n\n async getNodes(ids: string[]): Promise<Node[]> {\n if (!this.drizzle || ids.length === 0) return [];\n\n const result = await this.drizzle.select().from(schema.nodes).where(inArray(schema.nodes.id, ids));\n\n return result.map((n) => this.deserializeNode(n));\n }\n\n async queryNodes(conditions: Record<string, unknown>, limit = 100, offset = 0): Promise<Node[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const whereConditions = Object.entries(conditions).map(([key, value]) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const column = (schema.nodes as any)[key];\n return eq(column, value);\n });\n\n const result = await this.drizzle\n .select()\n .from(schema.nodes)\n .where(and(...whereConditions))\n .limit(limit)\n .offset(offset);\n\n return result.map((n) => this.deserializeNode(n));\n }\n\n // Edge operations\n async insertEdge(edge: NewEdge): Promise<Edge> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.edges).values(edge).returning();\n const insertedEdge = result[0];\n if (!insertedEdge) throw new Error('Failed to create edge');\n return this.deserializeEdge(insertedEdge);\n }\n\n async updateEdge(id: string, updates: Partial<NewEdge>): Promise<Edge | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.update(schema.edges).set(updates).where(eq(schema.edges.id, id)).returning();\n\n return result[0] ? this.deserializeEdge(result[0]) : null;\n }\n\n async deleteEdge(id: string): Promise<boolean> {\n const query = `DELETE FROM kg_edges WHERE id = ?`;\n await this.execute(query, [id]);\n return true;\n }\n\n async getEdge(id: string): Promise<Edge | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.select().from(schema.edges).where(eq(schema.edges.id, id)).limit(1);\n\n return result[0] ? this.deserializeEdge(result[0]) : null;\n }\n\n async getEdges(ids: string[]): Promise<Edge[]> {\n if (!this.drizzle || ids.length === 0) return [];\n\n const result = await this.drizzle.select().from(schema.edges).where(inArray(schema.edges.id, ids));\n\n return result.map((e) => this.deserializeEdge(e));\n }\n\n async queryEdges(conditions: Record<string, unknown>, limit = 100, offset = 0): Promise<Edge[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const whereConditions = Object.entries(conditions).map(([key, value]) => {\n // Map camelCase to snake_case for schema fields (Drizzle schema uses camelCase in TypeScript)\n const fieldMap: Record<string, string> = {\n 'from_node_id': 'fromNodeId',\n 'to_node_id': 'toNodeId',\n 'created_at': 'createdAt',\n 'updated_at': 'updatedAt',\n 'source_session_ids': 'sourceSessionIds'\n };\n const schemaKey = fieldMap[key] || key;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const column = (schema.edges as any)[schemaKey];\n return eq(column, value);\n });\n\n const result = await this.drizzle\n .select()\n .from(schema.edges)\n .where(and(...whereConditions))\n .limit(limit)\n .offset(offset);\n\n return result.map((e) => this.deserializeEdge(e));\n }\n\n // Index operations\n async insertNodeIndex(index: NewNodeIndex): Promise<NodeIndex> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.nodeIndices).values(index).returning();\n const insertedIndex = result[0];\n if (!insertedIndex) throw new Error('Failed to create node index');\n return insertedIndex;\n }\n\n async deleteNodeIndex(indexKey: string, nodeId?: string): Promise<number> {\n const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`;\n const params = nodeId ? [indexKey, nodeId] : [indexKey];\n\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run(params);\n return info.changes;\n }\n\n async getNodeIndices(indexKey: string): Promise<NodeIndex[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n return await this.drizzle.select().from(schema.nodeIndices).where(eq(schema.nodeIndices.indexKey, indexKey));\n }\n\n async insertEdgeIndex(index: NewEdgeIndex): Promise<EdgeIndex> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.edgeIndices).values(index).returning();\n const insertedIndex = result[0];\n if (!insertedIndex) throw new Error('Failed to create edge index');\n return insertedIndex;\n }\n\n async deleteEdgeIndex(indexKey: string, edgeId?: string): Promise<number> {\n const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`;\n const params = edgeId ? [indexKey, edgeId] : [indexKey];\n\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run(params);\n return info.changes;\n }\n\n async getEdgeIndices(indexKey: string): Promise<EdgeIndex[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n return await this.drizzle.select().from(schema.edgeIndices).where(eq(schema.edgeIndices.indexKey, indexKey));\n }\n\n // Search operations\n async insertSearchIndex(index: NewSearchIndex): Promise<SearchIndex> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.searchIndex).values(index).returning();\n const insertedIndex = result[0];\n if (!insertedIndex) throw new Error('Failed to create search index');\n return insertedIndex;\n }\n\n async deleteSearchIndex(nodeId: string): Promise<number> {\n const query = `DELETE FROM kg_search_index WHERE node_id = ?`;\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run([nodeId]);\n return info.changes;\n }\n\n async searchNodes(term: string, limit = 50): Promise<SearchIndex[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n return await this.drizzle\n .select()\n .from(schema.searchIndex)\n .where(like(schema.searchIndex.term, `%${term}%`))\n .limit(limit);\n }\n\n // Batch operations\n async batchInsertNodes(nodes: NewNode[]): Promise<Node[]> {\n if (!this.drizzle || nodes.length === 0) return [];\n\n const result = await this.drizzle.insert(schema.nodes).values(nodes).returning();\n return result.map((n) => this.deserializeNode(n));\n }\n\n async batchInsertEdges(edges: NewEdge[]): Promise<Edge[]> {\n if (!this.drizzle || edges.length === 0) return [];\n\n const result = await this.drizzle.insert(schema.edges).values(edges).returning();\n return result.map((e) => this.deserializeEdge(e));\n }\n\n async batchDeleteNodes(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n\n const placeholders = ids.map(() => '?').join(',');\n const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`;\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run(ids);\n return info.changes;\n }\n\n async batchDeleteEdges(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n\n const placeholders = ids.map(() => '?').join(',');\n const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`;\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run(ids);\n return info.changes;\n }\n\n // Maintenance operations\n async vacuum(): Promise<void> {\n if (!this.db) throw new Error('Database not initialized');\n\n this.db.exec('VACUUM');\n this.log('Database vacuumed');\n }\n\n async getStats(): Promise<DatabaseStats> {\n if (!this.db) throw new Error('Database not initialized');\n\n const nodeCount = this.db.prepare('SELECT COUNT(*) as count FROM kg_nodes').get() as {count: number};\n const edgeCount = this.db.prepare('SELECT COUNT(*) as count FROM kg_edges').get() as {count: number};\n const indexCount = this.db.prepare('SELECT COUNT(*) as count FROM kg_node_indices').get() as {count: number};\n\n return {\n nodeCount: nodeCount.count,\n edgeCount: edgeCount.count,\n indexCount: indexCount.count,\n };\n }\n\n async close(): Promise<void> {\n if (this.db) {\n this.db.close();\n this.db = null;\n this.drizzle = null;\n this.log('Database connection closed');\n }\n }\n\n // Helper methods\n private deserializeNode(node: schema.Node): Node {\n return {\n ...node,\n properties: JSON.parse(node.properties),\n sourceSessionIds: node.sourceSessionIds ? JSON.parse(node.sourceSessionIds) : undefined,\n };\n }\n\n private deserializeEdge(edge: schema.Edge): Edge {\n return {\n ...edge,\n properties: JSON.parse(edge.properties),\n sourceSessionIds: edge.sourceSessionIds ? JSON.parse(edge.sourceSessionIds) : undefined,\n };\n }\n}\n","import { sqliteTable, text, integer, index, primaryKey, real } from 'drizzle-orm/sqlite-core';\nimport { InferSelectModel, InferInsertModel } from 'drizzle-orm';\n\n/**\n * Nodes table - stores all graph nodes\n */\nexport const nodes = sqliteTable('kg_nodes', {\n id: text('id').primaryKey(),\n type: text('type').notNull(),\n label: text('label').notNull(),\n properties: text('properties').notNull(), // JSON string\n confidence: real('confidence').notNull().default(1.0),\n createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),\n updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),\n sourceSessionIds: text('source_session_ids'), // JSON array of session IDs\n}, (table) => ({\n typeIdx: index('idx_nodes_type').on(table.type),\n labelIdx: index('idx_nodes_label').on(table.label),\n createdAtIdx: index('idx_nodes_created_at').on(table.createdAt),\n}));\n\n/**\n * Edges table - stores relationships between nodes\n */\nexport const edges = sqliteTable('kg_edges', {\n id: text('id').primaryKey(),\n type: text('type').notNull(),\n fromNodeId: text('from_node_id').notNull().references(() => nodes.id, { onDelete: 'cascade' }),\n toNodeId: text('to_node_id').notNull().references(() => nodes.id, { onDelete: 'cascade' }),\n properties: text('properties').notNull().default('{}'), // JSON string\n confidence: real('confidence').notNull().default(1.0),\n createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),\n sourceSessionIds: text('source_session_ids'), // JSON array of session IDs\n}, (table) => ({\n typeIdx: index('idx_edges_type').on(table.type),\n fromNodeIdx: index('idx_edges_from_node').on(table.fromNodeId),\n toNodeIdx: index('idx_edges_to_node').on(table.toNodeId),\n fromTypeIdx: index('idx_edges_from_type').on(table.fromNodeId, table.type),\n toTypeIdx: index('idx_edges_to_type').on(table.toNodeId, table.type),\n}));\n\n/**\n * Node indices table - for efficient node lookups\n */\nexport const nodeIndices = sqliteTable('kg_node_indices', {\n indexKey: text('index_key').notNull(),\n nodeId: text('node_id').notNull().references(() => nodes.id, { onDelete: 'cascade' }),\n createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),\n}, (table) => ({\n pk: primaryKey({ columns: [table.indexKey, table.nodeId] }),\n keyIdx: index('idx_node_indices_key').on(table.indexKey),\n nodeIdx: index('idx_node_indices_node').on(table.nodeId),\n}));\n\n/**\n * Edge indices table - for efficient edge lookups\n */\nexport const edgeIndices = sqliteTable('kg_edge_indices', {\n indexKey: text('index_key').notNull(),\n edgeId: text('edge_id').notNull().references(() => edges.id, { onDelete: 'cascade' }),\n createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),\n}, (table) => ({\n pk: primaryKey({ columns: [table.indexKey, table.edgeId] }),\n keyIdx: index('idx_edge_indices_key').on(table.indexKey),\n edgeIdx: index('idx_edge_indices_edge').on(table.edgeId),\n}));\n\n/**\n * Search index table - for full-text search capabilities\n */\nexport const searchIndex = sqliteTable('kg_search_index', {\n term: text('term').notNull(),\n nodeId: text('node_id').notNull().references(() => nodes.id, { onDelete: 'cascade' }),\n field: text('field').notNull(), // 'label', 'property:key', etc.\n weight: real('weight').notNull().default(1.0),\n}, (table) => ({\n pk: primaryKey({ columns: [table.term, table.nodeId, table.field] }),\n termIdx: index('idx_search_term').on(table.term),\n nodeIdx: index('idx_search_node').on(table.nodeId),\n}));\n\n/**\n * Graph metadata table - stores graph-level information\n */\nexport const graphMetadata = sqliteTable('kg_graph_metadata', {\n key: text('key').primaryKey(),\n value: text('value').notNull(),\n updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),\n});\n\n// Type exports\nexport type Node = InferSelectModel<typeof nodes>;\nexport type NewNode = InferInsertModel<typeof nodes>;\nexport type Edge = InferSelectModel<typeof edges>;\nexport type NewEdge = InferInsertModel<typeof edges>;\nexport type NodeIndex = InferSelectModel<typeof nodeIndices>;\nexport type NewNodeIndex = InferInsertModel<typeof nodeIndices>;\nexport type EdgeIndex = InferSelectModel<typeof edgeIndices>;\nexport type NewEdgeIndex = InferInsertModel<typeof edgeIndices>;\nexport type SearchIndex = InferSelectModel<typeof searchIndex>;\nexport type NewSearchIndex = InferInsertModel<typeof searchIndex>;\nexport type GraphMetadata = InferSelectModel<typeof graphMetadata>;\nexport type NewGraphMetadata = InferInsertModel<typeof graphMetadata>;","import type { D1Database } from '@cloudflare/workers-types';\nimport { BaseAdapter, TransactionContext, DatabaseStats, AdapterConfig } from './base';\nimport type { \n Node, Edge, NodeIndex, EdgeIndex, SearchIndex,\n NewNode, NewEdge, NewNodeIndex, NewEdgeIndex, NewSearchIndex\n} from '../schema';\n\n/**\n * Cloudflare D1 adapter implementation\n * Uses raw SQL queries for compatibility with D1 in Durable Objects\n */\nexport class D1Adapter extends BaseAdapter {\n private db: D1Database | null = null;\n \n constructor(config: AdapterConfig & { database?: D1Database }) {\n super(config);\n if (config.database) {\n this.db = config.database;\n }\n }\n \n setDatabase(db: D1Database): void {\n this.db = db;\n }\n \n async initialize(): Promise<void> {\n if (!this.db) {\n throw new Error('D1 database not provided. Use setDatabase() or pass it in config.');\n }\n \n if (this.config.autoCreate !== false) {\n await this.createTables();\n }\n \n this.log('D1 adapter initialized');\n }\n \n private async createTables(): Promise<void> {\n if (!this.db) throw new Error('Database not initialized');\n \n // Create nodes table\n await this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_nodes (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n label TEXT NOT NULL,\n properties TEXT NOT NULL DEFAULT '{}',\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n source_session_ids TEXT\n );\n `);\n \n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at);`);\n \n // Create edges table\n await this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_edges (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n from_node_id TEXT NOT NULL,\n to_node_id TEXT NOT NULL,\n properties TEXT NOT NULL DEFAULT '{}',\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n source_session_ids TEXT,\n FOREIGN KEY (from_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE,\n FOREIGN KEY (to_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE\n );\n `);\n \n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type);`);\n \n // Create indices tables\n await this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_node_indices (\n index_key TEXT NOT NULL,\n node_id TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (index_key, node_id),\n FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE\n );\n `);\n \n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id);`);\n \n await this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_edge_indices (\n index_key TEXT NOT NULL,\n edge_id TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (index_key, edge_id),\n FOREIGN KEY (edge_id) REFERENCES kg_edges(id) ON DELETE CASCADE\n );\n `);\n \n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id);`);\n \n // Create search index table\n await this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_search_index (\n term TEXT NOT NULL,\n node_id TEXT NOT NULL,\n field TEXT NOT NULL,\n weight REAL NOT NULL DEFAULT 1.0,\n PRIMARY KEY (term, node_id, field),\n FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE\n );\n `);\n \n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term);`);\n await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id);`);\n \n // Create metadata table\n await this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_graph_metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n );\n `);\n }\n \n async execute<T = unknown>(query: string, params: unknown[] = []): Promise<T[]> {\n if (!this.db) throw new Error('Database not initialized');\n \n try {\n const stmt = this.db.prepare(query).bind(...params);\n const result = await stmt.all();\n return result.results as T[];\n } catch (error) {\n this.error('Query execution failed', { query, params, error });\n throw error;\n }\n }\n \n async transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T> {\n if (!this.db) throw new Error('Database not initialized');\n \n // D1 doesn't support explicit transactions in the same way\n // We'll simulate it with a try-catch and manual rollback if needed\n const tx: TransactionContext = {\n execute: async <U = unknown>(query: string, params: unknown[] = []): Promise<U[]> => {\n return this.execute<U>(query, params);\n },\n rollback: async () => {\n throw new Error('Transaction rollback');\n }\n };\n \n try {\n return await fn(tx);\n } catch (error) {\n this.error('Transaction failed', error);\n throw error;\n }\n }\n \n // Node operations\n async insertNode(node: NewNode): Promise<Node> {\n const id = node.id || crypto.randomUUID();\n const now = Date.now();\n \n const query = `\n INSERT INTO kg_nodes (id, type, label, properties, confidence, created_at, updated_at, source_session_ids)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `;\n \n await this.execute(query, [\n id,\n node.type,\n node.label,\n JSON.stringify(node.properties || {}),\n node.confidence || 1.0,\n node.createdAt?.getTime() || now,\n node.updatedAt?.getTime() || now,\n node.sourceSessionIds ? JSON.stringify(node.sourceSessionIds) : null\n ]);\n \n return this.getNode(id) as Promise<Node>;\n }\n \n async updateNode(id: string, updates: Partial<NewNode>): Promise<Node | null> {\n const setClauses: string[] = [];\n const params: unknown[] = [];\n \n if (updates.type !== undefined) {\n setClauses.push('type = ?');\n params.push(updates.type);\n }\n if (updates.label !== undefined) {\n setClauses.push('label = ?');\n params.push(updates.label);\n }\n if (updates.properties !== undefined) {\n setClauses.push('properties = ?');\n params.push(JSON.stringify(updates.properties));\n }\n if (updates.confidence !== undefined) {\n setClauses.push('confidence = ?');\n params.push(updates.confidence);\n }\n if (updates.sourceSessionIds !== undefined) {\n setClauses.push('source_session_ids = ?');\n params.push(JSON.stringify(updates.sourceSessionIds));\n }\n \n setClauses.push('updated_at = ?');\n params.push(Date.now());\n \n params.push(id);\n \n const query = `UPDATE kg_nodes SET ${setClauses.join(', ')} WHERE id = ?`;\n await this.execute(query, params);\n \n return this.getNode(id);\n }\n \n async deleteNode(id: string): Promise<boolean> {\n const query = `DELETE FROM kg_nodes WHERE id = ?`;\n await this.execute(query, [id]);\n return true;\n }\n \n async getNode(id: string): Promise<Node | null> {\n const query = `SELECT * FROM kg_nodes WHERE id = ? LIMIT 1`;\n const results = await this.execute<Record<string, unknown>>(query, [id]);\n \n if (results.length === 0) return null;\n \n return this.deserializeNode(results[0]!);\n }\n \n async getNodes(ids: string[]): Promise<Node[]> {\n if (ids.length === 0) return [];\n \n const placeholders = ids.map(() => '?').join(',');\n const query = `SELECT * FROM kg_nodes WHERE id IN (${placeholders})`;\n const results = await this.execute<Record<string, unknown>>(query, ids);\n \n return results.map(n => this.deserializeNode(n));\n }\n \n async queryNodes(conditions: Record<string, unknown>, limit = 100, offset = 0): Promise<Node[]> {\n const whereClauses: string[] = [];\n const params: unknown[] = [];\n \n for (const [key, value] of Object.entries(conditions)) {\n whereClauses.push(`${key} = ?`);\n params.push(value);\n }\n \n params.push(limit, offset);\n \n const query = `\n SELECT * FROM kg_nodes \n ${whereClauses.length > 0 ? 'WHERE ' + whereClauses.join(' AND ') : ''}\n LIMIT ? OFFSET ?\n `;\n \n const results = await this.execute<Record<string, unknown>>(query, params);\n return results.map(n => this.deserializeNode(n));\n }\n \n // Edge operations\n async insertEdge(edge: NewEdge): Promise<Edge> {\n const id = edge.id || crypto.randomUUID();\n const now = Date.now();\n \n const query = `\n INSERT INTO kg_edges (id, type, from_node_id, to_node_id, properties, confidence, created_at, source_session_ids)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `;\n \n await this.execute(query, [\n id,\n edge.type,\n edge.fromNodeId,\n edge.toNodeId,\n JSON.stringify(edge.properties || {}),\n edge.confidence || 1.0,\n edge.createdAt?.getTime() || now,\n edge.sourceSessionIds ? JSON.stringify(edge.sourceSessionIds) : null\n ]);\n \n return this.getEdge(id) as Promise<Edge>;\n }\n \n async updateEdge(id: string, updates: Partial<NewEdge>): Promise<Edge | null> {\n const setClauses: string[] = [];\n const params: unknown[] = [];\n \n if (updates.type !== undefined) {\n setClauses.push('type = ?');\n params.push(updates.type);\n }\n if (updates.fromNodeId !== undefined) {\n setClauses.push('from_node_id = ?');\n params.push(updates.fromNodeId);\n }\n if (updates.toNodeId !== undefined) {\n setClauses.push('to_node_id = ?');\n params.push(updates.toNodeId);\n }\n if (updates.properties !== undefined) {\n setClauses.push('properties = ?');\n params.push(JSON.stringify(updates.properties));\n }\n if (updates.confidence !== undefined) {\n setClauses.push('confidence = ?');\n params.push(updates.confidence);\n }\n if (updates.sourceSessionIds !== undefined) {\n setClauses.push('source_session_ids = ?');\n params.push(JSON.stringify(updates.sourceSessionIds));\n }\n \n params.push(id);\n \n const query = `UPDATE kg_edges SET ${setClauses.join(', ')} WHERE id = ?`;\n await this.execute(query, params);\n \n return this.getEdge(id);\n }\n \n async deleteEdge(id: string): Promise<boolean> {\n const query = `DELETE FROM kg_edges WHERE id = ?`;\n await this.execute(query, [id]);\n return true;\n }\n \n async getEdge(id: string): Promise<Edge | null> {\n const query = `SELECT * FROM kg_edges WHERE id = ? LIMIT 1`;\n const results = await this.execute<Record<string, unknown>>(query, [id]);\n \n if (results.length === 0) return null;\n \n return this.deserializeEdge(results[0]!);\n }\n \n async getEdges(ids: string[]): Promise<Edge[]> {\n if (ids.length === 0) return [];\n \n const placeholders = ids.map(() => '?').join(',');\n const query = `SELECT * FROM kg_edges WHERE id IN (${placeholders})`;\n const results = await this.execute<Record<string, unknown>>(query, ids);\n \n return results.map(e => this.deserializeEdge(e));\n }\n \n async queryEdges(conditions: Record<string, unknown>, limit = 100, offset = 0): Promise<Edge[]> {\n const whereClauses: string[] = [];\n const params: unknown[] = [];\n \n // Map camelCase to snake_case for query conditions\n const fieldMap: Record<string, string> = {\n 'fromNodeId': 'from_node_id',\n 'toNodeId': 'to_node_id',\n 'from_node_id': 'from_node_id',\n 'to_node_id': 'to_node_id',\n 'createdAt': 'created_at',\n 'updatedAt': 'updated_at',\n 'sourceSessionIds': 'source_session_ids'\n };\n \n for (const [key, value] of Object.entries(conditions)) {\n const dbKey = fieldMap[key] || key;\n whereClauses.push(`${dbKey} = ?`);\n params.push(value);\n }\n \n params.push(limit, offset);\n \n const query = `\n SELECT * FROM kg_edges \n ${whereClauses.length > 0 ? 'WHERE ' + whereClauses.join(' AND ') : ''}\n LIMIT ? OFFSET ?\n `;\n \n const results = await this.execute<Record<string, unknown>>(query, params);\n return results.map(e => this.deserializeEdge(e));\n }\n \n // Index operations\n async insertNodeIndex(index: NewNodeIndex): Promise<NodeIndex> {\n const query = `\n INSERT INTO kg_node_indices (index_key, node_id, created_at)\n VALUES (?, ?, ?)\n `;\n \n await this.execute(query, [\n index.indexKey,\n index.nodeId,\n index.createdAt?.getTime() || Date.now()\n ]);\n \n return index as NodeIndex;\n }\n \n async deleteNodeIndex(indexKey: string, nodeId?: string): Promise<number> {\n const query = nodeId\n ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?`\n : `DELETE FROM kg_node_indices WHERE index_key = ?`;\n const params = nodeId ? [indexKey, nodeId] : [indexKey];\n \n await this.execute(query, params);\n return 1; // D1 doesn't provide change count easily\n }\n \n async getNodeIndices(indexKey: string): Promise<NodeIndex[]> {\n const query = `SELECT * FROM kg_node_indices WHERE index_key = ?`;\n const results = await this.execute<Record<string, unknown>>(query, [indexKey]);\n return results.map(r => this.deserializeNodeIndex(r));\n }\n \n async insertEdgeIndex(index: NewEdgeIndex): Promise<EdgeIndex> {\n const query = `\n INSERT INTO kg_edge_indices (index_key, edge_id, created_at)\n VALUES (?, ?, ?)\n `;\n \n await this.execute(query, [\n index.indexKey,\n index.edgeId,\n index.createdAt?.getTime() || Date.now()\n ]);\n \n return index as EdgeIndex;\n }\n \n async deleteEdgeIndex(indexKey: string, edgeId?: string): Promise<number> {\n const query = edgeId\n ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?`\n : `DELETE FROM kg_edge_indices WHERE index_key = ?`;\n const params = edgeId ? [indexKey, edgeId] : [indexKey];\n \n await this.execute(query, params);\n return 1;\n }\n \n async getEdgeIndices(indexKey: string): Promise<EdgeIndex[]> {\n const query = `SELECT * FROM kg_edge_indices WHERE index_key = ?`;\n const results = await this.execute<Record<string, unknown>>(query, [indexKey]);\n return results.map(r => this.deserializeEdgeIndex(r));\n }\n \n // Search operations\n async insertSearchIndex(index: NewSearchIndex): Promise<SearchIndex> {\n const query = `\n INSERT OR REPLACE INTO kg_search_index (term, node_id, field, weight)\n VALUES (?, ?, ?, ?)\n `;\n \n await this.execute(query, [\n index.term,\n index.nodeId,\n index.field,\n index.weight || 1.0\n ]);\n \n return index as SearchIndex;\n }\n \n async deleteSearchIndex(nodeId: string): Promise<number> {\n const query = `DELETE FROM kg_search_index WHERE node_id = ?`;\n await this.execute(query, [nodeId]);\n return 1;\n }\n \n async searchNodes(term: string, limit = 50): Promise<SearchIndex[]> {\n const query = `\n SELECT * FROM kg_search_index \n WHERE term LIKE ? \n ORDER BY weight DESC \n LIMIT ?\n `;\n \n const results = await this.execute<Record<string, unknown>>(query, [`%${term}%`, limit]);\n return results.map(r => this.deserializeSearchIndex(r));\n }\n \n // Batch operations\n async batchInsertNodes(nodes: NewNode[]): Promise<Node[]> {\n const insertedNodes: Node[] = [];\n \n for (const node of nodes) {\n const inserted = await this.insertNode(node);\n insertedNodes.push(inserted);\n }\n \n return insertedNodes;\n }\n \n async batchInsertEdges(edges: NewEdge[]): Promise<Edge[]> {\n const insertedEdges: Edge[] = [];\n \n for (const edge of edges) {\n const inserted = await this.insertEdge(edge);\n insertedEdges.push(inserted);\n }\n \n return insertedEdges;\n }\n \n async batchDeleteNodes(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n \n const placeholders = ids.map(() => '?').join(',');\n const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`;\n await this.execute(query, ids);\n return ids.length;\n }\n \n async batchDeleteEdges(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n \n const placeholders = ids.map(() => '?').join(',');\n const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`;\n await this.execute(query, ids);\n return ids.length;\n }\n \n // Maintenance operations\n async vacuum(): Promise<void> {\n // D1 handles vacuum automatically\n this.log('Vacuum not needed for D1 (handled automatically)');\n }\n \n async getStats(): Promise<DatabaseStats> {\n const nodeCount = await this.execute<{count: number}>('SELECT COUNT(*) as count FROM kg_nodes');\n const edgeCount = await this.execute<{count: number}>('SELECT COUNT(*) as count FROM kg_edges');\n const indexCount = await this.execute<{count: number}>('SELECT COUNT(*) as count FROM kg_node_indices');\n \n return {\n nodeCount: nodeCount[0]?.count || 0,\n edgeCount: edgeCount[0]?.count || 0,\n indexCount: indexCount[0]?.count || 0,\n };\n }\n \n async close(): Promise<void> {\n // D1 doesn't need explicit close\n this.db = null;\n this.log('D1 adapter closed');\n }\n \n // Helper methods\n private deserializeNode(row: Record<string, unknown>): Node {\n return {\n id: row.id as string,\n type: row.type as string,\n label: row.label as string,\n properties: typeof row.properties === 'string' ? JSON.parse(row.properties) : row.properties || {},\n confidence: row.confidence as number,\n createdAt: new Date(row.created_at as string | number),\n updatedAt: new Date(row.updated_at as string | number),\n sourceSessionIds: row.source_session_ids ? JSON.parse(row.source_session_ids as string) : undefined,\n };\n }\n \n private deserializeEdge(row: Record<string, unknown>): Edge {\n return {\n id: row.id as string,\n type: row.type as string,\n fromNodeId: row.from_node_id as string,\n toNodeId: row.to_node_id as string,\n properties: typeof row.properties === 'string' ? JSON.parse(row.properties) : row.properties || {},\n confidence: row.confidence as number,\n createdAt: new Date(row.created_at as string | number),\n sourceSessionIds: row.source_session_ids ? JSON.parse(row.source_session_ids as string) : undefined,\n };\n }\n \n private deserializeNodeIndex(row: Record<string, unknown>): NodeIndex {\n return {\n indexKey: row.index_key as string,\n nodeId: row.node_id as string,\n createdAt: new Date(row.created_at as string | number),\n };\n }\n \n private deserializeEdgeIndex(row: Record<string, unknown>): EdgeIndex {\n return {\n indexKey: row.index_key as string,\n edgeId: row.edge_id as string,\n createdAt: new Date(row.created_at as string | number),\n };\n }\n \n private deserializeSearchIndex(row: Record<string, unknown>): SearchIndex {\n return {\n term: row.term as string,\n nodeId: row.node_id as string,\n field: row.field as string,\n weight: row.weight as number,\n };\n }\n}","import { BaseAdapter, TransactionContext, DatabaseStats, AdapterConfig } from './base';\nimport type { Node, Edge, NodeIndex, EdgeIndex, SearchIndex, NewNode, NewEdge, NewNodeIndex, NewEdgeIndex, NewSearchIndex } from '../schema';\n\n/**\n * SqlStorage interface - matches the browser's SqlStorage API\n */\ninterface SqlStorage {\n exec(query: string, ...params: any[]): {\n toArray(): any[];\n rowsWritten?: number;\n };\n}\n\n/**\n * SqlStorage adapter for browser environments\n * Works with browser-based SQL storage implementations\n */\nexport class SqlStorageAdapter extends BaseAdapter {\n private sql: SqlStorage | null = null;\n\n constructor(config: AdapterConfig = {}) {\n super(config);\n }\n\n /**\n * Set the SqlStorage instance\n * Must be called before using the adapter\n */\n setSqlStorage(sql: SqlStorage): void {\n this.sql = sql;\n }\n\n async initialize(): Promise<void> {\n if (!this.sql) {\n throw new Error('SqlStorage not set. Call setSqlStorage() first.');\n }\n\n try {\n if (this.config.autoCreate !== false) {\n await this.crea