aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
199 lines (197 loc) • 7.63 kB
JavaScript
/**
* SQLite Graph Backend
*
* Optional implementation of GraphBackend using better-sqlite3.
* Provides persistent on-disk storage, native SQL set operations
* (INTERSECT/EXCEPT/UNION), recursive CTE traversal, and cross-graph
* federation via ATTACH DATABASE.
*
* Install: npm install better-sqlite3 @types/better-sqlite3
*
* @implements #729
* @source @src/artifacts/graph-backend.ts
* @tests @test/unit/artifacts/sqlite-backend.test.ts
*/
import { normalizeEdges } from '../types.js';
/**
* SQLite-backed graph with persistent storage and native SQL operations.
*
* Each graph lives in a `.db` file under `.aiwg/.index/{graphName}/`.
* Uses WAL mode for concurrent read access.
*/
export class SqliteGraphBackend {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
db;
/**
* Create a new SQLite graph backend.
*
* @param dbPath - Path to the SQLite database file. Use ':memory:' for in-memory.
*/
constructor(dbPath = ':memory:') {
try {
// Dynamic require so missing package gives a clear error
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Database = require('better-sqlite3');
this.db = new Database(dbPath);
}
catch {
throw new Error('sqlite backend requires: npm install better-sqlite3 @types/better-sqlite3');
}
this.db.pragma('journal_mode = WAL');
this.initSchema();
}
initSchema() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS nodes (
id TEXT PRIMARY KEY,
type TEXT,
phase TEXT,
title TEXT,
summary TEXT,
checksum TEXT,
attrs TEXT DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS edges (
source TEXT NOT NULL,
target TEXT NOT NULL,
edge_type TEXT NOT NULL DEFAULT 'depends-on',
attrs TEXT DEFAULT '{}',
PRIMARY KEY (source, target, edge_type)
);
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target, edge_type);
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source, edge_type);
`);
}
// --- Mutation ---
addNode(id, attrs) {
const existing = this.db.prepare('SELECT attrs FROM nodes WHERE id = ?').get(id);
if (!existing) {
this.db.prepare('INSERT INTO nodes (id, attrs) VALUES (?, ?)').run(id, JSON.stringify(attrs ?? {}));
}
else if (attrs) {
const merged = { ...JSON.parse(existing.attrs), ...attrs };
this.db.prepare('UPDATE nodes SET attrs = ? WHERE id = ?').run(JSON.stringify(merged), id);
}
}
addEdge(source, target, type = 'depends-on', attrs) {
this.addNode(source);
this.addNode(target);
this.db.prepare('INSERT OR IGNORE INTO edges (source, target, edge_type, attrs) VALUES (?, ?, ?, ?)').run(source, target, type, JSON.stringify(attrs ?? {}));
}
// --- Query ---
hasNode(id) {
return !!this.db.prepare('SELECT 1 FROM nodes WHERE id = ?').get(id);
}
hasEdge(source, target, edgeType) {
if (edgeType) {
return !!this.db.prepare('SELECT 1 FROM edges WHERE source = ? AND target = ? AND edge_type = ?').get(source, target, edgeType);
}
return !!this.db.prepare('SELECT 1 FROM edges WHERE source = ? AND target = ?').get(source, target);
}
getNodeAttrs(id) {
const row = this.db.prepare('SELECT attrs FROM nodes WHERE id = ?').get(id);
if (!row)
return undefined;
return JSON.parse(row.attrs);
}
nodes() {
return this.db.prepare('SELECT id FROM nodes').all().map((r) => r.id);
}
// --- Traversal ---
neighbors(nodeId, direction, edgeType) {
const results = new Set();
if (direction === 'in' || direction === 'both') {
const sql = edgeType
? 'SELECT source FROM edges WHERE target = ? AND edge_type = ?'
: 'SELECT source FROM edges WHERE target = ?';
const rows = edgeType
? this.db.prepare(sql).all(nodeId, edgeType)
: this.db.prepare(sql).all(nodeId);
for (const row of rows)
results.add(row.source);
}
if (direction === 'out' || direction === 'both') {
const sql = edgeType
? 'SELECT target FROM edges WHERE source = ? AND edge_type = ?'
: 'SELECT target FROM edges WHERE source = ?';
const rows = edgeType
? this.db.prepare(sql).all(nodeId, edgeType)
: this.db.prepare(sql).all(nodeId);
for (const row of rows)
results.add(row.target);
}
return [...results];
}
// --- Set operations (native SQL) ---
intersection(setA, setB) {
if (setA.length === 0 || setB.length === 0)
return [];
const b = new Set(setB);
return setA.filter(x => b.has(x));
}
difference(setA, setB) {
const b = new Set(setB);
return setA.filter(x => !b.has(x));
}
union(setA, setB) {
return [...new Set([...setA, ...setB])];
}
// --- Persistence ---
serialize() {
const result = {};
const allNodes = this.db.prepare('SELECT id FROM nodes').all();
for (const { id } of allNodes) {
result[id] = { upstream: [], downstream: [] };
}
const allEdges = this.db.prepare('SELECT source, target, edge_type FROM edges').all();
for (const { source, target, edge_type } of allEdges) {
if (!result[source])
result[source] = { upstream: [], downstream: [] };
if (!result[target])
result[target] = { upstream: [], downstream: [] };
result[source].downstream.push({ path: target, type: edge_type });
result[target].upstream.push({ path: source, type: edge_type });
}
return result;
}
deserialize(data) {
// Clear existing data
this.db.exec('DELETE FROM edges; DELETE FROM nodes;');
const insertNode = this.db.prepare('INSERT OR IGNORE INTO nodes (id) VALUES (?)');
const insertEdge = this.db.prepare('INSERT OR IGNORE INTO edges (source, target, edge_type) VALUES (?, ?, ?)');
const runBatch = this.db.transaction(() => {
// Add all nodes
for (const id of Object.keys(data)) {
insertNode.run(id);
}
// Add edges from upstream relationships
for (const [id, node] of Object.entries(data)) {
const upEdges = normalizeEdges(node.upstream);
for (const edge of upEdges) {
insertNode.run(edge.path); // Ensure referenced nodes exist
insertEdge.run(edge.path, id, edge.type);
}
const downEdges = normalizeEdges(node.downstream);
for (const edge of downEdges) {
insertNode.run(edge.path);
insertEdge.run(id, edge.path, edge.type);
}
}
});
runBatch();
}
nodeCount() {
return this.db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
}
edgeCount() {
return this.db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
}
/**
* Close the database connection.
* Call this when the backend is no longer needed.
*/
close() {
this.db.close();
}
}
//# sourceMappingURL=sqlite-backend.js.map