Skip to content

feat: PostgreSQL driver integration + ANALYZE API + multi-hop perf#95

Merged
pdlug merged 2 commits into
mainfrom
feat/postgres-driver-integration
Apr 27, 2026
Merged

feat: PostgreSQL driver integration + ANALYZE API + multi-hop perf#95
pdlug merged 2 commits into
mainfrom
feat/postgres-driver-integration

Conversation

@pdlug
Copy link
Copy Markdown
Contributor

@pdlug pdlug commented Apr 25, 2026

  • Four PostgreSQL drivers officially supported via the same createPostgresBackend(db) entry point: node-postgres, postgres-js, @neondatabase/serverless (WebSocket Pool), and @neondatabase/serverless (HTTP). Driver detection, capability surface, and a fast-path execution adapter are all driver-aware. neon-http is auto-detected and capabilities.transactions is set to false so transactional code paths fall through to sequential execution rather than throwing.
  • ~6× faster on multi-hop traversals via server-side prepared statements on the node-postgres / neon-serverless fast path (3-hop: 7.5ms → 0.8ms median). Combined with bypassing Drizzle's session wrapper for raw SQL, this brings TypeGraph-on-PostgreSQL to parity with Neo4j 5.26 on every single-query and multi-hop shape we measure.
  • New store.refreshStatistics() / backend.refreshStatistics() API that runs ANALYZE on the TypeGraph-managed tables. Without it the planner works from stale post-bulk-load stats — the difference between a 0.5ms and a 5ms forward traversal on PG, or a 0.9ms vs 23ms fulltext query on SQLite — until autovacuum catches up. Now a one-liner after any bulk import.
  • Edge-runtime safety: the postgres execution adapter has zero static node:* imports, so @nicia-ai/typegraph/postgres loads cleanly on Cloudflare Workers / Vercel Edge.
  • Type surface change: GraphBackend now requires refreshStatistics(): Promise<void>. TransactionBackend excludes it. External implementations (uncommon) need a no-op or proper implementation.
  • New options on PostgresBackendOptions: capabilities?: Partial<BackendCapabilities> for explicit overrides on custom HTTP-style drivers.
  • Optional peer deps: pg, postgres, and @neondatabase/serverless declared so installs surface the driver choice clearly.

`createPostgresBackend` now officially supports four Drizzle PostgreSQL
adapters: `node-postgres`, `postgres-js`, `neon-serverless` (WebSocket),
and `neon-http` (auto-detected; transactions disabled since HTTP can't
hold a session). The pg + postgres-js paths are exercised end-to-end by
the existing adapter and integration test suites (~250 tests each); the
two Neon paths get wiring smoke tests covering driver detection,
fast-path routing, type coercion, and capability surface.

Two perf wins on PostgreSQL came from this work:

- Server-side prepared statements on the node-postgres / neon-serverless
  fast path. Each unique compiled SQL string gets a stable counter-based
  statement name so PostgreSQL caches the plan after first execution.
  Combined with bypassing Drizzle's session wrapper for raw SQL
  execution, this drops the 3-hop benchmark from ~7.5ms to ~0.8ms median
  (9x), bringing TypeGraph-on-PostgreSQL to parity with Neo4j 5.26 on
  every single-query and multi-hop shape we measure.

- `store.refreshStatistics()` / `backend.refreshStatistics()` API that
  runs ANALYZE on the TypeGraph-managed tables. Without it the planner
  works from stale post-bulk-load stats — 5ms forward traversal instead
  of 0.5ms — until autovacuum catches up. Both SQLite and PostgreSQL
  backends implement it.

Other notable changes:

- New `PostgresBackendOptions.capabilities?: Partial<BackendCapabilities>`
  for explicit capability overrides (e.g., custom HTTP-style drivers).
- `pg`, `postgres`, and `@neondatabase/serverless` declared as optional
  peer deps so installs surface the driver choice clearly.
- Edge-runtime safety: the postgres execution adapter has no static
  `node:*` imports, so the `@nicia-ai/typegraph/postgres` entry point
  loads cleanly under Cloudflare Workers / Vercel Edge.
- Identifier escaping in `refreshStatistics` uses `quoteIdentifier` +
  `sql.join` so user-supplied table names round-trip safely.
- Benchmark harness gains `--postgres-driver=pg|postgres-js` and runs
  ANALYZE post-seed for steady-state measurement.
- Documentation in `apps/docs/src/content/docs/backend-setup.md` covers
  all four drivers with install + setup snippets and a runtime-to-driver
  matrix.

Type surface change: `GraphBackend` now requires
`refreshStatistics(): Promise<void>`. `TransactionBackend` excludes it
(stats refresh isn't meaningful inside a transaction). External
implementations (uncommon) need a no-op or proper implementation.
@pdlug pdlug force-pushed the feat/postgres-driver-integration branch from 85a82ad to a948975 Compare April 25, 2026 05:24
Doc and behavior fixes for the no-transactions fallthrough, the new
refreshStatistics() API, and the prepared-statement cache.

Behavior:
- LRU-bound the module-scope statement-name cache in the PostgreSQL
  fast path (default 256, parity with the SQLite adapter). High-
  cardinality SQL text — variable-length IN-list expansions, custom
  backend.execute() calls, generated aliases — would otherwise grow
  both the JS Map and per-session prepared-statement memory inside
  PostgreSQL without bound. The monotonic counter is independent of
  cache size so eviction never recycles a name; recycling could
  collide with a still-prepared statement on a long-lived pg
  connection.
- Add prepareStatements: false opt-out on createPostgresBackend for
  pgbouncer transaction-pool mode (named statements registered on one
  backend connection aren't visible on the next).
- Extract a shared getOrCreateLru<K, V> helper used by both the
  PostgreSQL statement-name cache and the SQLite prepared-statement
  cache; the duplicated eviction logic now lives in one place.

Docs:
- Document the no-transactions fallthrough on store.transaction() and
  store.batch() JSDocs — the behavior is deliberate and tested, but
  the public docstrings still claimed unconditional atomicity /
  snapshot consistency.
- Replace the limitations.md and schemas-stores.md sections that said
  store.transaction() throws ConfigurationError on D1; the toplevel
  store now falls through to sequential execution on any backend that
  reports transactions: false.
- Replace the raw "ANALYZE typegraph_nodes, ..." snippet in
  backend-setup.md with the new store.refreshStatistics() API; the
  public API handles custom table names and the SQLite path correctly.
- Fix backend.capabilities.vectorSearch typo (actual API is
  backend.capabilities.vector?.supported).
- Fix store.bulkCreate(batch) snippet to the collection-scoped
  store.nodes.Document.bulkCreate(batch) in store.ts JSDoc and the
  changeset.
- Add prepareStatements and preparedStatementCacheMax to the
  createPostgresBackend signature in the API reference.
@pdlug pdlug force-pushed the feat/postgres-driver-integration branch from 90b2918 to 5789afd Compare April 27, 2026 01:51
@pdlug pdlug merged commit 6f3bf30 into main Apr 27, 2026
10 checks passed
@pdlug pdlug deleted the feat/postgres-driver-integration branch April 27, 2026 01:55
@github-actions github-actions Bot mentioned this pull request Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant