@nicia-ai/typegraph 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/backend/postgres/index.cjs +167 -71
  2. package/dist/backend/postgres/index.cjs.map +1 -1
  3. package/dist/backend/postgres/index.d.cts +6 -8
  4. package/dist/backend/postgres/index.d.ts +6 -8
  5. package/dist/backend/postgres/index.js +119 -27
  6. package/dist/backend/postgres/index.js.map +1 -1
  7. package/dist/backend/sqlite/index.cjs +16 -15
  8. package/dist/backend/sqlite/index.d.cts +11 -26
  9. package/dist/backend/sqlite/index.d.ts +11 -26
  10. package/dist/backend/sqlite/index.js +6 -5
  11. package/dist/backend/sqlite/libsql.cjs +9 -8
  12. package/dist/backend/sqlite/libsql.cjs.map +1 -1
  13. package/dist/backend/sqlite/libsql.d.cts +4 -3
  14. package/dist/backend/sqlite/libsql.d.ts +4 -3
  15. package/dist/backend/sqlite/libsql.js +6 -5
  16. package/dist/backend/sqlite/libsql.js.map +1 -1
  17. package/dist/backend/sqlite/local.cjs +11 -10
  18. package/dist/backend/sqlite/local.cjs.map +1 -1
  19. package/dist/backend/sqlite/local.d.cts +4 -3
  20. package/dist/backend/sqlite/local.d.ts +4 -3
  21. package/dist/backend/sqlite/local.js +6 -5
  22. package/dist/backend/sqlite/local.js.map +1 -1
  23. package/dist/{chunk-LVCSL6A3.js → chunk-3W6AIFT2.js} +11 -34
  24. package/dist/chunk-3W6AIFT2.js.map +1 -0
  25. package/dist/chunk-4FJVSF3K.cjs +12 -0
  26. package/dist/{chunk-D2GO3RBL.cjs.map → chunk-4FJVSF3K.cjs.map} +1 -1
  27. package/dist/{chunk-7W3KAMVQ.cjs → chunk-4TJI44WP.cjs} +94 -119
  28. package/dist/chunk-4TJI44WP.cjs.map +1 -0
  29. package/dist/chunk-5H3BAMTB.js +32 -0
  30. package/dist/chunk-5H3BAMTB.js.map +1 -0
  31. package/dist/{chunk-OJ6IQVZG.js → chunk-DDVA4QBA.js} +177 -37
  32. package/dist/chunk-DDVA4QBA.js.map +1 -0
  33. package/dist/{chunk-7CDXX3QZ.cjs → chunk-DN4RUDF3.cjs} +2 -2
  34. package/dist/chunk-DN4RUDF3.cjs.map +1 -0
  35. package/dist/{chunk-NHQWXSTS.js → chunk-E5ROA3MN.js} +3 -3
  36. package/dist/{chunk-NHQWXSTS.js.map → chunk-E5ROA3MN.js.map} +1 -1
  37. package/dist/{chunk-FPCOUJSL.js → chunk-ENXJLDRK.js} +2 -2
  38. package/dist/chunk-ENXJLDRK.js.map +1 -0
  39. package/dist/chunk-IUIWCFG3.cjs +36 -0
  40. package/dist/chunk-IUIWCFG3.cjs.map +1 -0
  41. package/dist/{chunk-EG6TJ3I5.js → chunk-OYLF3IC7.js} +3 -3
  42. package/dist/{chunk-EG6TJ3I5.js.map → chunk-OYLF3IC7.js.map} +1 -1
  43. package/dist/{chunk-JTI5WT4P.cjs → chunk-PN37HVAV.cjs} +32 -10
  44. package/dist/chunk-PN37HVAV.cjs.map +1 -0
  45. package/dist/{chunk-RCAJZG67.cjs → chunk-PYJOBZR3.cjs} +10 -10
  46. package/dist/{chunk-RCAJZG67.cjs.map → chunk-PYJOBZR3.cjs.map} +1 -1
  47. package/dist/{chunk-WI5VRP3X.js → chunk-RF5EUQWS.js} +371 -47
  48. package/dist/chunk-RF5EUQWS.js.map +1 -0
  49. package/dist/{chunk-ZEMNF3QK.cjs → chunk-RVT2N4RF.cjs} +209 -69
  50. package/dist/chunk-RVT2N4RF.cjs.map +1 -0
  51. package/dist/{chunk-4BOWVUGQ.js → chunk-TSGBRPNE.js} +3 -3
  52. package/dist/{chunk-4BOWVUGQ.js.map → chunk-TSGBRPNE.js.map} +1 -1
  53. package/dist/{chunk-O2P3KAUT.cjs → chunk-TVUARC7S.cjs} +445 -110
  54. package/dist/chunk-TVUARC7S.cjs.map +1 -0
  55. package/dist/{chunk-DOTEN2FG.js → chunk-U5ROKVRF.js} +26 -3
  56. package/dist/chunk-U5ROKVRF.js.map +1 -0
  57. package/dist/{chunk-E5JZSEGJ.cjs → chunk-WFUN6DCM.cjs} +8 -8
  58. package/dist/{chunk-E5JZSEGJ.cjs.map → chunk-WFUN6DCM.cjs.map} +1 -1
  59. package/dist/{chunk-W5MTHMTE.cjs → chunk-YEDQLNVE.cjs} +26 -2
  60. package/dist/chunk-YEDQLNVE.cjs.map +1 -0
  61. package/dist/{chunk-YZZBDCLV.js → chunk-ZTREJOGB.js} +32 -11
  62. package/dist/chunk-ZTREJOGB.js.map +1 -0
  63. package/dist/{ddl-B2jjLjpR.d.cts → ddl-9kw8UADK.d.ts} +697 -29
  64. package/dist/{ddl-DpaeAKPp.d.ts → ddl-Cw82BPM4.d.cts} +697 -29
  65. package/dist/{extension-types-D07e_NTw.d.cts → extension-types-B78CZfA5.d.cts} +1 -1
  66. package/dist/{extension-types-D07e_NTw.d.ts → extension-types-B78CZfA5.d.ts} +1 -1
  67. package/dist/graph-extension/index.cjs +18 -18
  68. package/dist/graph-extension/index.d.cts +4 -4
  69. package/dist/graph-extension/index.d.ts +4 -4
  70. package/dist/graph-extension/index.js +3 -3
  71. package/dist/{index-CT6nvtxL.d.cts → index-Bilw0pYm.d.cts} +34 -3
  72. package/dist/{index-DQheiVQV.d.ts → index-CWdBFWbI.d.ts} +34 -3
  73. package/dist/index.cjs +460 -361
  74. package/dist/index.cjs.map +1 -1
  75. package/dist/index.d.cts +13 -11
  76. package/dist/index.d.ts +13 -11
  77. package/dist/index.js +131 -36
  78. package/dist/index.js.map +1 -1
  79. package/dist/indexes/index.cjs +13 -13
  80. package/dist/indexes/index.d.cts +5 -5
  81. package/dist/indexes/index.d.ts +5 -5
  82. package/dist/indexes/index.js +1 -1
  83. package/dist/interchange/index.cjs +7 -7
  84. package/dist/interchange/index.d.cts +7 -5
  85. package/dist/interchange/index.d.ts +7 -5
  86. package/dist/interchange/index.js +2 -2
  87. package/dist/{manager-BpjBW1JK.d.cts → manager-Aqzmztkb.d.cts} +11 -3
  88. package/dist/{manager-BKaPQbns.d.ts → manager-C3LxzPcX.d.ts} +11 -3
  89. package/dist/{materialize-indexes-BB9N5uQl.d.cts → materialize-indexes-BQfaAZKA.d.cts} +1 -1
  90. package/dist/{materialize-indexes-BYpU2RKh.d.ts → materialize-indexes-DFavFQah.d.ts} +1 -1
  91. package/dist/profiler/index.d.cts +9 -7
  92. package/dist/profiler/index.d.ts +9 -7
  93. package/dist/schema/index.cjs +28 -27
  94. package/dist/schema/index.cjs.map +1 -1
  95. package/dist/schema/index.d.cts +9 -7
  96. package/dist/schema/index.d.ts +9 -7
  97. package/dist/schema/index.js +6 -5
  98. package/dist/schema/index.js.map +1 -1
  99. package/dist/{sqlite-Myo8e3Lr.d.cts → sqlite-BvSEbybu.d.cts} +320 -1
  100. package/dist/{sqlite-CU3j6rc7.d.ts → sqlite-GKvcDPVn.d.ts} +320 -1
  101. package/dist/{store-BAGuOHDh.d.cts → store-CiS_AGNH.d.ts} +112 -4
  102. package/dist/{store-De98Lod4.d.ts → store-DbQLsSor.d.cts} +112 -4
  103. package/dist/{types-CYga3lZp.d.cts → types-CF7nDH3q.d.cts} +2 -2
  104. package/dist/{types-BLmJq4wI.d.ts → types-Cars4JZ1.d.ts} +2 -2
  105. package/dist/{types--0EslBx5.d.cts → types-DZPBXZOK.d.cts} +275 -8
  106. package/dist/{types-BNWYZ-0-.d.ts → types-Dzkr0Eqi.d.ts} +275 -8
  107. package/dist/{where-m1uWsdua.d.cts → where-BmtjfWuS.d.cts} +2 -2
  108. package/dist/{where-CluR73rs.d.ts → where-kk1Zz2En.d.ts} +2 -2
  109. package/package.json +3 -1
  110. package/dist/chunk-7CDXX3QZ.cjs.map +0 -1
  111. package/dist/chunk-7W3KAMVQ.cjs.map +0 -1
  112. package/dist/chunk-D2GO3RBL.cjs +0 -12
  113. package/dist/chunk-DOTEN2FG.js.map +0 -1
  114. package/dist/chunk-FPCOUJSL.js.map +0 -1
  115. package/dist/chunk-JTI5WT4P.cjs.map +0 -1
  116. package/dist/chunk-LVCSL6A3.js.map +0 -1
  117. package/dist/chunk-O2P3KAUT.cjs.map +0 -1
  118. package/dist/chunk-OJ6IQVZG.js.map +0 -1
  119. package/dist/chunk-W5MTHMTE.cjs.map +0 -1
  120. package/dist/chunk-WI5VRP3X.js.map +0 -1
  121. package/dist/chunk-YZZBDCLV.js.map +0 -1
  122. package/dist/chunk-ZEMNF3QK.cjs.map +0 -1
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { N as NodeType, d as EdgeType, U as UniquenessScope, C as Collation, D as DeleteBehavior, e as KindAnnotations, f as Cardinality, g as EndpointExistence, T as TemporalMode, G as GraphExtension, I as IndexEntity, K as KindEntity, a as NullCheckOp, A as AnyEdgeType } from './extension-types-D07e_NTw.js';
2
+ import { N as NodeType, e as EdgeType, o as UniquenessScope, g as Collation, D as DeleteBehavior, d as KindAnnotations, f as Cardinality, k as EndpointExistence, T as TemporalMode, G as GraphExtension, I as IndexEntity, K as KindEntity, a as NullCheckOp, A as AnyEdgeType } from './extension-types-B78CZfA5.js';
3
3
  import { SQL } from 'drizzle-orm';
