ai-database 2.1.3 → 2.3.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 (260) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +880 -669
  3. package/dist/actions.d.ts +2 -2
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +1 -1
  6. package/dist/actions.js.map +1 -1
  7. package/dist/ai-promise-db.d.ts +49 -23
  8. package/dist/ai-promise-db.d.ts.map +1 -1
  9. package/dist/ai-promise-db.js +91 -63
  10. package/dist/ai-promise-db.js.map +1 -1
  11. package/dist/authorization.d.ts.map +1 -1
  12. package/dist/authorization.js +38 -30
  13. package/dist/authorization.js.map +1 -1
  14. package/dist/cascade-orchestrator.d.ts +404 -0
  15. package/dist/cascade-orchestrator.d.ts.map +1 -0
  16. package/dist/cascade-orchestrator.js +828 -0
  17. package/dist/cascade-orchestrator.js.map +1 -0
  18. package/dist/cascade-write-strategy.d.ts +584 -0
  19. package/dist/cascade-write-strategy.d.ts.map +1 -0
  20. package/dist/cascade-write-strategy.js +590 -0
  21. package/dist/cascade-write-strategy.js.map +1 -0
  22. package/dist/ch-adapter.d.ts +358 -0
  23. package/dist/ch-adapter.d.ts.map +1 -0
  24. package/dist/ch-adapter.js +929 -0
  25. package/dist/ch-adapter.js.map +1 -0
  26. package/dist/client/index.d.ts +42 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +43 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client.d.ts +266 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +81 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/constants.d.ts +64 -1
  35. package/dist/constants.d.ts.map +1 -1
  36. package/dist/constants.js +52 -2
  37. package/dist/constants.js.map +1 -1
  38. package/dist/dataloader.d.ts +99 -0
  39. package/dist/dataloader.d.ts.map +1 -0
  40. package/dist/dataloader.js +225 -0
  41. package/dist/dataloader.js.map +1 -0
  42. package/dist/db-provider-port.d.ts +501 -0
  43. package/dist/db-provider-port.d.ts.map +1 -0
  44. package/dist/db-provider-port.js +113 -0
  45. package/dist/db-provider-port.js.map +1 -0
  46. package/dist/digital-objects-provider.d.ts +49 -0
  47. package/dist/digital-objects-provider.d.ts.map +1 -0
  48. package/dist/digital-objects-provider.js +55 -0
  49. package/dist/digital-objects-provider.js.map +1 -0
  50. package/dist/do-sqlite-adapter.d.ts +402 -0
  51. package/dist/do-sqlite-adapter.d.ts.map +1 -0
  52. package/dist/do-sqlite-adapter.js +745 -0
  53. package/dist/do-sqlite-adapter.js.map +1 -0
  54. package/dist/docs-rels/custom-types.d.ts +134 -0
  55. package/dist/docs-rels/custom-types.d.ts.map +1 -0
  56. package/dist/docs-rels/custom-types.js +70 -0
  57. package/dist/docs-rels/custom-types.js.map +1 -0
  58. package/dist/docs-rels/index.d.ts +16 -0
  59. package/dist/docs-rels/index.d.ts.map +1 -0
  60. package/dist/docs-rels/index.js +16 -0
  61. package/dist/docs-rels/index.js.map +1 -0
  62. package/dist/docs-rels/migrations/index.d.ts +30 -0
  63. package/dist/docs-rels/migrations/index.d.ts.map +1 -0
  64. package/dist/docs-rels/migrations/index.js +128 -0
  65. package/dist/docs-rels/migrations/index.js.map +1 -0
  66. package/dist/docs-rels/schema.d.ts +2961 -0
  67. package/dist/docs-rels/schema.d.ts.map +1 -0
  68. package/dist/docs-rels/schema.js +244 -0
  69. package/dist/docs-rels/schema.js.map +1 -0
  70. package/dist/durable-clickhouse.d.ts.map +1 -1
  71. package/dist/durable-clickhouse.js +16 -13
  72. package/dist/durable-clickhouse.js.map +1 -1
  73. package/dist/durable-promise.d.ts.map +1 -1
  74. package/dist/durable-promise.js +34 -15
  75. package/dist/durable-promise.js.map +1 -1
  76. package/dist/errors.d.ts +127 -0
  77. package/dist/errors.d.ts.map +1 -0
  78. package/dist/errors.js +210 -0
  79. package/dist/errors.js.map +1 -0
  80. package/dist/eventbridge.d.ts +117 -0
  81. package/dist/eventbridge.d.ts.map +1 -0
  82. package/dist/eventbridge.js +238 -0
  83. package/dist/eventbridge.js.map +1 -0
  84. package/dist/events.d.ts +2 -2
  85. package/dist/events.d.ts.map +1 -1
  86. package/dist/events.js +1 -1
  87. package/dist/events.js.map +1 -1
  88. package/dist/execution-queue.d.ts.map +1 -1
  89. package/dist/execution-queue.js +4 -5
  90. package/dist/execution-queue.js.map +1 -1
  91. package/dist/index.d.ts +35 -8
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +106 -6
  94. package/dist/index.js.map +1 -1
  95. package/dist/linguistic.d.ts +3 -108
  96. package/dist/linguistic.d.ts.map +1 -1
  97. package/dist/linguistic.js +3 -372
  98. package/dist/linguistic.js.map +1 -1
  99. package/dist/logger.d.ts +132 -0
  100. package/dist/logger.d.ts.map +1 -0
  101. package/dist/logger.js +137 -0
  102. package/dist/logger.js.map +1 -0
  103. package/dist/memory-provider.d.ts +128 -0
  104. package/dist/memory-provider.d.ts.map +1 -1
  105. package/dist/memory-provider.js +592 -257
  106. package/dist/memory-provider.js.map +1 -1
  107. package/dist/pg-adapter.d.ts +424 -0
  108. package/dist/pg-adapter.d.ts.map +1 -0
  109. package/dist/pg-adapter.js +921 -0
  110. package/dist/pg-adapter.js.map +1 -0
  111. package/dist/pipelines-iceberg-emitter.d.ts +327 -0
  112. package/dist/pipelines-iceberg-emitter.d.ts.map +1 -0
  113. package/dist/pipelines-iceberg-emitter.js +351 -0
  114. package/dist/pipelines-iceberg-emitter.js.map +1 -0
  115. package/dist/provider-capabilities.d.ts +146 -0
  116. package/dist/provider-capabilities.d.ts.map +1 -0
  117. package/dist/provider-capabilities.js +214 -0
  118. package/dist/provider-capabilities.js.map +1 -0
  119. package/dist/rdb-provider-adapter.d.ts +195 -0
  120. package/dist/rdb-provider-adapter.d.ts.map +1 -0
  121. package/dist/rdb-provider-adapter.js +291 -0
  122. package/dist/rdb-provider-adapter.js.map +1 -0
  123. package/dist/schema/cascade.d.ts +48 -17
  124. package/dist/schema/cascade.d.ts.map +1 -1
  125. package/dist/schema/cascade.js +477 -278
  126. package/dist/schema/cascade.js.map +1 -1
  127. package/dist/schema/definition-caches.d.ts +24 -0
  128. package/dist/schema/definition-caches.d.ts.map +1 -0
  129. package/dist/schema/definition-caches.js +26 -0
  130. package/dist/schema/definition-caches.js.map +1 -0
  131. package/dist/schema/dependency-graph.d.ts +21 -109
  132. package/dist/schema/dependency-graph.d.ts.map +1 -1
  133. package/dist/schema/dependency-graph.js +25 -333
  134. package/dist/schema/dependency-graph.js.map +1 -1
  135. package/dist/schema/diff.d.ts +103 -0
  136. package/dist/schema/diff.d.ts.map +1 -0
  137. package/dist/schema/diff.js +329 -0
  138. package/dist/schema/diff.js.map +1 -0
  139. package/dist/schema/entity-operations.d.ts +99 -0
  140. package/dist/schema/entity-operations.d.ts.map +1 -0
  141. package/dist/schema/entity-operations.js +818 -0
  142. package/dist/schema/entity-operations.js.map +1 -0
  143. package/dist/schema/index.d.ts +28 -34
  144. package/dist/schema/index.d.ts.map +1 -1
  145. package/dist/schema/index.js +454 -521
  146. package/dist/schema/index.js.map +1 -1
  147. package/dist/schema/migration.d.ts +205 -0
  148. package/dist/schema/migration.d.ts.map +1 -0
  149. package/dist/schema/migration.js +327 -0
  150. package/dist/schema/migration.js.map +1 -0
  151. package/dist/schema/nl-query-generator.d.ts +68 -0
  152. package/dist/schema/nl-query-generator.d.ts.map +1 -0
  153. package/dist/schema/nl-query-generator.js +362 -0
  154. package/dist/schema/nl-query-generator.js.map +1 -0
  155. package/dist/schema/nl-query.d.ts +65 -0
  156. package/dist/schema/nl-query.d.ts.map +1 -0
  157. package/dist/schema/nl-query.js +178 -0
  158. package/dist/schema/nl-query.js.map +1 -0
  159. package/dist/schema/parse.d.ts.map +1 -1
  160. package/dist/schema/parse.js +144 -89
  161. package/dist/schema/parse.js.map +1 -1
  162. package/dist/schema/provider.d.ts +37 -0
  163. package/dist/schema/provider.d.ts.map +1 -1
  164. package/dist/schema/provider.js +15 -7
  165. package/dist/schema/provider.js.map +1 -1
  166. package/dist/schema/resolve.d.ts +46 -5
  167. package/dist/schema/resolve.d.ts.map +1 -1
  168. package/dist/schema/resolve.js +237 -95
  169. package/dist/schema/resolve.js.map +1 -1
  170. package/dist/schema/search-utils.d.ts +76 -0
  171. package/dist/schema/search-utils.d.ts.map +1 -0
  172. package/dist/schema/search-utils.js +86 -0
  173. package/dist/schema/search-utils.js.map +1 -0
  174. package/dist/schema/seed.d.ts +53 -0
  175. package/dist/schema/seed.d.ts.map +1 -0
  176. package/dist/schema/seed.js +94 -0
  177. package/dist/schema/seed.js.map +1 -0
  178. package/dist/schema/semantic.d.ts +10 -0
  179. package/dist/schema/semantic.d.ts.map +1 -1
  180. package/dist/schema/semantic.js +192 -86
  181. package/dist/schema/semantic.js.map +1 -1
  182. package/dist/schema/sub-apis.d.ts +52 -0
  183. package/dist/schema/sub-apis.d.ts.map +1 -0
  184. package/dist/schema/sub-apis.js +216 -0
  185. package/dist/schema/sub-apis.js.map +1 -0
  186. package/dist/schema/system-entities.d.ts +42 -0
  187. package/dist/schema/system-entities.d.ts.map +1 -0
  188. package/dist/schema/system-entities.js +101 -0
  189. package/dist/schema/system-entities.js.map +1 -0
  190. package/dist/schema/types.d.ts +91 -9
  191. package/dist/schema/types.d.ts.map +1 -1
  192. package/dist/schema/union-fallback.d.ts.map +1 -1
  193. package/dist/schema/union-fallback.js +21 -15
  194. package/dist/schema/union-fallback.js.map +1 -1
  195. package/dist/schema/value-generators/ai.d.ts +54 -0
  196. package/dist/schema/value-generators/ai.d.ts.map +1 -0
  197. package/dist/schema/value-generators/ai.js +136 -0
  198. package/dist/schema/value-generators/ai.js.map +1 -0
  199. package/dist/schema/value-generators/index.d.ts +126 -0
  200. package/dist/schema/value-generators/index.d.ts.map +1 -0
  201. package/dist/schema/value-generators/index.js +219 -0
  202. package/dist/schema/value-generators/index.js.map +1 -0
  203. package/dist/schema/value-generators/placeholder.d.ts +52 -0
  204. package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
  205. package/dist/schema/value-generators/placeholder.js +328 -0
  206. package/dist/schema/value-generators/placeholder.js.map +1 -0
  207. package/dist/schema/value-generators/types.d.ts +116 -0
  208. package/dist/schema/value-generators/types.d.ts.map +1 -0
  209. package/dist/schema/value-generators/types.js +11 -0
  210. package/dist/schema/value-generators/types.js.map +1 -0
  211. package/dist/schema/version.d.ts +111 -0
  212. package/dist/schema/version.d.ts.map +1 -0
  213. package/dist/schema/version.js +190 -0
  214. package/dist/schema/version.js.map +1 -0
  215. package/dist/schema.d.ts +1095 -24
  216. package/dist/schema.d.ts.map +1 -1
  217. package/dist/schema.js +2852 -40
  218. package/dist/schema.js.map +1 -1
  219. package/dist/semantic-vectors.d.ts +39 -0
  220. package/dist/semantic-vectors.d.ts.map +1 -0
  221. package/dist/semantic-vectors.js +334 -0
  222. package/dist/semantic-vectors.js.map +1 -0
  223. package/dist/semantic.d.ts +29 -1
  224. package/dist/semantic.d.ts.map +1 -1
  225. package/dist/semantic.js +26 -16
  226. package/dist/semantic.js.map +1 -1
  227. package/dist/telemetry.d.ts +128 -0
  228. package/dist/telemetry.d.ts.map +1 -0
  229. package/dist/telemetry.js +305 -0
  230. package/dist/telemetry.js.map +1 -0
  231. package/dist/tests.d.ts.map +1 -1
  232. package/dist/tests.js +30 -22
  233. package/dist/tests.js.map +1 -1
  234. package/dist/type-guards.d.ts +50 -5
  235. package/dist/type-guards.d.ts.map +1 -1
  236. package/dist/type-guards.js +87 -16
  237. package/dist/type-guards.js.map +1 -1
  238. package/dist/types.d.ts +33 -245
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +62 -72
  241. package/dist/types.js.map +1 -1
  242. package/dist/validation.d.ts +2 -5
  243. package/dist/validation.d.ts.map +1 -1
  244. package/dist/validation.js +65 -93
  245. package/dist/validation.js.map +1 -1
  246. package/dist/worker/db-provider.d.ts +168 -0
  247. package/dist/worker/db-provider.d.ts.map +1 -0
  248. package/dist/worker/db-provider.js +277 -0
  249. package/dist/worker/db-provider.js.map +1 -0
  250. package/dist/worker/index.d.ts +35 -0
  251. package/dist/worker/index.d.ts.map +1 -0
  252. package/dist/worker/index.js +37 -0
  253. package/dist/worker/index.js.map +1 -0
  254. package/dist/worker.d.ts +779 -0
  255. package/dist/worker.d.ts.map +1 -0
  256. package/dist/worker.js +2786 -0
  257. package/dist/worker.js.map +1 -0
  258. package/package.json +46 -16
  259. package/src/docs-rels/migrations/0001-init.sql +125 -0
  260. package/LICENSE +0 -21
