/** * Graph-database backend for {@link IGraphStore} (phase-2 implementation). * * This adapter is the second implementation behind the `IGraphStore` seam. * DuckDbStore remains the default; this file ships the full lifecycle + * bulk-load surface so the lbug graph backend already drives a * round-trip-clean graph write. * * Design notes: * 1. Rel tables are polymorphic per edge kind — one named rel table per * relation type, each with multiple `FROM/TO` pairs. The DDL lives in * {@link graphdb-schema.ts}; this file never emits DDL inline. * 2. Source-level naming avoids the banned clean-room literals. The class * is {@link GraphDbStore}; files are `graphdb-*.ts`. The native binding * package `@ladybugdb/core` is a dep, not a source-level identifier. * 3. Every mutating path uses parameterized Cypher via the pool — no * string-concatenated values ever touch the connection. * * Lifecycle mirrors {@link DuckDbStore}: open → createSchema → bulkLoad → * query / search / vectorSearch / traverse → close. */ import type { CodeRelation, DependencyNode, FindingNode, GraphNode, KnowledgeGraph, NodeKind, NodeOfKind, RelationType, RepoNode, RouteNode } from "@opencodehub/core-types"; import { type GraphDbPoolConfig } from "./graphdb-pool.js"; import type { AncestorTraversalOptions, BulkLoadOptions, BulkLoadStats, ConsumerProducerEdge, DescendantTraversalOptions, EmbeddingRow, GraphDialect, IGraphStore, ListDependenciesOptions, ListEdgesByTypeOptions, ListEdgesOptions, ListEmbeddingsOptions, ListFindingsOptions, ListNodesByKindOptions, ListNodesByNameOptions, ListNodesOptions, ListRoutesOptions, SearchQuery, SearchResult, SqlParam, StoreMeta, TraverseQuery, TraverseResult, VectorQuery, VectorResult } from "./interface.js"; export interface GraphDbStoreOptions { readonly readOnly?: boolean; /** Fixed vector dimension for the embeddings rel table. Default 768. */ readonly embeddingDim?: number; /** Default query timeout for `query()` calls in ms. Default 5000. */ readonly timeoutMs?: number; /** * Overrides for the underlying connection pool. Tests inject a fake * `binding` to avoid the native dep; production callers rely on * defaults. */ readonly poolConfig?: GraphDbPoolConfig; } /** * Thrown by adapter surfaces that are not yet wired. The cochange + symbol * summary surfaces live on {@link ITemporalStore}, never on the graph * adapter. The class export is retained because downstream packages still * import it for typed fallback handling on graph-only failure modes. */ export declare class NotImplementedError extends Error { constructor(method: string); } /** * Missing peer-binding error. Surfaced when the native `@ladybugdb/core` * module is not available on the current platform (no prebuilt binary, or * the package was pruned by a `--production` install). */ export declare class GraphDbBindingError extends Error { constructor(cause: unknown); } /** * Column → node-field descriptors used by the round-trip readback path. * `rebuildGraphFromStore` walks this list so the returned graph carries * the same field set the bulk writer ingested. */ export declare const ROUND_TRIP_COLUMN_MAP: readonly (readonly [ string, string, "string" | "number" | "boolean" | "string[]" ])[]; /** * True for the transient lbug WAL→checkpoint rename failure that surfaces * under load — e.g. `IO exception: Error renaming file .wal to * .wal.checkpoint. ErrorMessage: No such file or directory`. The data is * already in the WAL (a reopen recovers it), so this specific failure is * safe to retry. Matched on the stable token trio (renaming + .wal + * checkpoint) rather than the OS-specific errno suffix, which varies by * platform. Every other error returns false and rethrows. */ export declare function isTransientCheckpointError(err: unknown): boolean; /** * Run `fn`, retrying up to `maxAttempts` times when it throws a transient * WAL→checkpoint rename error (see {@link isTransientCheckpointError}). Any * other error rethrows immediately; the transient error on the final attempt * also rethrows. Backoff scales with attempt (25ms, 50ms, …) to let the OS * settle the WAL file. Extracted as a pure helper so the retry policy is unit- * testable without provoking a native race. Used only by replace-mode-safe * bulk-load, which is idempotent (truncate-then-insert). */ export declare function retryTransientCheckpoint(fn: () => Promise, maxAttempts?: number, backoff?: (attempt: number) => Promise): Promise; export declare class GraphDbStore implements IGraphStore { #private; /** * Cypher dialect marker. The graph-db backend speaks Cypher natively; * the optional {@link IGraphStore.execCypher} escape hatch is wired * below so community tooling that needs raw Cypher (APOC analogues, * etc.) can call through. */ readonly dialect: GraphDialect; private readonly path; private readonly readOnly; private readonly embeddingDim; private readonly defaultTimeoutMs; private readonly poolConfig; private pool; private ftsExtensionLoaded; private vectorExtensionLoaded; private ftsIndexBuilt; private vectorIndexBuilt; constructor(path: string, opts?: GraphDbStoreOptions); open(): Promise; close(): Promise; createSchema(): Promise; /** * Bulk-load with a bounded retry for the transient lbug WAL→checkpoint * IO race. Under CPU/IO pressure the native binding's auto-checkpoint can * fail to rename `graph.lbug.wal` → `.wal.checkpoint` ("No such file or * directory") even though the data is already durably in the WAL. The * write otherwise succeeds (a reopen recovers the WAL), so the failure is * a flaky teardown artifact, not data loss — but unretried it bubbles to * the CLI's top-level catch and fails `analyze` with exit 1. Observed only * on loaded CI runners (varies by leg), never on an idle box. * * replace-mode bulkLoad is idempotent (truncate-then-insert fully replaces * prior contents), so a retry is safe. We retry only the transient * checkpoint-rename class; every other error rethrows immediately. */ bulkLoad(graph: KnowledgeGraph, opts?: BulkLoadOptions): Promise; private truncateAll; /** * Return the subset of `candidateIds` that already exist as CodeNodes in the * store. Used by upsert-mode bulkLoad to avoid synthesizing a placeholder * (and then `mergeNodes`-clobbering) for an edge endpoint that is a real, * previously-persisted node not present in the current batch. Reuses the * `listNodes({ ids })` finder so id decoding stays in one place; passes no * `limit` so every match is returned. */ private filterExistingNodeIds; private insertNodes; private insertEdgesForKind; upsertEmbeddings(rows: readonly EmbeddingRow[]): Promise; listEmbeddingHashes(): Promise>; query(sql: string, params?: readonly SqlParam[], opts?: { readonly timeoutMs?: number; }): Promise[]>; /** * Enumerate fully-rehydrated GraphNodes by kind. Mirror of the * DuckStore implementation — same input/output contract so the M5 BOM * bodies render identical results regardless of which backend the user * picked. * * The graph-db schema stores every kind under the single label * `:CodeNode` with `kind` as a discriminator property (see * graphdb-schema.ts). One MATCH plus an optional `WHERE n.kind IN [...]` * predicate is therefore sufficient — no per-kind table fan-out. * * Determinism: ORDER BY n.id ASC at the Cypher layer, plus a JS-side * lex-stable tiebreak on the rehydrated nodes so the output matches * DuckStore byte-for-byte. */ listNodes(opts?: ListNodesOptions): Promise; /** Single-kind shorthand. Mirror of {@link DuckDbStore.listNodesByKind}. */ listNodesByKind(kind: K, opts?: ListNodesByKindOptions): Promise[]>; /** All edges, optionally filtered + paged. Mirrors DuckDb ordering. */ listEdges(opts?: ListEdgesOptions): Promise; /** Single-type shorthand. Pins the type and forwards to {@link listEdges}. */ listEdgesByType(type: RelationType, opts?: ListEdgesByTypeOptions): Promise; /** Findings filter. Mirrors {@link DuckDbStore.listFindings} on Cypher. */ listFindings(opts?: ListFindingsOptions): Promise; /** Dependencies filter. License classification matches DuckDb. */ listDependencies(opts?: ListDependenciesOptions): Promise; /** Routes filter. Mirrors {@link DuckDbStore.listRoutes} on Cypher. */ listRoutes(opts?: ListRoutesOptions): Promise; /** Repo-node by id. Returns `undefined` when row is missing or non-Repo. */ getRepoNode(id: string): Promise; /** * Specialized finder for `analysis/impact.ts:131-135`. Cypher mirror of * the DuckDB `WHERE entry_point_id = ?` predicate; the property name is * the snake-cased column the writer emits via `nodeToParams`. */ listNodesByEntryPoint(entryPointId: string): Promise; /** * Specialized finder for `analysis/rename.ts:51,59` — `WHERE name = ?` * with optional `kinds` / `filePath` narrowing. Mirrors * {@link DuckDbStore.listNodesByName} exactly. */ listNodesByName(name: string, opts?: ListNodesByNameOptions): Promise; /** Counts grouped by kind. Same backfill semantics as DuckDb. */ countNodesByKind(kinds?: readonly NodeKind[]): Promise>; /** Counts grouped by edge type. Walks every relation kind (no per-type rel-table fan-out). */ countEdgesByType(types?: readonly RelationType[]): Promise>; /** * Stream embeddings via Cypher MATCH against the `Embedding` nodes. * `async function*` so the caller can `for await` without * materializing the full row set. */ listEmbeddings(opts?: ListEmbeddingsOptions): AsyncIterable; /** Ancestor traversal via a Cypher variable-length upward walk (the lbug analogue of a recursive ancestor query). */ traverseAncestors(opts: AncestorTraversalOptions): Promise; /** Symmetric of {@link traverseAncestors}. */ traverseDescendants(opts: DescendantTraversalOptions): Promise; /** * Producer-consumer edges across repos. Cypher mirror of the DuckDB * FETCHES + Operation join. The graph-db schema collapses every node * kind into a single `:CodeNode` label, so this is a simple two-hop * pattern with property predicates rather than a true table join. */ listConsumerProducerEdges(opts?: { readonly repoUris?: readonly string[]; }): Promise; /** * Shared `listEdges` body. The graph-db schema partitions edges into * per-type rel tables, so a no-types query needs to walk every label — * we fall back to the canonical relation list and emit one MATCH per * type, then merge + sort. With a `types` filter the pattern is one * MATCH per requested type, which keeps the round-trip cost * proportional to the filter set. */ private listEdgesInternalGd; /** * Shared body for ancestor/descendant traversal. Defers to the existing * {@link traverse} method which handles the variable-length pattern * inlining for the native graph-db engine. */ private traverseDirectionalGd; search(q: SearchQuery): Promise; vectorSearch(q: VectorQuery): Promise; traverse(q: TraverseQuery): Promise; getMeta(): Promise; setMeta(meta: StoreMeta): Promise; healthCheck(): Promise<{ ok: boolean; message?: string; }>; /** * {@link IGraphStore.execCypher} implementation. Delegates to the * pre-existing {@link query} method which already enforces read-only * Cypher via {@link assertReadOnlyCypher}. * * OCH core never calls this — it exists so community tooling that * needs raw Cypher (e.g. APOC analogues on a Neo4j adapter fork) can * route through `OpenStoreResult.graph.execCypher(...)`. The signature * accepts a `Record` params bag (Cypher's bound-name * model) rather than the positional `SqlParam[]` shape the legacy * `query` method takes. */ execCypher(statement: string, params?: Record): Promise[]>; private requirePool; private ensureFtsExtension; private ensureVectorExtension; private ensureFtsIndex; private ensureVectorIndex; getPath(): string; isReadOnly(): boolean; getEmbeddingDim(): number; getDefaultTimeoutMs(): number; } //# sourceMappingURL=graphdb-adapter.d.ts.map