4
+ import { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
5
+ import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
4
6
 
5
7
  /** Brand key for MetaEdge */
6
8
  declare const META_EDGE_BRAND: "__metaEdge";
@@ -170,6 +172,64 @@ declare function createSqlSchema(names?: Partial<SqlTableNames>): SqlSchema;
170
172
  */
171
173
  declare const DEFAULT_SQL_SCHEMA: SqlSchema;
172
174
 
175
+ /**
176
+ * A single table TypeGraph owns.
177
+ */
178
+ type TableContribution = Readonly<{
179
+ /**
180
+ * Stable, graph- and deployment-independent identity for the logical
181
+ * slot this contribution fills (e.g. `"fulltext"`). NOT the physical
182
+ * table name. Stable across table-name overrides and across strategy
183
+ * swaps of the *same* logical slot, so #135's durable marker can
184
+ * survive both.
185
+ */
186
+ logicalName: string;
187
+ /**
188
+ * Identifies the producer of this contribution (e.g. a strategy id
189
+ * like `"tsvector"` / `"fts5"`, or `"base"` for core schema tables).
190
+ * Part of the #135 materialization identity and an input to the
191
+ * drift signature — a strategy swap on the same `logicalName` is a
192
+ * legitimate, detectable drift, not a silent reuse.
193
+ */
194
+ owner: string;
195
+ /**
196
+ * Resolved physical table name after any per-deployment name
197
+ * override. Part of the #135 materialization identity (custom names
198
+ * must be distinguishable) and used by diagnostics / the focused
199
+ * bootstrap ensure.
200
+ */
201
+ tableName: string;
202
+ /**
203
+ * Idempotent (`CREATE ... IF NOT EXISTS`) statements that
204
+ * materialize this contribution's table **and its supporting
205
+ * indexes**. Running the full list is how the runtime ensure
206
+ * self-heals partial states (table present, index missing) — it is
207
+ * not a probe-and-skip. Deterministic for a given resolved
208
+ * configuration: the canonical normalized input to #135's drift
209
+ * signature.
210
+ */
211
+ createDdl: readonly string[];
212
+ /**
213
+ * When `true`, the post-schema-load focused ensure
214
+ * (`loadActiveSchemaWithBootstrap`) materializes this contribution
215
+ * on every successful schema load.
216
+ *
217
+ * **Invariant: only strategy-declared contributions may set this.**
218
+ * Core/base tables are always `false` — they are created by
219
+ * drizzle-kit / `bootstrapTables`. This is what lets the boot path
220
+ * derive runtime contributions straight from the strategy
221
+ * (`fulltextStrategy.ownedTables`) without walking — and generating
222
+ * DDL for — every base table.
223
+ */
224
+ runtimeEnsure: boolean;
225
+ }>;
226
+ /**
227
+ * A table a {@link FulltextStrategy} declares it owns. An alias, not a
228
+ * distinct shape: a strategy's declaration is already authoritative
229
+ * (no resolution step). The name documents the producer role.
230
+ */
231
+ type StrategyTableContribution = TableContribution;
232
+
173
233
  /**
174
234
  * Fulltext Strategy — pluggable DDL + SQL generation for a dialect's
175
235
  * fulltext stack. A strategy owns every SQL statement that touches its
@@ -245,11 +305,20 @@ interface FulltextStrategy {
245
305
  */
246
306
  snippetExpression(tableName: string, query: string, mode: FulltextQueryMode, language?: string): SQL;
247
307
  /**
248
- * DDL statements to create the backing table and any supporting
249
- * indexes. Idempotent (`CREATE ... IF NOT EXISTS`). Called once from
250
- * `bootstrapTables()` on a fresh database.
308
+ * The tables this strategy owns, as Drizzle-free, already
309
+ * authoritative `TableContribution`s (`logicalName`, `owner`,
310
+ * resolved `tableName`, idempotent `createDdl` for the table **and
311
+ * its supporting indexes**, `runtimeEnsure`). A strategy never
312
+ * constructs a Drizzle table itself; drizzle-kit visibility, when
313
+ * applicable, is the schema barrel's responsibility (the default
314
+ * Postgres strategy's `schema/postgres.ts` exports a matching
315
+ * `tables.fulltext` — a non-default strategy must export its own).
316
+ *
317
+ * Replaces the former `generateDdl(tableName)`: DDL is now one field
318
+ * of a contribution rather than the strategy's whole storage
319
+ * surface. (Public API change — see #129.)
251
320
  */
252
- generateDdl(tableName: string): readonly string[];
321
+ ownedTables(primaryTableName: string): readonly StrategyTableContribution[];
253
322
  /**
254
323
  * Emits the statements that upsert a single fulltext row. Returns one
255
324
  * or more statements — some backends (SQLite FTS5) cannot emulate
@@ -445,6 +514,33 @@ type SerializedSchema = Readonly<{
445
514
  */
446
515
  type SchemaHash = string;
447
516
 
517
+ type AnyPgDatabase = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
518
+
519
+ /**
520
+ * Controls how the backend manages SQLite transactions.
521
+ *
522
+ * - `"sql"`: TypeGraph issues BEGIN / COMMIT / ROLLBACK SQL directly.
523
+ * Default for sync drivers (better-sqlite3, bun:sqlite).
524
+ * - `"drizzle"`: Delegates to Drizzle's `db.transaction()` method.
525
+ * Default for async drivers (libsql, sql.js).
526
+ * - `"do-sqlite"`: Delegates to the Cloudflare Durable Objects async
527
+ * storage transaction runner (`db.$client.transaction`,
528
+ * i.e. `ctx.storage.transaction(async ...)`). Drizzle's
529
+ * own `db.transaction()` on DO is
530
+ * `ctx.storage.transactionSync` and cannot span `await`,
531
+ * so it is deliberately not used. Auto-detected for
532
+ * `drizzle(ctx.storage)` (#140).
533
+ * - `"none"`: Transactions disabled. Default for Cloudflare D1
534
+ * (`D1Database.batch` is transactional but not an
535
+ * interactive runner — tracked separately).
536
+ */
537
+ type SqliteTransactionMode = "sql" | "drizzle" | "none" | "do-sqlite";
538
+ type SqliteExecutionProfileHints = Readonly<{
539
+ isSync?: boolean;
540
+ transactionMode?: SqliteTransactionMode;
541
+ }>;
542
+ type AnySqliteDatabase = BaseSQLiteDatabase<"sync" | "async", unknown>;
543
+
448
544
  /**
449
545
  * SQL Dialect Abstraction Layer
450
546
  *
@@ -914,6 +1010,63 @@ type RecordIndexMaterializationParams = Readonly<{
914
1010
  /** Error message on failure; undefined on success (clears existing). */
915
1011
  error: string | undefined;
916
1012
  }>;
1013
+ /**
1014
+ * Durable identity of a strategy-owned table contribution (#129),
1015
+ * scoped to a graph. This is the primary key of
1016
+ * `typegraph_contribution_materializations`.
1017
+ *
1018
+ * `logicalName` is the stable slot ("fulltext"); `owner` is the
1019
+ * producing strategy ("tsvector" / "fts5"); `tableName` is the resolved
1020
+ * physical name (custom per-deployment names must be distinguishable).
1021
+ * `graphId` is part of identity here — unlike the index status table
1022
+ * where the physical index name is database-global, two graphs can each
1023
+ * own a logically-identical fulltext contribution.
1024
+ */
1025
+ type ContributionMaterializationIdentity = Readonly<{
1026
+ graphId: string;
1027
+ logicalName: string;
1028
+ owner: string;
1029
+ tableName: string;
1030
+ }>;
1031
+ /**
1032
+ * Per-deployment record that a strategy-owned contribution has been
1033
+ * durably materialized against this database.
1034
+ *
1035
+ * `signature` is intentionally NOT part of the identity: a row with the
1036
+ * same identity but a different signature means "materialized artifact
1037
+ * is stale/drifted" — a loud error on the hot path, never a silent
1038
+ * re-materialize. `materializedAt` is undefined until the first
1039
+ * successful materialization; `lastAttemptedAt` is always set.
1040
+ */
1041
+ type ContributionMaterializationRow = Readonly<{
1042
+ graphId: string;
1043
+ logicalName: string;
1044
+ owner: string;
1045
+ tableName: string;
1046
+ signature: string;
1047
+ materializedAt: string | undefined;
1048
+ lastAttemptedAt: string;
1049
+ lastError: string | undefined;
1050
+ }>;
1051
+ /**
1052
+ * Parameters for upserting a contribution-materialization attempt.
1053
+ * Same success/failure contract as
1054
+ * {@link RecordIndexMaterializationParams}: on failure pass undefined
1055
+ * `materializedAt` so a prior successful timestamp is preserved via
1056
+ * COALESCE, and the error message.
1057
+ */
1058
+ type RecordContributionMaterializationParams = Readonly<{
1059
+ graphId: string;
1060
+ logicalName: string;
1061
+ owner: string;
1062
+ tableName: string;
1063
+ signature: string;
1064
+ attemptedAt: string;
1065
+ /** ISO timestamp on success; undefined on failure (preserves existing). */
1066
+ materializedAt: string | undefined;
1067
+ /** Error message on failure; undefined on success (clears existing). */
1068
+ error: string | undefined;
1069
+ }>;
917
1070
  /**
918
1071
  * One row of the per-deployment `typegraph_kind_removals` table:
919
1072
  * a graph-extension kind that has been removed from the schema and whose
@@ -955,6 +1108,21 @@ type TransactionOptions = Readonly<{
955
1108
  /** Transaction isolation level (if supported) */
956
1109
  isolationLevel?: "read_uncommitted" | "read_committed" | "repeatable_read" | "serializable";
957
1110
  }>;