@@ -0,0 +1,921 @@
1
+ /**
2
+ * Postgres + pgvector DBProvider adapter (Stack A transactional layer)
3
+ *
4
+ * First-class adapter for Stack A per
5
+ * [ADR-0003](../../../docs/adr/0003-storage-strategy-pg-clickhouse-default.md).
6
+ * Implements the {@link DBProviderPort} surface with:
7
+ *
8
+ * - **Tier 1 (entity CRUD)** against a `things(ns, id, type, data jsonb)` table.
9
+ * - **Tier 2 (graph traversal)** via Actions: subject/object/recipient/...
10
+ * queries against an `actions(ns, id, verb, subject, object, roles jsonb,
11
+ * data jsonb, status, created_at, completed_at)` table.
12
+ * - **Tier 3 (analytics)** declared with caveats: aggregations + time-series
13
+ * work, but adapters running large scans contend with transactional load.
14
+ * `analyticsQuery` is exposed for ad-hoc SQL.
15
+ * - **Tier 4 (vector search)** native via `pgvector` (max 16,000 dims;
16
+ * `cosine`/`l2`/`dot` metrics implemented). Embeddings live in a
17
+ * companion `embeddings` table joined to `things`; callers seed it via
18
+ * {@link PostgresProvider.upsertEmbedding} and query via
19
+ * {@link PostgresProvider.vectorSearch}. Frame-aware role filtering
20
+ * (e.g. "only Things appearing as `subject` of a Verb") is deferred to
21
+ * a follow-up refinement bead.
22
+ * - **SVO Action recording** + **Verb registry** declared via
23
+ * `hasActionRecording: true` and `hasVerbRegistry: true`.
24
+ * - **Sharding**: `partitioned-by-tenant` by default; the `ns` namespace
25
+ * column doubles as the partition key. Callers can run a shared schema
26
+ * across tenants (one row per `(ns, id)`) or partition the underlying
27
+ * `things` / `actions` tables on `ns` for physical isolation. Either
28
+ * way, this adapter writes through `ns`.
29
+ *
30
+ * ## Cascade write path
31
+ *
32
+ * Per the substrate-write-probes verdict (`docs/reviews/2026-05-05-cascade-poc-evaluation.md`),
33
+ * the cascade-scale write path is **bulk-VALUES CTE with `ON CONFLICT DO
34
+ * NOTHING`** — 91 ms p50 for 500 things + 499 actions in one round-trip
35
+ * on Neon HTTP. {@link PostgresProvider.commitBatch} implements that
36
+ * shape directly.
37
+ *
38
+ * ## Driver choice
39
+ *
40
+ * The adapter is driver-agnostic — it consumes any {@link PgExecutor}
41
+ * (a function that takes SQL + positional params and returns rows).
42
+ * Helpers ship for the two common shapes:
43
+ *
44
+ * - {@link createNeonHttpExecutor} — `@neondatabase/serverless` HTTP
45
+ * driver. **Preferred for the cascade workload** per the probes (~2x
46
+ * over Hyperdrive on every short-burst write shape; sublinear scaling
47
+ * above N=100; 91 ms for 500 docs + 499 rels). Works in Cloudflare
48
+ * Workers and Node.
49
+ * - {@link createPgClientExecutor} — wraps a `postgres.js` (Hyperdrive)
50
+ * client. Use when the deployment specifically needs connection
51
+ * pooling / pipelining; **not** the cascade fast path. Hyperdrive's
52
+ * parameterless-only response cache is a **trap** — once `$1` appears,
53
+ * it stops caching. Document this for callers.
54
+ *
55
+ * ## Schema
56
+ *
57
+ * The adapter assumes a schema named `aidb` (configurable). DDL:
58
+ *
59
+ * ```sql
60
+ * CREATE EXTENSION IF NOT EXISTS vector;
61
+ * CREATE SCHEMA IF NOT EXISTS aidb;
62
+ *
63
+ * CREATE TABLE IF NOT EXISTS aidb.things (
64
+ * ns text NOT NULL,
65
+ * id text NOT NULL,
66
+ * type text NOT NULL,
67
+ * data jsonb NOT NULL DEFAULT '{}'::jsonb,
68
+ * created_at timestamptz NOT NULL DEFAULT now(),
69
+ * updated_at timestamptz NOT NULL DEFAULT now(),
70
+ * CONSTRAINT things_pk PRIMARY KEY (ns, id)
71
+ * );
72
+ * CREATE INDEX IF NOT EXISTS things_type_idx ON aidb.things (ns, type);
73
+ * CREATE INDEX IF NOT EXISTS things_data_gin_idx
74
+ * ON aidb.things USING gin (data jsonb_path_ops);
75
+ *
76
+ * CREATE TABLE IF NOT EXISTS aidb.actions (
77
+ * ns text NOT NULL,
78
+ * id text NOT NULL,
79
+ * verb text NOT NULL,
80
+ * subject text,
81
+ * object text,
82
+ * roles jsonb NOT NULL DEFAULT '{}'::jsonb,
83
+ * data jsonb NOT NULL DEFAULT '{}'::jsonb,
84
+ * status text NOT NULL DEFAULT 'pending',
85
+ * created_at timestamptz NOT NULL DEFAULT now(),
86
+ * completed_at timestamptz,
87
+ * CONSTRAINT actions_pk PRIMARY KEY (ns, id)
88
+ * );
89
+ * CREATE INDEX IF NOT EXISTS actions_verb_idx ON aidb.actions (ns, verb);
90
+ * CREATE INDEX IF NOT EXISTS actions_subject_idx ON aidb.actions (ns, subject);
91
+ * CREATE INDEX IF NOT EXISTS actions_object_idx ON aidb.actions (ns, object);
92
+ *
93
+ * CREATE TABLE IF NOT EXISTS aidb.verbs (
94
+ * name text PRIMARY KEY,
95
+ * data jsonb NOT NULL DEFAULT '{}'::jsonb,
96
+ * created_at timestamptz NOT NULL DEFAULT now()
97
+ * );
98
+ * ```
99
+ *
100
+ * `bootstrapSchema(executor)` ships the DDL above so callers can stand
101
+ * up a fresh database in one call.
102
+ *
103
+ * @packageDocumentation
104
+ */
105
+ import { validateTypeName, validateEntityId, validateEntityData, validateRelationName, validateSearchQuery, validateListOptions, validateSearchOptions, } from './validation.js';
106
+ import { EntityNotFoundError } from './errors.js';
107
+ /**
108
+ * Wrap a `@neondatabase/serverless` HTTP query function as a {@link PgExecutor}.
109
+ *
110
+ * The Neon HTTP driver is **the preferred cascade write path** per the
111
+ * substrate-write-probes Phase 1 verdict — ~2x faster than Hyperdrive on
112
+ * every short-burst write shape, and the only driver/shape combination
113
+ * that achieves sublinear scaling above N=100.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * import { neon } from '@neondatabase/serverless'
118
+ * import { createNeonHttpExecutor, createPostgresProvider } from 'ai-database'
119
+ *
120
+ * const sql = neon(env.DATABASE_URL)
121
+ * const provider = createPostgresProvider({
122
+ * executor: createNeonHttpExecutor(sql),
123
+ * namespace: 'tenant-9',
124
+ * })
125
+ * ```
126
+ */
127
+ export function createNeonHttpExecutor(neonFn) {
128
+ return async (sql, params) => {
129
+ const result = await neonFn(sql, params ?? []);
130
+ // Neon returns rows directly as an array of objects.
131
+ return Array.isArray(result) ? result : [];
132
+ };
133
+ }
134
+ /**
135
+ * Wrap a `postgres.js` client as a {@link PgExecutor}.
136
+ *
137
+ * Use this when the deployment requires Hyperdrive (e.g., for
138
+ * connection pooling / WebSocket pipelining). **Not the cascade fast
139
+ * path** — see ADR-0003 and the substrate-write-probes notes:
140
+ * Hyperdrive's response cache only fires on parameterless queries
141
+ * (simple protocol). Once `$1` appears, postgres.js switches to
142
+ * extended protocol and Hyperdrive stops caching.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * import postgres from 'postgres'
147
+ * import { createPgClientExecutor, createPostgresProvider } from 'ai-database'
148
+ *
149
+ * const sql = postgres(env.HYPERDRIVE.connectionString)
150
+ * const provider = createPostgresProvider({
151
+ * executor: createPgClientExecutor(sql),
152
+ * namespace: 'tenant-9',
153
+ * })
154
+ * ```
155
+ */
156
+ export function createPgClientExecutor(client) {
157
+ return async (sql, params) => {
158
+ const result = await client.unsafe(sql, params ?? []);
159
+ return Array.isArray(result) ? result : [];
160
+ };
161
+ }
162
+ // =============================================================================
163
+ // SQL helpers
164
+ // =============================================================================
165
+ const DEFAULT_SCHEMA = 'aidb';
166
+ const DEFAULT_NAMESPACE = 'default';
167
+ const DEFAULT_VECTOR_DIMS = 1536;
168
+ /** Generate a UUID for adapter-issued ids. */
169
+ function genId() {
170
+ return crypto.randomUUID();
171
+ }
172
+ /**
173
+ * Coerce arbitrary jsonb-shaped column values from the executor into
174
+ * a plain object. Drivers vary: neon returns parsed objects, postgres.js
175
+ * may return strings depending on type configuration.
176
+ */
177
+ function asJsonb(value) {
178
+ if (!value)
179
+ return {};
180
+ if (typeof value === 'string') {
181
+ try {
182
+ return JSON.parse(value);
183
+ }
184
+ catch {
185
+ return {};
186
+ }
187
+ }
188
+ if (typeof value === 'object')
189
+ return value;
190
+ return {};
191
+ }
192
+ function asDate(value) {
193
+ if (!value)
194
+ return undefined;
195
+ if (value instanceof Date)
196
+ return value;
197
+ if (typeof value === 'string' || typeof value === 'number') {
198
+ const d = new Date(value);
199
+ return Number.isNaN(d.getTime()) ? undefined : d;
200
+ }
201
+ return undefined;
202
+ }
203
+ function asString(value) {
204
+ if (value === null || value === undefined)
205
+ return undefined;
206
+ return String(value);
207
+ }
208
+ /**
209
+ * Format a numeric array as a pgvector literal: `'[0.1,0.2,0.3]'`. Cast
210
+ * to `vector` at the SQL site (`$N::vector`).
211
+ */
212
+ function embeddingLiteral(embedding) {
213
+ // pgvector parses `[v1,v2,...]` syntax. We avoid scientific notation
214
+ // edge cases by relying on JS Number.toString — acceptable for cascade
215
+ // workloads (embeddings are 4-byte floats; JS numbers carry more
216
+ // precision than needed). NaN/Infinity are explicitly rejected.
217
+ const parts = [];
218
+ for (const v of embedding) {
219
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
220
+ throw new Error('embeddingLiteral: embedding values must be finite numbers');
221
+ }
222
+ parts.push(String(v));
223
+ }
224
+ return `[${parts.join(',')}]`;
225
+ }
226
+ // =============================================================================
227
+ // PostgresProvider
228
+ // =============================================================================
229
+ /**
230
+ * Postgres + pgvector adapter implementing {@link DBProviderPort} and
231
+ * {@link DBProviderSVO}.
232
+ */
233
+ export class PostgresProvider {
234
+ executor;
235
+ namespace;
236
+ schema;
237
+ vectorDimensions;
238
+ _shardingModel;
239
+ driver;
240
+ constructor(options) {
241
+ this.executor = options.executor;
242
+ this.namespace = options.namespace ?? DEFAULT_NAMESPACE;
243
+ this.schema = options.schema ?? DEFAULT_SCHEMA;
244
+ this.vectorDimensions = options.vectorDimensions ?? DEFAULT_VECTOR_DIMS;
245
+ this._shardingModel = options.shardingModel ?? 'partitioned-by-tenant';
246
+ this.driver = options.driver ?? 'pg';
247
+ }
248
+ /**
249
+ * Tier capability declaration. Declares Tier 3 with caveats and Tier 4
250
+ * (pgvector). `hasActionRecording` and `hasVerbRegistry` are both
251
+ * `true` — see the SVO methods on this class.
252
+ */
253
+ get capabilities() {
254
+ return {
255
+ adapter: 'pg+pgvector',
256
+ shardingModel: this._shardingModel,
257
+ analytics: {
258
+ hasAggregations: true,
259
+ hasTimeSeries: true,
260
+ // ADR-0003: Postgres "works at moderate scale; long scans contend
261
+ // with transactional load". Cascade ceiling: ~few-thousand
262
+ // inserts/sec on a single instance.
263
+ hasLargeScans: false,
264
+ },
265
+ vectorSearch: {
266
+ maxDimensions: this.vectorDimensions,
267
+ metrics: ['cosine', 'l2', 'dot'],
268
+ implementation: 'native',
269
+ },
270
+ hasActionRecording: true,
271
+ hasVerbRegistry: true,
272
+ };
273
+ }
274
+ // ===========================================================================
275
+ // Tier 1 — Entity CRUD
276
+ // ===========================================================================
277
+ async get(type, id) {
278
+ validateTypeName(type);
279
+ validateEntityId(id);
280
+ const rows = await this.executor(`SELECT data FROM ${this.schema}.things WHERE ns = $1 AND type = $2 AND id = $3 LIMIT 1`, [this.namespace, type, id]);
281
+ if (rows.length === 0)
282
+ return null;
283
+ const data = asJsonb(rows[0]['data']);
284
+ return { ...data, $id: id, $type: type };
285
+ }
286
+ async list(type, options) {
287
+ validateTypeName(type);
288
+ validateListOptions(options);
289
+ const limit = options?.limit ?? 1000;
290
+ const offset = options?.offset ?? 0;
291
+ // We ORDER BY id by default — adapters can layer richer ordering on
292
+ // top via $.data->>'field' if needed. Field-level orderBy is left to
293
+ // a future bead because it requires query-builder logic.
294
+ const rows = await this.executor(`SELECT id, data FROM ${this.schema}.things
295
+ WHERE ns = $1 AND type = $2
296
+ ORDER BY id
297
+ LIMIT $3 OFFSET $4`, [this.namespace, type, limit, offset]);
298
+ let result = rows.map((row) => {
299
+ const data = asJsonb(row['data']);
300
+ return { ...data, $id: String(row['id']), $type: type };
301
+ });
302
+ // Apply where filter client-side for parity with MemoryProvider.
303
+ // For high-volume paths, callers should use raw SQL via
304
+ // analyticsQuery() — this is the simple convenience path.
305
+ if (options?.where) {
306
+ result = result.filter((entity) => {
307
+ for (const [key, value] of Object.entries(options.where)) {
308
+ if (entity[key] !== value)
309
+ return false;
310
+ }
311
+ return true;
312
+ });
313
+ }
314
+ return result;
315
+ }
316
+ async search(type, query, options) {
317
+ validateTypeName(type);
318
+ validateSearchQuery(query);
319
+ validateSearchOptions(options);
320
+ const limit = options?.limit ?? 100;
321
+ // Use to_tsvector over the jsonb-as-text representation for a simple
322
+ // full-text path; richer search lands later. ILIKE fallback ensures
323
+ // tests work without `pg_trgm` / `tsvector` configuration.
324
+ const rows = await this.executor(`SELECT id, data FROM ${this.schema}.things
325
+ WHERE ns = $1 AND type = $2 AND data::text ILIKE $3
326
+ ORDER BY id
327
+ LIMIT $4`, [this.namespace, type, `%${query}%`, limit]);
328
+ return rows.map((row) => {
329
+ const data = asJsonb(row['data']);
330
+ return { ...data, $id: String(row['id']), $type: type };
331
+ });
332
+ }
333
+ async create(type, id, data) {
334
+ validateTypeName(type);
335
+ if (id !== undefined)
336
+ validateEntityId(id);
337
+ validateEntityData(data);
338
+ const entityId = id ?? genId();
339
+ const now = new Date().toISOString();
340
+ const payload = { ...data, createdAt: now, updatedAt: now };
341
+ await this.executor(`INSERT INTO ${this.schema}.things (ns, id, type, data) VALUES ($1, $2, $3, $4::jsonb)
342
+ ON CONFLICT ON CONSTRAINT things_pk DO NOTHING`, [this.namespace, entityId, type, JSON.stringify(payload)]);
343
+ return { ...payload, $id: entityId, $type: type };
344
+ }
345
+ async update(type, id, data) {
346
+ validateTypeName(type);
347
+ validateEntityId(id);
348
+ validateEntityData(data);
349
+ const existing = await this.get(type, id);
350
+ if (!existing)
351
+ throw new EntityNotFoundError(type, id, 'update');
352
+ const { $id: _id, $type: _type, ...rest } = existing;
353
+ const merged = { ...rest, ...data, updatedAt: new Date().toISOString() };
354
+ await this.executor(`UPDATE ${this.schema}.things SET data = $1::jsonb, updated_at = now()
355
+ WHERE ns = $2 AND type = $3 AND id = $4`, [JSON.stringify(merged), this.namespace, type, id]);
356
+ return { ...merged, $id: id, $type: type };
357
+ }
358
+ async delete(type, id) {
359
+ validateTypeName(type);
360
+ validateEntityId(id);
361
+ // postgres.js / neon do not consistently return rowCount through the
362
+ // executor surface; we read existence first to return a stable boolean.
363
+ const existing = await this.get(type, id);
364
+ if (!existing)
365
+ return false;
366
+ await this.executor(`DELETE FROM ${this.schema}.things WHERE ns = $1 AND type = $2 AND id = $3`, [this.namespace, type, id]);
367
+ // Cascade-delete actions where this thing is subject or object.
368
+ await this.executor(`DELETE FROM ${this.schema}.actions
369
+ WHERE ns = $1 AND (subject = $2 OR object = $2)`, [this.namespace, id]);
370
+ return true;
371
+ }
372
+ // ===========================================================================
373
+ // Tier 2 — Graph traversal via Actions
374
+ // ===========================================================================
375
+ async related(type, id, relation) {
376
+ validateTypeName(type);
377
+ validateEntityId(id);
378
+ validateRelationName(relation);
379
+ // Reads "all things this thing is subject of via this verb, returning
380
+ // the object thing". Mirrors MemoryProvider.related semantics.
381
+ const rows = await this.executor(`SELECT t.id, t.type, t.data
382
+ FROM ${this.schema}.actions a
383
+ JOIN ${this.schema}.things t ON t.ns = a.ns AND t.id = a.object
384
+ WHERE a.ns = $1 AND a.subject = $2 AND a.verb = $3`, [this.namespace, id, relation]);
385
+ return rows.map((row) => {
386
+ const data = asJsonb(row['data']);
387
+ return {
388
+ ...data,
389
+ $id: String(row['id']),
390
+ $type: String(row['type']),
391
+ };
392
+ });
393
+ }
394
+ async relate(fromType, fromId, relation, toType, toId, metadata) {
395
+ validateTypeName(fromType);
396
+ validateEntityId(fromId);
397
+ validateRelationName(relation);
398
+ validateTypeName(toType);
399
+ validateEntityId(toId);
400
+ // relate() lands as a completed Action with the verb as `relation`.
401
+ await this.recordAction({
402
+ verb: relation,
403
+ subject: fromId,
404
+ object: toId,
405
+ data: {
406
+ fromType,
407
+ toType,
408
+ ...(metadata ? { metadata } : {}),
409
+ },
410
+ status: 'completed',
411
+ });
412
+ }
413
+ async unrelate(fromType, fromId, relation, toType, toId) {
414
+ validateTypeName(fromType);
415
+ validateEntityId(fromId);
416
+ validateRelationName(relation);
417
+ validateTypeName(toType);
418
+ validateEntityId(toId);
419
+ await this.executor(`DELETE FROM ${this.schema}.actions
420
+ WHERE ns = $1 AND verb = $2 AND subject = $3 AND object = $4`, [this.namespace, relation, fromId, toId]);
421
+ }
422
+ // ===========================================================================
423
+ // SVO Action recording (DBProviderSVO)
424
+ // ===========================================================================
425
+ async recordAction(input) {
426
+ const id = genId();
427
+ const status = input.status ?? 'pending';
428
+ const createdAt = new Date();
429
+ const completedAt = status === 'completed' || status === 'failed' || status === 'cancelled'
430
+ ? createdAt
431
+ : undefined;
432
+ await this.executor(`INSERT INTO ${this.schema}.actions
433
+ (ns, id, verb, subject, object, roles, data, status, created_at, completed_at)
434
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8, $9, $10)`, [
435
+ this.namespace,
436
+ id,
437
+ input.verb,
438
+ input.subject ?? null,
439
+ input.object ?? null,
440
+ JSON.stringify(input.roles ?? {}),
441
+ JSON.stringify(input.data ?? {}),
442
+ status,
443
+ createdAt.toISOString(),
444
+ completedAt ? completedAt.toISOString() : null,
445
+ ]);
446
+ const action = {
447
+ id,
448
+ verb: input.verb,
449
+ ...(input.subject !== undefined && { subject: input.subject }),
450
+ ...(input.object !== undefined && { object: input.object }),
451
+ ...(input.roles !== undefined && { roles: input.roles }),
452
+ ...(input.data !== undefined && { data: input.data }),
453
+ status,
454
+ createdAt,
455
+ ...(completedAt !== undefined && { completedAt }),
456
+ };
457
+ return action;
458
+ }
459
+ async queryActions(query = {}) {
460
+ const conditions = ['ns = $1'];
461
+ const params = [this.namespace];
462
+ let n = 2;
463
+ if (query.verb !== undefined) {
464
+ conditions.push(`verb = $${n++}`);
465
+ params.push(query.verb);
466
+ }
467
+ if (query.subject !== undefined) {
468
+ conditions.push(`subject = $${n++}`);
469
+ params.push(query.subject);
470
+ }
471
+ if (query.object !== undefined) {
472
+ conditions.push(`object = $${n++}`);
473
+ params.push(query.object);
474
+ }
475
+ if (query.role) {
476
+ for (const [role, value] of Object.entries(query.role)) {
477
+ if (role === 'subject' || role === 'object') {
478
+ conditions.push(`${role} = $${n++}`);
479
+ }
480
+ else {
481
+ conditions.push(`roles->>'${role}' = $${n++}`);
482
+ }
483
+ params.push(value);
484
+ }
485
+ }
486
+ if (query.status) {
487
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
488
+ const placeholders = statuses.map(() => `$${n++}`).join(', ');
489
+ conditions.push(`status IN (${placeholders})`);
490
+ for (const s of statuses)
491
+ params.push(s);
492
+ }
493
+ if (query.since) {
494
+ conditions.push(`created_at >= $${n++}`);
495
+ params.push(query.since.toISOString());
496
+ }
497
+ if (query.until) {
498
+ conditions.push(`created_at <= $${n++}`);
499
+ params.push(query.until.toISOString());
500
+ }
501
+ const limit = query.limit ?? 1000;
502
+ const offset = query.offset ?? 0;
503
+ params.push(limit, offset);
504
+ const rows = await this.executor(`SELECT id, verb, subject, object, roles, data, status, created_at, completed_at
505
+ FROM ${this.schema}.actions
506
+ WHERE ${conditions.join(' AND ')}
507
+ ORDER BY created_at ASC
508
+ LIMIT $${n++} OFFSET $${n++}`, params);
509
+ return rows.map((row) => {
510
+ const action = {
511
+ id: String(row['id']),
512
+ verb: String(row['verb']),
513
+ status: row['status'] ?? 'pending',
514
+ createdAt: asDate(row['created_at']) ?? new Date(0),
515
+ };
516
+ const subject = asString(row['subject']);
517
+ if (subject !== undefined)
518
+ action.subject = subject;
519
+ const object = asString(row['object']);
520
+ if (object !== undefined)
521
+ action.object = object;
522
+ const roles = asJsonb(row['roles']);
523
+ if (Object.keys(roles).length > 0) {
524
+ action.roles = roles;
525
+ }
526
+ const data = asJsonb(row['data']);
527
+ if (Object.keys(data).length > 0) {
528
+ action.data = data;
529
+ }
530
+ const completedAt = asDate(row['completed_at']);
531
+ if (completedAt)
532
+ action.completedAt = completedAt;
533
+ return action;
534
+ });
535
+ }
536
+ // ===========================================================================
537
+ // Verb registry (DBProviderSVO)
538
+ // ===========================================================================
539
+ async defineVerb(def) {
540
+ const verb = {
541
+ name: def.name,
542
+ action: def.action ?? def.name,
543
+ act: def.act ?? `${def.name}s`,
544
+ activity: def.activity ?? `${def.name}ing`,
545
+ event: def.event ?? `${def.name}d`,
546
+ ...(def.reverseBy !== undefined && { reverseBy: def.reverseBy }),
547
+ ...(def.reverseAt !== undefined && { reverseAt: def.reverseAt }),
548
+ ...(def.reverseIn !== undefined && { reverseIn: def.reverseIn }),
549
+ ...(def.inverse !== undefined && { inverse: def.inverse }),
550
+ ...(def.description !== undefined && { description: def.description }),
551
+ ...(def.frame !== undefined && { frame: def.frame }),
552
+ ...(def.source !== undefined && { source: def.source }),
553
+ ...(def.canonical !== undefined && { canonical: def.canonical }),
554
+ createdAt: new Date(),
555
+ };
556
+ await this.executor(`INSERT INTO ${this.schema}.verbs (name, data) VALUES ($1, $2::jsonb)
557
+ ON CONFLICT (name) DO NOTHING`, [verb.name, JSON.stringify(verb)]);
558
+ return verb;
559
+ }
560
+ async getVerb(name) {
561
+ const rows = await this.executor(`SELECT data, created_at FROM ${this.schema}.verbs WHERE name = $1 LIMIT 1`, [name]);
562
+ if (rows.length === 0)
563
+ return null;
564
+ const data = asJsonb(rows[0]['data']);
565
+ const createdAt = asDate(rows[0]['created_at']) ?? new Date(0);
566
+ return { ...data, createdAt };
567
+ }
568
+ async listVerbs() {
569
+ const rows = await this.executor(`SELECT data, created_at FROM ${this.schema}.verbs ORDER BY name ASC`);
570
+ return rows.map((row) => {
571
+ const data = asJsonb(row['data']);
572
+ const createdAt = asDate(row['created_at']) ?? new Date(0);
573
+ return { ...data, createdAt };
574
+ });
575
+ }
576
+ // ===========================================================================
577
+ // Cascade write fast path — CTE jsonb-bulk
578
+ // ===========================================================================
579
+ /**
580
+ * Bulk-commit Things and Actions in a single round-trip via a CTE chain
581
+ * with bulk `VALUES (...), (...)` inserts and `ON CONFLICT DO NOTHING`.
582
+ *
583
+ * **This is the cascade-scale write path** per the substrate-write-probes
584
+ * Phase 1 verdict: 91 ms p50 for 500 things + 499 actions on Neon HTTP;
585
+ * sublinear scaling above N=100; ~5,500 cascade-startups/sec/worker
586
+ * write ceiling on the pg portion alone (LLM cost dominates).
587
+ *
588
+ * Both arrays may be empty independently; the SQL omits the
589
+ * corresponding CTE and skips its placeholders. Returns the count of
590
+ * rows actually inserted (excludes conflicts).
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * const { thingsInserted, actionsInserted } = await provider.commitBatch({
595
+ * things: [
596
+ * { type: 'Customer', id: 'c1', data: { name: 'Acme' } },
597
+ * { type: 'Order', id: 'o1', data: { total: 100 } },
598
+ * ],
599
+ * actions: [
600
+ * { id: 'a1', verb: 'placedBy', subject: 'o1', object: 'c1', status: 'completed' },
601
+ * ],
602
+ * })
603
+ * ```
604
+ */
605
+ async commitBatch(input) {
606
+ const things = input.things ?? [];
607
+ const actions = input.actions ?? [];
608
+ if (things.length === 0 && actions.length === 0) {
609
+ return { thingsInserted: 0, actionsInserted: 0 };
610
+ }
611
+ // Things take 4 columns: ns, id, type, data
612
+ const thingCols = 4;
613
+ // Actions take 10 columns: ns, id, verb, subject, object, roles, data,
614
+ // status, created_at, completed_at
615
+ const actionCols = 10;
616
+ const thingOffset = things.length * thingCols;
617
+ const thingPlaceholders = things
618
+ .map((_, i) => {
619
+ const base = i * thingCols;
620
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb)`;
621
+ })
622
+ .join(', ');
623
+ const actionPlaceholders = actions
624
+ .map((_, i) => {
625
+ const base = thingOffset + i * actionCols;
626
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}::jsonb, $${base + 7}::jsonb, $${base + 8}, $${base + 9}, $${base + 10})`;
627
+ })
628
+ .join(', ');
629
+ const now = new Date();
630
+ const params = [];
631
+ for (const t of things) {
632
+ params.push(this.namespace, t.id, t.type, JSON.stringify(t.data));
633
+ }
634
+ for (const a of actions) {
635
+ const status = a.status ?? 'pending';
636
+ const completedAt = status === 'completed' || status === 'failed' || status === 'cancelled'
637
+ ? now.toISOString()
638
+ : null;
639
+ params.push(this.namespace, a.id ?? genId(), a.verb, a.subject ?? null, a.object ?? null, JSON.stringify(a.roles ?? {}), JSON.stringify(a.data ?? {}), status, now.toISOString(), completedAt);
640
+ }
641
+ const ctes = [];
642
+ if (things.length > 0) {
643
+ ctes.push(`inserted_things AS (
644
+ INSERT INTO ${this.schema}.things (ns, id, type, data) VALUES ${thingPlaceholders}
645
+ ON CONFLICT ON CONSTRAINT things_pk DO NOTHING
646
+ RETURNING 1
647
+ )`);
648
+ }
649
+ if (actions.length > 0) {
650
+ ctes.push(`inserted_actions AS (
651
+ INSERT INTO ${this.schema}.actions
652
+ (ns, id, verb, subject, object, roles, data, status, created_at, completed_at)
653
+ VALUES ${actionPlaceholders}
654
+ ON CONFLICT ON CONSTRAINT actions_pk DO NOTHING
655
+ RETURNING 1
656
+ )`);
657
+ }
658
+ const sqlText = `WITH ${ctes.join(', ')}
659
+ SELECT
660
+ ${things.length > 0 ? `(SELECT COUNT(*) FROM inserted_things)::int` : '0'} AS things_inserted,
661
+ ${actions.length > 0 ? `(SELECT COUNT(*) FROM inserted_actions)::int` : '0'} AS actions_inserted`;
662
+ const rows = await this.executor(sqlText, params);
663
+ const row = rows[0];
664
+ return {
665
+ thingsInserted: Number(row?.['things_inserted'] ?? 0),
666
+ actionsInserted: Number(row?.['actions_inserted'] ?? 0),
667
+ };
668
+ }
669
+ // ===========================================================================
670
+ // Tier 3 — analytics (declared)
671
+ // ===========================================================================
672
+ /**
673
+ * Pass-through for ad-hoc analytical SQL. The query runs verbatim
674
+ * against the underlying executor; callers are responsible for
675
+ * parameter quoting. Use for time-series rollups, aggregations,
676
+ * or callers that want to reach into PG without going through the
677
+ * adapter's CRUD surface.
678
+ */
679
+ async analyticsQuery(query, params) {
680
+ // We accept a record but currently send positional params only;
681
+ // callers can pre-format SQL with $1, $2 placeholders.
682
+ const positional = params ? Object.values(params) : [];
683
+ return this.executor(query, positional);
684
+ }
685
+ // ===========================================================================
686
+ // Tier 4 — vector search via pgvector
687
+ // ===========================================================================
688
+ /**
689
+ * Upsert an embedding vector for a Thing into the `embeddings` table.
690
+ *
691
+ * The embedding is stored separately from the `things` row so that
692
+ * Tier 1 callers don't pay the vector serialization cost when they
693
+ * don't need it. Callers generate the embedding upstream (out of scope
694
+ * for this adapter) and pass it here.
695
+ *
696
+ * @example
697
+ * ```ts
698
+ * await provider.upsertEmbedding('Document', 'doc-1', new Array(1536).fill(0))
699
+ * ```
700
+ */
701
+ async upsertEmbedding(type, id, embedding) {
702
+ validateTypeName(type);
703
+ validateEntityId(id);
704
+ if (!Array.isArray(embedding) || embedding.length === 0) {
705
+ throw new Error('upsertEmbedding: embedding must be a non-empty array of numbers');
706
+ }
707
+ if (embedding.length > this.vectorDimensions) {
708
+ throw new Error(`upsertEmbedding: embedding length ${embedding.length} exceeds adapter vectorDimensions ${this.vectorDimensions}`);
709
+ }
710
+ const literal = embeddingLiteral(embedding);
711
+ await this.executor(`INSERT INTO ${this.schema}.embeddings (ns, thing_id, type, embedding)
712
+ VALUES ($1, $2, $3, $4::vector)
713
+ ON CONFLICT (ns, thing_id) DO UPDATE SET embedding = EXCLUDED.embedding, type = EXCLUDED.type`, [this.namespace, id, type, literal]);
714
+ }
715
+ /**
716
+ * Tier 4 — vector search via pgvector operators.
717
+ *
718
+ * Operator selection by metric:
719
+ * - `'cosine'` (default): `embedding <=> $vector` — cosine distance.
720
+ * Score returned is `1 - distance` so higher is more similar.
721
+ * - `'l2'`: `embedding <-> $vector` — Euclidean distance. Score is
722
+ * `-distance` (closer => higher score; never positive).
723
+ * - `'dot'`: `embedding <#> $vector` — negative inner product (pgvector
724
+ * convention so smaller-is-better). Score returned is `-result`
725
+ * (i.e. the inner product itself; higher is more similar).
726
+ *
727
+ * Frame-aware role filtering (e.g. only search Things that appear as
728
+ * `subject` of a particular Action) is **deferred** to a follow-up
729
+ * bead — refinement, not blocking for Phase 1. The current method
730
+ * filters by `type` only.
731
+ *
732
+ * @example
733
+ * ```ts
734
+ * const hits = await provider.vectorSearch('Document', queryVec, {
735
+ * metric: 'cosine',
736
+ * limit: 10,
737
+ * })
738
+ * ```
739
+ */
740
+ async vectorSearch(type, queryEmbedding, options) {
741
+ validateTypeName(type);
742
+ if (!Array.isArray(queryEmbedding) || queryEmbedding.length === 0) {
743
+ throw new Error('vectorSearch: queryEmbedding must be a non-empty array of numbers');
744
+ }
745
+ if (queryEmbedding.length > this.vectorDimensions) {
746
+ throw new Error(`vectorSearch: query length ${queryEmbedding.length} exceeds adapter vectorDimensions ${this.vectorDimensions}`);
747
+ }
748
+ const metric = options?.metric ?? 'cosine';
749
+ const limit = Math.max(1, options?.limit ?? 10);
750
+ // Operator + score expression per metric. pgvector convention: all
751
+ // operators return "distance" where smaller is more similar; we wrap
752
+ // so the returned `score` is monotonically higher-is-more-similar.
753
+ let operator;
754
+ let scoreExpr;
755
+ switch (metric) {
756
+ case 'cosine':
757
+ operator = '<=>';
758
+ scoreExpr = `1 - (e.embedding <=> $2::vector)`;
759
+ break;
760
+ case 'l2':
761
+ operator = '<->';
762
+ scoreExpr = `-(e.embedding <-> $2::vector)`;
763
+ break;
764
+ case 'dot':
765
+ // pgvector returns negative inner product; flip back so positive
766
+ // dot product => positive score.
767
+ operator = '<#>';
768
+ scoreExpr = `-(e.embedding <#> $2::vector)`;
769
+ break;
770
+ case 'hamming':
771
+ throw new Error('vectorSearch: pgvector adapter does not support hamming metric');
772
+ default:
773
+ throw new Error(`vectorSearch: unsupported metric "${String(metric)}"`);
774
+ }
775
+ const literal = embeddingLiteral(queryEmbedding);
776
+ const sql = `SELECT t.id AS id, t.type AS type, t.data AS data, ${scoreExpr} AS score
777
+ FROM ${this.schema}.embeddings e
778
+ JOIN ${this.schema}.things t ON t.ns = e.ns AND t.id = e.thing_id
779
+ WHERE e.ns = $1 AND t.type = $3
780
+ ORDER BY e.embedding ${operator} $2::vector
781
+ LIMIT $4`;
782
+ const rows = await this.executor(sql, [this.namespace, literal, type, limit]);
783
+ let hits = rows.map((row) => {
784
+ const data = asJsonb(row['data']);
785
+ const score = typeof row['score'] === 'number' ? row['score'] : Number(row['score'] ?? 0);
786
+ return {
787
+ entity: { ...data, $id: String(row['id']), $type: String(row['type']) },
788
+ score,
789
+ };
790
+ });
791
+ if (options?.minScore !== undefined) {
792
+ const min = options.minScore;
793
+ hits = hits.filter((h) => h.score >= min);
794
+ }
795
+ return hits;
796
+ }
797
+ /**
798
+ * Driver / connection metadata for diagnostics.
799
+ */
800
+ describe() {
801
+ return {
802
+ adapter: 'pg+pgvector',
803
+ driver: this.driver,
804
+ namespace: this.namespace,
805
+ schema: this.schema,
806
+ };
807
+ }
808
+ }
809
+ // =============================================================================
810
+ // Schema bootstrap
811
+ // =============================================================================
812
+ /**
813
+ * Run the canonical DDL against an executor. Idempotent — uses
814
+ * `IF NOT EXISTS` throughout, including the `vector` extension. Skip the
815
+ * `vector` extension installation if your database does not have
816
+ * pgvector available (set `withVector: false`).
817
+ *
818
+ * Designed for one-shot cluster bootstrap and for PGLite-style test
819
+ * harnesses. Production deployments typically run schema migrations via
820
+ * a tool like `dbmate` or `node-pg-migrate`; this helper is a
821
+ * convenience, not a migration framework.
822
+ */
823
+ export async function bootstrapSchema(executor, options = {}) {
824
+ const schema = options.schema ?? DEFAULT_SCHEMA;
825
+ const withVector = options.withVector ?? true;
826
+ const dims = options.vectorDimensions ?? DEFAULT_VECTOR_DIMS;
827
+ if (withVector) {
828
+ try {
829
+ await executor(`CREATE EXTENSION IF NOT EXISTS vector`);
830
+ }
831
+ catch {
832
+ // Extension may not be available; downstream vector queries will
833
+ // fail clearly. We swallow here to keep bootstrap idempotent.
834
+ }
835
+ }
836
+ await executor(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
837
+ await executor(`CREATE TABLE IF NOT EXISTS ${schema}.things (
838
+ ns text NOT NULL,
839
+ id text NOT NULL,
840
+ type text NOT NULL,
841
+ data jsonb NOT NULL DEFAULT '{}'::jsonb,
842
+ created_at timestamptz NOT NULL DEFAULT now(),
843
+ updated_at timestamptz NOT NULL DEFAULT now(),
844
+ CONSTRAINT things_pk PRIMARY KEY (ns, id)
845
+ )`);
846
+ await executor(`CREATE INDEX IF NOT EXISTS things_type_idx ON ${schema}.things (ns, type)`);
847
+ await executor(`CREATE TABLE IF NOT EXISTS ${schema}.actions (
848
+ ns text NOT NULL,
849
+ id text NOT NULL,
850
+ verb text NOT NULL,
851
+ subject text,
852
+ object text,
853
+ roles jsonb NOT NULL DEFAULT '{}'::jsonb,
854
+ data jsonb NOT NULL DEFAULT '{}'::jsonb,
855
+ status text NOT NULL DEFAULT 'pending',
856
+ created_at timestamptz NOT NULL DEFAULT now(),
857
+ completed_at timestamptz,
858
+ CONSTRAINT actions_pk PRIMARY KEY (ns, id)
859
+ )`);
860
+ await executor(`CREATE INDEX IF NOT EXISTS actions_verb_idx ON ${schema}.actions (ns, verb)`);
861
+ await executor(`CREATE INDEX IF NOT EXISTS actions_subject_idx ON ${schema}.actions (ns, subject)`);
862
+ await executor(`CREATE INDEX IF NOT EXISTS actions_object_idx ON ${schema}.actions (ns, object)`);
863
+ await executor(`CREATE TABLE IF NOT EXISTS ${schema}.verbs (
864
+ name text PRIMARY KEY,
865
+ data jsonb NOT NULL DEFAULT '{}'::jsonb,
866
+ created_at timestamptz NOT NULL DEFAULT now()
867
+ )`);
868
+ if (withVector) {
869
+ try {
870
+ await executor(`CREATE TABLE IF NOT EXISTS ${schema}.embeddings (
871
+ ns text NOT NULL,
872
+ thing_id text NOT NULL,
873
+ type text NOT NULL DEFAULT '',
874
+ embedding vector(${dims}),
875
+ PRIMARY KEY (ns, thing_id)
876
+ )`);
877
+ // Best-effort: add `type` column to existing embeddings tables that
878
+ // were created before vector search shipped. Idempotent via
879
+ // IF NOT EXISTS (Postgres 9.6+).
880
+ await executor(`ALTER TABLE ${schema}.embeddings ADD COLUMN IF NOT EXISTS type text NOT NULL DEFAULT ''`);
881
+ await executor(`CREATE INDEX IF NOT EXISTS embeddings_ns_type_idx ON ${schema}.embeddings (ns, type)`);
882
+ // ANN index — ivfflat is the broadly-available default; HNSW is
883
+ // pgvector 0.5+. We try HNSW first (better recall/latency) and
884
+ // fall back to ivfflat. Both are best-effort.
885
+ try {
886
+ await executor(`CREATE INDEX IF NOT EXISTS embeddings_hnsw_cosine_idx
887
+ ON ${schema}.embeddings USING hnsw (embedding vector_cosine_ops)`);
888
+ }
889
+ catch {
890
+ // pgvector < 0.5 doesn't have HNSW; skip silently.
891
+ }
892
+ }
893
+ catch {
894
+ // Vector extension not present; the embeddings table is optional.
895
+ }
896
+ }
897
+ // Hint for the unused-var linter on `_` destructure pattern downstream.
898
+ void true;
899
+ }
900
+ // =============================================================================
901
+ // Factory
902
+ // =============================================================================
903
+ /**
904
+ * Convenience factory for {@link PostgresProvider}.
905
+ *
906
+ * @example
907
+ * ```ts
908
+ * import { neon } from '@neondatabase/serverless'
909
+ * import { createPostgresProvider, createNeonHttpExecutor } from 'ai-database'
910
+ *
911
+ * const sql = neon(env.DATABASE_URL)
912
+ * const provider = createPostgresProvider({
913
+ * executor: createNeonHttpExecutor(sql),
914
+ * namespace: 'tenant-9',
915
+ * })
916
+ * ```
917
+ */
918
+ export function createPostgresProvider(options) {
919
+ return new PostgresProvider(options);
920
+ }
921
+ //# sourceMappingURL=pg-adapter.js.map