1111
+ /**
1112
+ * A caller-owned, already-open Drizzle transaction handle that a
1113
+ * TypeGraph store can adopt so both layers commit/rollback together.
1114
+ *
1115
+ * The literal client the caller's transaction runs on — a `PgDatabase`
1116
+ * transaction (node-postgres / `neon-serverless` Pool) or a
1117
+ * `BaseSQLiteDatabase` connection. Async drivers obtain it from
1118
+ * `db.transaction(async (tx) => …)`; synchronous `better-sqlite3` (whose
1119
+ * driver rejects an `async` transaction callback) passes the connection
1120
+ * itself under an explicit `BEGIN`/`COMMIT`/`ROLLBACK`. Passing it to
1121
+ * {@link GraphBackend.adoptTransaction} threads that *literal* client
1122
+ * through TypeGraph's SQL, so graph writes and the caller's own
1123
+ * relational writes share one Postgres/SQLite transaction (#134).
1124
+ */
1125
+ type AdoptedTransaction = AnyPgDatabase | AnySqliteDatabase;
958
1126
  /**
959
1127
  * Transaction backend — a backend scoped to a transaction.
960
1128
  *
@@ -968,7 +1136,7 @@ type TransactionOptions = Readonly<{
968
1136
  * exists to eliminate. Schema commits go through the top-level backend
969
1137
  * methods only.
970
1138
  */
971
- type TransactionBackend = Omit<GraphBackend, "transaction" | "close" | "refreshStatistics" | "commitSchemaVersion" | "setActiveVersion">;
1139
+ type TransactionBackend = Omit<GraphBackend, "transaction" | "adoptTransaction" | "close" | "refreshStatistics" | "commitSchemaVersion" | "setActiveVersion">;
972
1140
  /**
973
1141
  * The GraphBackend interface abstracts database operations.
974
1142
  *
@@ -1107,6 +1275,40 @@ type GraphBackend = Readonly<{
1107
1275
  * timestamp survives across error windows.
1108
1276
  */
1109
1277
  recordIndexMaterialization?: (params: RecordIndexMaterializationParams) => Promise<void>;
1278
+ /**
1279
+ * Idempotently ensure ONLY the
1280
+ * `typegraph_contribution_materializations` table exists. Same
1281
+ * focused-bootstrap rationale as `ensureIndexMaterializationsTable`:
1282
+ * a single `CREATE TABLE IF NOT EXISTS` is concurrency-safe under
1283
+ * replica startup, where the full `bootstrapTables` set risks a
1284
+ * Postgres SHARE-lock deadlock.
1285
+ */
1286
+ ensureContributionMaterializationsTable?: () => Promise<void>;
1287
+ /**
1288
+ * Look up the durable materialization marker for one strategy-owned
1289
+ * contribution identity. Returns `undefined` when no row exists
1290
+ * ("never initialized").
1291
+ */
1292
+ getContributionMaterialization?: (identity: ContributionMaterializationIdentity) => Promise<ContributionMaterializationRow | undefined>;
1293
+ /**
1294
+ * Upsert a contribution-materialization attempt — success or failure.
1295
+ * Failure rows preserve any prior `materializedAt` via COALESCE so a
1296
+ * later failed re-attempt doesn't erase the historical success.
1297
+ */
1298
+ recordContributionMaterialization?: (params: RecordContributionMaterializationParams) => Promise<void>;
1299
+ /**
1300
+ * Resolve (once per backend instance, cached) and assert the durable
1301
+ * materialization markers for every `runtimeEnsure` contribution this
1302
+ * backend's strategy declares, for `graphId`. Throws
1303
+ * `StoreNotInitializedError` when a marker is missing, stale
1304
+ * (signature drift), or recorded a failed last attempt.
1305
+ *
1306
+ * This is the single read-side gate the fulltext hot-path wrappers
1307
+ * and `store.transaction()` consult. It performs ZERO DDL and ZERO
1308
+ * marker writes — initialization is the exclusive job of the async
1309
+ * boot path (`createStoreWithSchema` → `ensureRuntimeContributions`).
1310
+ */
1311
+ assertRuntimeContributionsInitialized?: (graphId: string) => Promise<void>;
1110
1312
  /**
1111
1313
  * Bootstraps the per-deployment `typegraph_kind_removals` table so
1112
1314
  * `store.removeKinds()` and `store.materializeRemovals()` can persist
@@ -1149,6 +1351,32 @@ type GraphBackend = Readonly<{
1149
1351
  * deadlock under concurrent replica startup.
1150
1352
  */
1151
1353
  ensureReconciliationMarkersTable?: () => Promise<void>;
1354
+ /**
1355
+ * Materializes every contribution flagged `runtimeEnsure` — the
1356
+ * strategy-owned runtime tables (fulltext today) that drizzle-kit-
1357
+ * managed setups don't create. Called once after a successful schema
1358
+ * load. Deliberately scoped: base/drizzle-visible tables are
1359
+ * `runtimeEnsure: false`, so this does not regress startup into
1360
+ * broad DDL/probing across every table.
1361
+ *
1362
+ * The canonical durable-marker writer (#135): for each runtime
1363
+ * contribution it short-circuits when the marker already records a
1364
+ * matching signature, otherwise runs the idempotent `createDdl` and
1365
+ * records the marker (success or failure) keyed by `graphId`.
1366
+ */
1367
+ ensureRuntimeContributions?: (graphId: string) => Promise<void>;
1368
+ /**
1369
+ * Bootstraps the fulltext storage table the active `FulltextStrategy`
1370
+ * owns. Same focused-bootstrap rationale as the other `ensure*Table`
1371
+ * methods: idempotent and concurrency-safe under replica startup.
1372
+ *
1373
+ * Superseded by `ensureRuntimeContributions()` (#129); retained as
1374
+ * a thin back-compat wrapper for backends/callers predating #129. Not
1375
+ * machine-`@deprecated` because the manager still calls it as the
1376
+ * pre-#129 fallback. #135 removed the remaining hot-path callers and
1377
+ * routed this through the durable-marker writer.
1378
+ */
1379
+ ensureFulltextTable?: (graphId: string) => Promise<void>;
1152
1380
  /**
1153
1381
  * Read the high-water mark schema version for which
1154
1382
  * `materializeRemovals` reconciliation has already verified history
@@ -1216,7 +1444,46 @@ type GraphBackend = Readonly<{
1216
1444
  * statement outside `transaction(...)`.
1217
1445
  */
1218
1446
  executeDdl?: (ddl: string) => Promise<void>;
1219
- transaction: <T>(fn: (tx: TransactionBackend) => Promise<T>, options?: TransactionOptions) => Promise<T>;
1447
+ /**
1448
+ * `fn` receives the tx-scoped backend and the raw Drizzle handle
1449
+ * **bound to that same transaction** (`AdoptedTransaction`). The
1450
+ * store surfaces the second argument as `TransactionContext.sql` so
1451
+ * callers can write their own relational tables inside the
1452
+ * graph-owned transaction. Implementations MUST pass the *exact*
1453
+ * handle the tx-scoped backend writes through (the Postgres/libsql
1454
+ * tx handle; for better-sqlite3 / do-sqlite the bound connection),
1455
+ * never a fresh one.
1456
+ */
1457
+ transaction: <T>(fn: (tx: TransactionBackend, sql: AdoptedTransaction) => Promise<T>, options?: TransactionOptions) => Promise<T>;
1458
+ /**
1459
+ * Adopt a caller-owned, already-open transaction (#134).
1460
+ *
1461
+ * Returns a transaction-scoped backend bound to the *exact*
1462
+ * `externalTx` client, reusing this backend's already-resolved
1463
+ * schema/strategy config — no `createStoreWithSchema` / `evolve` /
1464
+ * `migrateSchema`, and **no DDL inside the caller's business
1465
+ * transaction**. The caller owns `BEGIN`/`COMMIT`/`ROLLBACK`; this
1466
+ * method neither opens nor closes a transaction. Async drivers
1467
+ * (node-postgres, `neon-serverless` Pool, libsql) wrap with
1468
+ * `db.transaction(async …)`; synchronous `better-sqlite3` must instead
1469
+ * issue explicit `BEGIN`/`COMMIT`/`ROLLBACK` (its driver rejects an
1470
+ * `async` transaction callback).
1471
+ *
1472
+ * Optional and presence-detected like the other capability-scoped
1473
+ * members: only the Drizzle Postgres/SQLite backends provide it.
1474
+ * Implementations MUST throw (not silently degrade) when
1475
+ * `capabilities.transactions` is `false` — a non-atomic fallback is
1476
+ * safe for graph-only writes but dangerous for cross-store flows,
1477
+ * where the caller's relational write *would* still commit.
1478
+ *
1479
+ * Fulltext stays safe by construction: the returned backend's
1480
+ * fulltext methods assert the durable materialization marker (a
1481
+ * cached SELECT, never DDL) at point of use and throw
1482
+ * `StoreNotInitializedError` on a missing/stale/failed marker rather
1483
+ * than migrating mid-transaction. Prefer booting the parent store via
1484
+ * `createStoreWithSchema` so that assertion is a warm-cache no-op.
1485
+ */
1486
+ adoptTransaction?: (externalTx: AdoptedTransaction) => TransactionBackend;
1220
1487
  close: () => Promise<void>;
1221
1488
  }>;
1222
1489
  /**
@@ -2120,4 +2387,4 @@ type RelationalIndexDeclaration = NodeIndexDeclaration | EdgeIndexDeclaration;
2120
2387
  type IndexDeclaration = RelationalIndexDeclaration | VectorIndexDeclaration;
2121
2388
  type SystemColumnName = "graph_id" | "kind" | "id" | "from_kind" | "from_id" | "to_kind" | "to_id" | "deleted_at" | "valid_from" | "valid_to" | "created_at" | "updated_at" | "version";
2122
2389
 
2123
- export { type JsonPointerFor as $, type AggregateExpr as A, type BackendCapabilities as B, type FieldRef as C, type ComparisonOp as D, type EdgeIndexConfig as E, type FulltextStrategy as F, type GraphBackend as G, type AggregateComparisonPredicate as H, type IndexDeclaration as I, type JsonPointer as J, type CommitSchemaVersionExpected as K, type CommitSchemaVersionParams as L, type MetaEdge as M, type NodeIndexConfig as N, type OntologyRelation as O, DEFAULT_SQL_SCHEMA as P, type QueryAst as Q, type RelationalIndexDeclaration as R, type SerializedNodeDef as S, type DeleteFulltextBatchParams as T, type FulltextBatchRow as U, type VectorIndexDeclaration as V, type FulltextCapabilities as W, type FulltextQueryMode as X, type FulltextSearchParams as Y, type FulltextSearchResult as Z, type HybridFusionOptions as _, type EdgeIndexDeclaration as a, type JsonPointerInput as a0, type JsonPointerSegment as a1, type JsonPointerSegments as a2, type JsonPointerSegmentsFor as a3, MAX_JSON_POINTER_DEPTH as a4, type MetaEdgeProperties as a5, type OrderSpec as a6, type ParameterRef as a7, type ResolveJsonPointer as a8, type ResolveJsonPointerSegments as a9, type NodeRow as aA, type SelectiveField as aB, type ComposableQuery as aC, type SetOperationType as aD, type SetOperation as aE, type VectorMetric as aF, type SetActiveVersionParams as aa, type SortDirection as ab, type SqlSchema as ac, type SqlTableNames as ad, type TransactionBackend as ae, type TraversalExpansion as af, type UpsertFulltextBatchParams as ag, type VectorCapabilities as ah, createSqlSchema as ai, fts5Strategy as aj, isMetaEdge as ak, joinJsonPointers as al, jsonPointer as am, normalizeJsonPointer as an, parseJsonPointer as ao, tsvectorStrategy as ap, type ValueType as aq, type PredicateExpression as ar, type VectorMetricType as as, type Traversal as at, type NodePredicate as au, type ProjectedField as av, type GroupBySpec as aw, type RecursiveCyclePolicy as ax, type TraversalDirection as ay, type EdgeRow as az, type NodeIndexDeclaration as b, type IndexWhereExpression as c, type SerializedEdgeDef as d, type SerializedMetaEdge as e, type SerializedOntologyRelation as f, type SerializedClosures as g, type SerializedSchema as h, type SchemaHash as i, type JsonSchema as j, type SerializedOntology as k, type SerializedUniqueConstraint as l, type SqlDialect as m, type EdgeIndexDirection as n, type EdgeIndexWhereBuilder as o, type IndexOrigin as p, type IndexScope as q, type IndexWhereFieldBuilder as r, type IndexWhereInput as s, type NodeIndexWhereBuilder as t, type SystemColumnName as u, type VectorIndexImplementation as v, type VectorIndexMetric as w, type VectorIndexParams as x, type SchemaVersionRow as y, type InferenceType as z };
2390
+ export { type FulltextCapabilities as $, type AnySqliteDatabase as A, type BackendCapabilities as B, type VectorIndexParams as C, type SchemaVersionRow as D, type EdgeIndexConfig as E, type FulltextStrategy as F, type GraphBackend as G, type InferenceType as H, type IndexDeclaration as I, type JsonPointer as J, type AggregateExpr as K, type FieldRef as L, type MetaEdge as M, type NodeIndexConfig as N, type OntologyRelation as O, type ComparisonOp as P, type QueryAst as Q, type RelationalIndexDeclaration as R, type SqliteExecutionProfileHints as S, type AggregateComparisonPredicate as T, type AdoptedTransaction as U, type VectorIndexDeclaration as V, type CommitSchemaVersionExpected as W, type CommitSchemaVersionParams as X, DEFAULT_SQL_SCHEMA as Y, type DeleteFulltextBatchParams as Z, type FulltextBatchRow as _, type SqliteTransactionMode as a, type FulltextQueryMode as a0, type FulltextSearchParams as a1, type FulltextSearchResult as a2, type HybridFusionOptions as a3, type JsonPointerFor as a4, type JsonPointerInput as a5, type JsonPointerSegment as a6, type JsonPointerSegments as a7, type JsonPointerSegmentsFor as a8, MAX_JSON_POINTER_DEPTH as a9, type Traversal as aA, type NodePredicate as aB, type ProjectedField as aC, type GroupBySpec as aD, type RecursiveCyclePolicy as aE, type TraversalDirection as aF, type EdgeRow as aG, type NodeRow as aH, type SelectiveField as aI, type ComposableQuery as aJ, type SetOperationType as aK, type SetOperation as aL, type VectorMetric as aM, type MetaEdgeProperties as aa, type OrderSpec as ab, type ParameterRef as ac, type ResolveJsonPointer as ad, type ResolveJsonPointerSegments as ae, type SetActiveVersionParams as af, type SortDirection as ag, type SqlSchema as ah, type SqlTableNames as ai, type StrategyTableContribution as aj, type TableContribution as ak, type TransactionBackend as al, type TraversalExpansion as am, type UpsertFulltextBatchParams as an, type VectorCapabilities as ao, createSqlSchema as ap, fts5Strategy as aq, isMetaEdge as ar, joinJsonPointers as as, jsonPointer as at, normalizeJsonPointer as au, parseJsonPointer as av, tsvectorStrategy as aw, type ValueType as ax, type PredicateExpression as ay, type VectorMetricType as az, type AnyPgDatabase as b, type EdgeIndexDeclaration as c, type NodeIndexDeclaration as d, type IndexWhereExpression as e, type SerializedNodeDef as f, type SerializedEdgeDef as g, type SerializedMetaEdge as h, type SerializedOntologyRelation as i, type SerializedClosures as j, type SerializedSchema as k, type SchemaHash as l, type JsonSchema as m, type SerializedOntology as n, type SerializedUniqueConstraint as o, type SqlDialect as p, type EdgeIndexDirection as q, type EdgeIndexWhereBuilder as r, type IndexOrigin as s, type IndexScope as t, type IndexWhereFieldBuilder as u, type IndexWhereInput as v, type NodeIndexWhereBuilder as w, type SystemColumnName as x, type VectorIndexImplementation as y, type VectorIndexMetric as z };
@@ -1,5 +1,5 @@
1
- import { A as AnyEdgeType, N as NodeType } from './extension-types-D07e_NTw.cjs';
2
- import { E as EdgeIndexConfig, a as EdgeIndexDeclaration, N as NodeIndexConfig, b as NodeIndexDeclaration, c as IndexWhereExpression } from './types--0EslBx5.cjs';
1
+ import { A as AnyEdgeType, N as NodeType } from './extension-types-B78CZfA5.cjs';
2
+ import { E as EdgeIndexConfig, c as EdgeIndexDeclaration, N as NodeIndexConfig, d as NodeIndexDeclaration, e as IndexWhereExpression } from './types-DZPBXZOK.cjs';
3
3
 
4
4
  declare function defineNodeIndex<N extends NodeType>(node: N, config: NodeIndexConfig<N>): NodeIndexDeclaration;
5
5
  declare function defineEdgeIndex<E extends AnyEdgeType>(edge: E, config: EdgeIndexConfig<E>): EdgeIndexDeclaration;
@@ -1,5 +1,5 @@
1
- import { A as AnyEdgeType, N as NodeType } from './extension-types-D07e_NTw.js';
2
- import { E as EdgeIndexConfig, a as EdgeIndexDeclaration, N as NodeIndexConfig, b as NodeIndexDeclaration, c as IndexWhereExpression } from './types-BNWYZ-0-.js';
1
+ import { A as AnyEdgeType, N as NodeType } from './extension-types-B78CZfA5.js';
2
+ import { E as EdgeIndexConfig, c as EdgeIndexDeclaration, N as NodeIndexConfig, d as NodeIndexDeclaration, e as IndexWhereExpression } from './types-Dzkr0Eqi.js';
3
3
 
4
4
  declare function defineNodeIndex<N extends NodeType>(node: N, config: NodeIndexConfig<N>): NodeIndexDeclaration;
5
5
  declare function defineEdgeIndex<E extends AnyEdgeType>(edge: E, config: EdgeIndexConfig<E>): EdgeIndexDeclaration;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nicia-ai/typegraph",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "TypeScript-first embedded knowledge graph library with ontological reasoning",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -121,6 +121,7 @@
121
121
  "nanoid": "^5.1.11"
122
122
  },
123
123
  "devDependencies": {
124
+ "@cloudflare/vitest-pool-workers": "^0.16.6",
124
125
  "@libsql/client": "^0.17.3",
125
126
  "@neondatabase/serverless": "^1.1.0",
126
127
  "@stryker-mutator/core": "^9.6.1",
@@ -164,6 +165,7 @@
164
165
  "test:perf:postgres": "pnpm --filter @nicia-ai/typegraph-benchmarks perf:check:postgres",
165
166
  "bench:perf:postgres": "pnpm --filter @nicia-ai/typegraph-benchmarks perf:postgres",
166
167
  "test:postgres": "./scripts/test-postgres.sh",
168
+ "test:do": "vitest run --config vitest.workers.config.ts",
167
169
  "test:coverage": "vitest run --coverage",
168
170
  "test:mutation": "stryker run"
169
171
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/backend/types.ts"],"names":[],"mappings":";;;AA48BO,SAAS,oBAAA,CACd,SACA,QAAA,EACc;AACd,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,OAAO;AAAA,IACL,GAAG,OAAA;AAAA,IACH,MAAM,KAAA,GAAuB;AAC3B,MAAA,IAAI,QAAA,EAAU;AACd,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,MAAM,QAAQ,KAAA,EAAM;AACpB,MAAA,MAAM,QAAA,EAAS;AAAA,IACjB;AAAA,GACF;AACF;AAeA,eAAsB,0BAAA,CACpB,OAAA,EACA,EAAA,EACA,QAAA,EACY;AACZ,EAAA,IAAI,CAAC,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc;AACtC,IAAA,OAAO,EAAA,CAAG,YAAY,OAAO,CAAA;AAAA,EAC/B;AACA,EAAA,OAAO,QAAQ,WAAA,CAAY,CAAC,EAAA,KAAO,EAAA,CAAG,EAAE,CAAC,CAAA;AAC3C;AA4OO,IAAM,mBAAA,GAA2C;AAAA,EACtD,KAAA,EAAO,KAAA;AAAA;AAAA,EACP,cAAA,EAAgB,IAAA;AAAA;AAAA,EAChB,UAAA,EAAY,KAAA;AAAA;AAAA,EACZ,GAAA,EAAK,IAAA;AAAA;AAAA,EACL,SAAA,EAAW,IAAA;AAAA;AAAA,EACX,YAAA,EAAc;AAAA;AAChB;AAKO,IAAM,qBAAA,GAA6C;AAAA,EACxD,KAAA,EAAO,IAAA;AAAA;AAAA,EACP,cAAA,EAAgB,IAAA;AAAA,EAChB,UAAA,EAAY,IAAA;AAAA,EACZ,GAAA,EAAK,IAAA;AAAA,EACL,SAAA,EAAW,IAAA;AAAA,EACX,YAAA,EAAc;AAAA;AAChB","file":"chunk-7CDXX3QZ.cjs","sourcesContent":["/**\n * Backend interface types for TypeGraph storage.\n *\n * The backend abstracts database operations, allowing different\n * SQL implementations (SQLite, PostgreSQL) behind a common interface.\n */\nimport { type SQL } from \"drizzle-orm\";\n\nimport {\n type IndexEntity,\n type KindEntity,\n type TemporalMode,\n} from \"../core/types\";\nimport { type SqlTableNames } from \"../query/compiler/schema\";\nimport { type FulltextStrategy } from \"../query/dialect/fulltext-strategy\";\nimport { type SerializedSchema } from \"../schema/types\";\n\n// ============================================================\n// Vector Search Types\n// ============================================================\n\n/**\n * Supported vector similarity metrics.\n */\nexport type VectorMetric = \"cosine\" | \"l2\" | \"inner_product\";\n\n/**\n * Supported vector index types.\n */\nexport type VectorIndexType = \"hnsw\" | \"ivfflat\" | \"none\";\n\n/**\n * Vector search capabilities.\n */\nexport type VectorCapabilities = Readonly<{\n /** Whether the backend supports vector operations */\n supported: boolean;\n /** Supported similarity metrics */\n metrics: readonly VectorMetric[];\n /** Supported index types */\n indexTypes: readonly VectorIndexType[];\n /** Maximum dimensions supported */\n maxDimensions: number;\n}>;\n\n// ============================================================\n// Fulltext Search Types\n// ============================================================\n\n/**\n * Query modes for fulltext search.\n *\n * - \"websearch\": Google-style syntax (quoted phrases, +required, -excluded).\n * Postgres: `websearch_to_tsquery`. SQLite: translated to FTS5 MATCH.\n * - \"phrase\": treats the whole query as a phrase.\n * Postgres: `phraseto_tsquery`. SQLite: FTS5 `\"...\"` phrase.\n * - \"plain\": splits on whitespace and ANDs terms.\n * Postgres: `plainto_tsquery`. SQLite: default FTS5 AND.\n * - \"raw\": dialect-native syntax passed through unchanged.\n */\nexport type FulltextQueryMode = \"websearch\" | \"phrase\" | \"plain\" | \"raw\";\n\n/**\n * Fulltext search capabilities declared by a backend.\n */\nexport type FulltextCapabilities = Readonly<{\n /** Whether the backend supports fulltext operations */\n supported: boolean;\n /**\n * Language / tokenizer names understood by this backend.\n * Postgres: installed regconfigs (english, simple, ...).\n * SQLite FTS5: tokenizer names (unicode61, porter, trigram).\n */\n languages: readonly string[];\n /** Whether phrase queries are supported. */\n phraseQueries: boolean;\n /** Whether prefix (`foo*`) queries are supported. */\n prefixQueries: boolean;\n /** Whether highlighting / snippets are supported. */\n highlighting: boolean;\n}>;\n\n// ============================================================\n// SQL Dialect & Capabilities\n// ============================================================\n\nimport { type SqlDialect } from \"../query/dialect/types\";\n\nexport type { SqlDialect } from \"../query/dialect/types\";\n\n/**\n * Backend capabilities that vary by dialect.\n */\nexport type BackendCapabilities = Readonly<{\n /** Whether the backend supports JSONB type (vs TEXT for JSON) */\n jsonb: boolean;\n /** Whether the backend supports partial indexes */\n partialIndexes: boolean;\n /** Whether the backend supports GIN indexes for JSON */\n ginIndexes: boolean;\n /** Whether the backend supports CTE (WITH) queries */\n cte: boolean;\n /** Whether the backend supports RETURNING clause */\n returning: boolean;\n /** Whether the backend supports atomic transactions (D1 does not) */\n transactions: boolean;\n /** Vector search capabilities (undefined if not configured) */\n vector?: VectorCapabilities | undefined;\n /** Fulltext search capabilities (undefined if not configured) */\n fulltext?: FulltextCapabilities | undefined;\n}>;\n\n// ============================================================\n// Row Types (Database Records)\n// ============================================================\n\n/**\n * A row from the typegraph_nodes table.\n */\nexport type NodeRow = Readonly<{\n graph_id: string;\n kind: string;\n id: string;\n props: string; // JSON string\n version: number;\n valid_from: string | undefined;\n valid_to: string | undefined;\n created_at: string;\n updated_at: string;\n deleted_at: string | undefined;\n}>;\n\n/**\n * A row from the typegraph_edges table.\n */\nexport type EdgeRow = Readonly<{\n graph_id: string;\n id: string;\n kind: string;\n from_kind: string;\n from_id: string;\n to_kind: string;\n to_id: string;\n props: string; // JSON string\n valid_from: string | undefined;\n valid_to: string | undefined;\n created_at: string;\n updated_at: string;\n deleted_at: string | undefined;\n}>;\n\n/**\n * A row from the typegraph_node_uniques table.\n */\nexport type UniqueRow = Readonly<{\n graph_id: string;\n node_kind: string;\n constraint_name: string;\n key: string;\n node_id: string;\n concrete_kind: string;\n deleted_at: string | undefined;\n}>;\n\n/**\n * A row from the typegraph_schema_versions table.\n */\nexport type SchemaVersionRow = Readonly<{\n graph_id: string;\n version: number;\n schema_hash: string;\n schema_doc: string; // JSON string\n created_at: string;\n is_active: boolean;\n}>;\n\n/**\n * A row from the typegraph_node_embeddings table.\n */\nexport type EmbeddingRow = Readonly<{\n graph_id: string;\n node_kind: string;\n node_id: string;\n field_path: string;\n embedding: readonly number[];\n dimensions: number;\n created_at: string;\n updated_at: string;\n}>;\n\n// ============================================================\n// Insert Parameters\n// ============================================================\n\n/**\n * Parameters for inserting a node.\n */\nexport type InsertNodeParams = Readonly<{\n graphId: string;\n kind: string;\n id: string;\n props: Readonly<Record<string, unknown>>;\n validFrom?: string;\n validTo?: string;\n}>;\n\n/**\n * Parameters for updating a node.\n */\nexport type UpdateNodeParams = Readonly<{\n graphId: string;\n kind: string;\n id: string;\n props: Readonly<Record<string, unknown>>;\n validTo?: string;\n incrementVersion?: boolean;\n /** If true, clears deleted_at (un-deletes the node). Used by upsert. */\n clearDeleted?: boolean;\n}>;\n\n/**\n * Parameters for deleting a node (soft delete).\n */\nexport type DeleteNodeParams = Readonly<{\n graphId: string;\n kind: string;\n id: string;\n}>;\n\n/**\n * Parameters for inserting an edge.\n */\nexport type InsertEdgeParams = Readonly<{\n graphId: string;\n id: string;\n kind: string;\n fromKind: string;\n fromId: string;\n toKind: string;\n toId: string;\n props: Readonly<Record<string, unknown>>;\n validFrom?: string;\n validTo?: string;\n}>;\n\n/**\n * Parameters for updating an edge.\n */\nexport type UpdateEdgeParams = Readonly<{\n graphId: string;\n id: string;\n props: Readonly<Record<string, unknown>>;\n validTo?: string;\n clearDeleted?: boolean;\n}>;\n\n/**\n * Parameters for deleting an edge (soft delete).\n */\nexport type DeleteEdgeParams = Readonly<{\n graphId: string;\n id: string;\n}>;\n\n/**\n * Parameters for hard deleting a node (permanent removal).\n */\nexport type HardDeleteNodeParams = Readonly<{\n graphId: string;\n kind: string;\n id: string;\n}>;\n\n/**\n * Parameters for hard deleting an edge (permanent removal).\n */\nexport type HardDeleteEdgeParams = Readonly<{\n graphId: string;\n id: string;\n}>;\n\n// ============================================================\n// Embedding Parameters\n// ============================================================\n\n/**\n * Parameters for inserting or updating an embedding.\n */\nexport type UpsertEmbeddingParams = Readonly<{\n graphId: string;\n nodeKind: string;\n nodeId: string;\n fieldPath: string;\n embedding: readonly number[];\n dimensions: number;\n}>;\n\n/**\n * Parameters for deleting an embedding.\n */\nexport type DeleteEmbeddingParams = Readonly<{\n graphId: string;\n nodeKind: string;\n nodeId: string;\n fieldPath: string;\n}>;\n\n/**\n * Parameters for vector similarity search.\n */\nexport type VectorSearchParams = Readonly<{\n graphId: string;\n nodeKind: string;\n fieldPath: string;\n queryEmbedding: readonly number[];\n metric: VectorMetric;\n limit: number;\n minScore?: number;\n}>;\n\n/**\n * Result from a vector similarity search.\n */\nexport type VectorSearchResult = Readonly<{\n nodeId: string;\n /**\n * Cosine metric returns similarity score (higher is better).\n * L2 and inner_product return raw distance (lower is better).\n */\n score: number;\n}>;\n\n/**\n * Parameters for creating a vector index.\n */\nexport type CreateVectorIndexParams = Readonly<{\n graphId: string;\n nodeKind: string;\n fieldPath: string;\n dimensions: number;\n metric: VectorMetric;\n indexType: VectorIndexType;\n /** Index-specific parameters */\n indexParams?: Readonly<{\n /** HNSW: max connections per layer */\n m?: number;\n /** HNSW: construction search depth */\n efConstruction?: number;\n /** IVFFlat: number of lists */\n lists?: number;\n }>;\n /**\n * Build the index without taking an `AccessExclusiveLock` on live\n * tables (Postgres `CREATE INDEX CONCURRENTLY`). Mirrors the\n * `concurrent` flag the relational DDL path uses inside\n * `materializeIndexes()`. Cannot be set inside a transaction —\n * callers (`materializeIndexes()`) run at top level. Backends that\n * don't have a CONCURRENTLY equivalent (SQLite) ignore this flag.\n */\n concurrent?: boolean;\n}>;\n\n/**\n * Parameters for dropping a vector index.\n */\nexport type DropVectorIndexParams = Readonly<{\n graphId: string;\n nodeKind: string;\n fieldPath: string;\n}>;\n\n// ============================================================\n// Fulltext Parameters\n// ============================================================\n\n/**\n * Parameters for inserting or updating a fulltext entry.\n *\n * One row per node. Callers concatenate the searchable fields into\n * `content` so a single MATCH query can find terms spread across fields.\n *\n * Note: `createFulltextIndex` / `dropFulltextIndex` were removed as\n * dead code in #PR_E. The fulltext table's canonical index (Postgres\n * GIN on `tsv`, SQLite FTS5 virtual table) is created with the table\n * itself by `bootstrapTables` per the active `FulltextStrategy`;\n * per-kind fulltext indexes are an \"advanced strategy\" surface that\n * doesn't fit the relational-style declaration model and is reserved\n * for future work.\n */\nexport type UpsertFulltextParams = Readonly<{\n graphId: string;\n nodeKind: string;\n nodeId: string;\n content: string;\n language: string;\n}>;\n\n/**\n * Parameters for deleting a single fulltext entry.\n */\nexport type DeleteFulltextParams = Readonly<{\n graphId: string;\n nodeKind: string;\n nodeId: string;\n}>;\n\n/**\n * A single row in a fulltext batch upsert.\n */\nexport type FulltextBatchRow = Readonly<{\n nodeId: string;\n content: string;\n language: string;\n}>;\n\n/**\n * Parameters for a batched fulltext upsert.\n *\n * Homogeneous: one graph, one node kind, many nodes. Duplicate `nodeId`\n * values within a single batch are deduplicated last-write-wins by the\n * builders before SQL generation — Postgres `ON CONFLICT` errors on\n * repeated conflict keys in one statement, and SQLite `DELETE + INSERT`\n * would create duplicate virtual-table rows otherwise.\n */\nexport type UpsertFulltextBatchParams = Readonly<{\n graphId: string;\n nodeKind: string;\n rows: readonly FulltextBatchRow[];\n}>;\n\n/**\n * Parameters for a batched fulltext delete.\n */\nexport type DeleteFulltextBatchParams = Readonly<{\n graphId: string;\n nodeKind: string;\n nodeIds: readonly string[];\n}>;\n\n/**\n * Parameters for fulltext search.\n */\nexport type FulltextSearchParams = Readonly<{\n graphId: string;\n nodeKind: string;\n /** The user-supplied query string. */\n query: string;\n /** How to parse `query`. Default: \"websearch\". */\n mode?: FulltextQueryMode;\n /**\n * Language override. Default: per-row language (as stored at insert time).\n * Postgres: passed as the regconfig to `to_tsquery` / `websearch_to_tsquery`.\n * SQLite: informational only (FTS5 tokenizer is fixed at table-create time).\n */\n language?: string;\n /** Max rows to return. */\n limit: number;\n /** Minimum rank to include (backend-dependent units). */\n minScore?: number;\n /** Whether to return a highlighted snippet per match. */\n includeSnippets?: boolean;\n}>;\n\n/**\n * Result from a fulltext search.\n *\n * Score semantics differ by backend; prefer `rank` (1-based) when fusing\n * with another source via RRF.\n */\nexport type FulltextSearchResult = Readonly<{\n nodeId: string;\n /**\n * Backend-native relevance score.\n * Postgres: `ts_rank_cd` (higher is better).\n * SQLite FTS5: negated `bm25()` (higher is better; FTS5 returns lower-is-better).\n */\n score: number;\n /** 1-based rank within the result set, suitable for RRF. */\n rank: number;\n /** Highlighted snippet of the content (if `includeSnippets` was set). */\n snippet?: string;\n}>;\n\n/**\n * Parameters for creating a fulltext index.\n *\n * The canonical index (Postgres GIN on `tsv`, SQLite FTS5 virtual table)\n * is created when the fulltext table itself is created. This is reserved\n * for advanced per-kind specializations.\n */\n\n// ============================================================\n// Index Materialization Status\n// ============================================================\n\n/**\n * Per-deployment record for a single declared index.\n *\n * Identified by `indexName` because SQL index names are physical,\n * database-global identifiers — `graphId` is provenance, not identity.\n * `materializedAt` is null until the first successful CREATE INDEX\n * completes; `lastAttemptedAt` is always set, even on failure.\n */\nexport type IndexMaterializationRow = Readonly<{\n indexName: string;\n graphId: string;\n entity: IndexEntity;\n kind: string;\n signature: string;\n schemaVersion: number;\n materializedAt: string | undefined;\n lastAttemptedAt: string;\n lastError: string | undefined;\n}>;\n\n/**\n * Parameters for upserting a materialization attempt.\n *\n * On success: pass `materializedAt` (ISO timestamp) and undefined `error`.\n * On failure: pass undefined `materializedAt` (preserve any existing\n * timestamp from a prior success) and the error message.\n */\nexport type RecordIndexMaterializationParams = Readonly<{\n indexName: string;\n graphId: string;\n entity: IndexEntity;\n kind: string;\n signature: string;\n schemaVersion: number;\n attemptedAt: string;\n /** ISO timestamp on success; undefined on failure (preserves existing). */\n materializedAt: string | undefined;\n /** Error message on failure; undefined on success (clears existing). */\n error: string | undefined;\n}>;\n\n// ============================================================\n// Kind Removals (data-cleanup status)\n// ============================================================\n\n/**\n * One row of the per-deployment `typegraph_kind_removals` table:\n * a graph-extension kind that has been removed from the schema and whose\n * data may or may not have been cleaned up yet.\n */\nexport type KindRemovalRow = Readonly<{\n graphId: string;\n kindName: string;\n entity: KindEntity;\n schemaVersion: number;\n /** ISO timestamp when the data-cleanup pass succeeded; undefined while pending. */\n removedAt: string | undefined;\n lastAttemptedAt: string;\n lastError: string | undefined;\n}>;\n\n/**\n * Upsert payload for a kind-removal status row. On removal-commit:\n * pass `removedAt: undefined` (the pending state). On successful\n * data-cleanup: pass `removedAt` set and `error: undefined`. On\n * cleanup failure: pass `removedAt: undefined` (preserves any prior\n * timestamp from a partial success on a different replica) and the\n * error message.\n */\nexport type RecordKindRemovalParams = Readonly<{\n graphId: string;\n kindName: string;\n entity: KindEntity;\n schemaVersion: number;\n attemptedAt: string;\n /** ISO timestamp on success; undefined while pending or on failure. */\n removedAt: string | undefined;\n /** Error message on failure; undefined on success or while pending. */\n error: string | undefined;\n}>;\n\n// ============================================================\n// Query Types\n// ============================================================\n\n/**\n * Transaction options.\n */\nexport type TransactionOptions = Readonly<{\n /** Transaction isolation level (if supported) */\n isolationLevel?:\n | \"read_uncommitted\"\n | \"read_committed\"\n | \"repeatable_read\"\n | \"serializable\";\n}>;\n\n// ============================================================\n// Backend Interface\n// ============================================================\n\n/**\n * Transaction backend — a backend scoped to a transaction.\n *\n * `commitSchemaVersion` and `setActiveVersion` are intentionally omitted:\n * the atomicity / CAS guarantees of those primitives depend on\n * dialect-specific write-locking (BEGIN IMMEDIATE on SQLite,\n * `pg_advisory_xact_lock` on Postgres) acquired by the top-level\n * backend wrappers, not the transaction itself. Calling them from a\n * user-supplied `backend.transaction(...)` callback would bypass that\n * locking and silently weaken the orphan-row crash window the primitive\n * exists to eliminate. Schema commits go through the top-level backend\n * methods only.\n */\nexport type TransactionBackend = Omit<\n GraphBackend,\n | \"transaction\"\n | \"close\"\n | \"refreshStatistics\"\n | \"commitSchemaVersion\"\n | \"setActiveVersion\"\n>;\n\n/**\n * The GraphBackend interface abstracts database operations.\n *\n * Implementations should provide:\n * - SQLite backend via better-sqlite3 or libsql\n * - PostgreSQL backend via pg or postgres\n */\nexport type GraphBackend = Readonly<{\n /** The SQL dialect */\n dialect: SqlDialect;\n /** Backend capabilities */\n capabilities: BackendCapabilities;\n /** Table names used by this backend (for query schema auto-derivation) */\n tableNames?: SqlTableNames | undefined;\n /**\n * Optional fulltext strategy override. When present, both the compiler\n * (for `$fulltext.matches()` in query builder) and backend-direct\n * search paths use this instead of the dialect's default strategy —\n * allowing a Postgres backend to ship pg_trgm, ParadeDB, pgroonga etc.\n * When absent, the dialect's default strategy is used.\n */\n fulltextStrategy?: FulltextStrategy | undefined;\n\n // === Node Operations ===\n insertNode: (params: InsertNodeParams) => Promise<NodeRow>;\n insertNodeNoReturn?: (params: InsertNodeParams) => Promise<void>;\n insertNodesBatch?: (params: readonly InsertNodeParams[]) => Promise<void>;\n insertNodesBatchReturning?: (\n params: readonly InsertNodeParams[],\n ) => Promise<readonly NodeRow[]>;\n updateNode: (params: UpdateNodeParams) => Promise<NodeRow>;\n deleteNode: (params: DeleteNodeParams) => Promise<void>;\n hardDeleteNode: (params: HardDeleteNodeParams) => Promise<void>;\n getNode: (\n graphId: string,\n kind: string,\n id: string,\n ) => Promise<NodeRow | undefined>;\n getNodes?: (\n graphId: string,\n kind: string,\n ids: readonly string[],\n ) => Promise<readonly NodeRow[]>;\n\n // === Edge Operations ===\n insertEdge: (params: InsertEdgeParams) => Promise<EdgeRow>;\n insertEdgeNoReturn?: (params: InsertEdgeParams) => Promise<void>;\n insertEdgesBatch?: (params: readonly InsertEdgeParams[]) => Promise<void>;\n insertEdgesBatchReturning?: (\n params: readonly InsertEdgeParams[],\n ) => Promise<readonly EdgeRow[]>;\n updateEdge: (params: UpdateEdgeParams) => Promise<EdgeRow>;\n deleteEdge: (params: DeleteEdgeParams) => Promise<void>;\n hardDeleteEdge: (params: HardDeleteEdgeParams) => Promise<void>;\n getEdge: (graphId: string, id: string) => Promise<EdgeRow | undefined>;\n getEdges?: (\n graphId: string,\n ids: readonly string[],\n ) => Promise<readonly EdgeRow[]>;\n\n // === Edge Cardinality Operations ===\n countEdgesFrom: (params: CountEdgesFromParams) => Promise<number>;\n edgeExistsBetween: (params: EdgeExistsBetweenParams) => Promise<boolean>;\n\n // === Edge Query Operations ===\n findEdgesConnectedTo: (\n params: FindEdgesConnectedToParams,\n ) => Promise<readonly EdgeRow[]>;\n\n // === Collection Query Operations ===\n findNodesByKind: (\n params: FindNodesByKindParams,\n ) => Promise<readonly NodeRow[]>;\n countNodesByKind: (params: CountNodesByKindParams) => Promise<number>;\n findEdgesByKind: (\n params: FindEdgesByKindParams,\n ) => Promise<readonly EdgeRow[]>;\n countEdgesByKind: (params: CountEdgesByKindParams) => Promise<number>;\n\n // === Unique Constraint Operations ===\n insertUnique: (params: InsertUniqueParams) => Promise<void>;\n deleteUnique: (params: DeleteUniqueParams) => Promise<void>;\n checkUnique: (params: CheckUniqueParams) => Promise<UniqueRow | undefined>;\n checkUniqueBatch?: (\n params: CheckUniqueBatchParams,\n ) => Promise<readonly UniqueRow[]>;\n\n // === Schema Operations ===\n getActiveSchema: (graphId: string) => Promise<SchemaVersionRow | undefined>;\n getSchemaVersion: (\n graphId: string,\n version: number,\n ) => Promise<SchemaVersionRow | undefined>;\n /**\n * Atomically inserts a new schema version and activates it as a single\n * transactional unit, with optimistic compare-and-swap on the currently\n * active version.\n *\n * - If `expected.kind === \"active\"` and the actual active version\n * differs, throws `StaleVersionError` (caller should refetch and\n * retry).\n * - If a row already exists at `params.version` with the same\n * `schemaHash`, returns it idempotently — reactivating it if it was\n * left inactive by an earlier crashed commit.\n * - If a row already exists at `params.version` with a different\n * `schemaHash`, throws `SchemaContentConflictError`.\n *\n * Requires `capabilities.transactions === true`. On non-transactional\n * backends (e.g. Cloudflare D1, drizzle-orm/neon-http) this method\n * throws `ConfigurationError` rather than running with degraded\n * atomicity that would silently re-introduce the orphan-row crash\n * window the primitive exists to eliminate.\n */\n commitSchemaVersion: (\n params: CommitSchemaVersionParams,\n ) => Promise<SchemaVersionRow>;\n /**\n * Atomically flips the active schema pointer to an existing version,\n * with optimistic compare-and-swap on the currently active version.\n * Used by `rollbackSchema` and any other \"promote/demote existing\n * version\" workflow. Throws `StaleVersionError` on CAS mismatch and\n * `MigrationError` if the target version row does not exist.\n *\n * Same transactional requirements as `commitSchemaVersion`.\n */\n setActiveVersion: (params: SetActiveVersionParams) => Promise<void>;\n\n // === Embedding Operations (optional - depends on vector capabilities) ===\n upsertEmbedding?: (params: UpsertEmbeddingParams) => Promise<void>;\n deleteEmbedding?: (params: DeleteEmbeddingParams) => Promise<void>;\n getEmbedding?: (\n graphId: string,\n nodeKind: string,\n nodeId: string,\n fieldPath: string,\n ) => Promise<EmbeddingRow | undefined>;\n vectorSearch?: (\n params: VectorSearchParams,\n ) => Promise<readonly VectorSearchResult[]>;\n createVectorIndex?: (params: CreateVectorIndexParams) => Promise<void>;\n dropVectorIndex?: (params: DropVectorIndexParams) => Promise<void>;\n\n // === Fulltext Operations (optional - depends on fulltext capabilities) ===\n upsertFulltext?: (params: UpsertFulltextParams) => Promise<void>;\n deleteFulltext?: (params: DeleteFulltextParams) => Promise<void>;\n /**\n * Batched variant of `upsertFulltext`. Optional — callers fall back to\n * per-row `upsertFulltext` when unset.\n */\n upsertFulltextBatch?: (params: UpsertFulltextBatchParams) => Promise<void>;\n /**\n * Batched variant of `deleteFulltext`. Optional — callers fall back to\n * per-row `deleteFulltext` when unset.\n */\n deleteFulltextBatch?: (params: DeleteFulltextBatchParams) => Promise<void>;\n fulltextSearch?: (\n params: FulltextSearchParams,\n ) => Promise<readonly FulltextSearchResult[]>;\n\n // === Index Materialization (used by store.materializeIndexes) ===\n /**\n * Idempotently ensure ONLY the `typegraph_index_materializations`\n * table exists — separate from `bootstrapTables` so that\n * `materializeIndexes` doesn't pull in the full base-table DDL set\n * just to access the status table.\n *\n * Why focused: `bootstrapTables` issues 20+ `CREATE TABLE / CREATE\n * INDEX IF NOT EXISTS` statements covering every base table. Two\n * concurrent calls (e.g. two replicas of the same `schema_doc` both\n * starting up and calling `materializeIndexes`) race on\n * Postgres SHARE locks and DEADLOCK. Restricting the ensure-step to\n * the single status table eliminates the cross-table race entirely\n * — concurrent `CREATE TABLE IF NOT EXISTS` for one specific table\n * is well-behaved on Postgres.\n */\n ensureIndexMaterializationsTable?: () => Promise<void>;\n\n /**\n * Look up a recorded materialization for a declared index by its\n * physical SQL index name. Returns `undefined` if no row exists.\n */\n getIndexMaterialization?: (\n indexName: string,\n ) => Promise<IndexMaterializationRow | undefined>;\n /**\n * Bulk variant of `getIndexMaterialization`: load every recorded\n * materialization whose `indexName` (status key) is in `statusKeys`,\n * in a single round-trip. Returned rows are unordered — callers index\n * by `indexName`. Optional; consumers fall back to per-key\n * `getIndexMaterialization` when unset.\n */\n getIndexMaterializations?: (\n statusKeys: readonly string[],\n ) => Promise<readonly IndexMaterializationRow[]>;\n /**\n * Upsert a materialization attempt — success or failure. Failure rows\n * preserve any prior `materializedAt` so the historical successful\n * timestamp survives across error windows.\n */\n recordIndexMaterialization?: (\n params: RecordIndexMaterializationParams,\n ) => Promise<void>;\n\n // === Kind Removal Status ===\n\n /**\n * Bootstraps the per-deployment `typegraph_kind_removals` table so\n * `store.removeKinds()` and `store.materializeRemovals()` can persist\n * removal status. Mirrors the focused-bootstrap rationale documented\n * on `ensureIndexMaterializationsTable` — the full `bootstrapTables`\n * touches every base table and risks Postgres SHARE-lock deadlock\n * under concurrent replica startup.\n */\n ensureKindRemovalsTable?: () => Promise<void>;\n\n /**\n * List graph-extension kind removals whose data-cleanup pass has not yet\n * succeeded for this `graphId`. Returns rows with\n * `removedAt: undefined`. Order is unspecified; callers materialize\n * one-at-a-time and don't depend on it.\n */\n getPendingKindRemovals?: (\n graphId: string,\n ) => Promise<readonly KindRemovalRow[]>;\n\n /**\n * List ALL kind-removal rows for a `graphId` — pending and completed.\n * Used by `materializeRemovals()` reconciliation to detect rows that\n * are missing entirely (the `removeKinds()` crash window) versus\n * already completed. Without this distinction the reconciler would\n * have to upsert every expected historical removal on every call,\n * churning `last_attempted_at` on rows that long since succeeded.\n * Order is unspecified.\n */\n getAllKindRemovals?: (graphId: string) => Promise<readonly KindRemovalRow[]>;\n\n /**\n * Upsert a kind-removal status row. `removedAt: undefined` records\n * the pending state at schema-commit time; `removedAt: <iso>`\n * marks the data cleanup successful. The COALESCE rule on `removedAt`\n * mirrors `recordIndexMaterialization` so a later failure doesn't\n * clobber the historical successful timestamp from another replica.\n */\n recordKindRemoval?: (params: RecordKindRemovalParams) => Promise<void>;\n\n // === Reconciliation Watermark ===\n\n /**\n * Bootstraps the per-deployment `typegraph_reconciliation_markers`\n * table so `materializeRemovals()` can persist reconciliation\n * progress. Same focused-bootstrap rationale as the other status\n * tables — full `bootstrapTables` risks Postgres SHARE-lock\n * deadlock under concurrent replica startup.\n */\n ensureReconciliationMarkersTable?: () => Promise<void>;\n\n /**\n * Read the high-water mark schema version for which\n * `materializeRemovals` reconciliation has already verified history\n * for `graphId`. Returns `undefined` when no marker has been\n * recorded yet. Used to skip already-checked transitions in the\n * recovery walk.\n */\n getReconciliationMarker?: (graphId: string) => Promise<number | undefined>;\n\n /**\n * Persist the reconciliation high-water mark for `graphId`. Called\n * after `materializeRemovals` completes a clean walk; subsequent\n * calls walk only versions newer than this marker. Idempotent\n * upsert by `graphId`.\n */\n setReconciliationMarker?: (graphId: string, version: number) => Promise<void>;\n\n // === Graph Lifecycle ===\n /**\n * Hard-deletes all data for a graph (nodes, edges, uniques, embeddings, schema versions).\n * Intended for import-replacement workflows. No hooks, no per-row logic.\n */\n clearGraph: (graphId: string) => Promise<void>;\n\n /**\n * Creates the base TypeGraph tables if they don't already exist.\n *\n * Called automatically by `createStoreWithSchema()` when a fresh database\n * is detected. Users who manage DDL themselves via `createStore()` never\n * hit this path.\n */\n bootstrapTables?: () => Promise<void>;\n\n /**\n * Refreshes the backend's query-planner statistics.\n *\n * Call this once after a large initial import or bulk backfill. Without\n * up-to-date statistics, the planner can pick suboptimal execution plans\n * — on PostgreSQL this is the difference between a 0.5ms and a 5ms\n * forward traversal; on SQLite it's the difference between 0.9ms and\n * 23ms fulltext search. Autovacuum / background statistics collection\n * will catch up eventually, but calling this explicitly after a bulk\n * load gives you correct latencies immediately.\n *\n * Implementations:\n * - SQLite runs `ANALYZE`, which populates `sqlite_stat1`\n * - PostgreSQL runs `ANALYZE` on the TypeGraph-managed tables\n *\n * Safe to call at any time; costs a few tens of milliseconds on the\n * sizes this library is designed for.\n */\n refreshStatistics: () => Promise<void>;\n\n // === Query Execution ===\n execute: <T>(query: SQL) => Promise<readonly T[]>;\n\n /** Execute pre-compiled SQL text with bound parameters. Available on sync SQLite and pg backends. */\n executeRaw?: <T>(\n sqlText: string,\n params: readonly unknown[],\n ) => Promise<readonly T[]>;\n\n /** Compile a Drizzle SQL object to { sql, params } without executing. */\n compileSql?: (\n query: SQL,\n ) => Readonly<{ sql: string; params: readonly unknown[] }>;\n\n /**\n * Execute a DDL statement that returns no rows (CREATE INDEX,\n * CREATE TABLE, ALTER TABLE, etc.). Separate from `executeRaw`\n * because some drivers (better-sqlite3) require `.run()` for DDL\n * and `.all()` for queries — the ambiguity can't be resolved by\n * inspecting the SQL string portably.\n *\n * Postgres path can use this for `CREATE INDEX CONCURRENTLY`, which\n * cannot run inside a transaction. Implementations must execute the\n * statement outside `transaction(...)`.\n */\n executeDdl?: (ddl: string) => Promise<void>;\n\n // === Transaction ===\n transaction: <T>(\n fn: (tx: TransactionBackend) => Promise<T>,\n options?: TransactionOptions,\n ) => Promise<T>;\n\n // === Lifecycle ===\n close: () => Promise<void>;\n}>;\n\n// ============================================================\n// Managed Backend Helper\n// ============================================================\n\n/**\n * Wraps a GraphBackend with idempotent close that also runs a teardown\n * callback (e.g. closing the underlying database connection).\n */\nexport function wrapWithManagedClose(\n backend: GraphBackend,\n teardown: () => void | Promise<void>,\n): GraphBackend {\n let isClosed = false;\n return {\n ...backend,\n async close(): Promise<void> {\n if (isClosed) return;\n isClosed = true;\n await backend.close();\n await teardown();\n },\n };\n}\n\n/**\n * Runs `fn` inside a transaction when the backend supports one, falling\n * through to a direct invocation otherwise. Lets call sites benefit from\n * atomicity on backends that have transactions while staying functional\n * on backends that don't (Cloudflare D1, `drizzle-orm/neon-http` over\n * HTTP). The single-statement race window is already implicit on any\n * backend that reports `transactions: false`; callers that cannot\n * tolerate it must branch on the capability themselves.\n *\n * Pass `fallback` only when the toplevel backend method would recurse\n * — pass the operation-level backend so the no-tx path doesn't loop\n * back through the same toplevel method.\n */\nexport async function runOptionallyInTransaction<T>(\n backend: GraphBackend,\n fn: (target: GraphBackend | TransactionBackend) => Promise<T>,\n fallback?: GraphBackend | TransactionBackend,\n): Promise<T> {\n if (!backend.capabilities.transactions) {\n return fn(fallback ?? backend);\n }\n return backend.transaction((tx) => fn(tx));\n}\n\n// ============================================================\n// Additional Parameter Types\n// ============================================================\n\n/**\n * Parameters for inserting a unique constraint entry.\n */\nexport type InsertUniqueParams = Readonly<{\n graphId: string;\n nodeKind: string;\n constraintName: string;\n key: string;\n nodeId: string;\n concreteKind: string;\n}>;\n\n/**\n * Parameters for deleting a unique constraint entry.\n */\nexport type DeleteUniqueParams = Readonly<{\n graphId: string;\n nodeKind: string;\n constraintName: string;\n key: string;\n}>;\n\n/**\n * Parameters for checking a unique constraint.\n */\nexport type CheckUniqueParams = Readonly<{\n graphId: string;\n nodeKind: string;\n constraintName: string;\n key: string;\n /** If true, also returns soft-deleted entries. Used by get-or-create operations. */\n includeDeleted?: boolean;\n}>;\n\n/**\n * Parameters for batch-checking unique constraints.\n */\nexport type CheckUniqueBatchParams = Readonly<{\n graphId: string;\n nodeKind: string;\n constraintName: string;\n keys: readonly string[];\n /** If true, also returns soft-deleted entries. Used by get-or-create operations. */\n includeDeleted?: boolean;\n}>;\n\n/**\n * Parameters for inserting a schema version.\n *\n * Used internally by backend implementations. Public callers go through\n * `commitSchemaVersion`, which handles the insert + activate atomically\n * with CAS guarantees.\n */\nexport type InsertSchemaParams = Readonly<{\n graphId: string;\n version: number;\n schemaHash: string;\n schemaDoc: SerializedSchema;\n isActive: boolean;\n}>;\n\n/**\n * The caller's claim about the currently-active schema version, used as\n * the optimistic compare-and-swap guard for `commitSchemaVersion`.\n *\n * - `{ kind: \"initial\" }` — caller is committing the first-ever version\n * for this graph and asserts no active version exists yet.\n * - `{ kind: \"active\", version: N }` — caller observed version N as\n * active and is committing version N+1 against that baseline. The\n * commit fails with `StaleVersionError` if some other writer has\n * advanced or rolled back the pointer in the meantime.\n *\n * Tagged-union form (rather than a magic `version: 0` sentinel) because\n * the two cases have materially different semantics: the initial path\n * skips the \"deactivate prior\" UPDATE, and the no-active-row state is\n * a *valid* expected state, not an out-of-band signal.\n */\nexport type CommitSchemaVersionExpected =\n | Readonly<{ kind: \"initial\" }>\n | Readonly<{ kind: \"active\"; version: number }>;\n\n/**\n * Parameters for `commitSchemaVersion`.\n */\nexport type CommitSchemaVersionParams = Readonly<{\n graphId: string;\n /** CAS guard — see `CommitSchemaVersionExpected`. */\n expected: CommitSchemaVersionExpected;\n /** The new version to insert and activate. */\n version: number;\n schemaHash: string;\n schemaDoc: SerializedSchema;\n}>;\n\n/**\n * Parameters for `setActiveVersion`.\n *\n * Flips the active pointer from `expected` to `version` for an existing\n * row. CAS prevents overwriting a concurrent rollback or commit.\n */\nexport type SetActiveVersionParams = Readonly<{\n graphId: string;\n /** CAS guard. Same semantics as `commitSchemaVersion.expected`. */\n expected: CommitSchemaVersionExpected;\n /** The version to mark active. Must already exist. */\n version: number;\n}>;\n\n/**\n * Parameters for counting edges from a source node.\n */\nexport type CountEdgesFromParams = Readonly<{\n graphId: string;\n edgeKind: string;\n fromKind: string;\n fromId: string;\n /** If true, only count edges where valid_to IS NULL */\n activeOnly?: boolean;\n}>;\n\n/**\n * Parameters for checking if an edge exists between two nodes.\n */\nexport type EdgeExistsBetweenParams = Readonly<{\n graphId: string;\n edgeKind: string;\n fromKind: string;\n fromId: string;\n toKind: string;\n toId: string;\n}>;\n\n/**\n * Parameters for finding edges connected to a node.\n */\nexport type FindEdgesConnectedToParams = Readonly<{\n graphId: string;\n nodeKind: string;\n nodeId: string;\n}>;\n\n/**\n * Parameters for finding nodes by kind.\n */\nexport type FindNodesByKindParams = Readonly<{\n graphId: string;\n kind: string;\n /** Max rows to return. */\n limit?: number;\n /** Offset. Present for backward compat; rebuild uses `after` instead. */\n offset?: number;\n /** If true, exclude deleted nodes. Default true. */\n excludeDeleted?: boolean;\n /** Temporal mode for filtering by validity period. */\n temporalMode?: TemporalMode;\n /** Timestamp for \"current\" and \"asOf\" temporal modes. */\n asOf?: string;\n /**\n * Stable ordering for keyset pagination. Default: \"created_at\" (existing\n * behavior). Rebuild should use \"id\" for iteration that is stable under\n * concurrent writes and shared timestamps.\n */\n orderBy?: \"id\" | \"created_at\";\n /**\n * Keyset cursor. Returns rows strictly greater (by `orderBy`) than this\n * value. When `orderBy: \"id\"`, compared lexicographically. Mutually\n * exclusive with `offset` — callers pick one.\n */\n after?: string;\n}>;\n\n/**\n * Parameters for counting nodes by kind.\n */\nexport type CountNodesByKindParams = Readonly<{\n graphId: string;\n kind: string;\n /** If true, exclude deleted nodes. Default true. */\n excludeDeleted?: boolean;\n /** Temporal mode for filtering by validity period. */\n temporalMode?: TemporalMode;\n /** Timestamp for \"current\" and \"asOf\" temporal modes. */\n asOf?: string;\n}>;\n\n/**\n * Parameters for finding edges by kind.\n */\nexport type FindEdgesByKindParams = Readonly<{\n graphId: string;\n kind: string;\n fromKind?: string;\n fromId?: string;\n toKind?: string;\n toId?: string;\n limit?: number;\n offset?: number;\n /** If true, exclude deleted edges. Default true. */\n excludeDeleted?: boolean;\n /** Temporal mode for filtering by validity period. */\n temporalMode?: TemporalMode;\n /** Timestamp for \"current\" and \"asOf\" temporal modes. */\n asOf?: string;\n}>;\n\n/**\n * Parameters for counting edges by kind.\n */\nexport type CountEdgesByKindParams = Readonly<{\n graphId: string;\n kind: string;\n fromKind?: string;\n fromId?: string;\n toKind?: string;\n toId?: string;\n /** If true, exclude deleted edges. Default true. */\n excludeDeleted?: boolean;\n /** Temporal mode for filtering by validity period. */\n temporalMode?: TemporalMode;\n /** Timestamp for \"current\" and \"asOf\" temporal modes. */\n asOf?: string;\n}>;\n\n// ============================================================\n// Default Capabilities\n// ============================================================\n\n/**\n * Default capabilities for SQLite.\n */\nexport const SQLITE_CAPABILITIES: BackendCapabilities = {\n jsonb: false, // SQLite uses TEXT with json functions\n partialIndexes: true, // SQLite supports WHERE in CREATE INDEX\n ginIndexes: false, // SQLite doesn't have GIN\n cte: true, // SQLite supports WITH\n returning: true, // SQLite 3.35+ supports RETURNING\n transactions: true, // SQLite supports transactions\n};\n\n/**\n * Default capabilities for PostgreSQL.\n */\nexport const POSTGRES_CAPABILITIES: BackendCapabilities = {\n jsonb: true, // PostgreSQL has native JSONB\n partialIndexes: true,\n ginIndexes: true,\n cte: true,\n returning: true,\n transactions: true, // PostgreSQL supports transactions\n};\n"]}