@kernl-sdk/pg 0.1.11 → 0.1.12

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 (153) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-check-types.log +36 -0
  3. package/CHANGELOG.md +32 -0
  4. package/README.md +124 -0
  5. package/dist/__tests__/integration.test.js +2 -2
  6. package/dist/__tests__/memory-integration.test.d.ts +2 -0
  7. package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
  8. package/dist/__tests__/memory-integration.test.js +287 -0
  9. package/dist/__tests__/memory.test.d.ts +2 -0
  10. package/dist/__tests__/memory.test.d.ts.map +1 -0
  11. package/dist/__tests__/memory.test.js +357 -0
  12. package/dist/index.d.ts +5 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -3
  15. package/dist/memory/sql.d.ts +30 -0
  16. package/dist/memory/sql.d.ts.map +1 -0
  17. package/dist/memory/sql.js +100 -0
  18. package/dist/memory/store.d.ts +41 -0
  19. package/dist/memory/store.d.ts.map +1 -0
  20. package/dist/memory/store.js +114 -0
  21. package/dist/migrations.d.ts +1 -1
  22. package/dist/migrations.d.ts.map +1 -1
  23. package/dist/migrations.js +9 -3
  24. package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
  25. package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
  26. package/dist/pgvector/__tests__/handle.test.js +277 -0
  27. package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
  28. package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
  29. package/dist/pgvector/__tests__/hit.test.js +134 -0
  30. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
  31. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
  32. package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
  33. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
  34. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
  35. package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
  36. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
  37. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  38. package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
  39. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  40. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  41. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
  42. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
  43. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
  44. package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
  45. package/dist/pgvector/__tests__/search.test.d.ts +2 -0
  46. package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
  47. package/dist/pgvector/__tests__/search.test.js +279 -0
  48. package/dist/pgvector/handle.d.ts +60 -0
  49. package/dist/pgvector/handle.d.ts.map +1 -0
  50. package/dist/pgvector/handle.js +213 -0
  51. package/dist/pgvector/hit.d.ts +10 -0
  52. package/dist/pgvector/hit.d.ts.map +1 -0
  53. package/dist/pgvector/hit.js +44 -0
  54. package/dist/pgvector/index.d.ts +7 -0
  55. package/dist/pgvector/index.d.ts.map +1 -0
  56. package/dist/pgvector/index.js +5 -0
  57. package/dist/pgvector/search.d.ts +60 -0
  58. package/dist/pgvector/search.d.ts.map +1 -0
  59. package/dist/pgvector/search.js +227 -0
  60. package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
  61. package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
  62. package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
  63. package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
  64. package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
  65. package/dist/pgvector/sql/__tests__/order.test.js +218 -0
  66. package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
  67. package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
  68. package/dist/pgvector/sql/__tests__/query.test.js +392 -0
  69. package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
  70. package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
  71. package/dist/pgvector/sql/__tests__/select.test.js +293 -0
  72. package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
  73. package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
  74. package/dist/pgvector/sql/__tests__/where.test.js +488 -0
  75. package/dist/pgvector/sql/index.d.ts +7 -0
  76. package/dist/pgvector/sql/index.d.ts.map +1 -0
  77. package/dist/pgvector/sql/index.js +6 -0
  78. package/dist/pgvector/sql/limit.d.ts +8 -0
  79. package/dist/pgvector/sql/limit.d.ts.map +1 -0
  80. package/dist/pgvector/sql/limit.js +20 -0
  81. package/dist/pgvector/sql/order.d.ts +9 -0
  82. package/dist/pgvector/sql/order.d.ts.map +1 -0
  83. package/dist/pgvector/sql/order.js +47 -0
  84. package/dist/pgvector/sql/query.d.ts +46 -0
  85. package/dist/pgvector/sql/query.d.ts.map +1 -0
  86. package/dist/pgvector/sql/query.js +54 -0
  87. package/dist/pgvector/sql/schema.d.ts +16 -0
  88. package/dist/pgvector/sql/schema.d.ts.map +1 -0
  89. package/dist/pgvector/sql/schema.js +47 -0
  90. package/dist/pgvector/sql/select.d.ts +11 -0
  91. package/dist/pgvector/sql/select.d.ts.map +1 -0
  92. package/dist/pgvector/sql/select.js +87 -0
  93. package/dist/pgvector/sql/where.d.ts +8 -0
  94. package/dist/pgvector/sql/where.d.ts.map +1 -0
  95. package/dist/pgvector/sql/where.js +137 -0
  96. package/dist/pgvector/types.d.ts +20 -0
  97. package/dist/pgvector/types.d.ts.map +1 -0
  98. package/dist/pgvector/types.js +1 -0
  99. package/dist/pgvector/utils.d.ts +18 -0
  100. package/dist/pgvector/utils.d.ts.map +1 -0
  101. package/dist/pgvector/utils.js +22 -0
  102. package/dist/postgres.d.ts +19 -26
  103. package/dist/postgres.d.ts.map +1 -1
  104. package/dist/postgres.js +15 -27
  105. package/dist/storage.d.ts +48 -0
  106. package/dist/storage.d.ts.map +1 -1
  107. package/dist/storage.js +32 -9
  108. package/dist/thread/sql.d.ts +38 -0
  109. package/dist/thread/sql.d.ts.map +1 -0
  110. package/dist/thread/sql.js +112 -0
  111. package/dist/thread/store.d.ts +2 -2
  112. package/dist/thread/store.d.ts.map +1 -1
  113. package/dist/thread/store.js +32 -102
  114. package/package.json +7 -4
  115. package/src/__tests__/integration.test.ts +15 -17
  116. package/src/__tests__/memory-integration.test.ts +355 -0
  117. package/src/__tests__/memory.test.ts +428 -0
  118. package/src/index.ts +19 -3
  119. package/src/memory/sql.ts +141 -0
  120. package/src/memory/store.ts +166 -0
  121. package/src/migrations.ts +13 -3
  122. package/src/pgvector/README.md +50 -0
  123. package/src/pgvector/__tests__/handle.test.ts +335 -0
  124. package/src/pgvector/__tests__/hit.test.ts +165 -0
  125. package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
  126. package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
  127. package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
  128. package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
  129. package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
  130. package/src/pgvector/__tests__/search.test.ts +366 -0
  131. package/src/pgvector/handle.ts +285 -0
  132. package/src/pgvector/hit.ts +56 -0
  133. package/src/pgvector/index.ts +7 -0
  134. package/src/pgvector/search.ts +330 -0
  135. package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
  136. package/src/pgvector/sql/__tests__/order.test.ts +248 -0
  137. package/src/pgvector/sql/__tests__/query.test.ts +548 -0
  138. package/src/pgvector/sql/__tests__/select.test.ts +367 -0
  139. package/src/pgvector/sql/__tests__/where.test.ts +554 -0
  140. package/src/pgvector/sql/index.ts +14 -0
  141. package/src/pgvector/sql/limit.ts +29 -0
  142. package/src/pgvector/sql/order.ts +55 -0
  143. package/src/pgvector/sql/query.ts +112 -0
  144. package/src/pgvector/sql/schema.ts +61 -0
  145. package/src/pgvector/sql/select.ts +100 -0
  146. package/src/pgvector/sql/where.ts +152 -0
  147. package/src/pgvector/types.ts +21 -0
  148. package/src/pgvector/utils.ts +24 -0
  149. package/src/postgres.ts +31 -33
  150. package/src/storage.ts +77 -9
  151. package/src/thread/sql.ts +159 -0
  152. package/src/thread/store.ts +40 -127
  153. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,279 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { Pool } from "pg";
3
+ import { PGSearchIndex } from "../search.js";
4
+ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
5
+ const SCHEMA = "kernl_search_idx_test";
6
+ describe.sequential("PGSearchIndex", () => {
7
+ if (!TEST_DB_URL) {
8
+ it.skip("requires KERNL_PG_TEST_URL environment variable", () => { });
9
+ return;
10
+ }
11
+ let pool;
12
+ let pgvec;
13
+ beforeAll(async () => {
14
+ pool = new Pool({ connectionString: TEST_DB_URL });
15
+ // Ensure pgvector extension exists
16
+ await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
17
+ // Clean slate
18
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
19
+ await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
20
+ pgvec = new PGSearchIndex({ pool });
21
+ });
22
+ afterAll(async () => {
23
+ await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
24
+ await pool.end();
25
+ });
26
+ describe("createIndex", () => {
27
+ it("creates a table with correct column types", async () => {
28
+ await pgvec.createIndex({
29
+ id: "articles",
30
+ schema: {
31
+ id: { type: "string", pk: true },
32
+ title: { type: "string" },
33
+ views: { type: "int" },
34
+ rating: { type: "float" },
35
+ published: { type: "boolean" },
36
+ created_at: { type: "date" },
37
+ },
38
+ providerOptions: { schema: SCHEMA },
39
+ });
40
+ // Verify table exists with correct columns
41
+ const result = await pool.query(`SELECT column_name, data_type
42
+ FROM information_schema.columns
43
+ WHERE table_schema = $1 AND table_name = $2
44
+ ORDER BY ordinal_position`, [SCHEMA, "articles"]);
45
+ const columns = Object.fromEntries(result.rows.map((r) => [r.column_name, r.data_type]));
46
+ expect(columns.id).toBe("text");
47
+ expect(columns.title).toBe("text");
48
+ expect(columns.views).toBe("integer");
49
+ expect(columns.rating).toBe("double precision");
50
+ expect(columns.published).toBe("boolean");
51
+ expect(columns.created_at).toBe("timestamp with time zone");
52
+ });
53
+ it("creates vector columns with correct dimensions", async () => {
54
+ await pgvec.createIndex({
55
+ id: "embeddings",
56
+ schema: {
57
+ id: { type: "string", pk: true },
58
+ embedding: { type: "vector", dimensions: 384 },
59
+ },
60
+ providerOptions: { schema: SCHEMA },
61
+ });
62
+ // Check the vector column type via pg_attribute
63
+ const result = await pool.query(`SELECT t.typname, a.atttypmod
64
+ FROM pg_attribute a
65
+ JOIN pg_class c ON a.attrelid = c.oid
66
+ JOIN pg_namespace n ON c.relnamespace = n.oid
67
+ JOIN pg_type t ON a.atttypid = t.oid
68
+ WHERE n.nspname = $1
69
+ AND c.relname = $2
70
+ AND a.attname = $3`, [SCHEMA, "embeddings", "embedding"]);
71
+ expect(result.rows[0]?.typname).toBe("vector");
72
+ // atttypmod encodes dimensions for vector type
73
+ expect(result.rows[0]?.atttypmod).toBe(384);
74
+ });
75
+ it("creates HNSW index for vector fields", async () => {
76
+ await pgvec.createIndex({
77
+ id: "searchable",
78
+ schema: {
79
+ id: { type: "string", pk: true },
80
+ embedding: { type: "vector", dimensions: 128, similarity: "cosine" },
81
+ },
82
+ providerOptions: { schema: SCHEMA },
83
+ });
84
+ const result = await pool.query(`SELECT indexname, indexdef
85
+ FROM pg_indexes
86
+ WHERE schemaname = $1 AND tablename = $2`, [SCHEMA, "searchable"]);
87
+ const hnswIndex = result.rows.find((r) => r.indexname === "searchable_embedding_idx");
88
+ expect(hnswIndex).toBeDefined();
89
+ expect(hnswIndex?.indexdef).toContain("hnsw");
90
+ expect(hnswIndex?.indexdef).toContain("vector_cosine_ops");
91
+ });
92
+ it("uses correct operator class for each similarity metric", async () => {
93
+ // Cosine
94
+ await pgvec.createIndex({
95
+ id: "cosine_idx",
96
+ schema: {
97
+ id: { type: "string", pk: true },
98
+ vec: { type: "vector", dimensions: 8, similarity: "cosine" },
99
+ },
100
+ providerOptions: { schema: SCHEMA },
101
+ });
102
+ // Euclidean
103
+ await pgvec.createIndex({
104
+ id: "euclidean_idx",
105
+ schema: {
106
+ id: { type: "string", pk: true },
107
+ vec: { type: "vector", dimensions: 8, similarity: "euclidean" },
108
+ },
109
+ providerOptions: { schema: SCHEMA },
110
+ });
111
+ // Dot product
112
+ await pgvec.createIndex({
113
+ id: "dot_idx",
114
+ schema: {
115
+ id: { type: "string", pk: true },
116
+ vec: { type: "vector", dimensions: 8, similarity: "dot_product" },
117
+ },
118
+ providerOptions: { schema: SCHEMA },
119
+ });
120
+ const result = await pool.query(`SELECT indexname, indexdef
121
+ FROM pg_indexes
122
+ WHERE schemaname = $1
123
+ ORDER BY indexname`, [SCHEMA]);
124
+ const indexes = Object.fromEntries(result.rows.map((r) => [r.indexname, r.indexdef]));
125
+ expect(indexes["cosine_idx_vec_idx"]).toContain("vector_cosine_ops");
126
+ expect(indexes["euclidean_idx_vec_idx"]).toContain("vector_l2_ops");
127
+ expect(indexes["dot_idx_vec_idx"]).toContain("vector_ip_ops");
128
+ });
129
+ it("throws if schema has no pk field", async () => {
130
+ await expect(pgvec.createIndex({
131
+ id: "no_pk",
132
+ schema: {
133
+ title: { type: "string" },
134
+ content: { type: "string" },
135
+ },
136
+ providerOptions: { schema: SCHEMA },
137
+ })).rejects.toThrow("schema must have a field with pk: true");
138
+ });
139
+ it("auto-binds the created index for immediate use", async () => {
140
+ await pgvec.createIndex({
141
+ id: "auto_bound",
142
+ schema: {
143
+ id: { type: "string", pk: true },
144
+ name: { type: "string" },
145
+ embedding: { type: "vector", dimensions: 3, similarity: "cosine" },
146
+ },
147
+ providerOptions: { schema: SCHEMA },
148
+ });
149
+ // Should be able to use the index immediately without bindIndex
150
+ const handle = pgvec.index("auto_bound");
151
+ expect(handle.id).toBe("auto_bound");
152
+ // Insert and query should work
153
+ await handle.upsert({
154
+ id: "test-1",
155
+ name: "Test Doc",
156
+ embedding: [0.1, 0.2, 0.3],
157
+ });
158
+ const results = await handle.query({
159
+ query: [{ embedding: [0.1, 0.2, 0.3] }],
160
+ topK: 1,
161
+ });
162
+ expect(results).toHaveLength(1);
163
+ expect(results[0].id).toBe("test-1");
164
+ });
165
+ });
166
+ describe("deleteIndex", () => {
167
+ it("drops the table and removes binding", async () => {
168
+ await pgvec.createIndex({
169
+ id: "to_delete",
170
+ schema: {
171
+ id: { type: "string", pk: true },
172
+ name: { type: "string" },
173
+ },
174
+ providerOptions: { schema: SCHEMA },
175
+ });
176
+ // Verify table exists
177
+ const before = await pool.query(`SELECT 1 FROM information_schema.tables
178
+ WHERE table_schema = $1 AND table_name = $2`, [SCHEMA, "to_delete"]);
179
+ expect(before.rows).toHaveLength(1);
180
+ await pgvec.deleteIndex("to_delete");
181
+ // Verify table is gone
182
+ const after = await pool.query(`SELECT 1 FROM information_schema.tables
183
+ WHERE table_schema = $1 AND table_name = $2`, [SCHEMA, "to_delete"]);
184
+ expect(after.rows).toHaveLength(0);
185
+ });
186
+ it("throws if index is not bound", async () => {
187
+ await expect(pgvec.deleteIndex("nonexistent")).rejects.toThrow('Index "nonexistent" not bound');
188
+ });
189
+ });
190
+ describe("listIndexes", () => {
191
+ it("returns empty page when no indexes match prefix", async () => {
192
+ const page = await pgvec.listIndexes({ prefix: "nonexistent_prefix_" });
193
+ expect(page.data).toEqual([]);
194
+ expect(page.last).toBe(true);
195
+ });
196
+ it("lists created indexes", async () => {
197
+ await pgvec.createIndex({
198
+ id: "list_test_a",
199
+ schema: { id: { type: "string", pk: true } },
200
+ providerOptions: { schema: SCHEMA },
201
+ });
202
+ await pgvec.createIndex({
203
+ id: "list_test_b",
204
+ schema: { id: { type: "string", pk: true } },
205
+ providerOptions: { schema: SCHEMA },
206
+ });
207
+ const page = await pgvec.listIndexes();
208
+ const ids = page.data.map((s) => s.id);
209
+ expect(ids).toContain("list_test_a");
210
+ expect(ids).toContain("list_test_b");
211
+ });
212
+ it("filters by prefix", async () => {
213
+ await pgvec.createIndex({
214
+ id: "prefix_foo_1",
215
+ schema: { id: { type: "string", pk: true } },
216
+ providerOptions: { schema: SCHEMA },
217
+ });
218
+ await pgvec.createIndex({
219
+ id: "prefix_bar_1",
220
+ schema: { id: { type: "string", pk: true } },
221
+ providerOptions: { schema: SCHEMA },
222
+ });
223
+ const page = await pgvec.listIndexes({ prefix: "prefix_foo" });
224
+ expect(page.data).toHaveLength(1);
225
+ expect(page.data[0].id).toBe("prefix_foo_1");
226
+ });
227
+ it("respects limit and provides cursor for pagination", async () => {
228
+ await pgvec.createIndex({
229
+ id: "page_1",
230
+ schema: { id: { type: "string", pk: true } },
231
+ providerOptions: { schema: SCHEMA },
232
+ });
233
+ await pgvec.createIndex({
234
+ id: "page_2",
235
+ schema: { id: { type: "string", pk: true } },
236
+ providerOptions: { schema: SCHEMA },
237
+ });
238
+ await pgvec.createIndex({
239
+ id: "page_3",
240
+ schema: { id: { type: "string", pk: true } },
241
+ providerOptions: { schema: SCHEMA },
242
+ });
243
+ const page1 = await pgvec.listIndexes({ prefix: "page_", limit: 2 });
244
+ expect(page1.data).toHaveLength(2);
245
+ expect(page1.last).toBe(false);
246
+ const page2 = await page1.next();
247
+ expect(page2).not.toBeNull();
248
+ expect(page2.data).toHaveLength(1);
249
+ expect(page2.last).toBe(true);
250
+ });
251
+ });
252
+ describe("describeIndex", () => {
253
+ it("returns stats for an index", async () => {
254
+ await pgvec.createIndex({
255
+ id: "describe_test",
256
+ schema: {
257
+ id: { type: "string", pk: true },
258
+ content: { type: "string" },
259
+ embedding: { type: "vector", dimensions: 128, similarity: "cosine" },
260
+ },
261
+ providerOptions: { schema: SCHEMA },
262
+ });
263
+ const handle = pgvec.index("describe_test");
264
+ const vec = new Array(128).fill(0.1);
265
+ await handle.upsert({ id: "doc-1", content: "hello", embedding: vec });
266
+ await handle.upsert({ id: "doc-2", content: "world", embedding: vec });
267
+ const stats = await pgvec.describeIndex("describe_test");
268
+ expect(stats.id).toBe("describe_test");
269
+ expect(stats.count).toBe(2);
270
+ expect(stats.sizeb).toBeGreaterThan(0);
271
+ expect(stats.dimensions).toBe(128);
272
+ expect(stats.similarity).toBe("cosine");
273
+ expect(stats.status).toBe("ready");
274
+ });
275
+ it("throws if index is not bound", async () => {
276
+ await expect(pgvec.describeIndex("nonexistent")).rejects.toThrow('Index "nonexistent" not bound');
277
+ });
278
+ });
279
+ });
@@ -0,0 +1,60 @@
1
+ import type { Pool } from "pg";
2
+ import type { IndexHandle, DocumentPatch, QueryInput, SearchHit, FieldSchema, UnknownDocument, UpsertResult, PatchResult, DeleteResult } from "@kernl-sdk/retrieval";
3
+ import type { PGIndexConfig } from "./types.js";
4
+ /**
5
+ * pgvector-backed IndexHandle.
6
+ */
7
+ export declare class PGIndexHandle<TDocument = UnknownDocument> implements IndexHandle<TDocument> {
8
+ readonly id: string;
9
+ private pool;
10
+ private ensureInit;
11
+ private config?;
12
+ constructor(pool: Pool, ensureInit: () => Promise<void>, id: string, config?: PGIndexConfig);
13
+ /**
14
+ * Query the index using vector search, full-text search, or filters.
15
+ */
16
+ query(input: QueryInput): Promise<SearchHit<TDocument>[]>;
17
+ /**
18
+ * Upsert one or more documents.
19
+ *
20
+ * Documents are flat objects. The pkey field (default "id") is used for
21
+ * conflict resolution. All other fields are written to matching columns.
22
+ */
23
+ upsert(docs: TDocument | TDocument[]): Promise<UpsertResult>;
24
+ /**
25
+ * Patch one or more documents.
26
+ *
27
+ * Only specified fields are updated. Set a field to `null` to unset it.
28
+ */
29
+ patch(patches: DocumentPatch<TDocument> | DocumentPatch<TDocument>[]): Promise<PatchResult>;
30
+ /**
31
+ * Delete one or more documents by ID.
32
+ */
33
+ delete(ids: string | string[]): Promise<DeleteResult>;
34
+ /**
35
+ * Add a field to the index schema.
36
+ *
37
+ * Not yet implemented - left as a stub until we have a concrete use case
38
+ * to inform the design (e.g. vector-only vs general columns, index creation).
39
+ */
40
+ addField(_field: string, _schema: FieldSchema): Promise<void>;
41
+ /**
42
+ * Resolve table config: use explicit binding or derive from conventions.
43
+ *
44
+ * Resolution order:
45
+ * - If a PGIndexConfig was provided or bound via PGSearchIndex.createIndex()
46
+ * / bindIndex(), that config is used (schema, table, pkey, fields).
47
+ * - Otherwise we fall back to convention-based parsing of the index id via
48
+ * parseIndexId(id):
49
+ * - "docs" → schema: "public", table: "docs"
50
+ * - "analytics.events" → schema: "analytics", table: "events"
51
+ *
52
+ * This allows callers to:
53
+ * - Point at existing tables without an explicit bindIndex call by using
54
+ * either "table" or "schema.table" ids, and
55
+ * - Rely on explicit configs (from createIndex/bindIndex) when using the
56
+ * higher-level SearchIndex API.
57
+ */
58
+ private get table();
59
+ }
60
+ //# sourceMappingURL=handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../../src/pgvector/handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EACb,UAAU,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,YAAY,EACZ,WAAW,EACX,YAAY,EACb,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7C;;GAEG;AACH,qBAAa,aAAa,CAAC,SAAS,GAAG,eAAe,CACpD,YAAW,WAAW,CAAC,SAAS,CAAC;IAEjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,MAAM,CAAC,CAAgB;gBAG7B,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC/B,EAAE,EAAE,MAAM,EACV,MAAM,CAAC,EAAE,aAAa;IAQxB;;OAEG;IACG,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC;IAqC/D;;;;;OAKG;IACG,MAAM,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IA6ElE;;;;OAIG;IACG,KAAK,CACT,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,GAAG,aAAa,CAAC,SAAS,CAAC,EAAE,GAC7D,OAAO,CAAC,WAAW,CAAC;IAkDvB;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAmB3D;;;;;OAKG;IACG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAMnE;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,KAAK,KAAK,GAQhB;CACF"}
@@ -0,0 +1,213 @@
1
+ import { normalizeQuery } from "@kernl-sdk/retrieval";
2
+ import { SEARCH_HIT } from "./hit.js";
3
+ import { sqlize, SQL_SELECT, SQL_WHERE, SQL_ORDER, SQL_LIMIT } from "./sql/index.js";
4
+ import { parseIndexId, isVector } from "./utils.js";
5
+ /**
6
+ * pgvector-backed IndexHandle.
7
+ */
8
+ export class PGIndexHandle {
9
+ id;
10
+ pool;
11
+ ensureInit;
12
+ config;
13
+ constructor(pool, ensureInit, id, config) {
14
+ this.id = id;
15
+ this.pool = pool;
16
+ this.ensureInit = ensureInit;
17
+ this.config = config;
18
+ }
19
+ /**
20
+ * Query the index using vector search, full-text search, or filters.
21
+ */
22
+ async query(input) {
23
+ await this.ensureInit();
24
+ const q = normalizeQuery(input);
25
+ const { schema, table, pkey } = this.table;
26
+ const sqlized = sqlize(q, { pkey, schema, table, binding: this.config });
27
+ const select = SQL_SELECT.encode(sqlized.select);
28
+ const where = SQL_WHERE.encode({
29
+ ...sqlized.where,
30
+ startIdx: select.params.length + 1,
31
+ });
32
+ const order = SQL_ORDER.encode(sqlized.order);
33
+ const limit = SQL_LIMIT.encode({
34
+ ...sqlized.limit,
35
+ startIdx: select.params.length + where.params.length + 1,
36
+ });
37
+ const sql = `
38
+ SELECT ${select.sql}
39
+ FROM "${schema}"."${table}"
40
+ ${where.sql ? `WHERE ${where.sql}` : ""}
41
+ ORDER BY ${order.sql}
42
+ ${limit.sql}
43
+ `;
44
+ const params = [...select.params, ...where.params, ...limit.params];
45
+ const result = await this.pool.query(sql, params);
46
+ return result.rows.map((row) => SEARCH_HIT.decode(row, this.id, this.config));
47
+ }
48
+ /* ---- document ops ---- */
49
+ /**
50
+ * Upsert one or more documents.
51
+ *
52
+ * Documents are flat objects. The pkey field (default "id") is used for
53
+ * conflict resolution. All other fields are written to matching columns.
54
+ */
55
+ async upsert(docs) {
56
+ const arr = Array.isArray(docs) ? docs : [docs];
57
+ if (arr.length === 0)
58
+ return { count: 0, inserted: 0, updated: 0 };
59
+ await this.ensureInit();
60
+ const { schema, table, pkey, fields } = this.table;
61
+ // Find which logical field maps to the pkey column
62
+ let pkeyField = pkey; // default: same name
63
+ for (const [fieldName, fieldCfg] of Object.entries(fields)) {
64
+ if (fieldCfg.column === pkey) {
65
+ pkeyField = fieldName;
66
+ break;
67
+ }
68
+ }
69
+ // collect field names from first doc (excluding the pkey field)
70
+ const first = arr[0];
71
+ const fieldNames = Object.keys(first).filter((k) => k !== pkeyField);
72
+ // map field name → column name (from binding or same name)
73
+ const colFor = (name) => fields[name]?.column ?? name;
74
+ const cols = [pkey, ...fieldNames.map(colFor)];
75
+ const params = [];
76
+ const rows = [];
77
+ for (const doc of arr) {
78
+ const d = doc;
79
+ const placeholders = [];
80
+ // pkey value - use the logical field name, not column name
81
+ const pkval = d[pkeyField];
82
+ if (typeof pkval !== "string") {
83
+ throw new Error(`Document missing string field "${pkeyField}"`);
84
+ }
85
+ params.push(pkval);
86
+ placeholders.push(`$${params.length}`);
87
+ // ...other fields
88
+ for (const field of fieldNames) {
89
+ const val = d[field] ?? null;
90
+ const binding = fields[field];
91
+ // detect vector: explicit binding or runtime check
92
+ const isVec = binding?.type === "vector" || isVector(val);
93
+ params.push(isVec ? JSON.stringify(val) : val);
94
+ placeholders.push(isVec ? `$${params.length}::vector` : `$${params.length}`);
95
+ }
96
+ rows.push(`(${placeholders.join(", ")})`);
97
+ }
98
+ // build SET clause for conflict (exclude pkey)
99
+ const sets = cols.slice(1).map((c) => `"${c}" = EXCLUDED."${c}"`);
100
+ // xmax = 0 means inserted, xmax != 0 means updated
101
+ const sql = `
102
+ INSERT INTO "${schema}"."${table}" (${cols.map((c) => `"${c}"`).join(", ")})
103
+ VALUES ${rows.join(", ")}
104
+ ON CONFLICT ("${pkey}") DO UPDATE SET ${sets.join(", ")}
105
+ RETURNING (xmax = 0) as inserted
106
+ `;
107
+ const result = await this.pool.query(sql, params);
108
+ const inserted = result.rows.filter((r) => r.inserted).length;
109
+ return {
110
+ count: result.rowCount ?? 0,
111
+ inserted,
112
+ updated: (result.rowCount ?? 0) - inserted,
113
+ };
114
+ }
115
+ /**
116
+ * Patch one or more documents.
117
+ *
118
+ * Only specified fields are updated. Set a field to `null` to unset it.
119
+ */
120
+ async patch(patches) {
121
+ const arr = Array.isArray(patches) ? patches : [patches];
122
+ if (arr.length === 0)
123
+ return { count: 0 };
124
+ await this.ensureInit();
125
+ const { schema, table, pkey, fields } = this.table;
126
+ let totalCount = 0;
127
+ // process each patch individually (different fields may be updated)
128
+ for (const patch of arr) {
129
+ const pkval = patch.id;
130
+ if (typeof pkval !== "string") {
131
+ throw new Error(`Patch missing string field "id"`);
132
+ }
133
+ // collect fields to update (excluding pkey)
134
+ const updates = [];
135
+ const params = [];
136
+ for (const [key, val] of Object.entries(patch)) {
137
+ if (key === "id" || key === pkey)
138
+ continue;
139
+ if (val === undefined)
140
+ continue;
141
+ const col = fields[key]?.column ?? key;
142
+ const binding = fields[key];
143
+ const isVec = binding?.type === "vector" || isVector(val);
144
+ params.push(isVec && val !== null ? JSON.stringify(val) : val);
145
+ updates.push(`"${col}" = $${params.length}${isVec && val !== null ? "::vector" : ""}`);
146
+ }
147
+ if (updates.length === 0)
148
+ continue;
149
+ params.push(pkval);
150
+ const sql = `
151
+ UPDATE "${schema}"."${table}"
152
+ SET ${updates.join(", ")}
153
+ WHERE "${pkey}" = $${params.length}
154
+ `;
155
+ const result = await this.pool.query(sql, params);
156
+ totalCount += result.rowCount ?? 0;
157
+ }
158
+ return { count: totalCount };
159
+ }
160
+ /**
161
+ * Delete one or more documents by ID.
162
+ */
163
+ async delete(ids) {
164
+ const arr = Array.isArray(ids) ? ids : [ids];
165
+ if (arr.length === 0)
166
+ return { count: 0 };
167
+ await this.ensureInit();
168
+ const { schema, table, pkey } = this.table;
169
+ const placeholders = arr.map((_, i) => `$${i + 1}`);
170
+ const sql = `
171
+ DELETE FROM "${schema}"."${table}"
172
+ WHERE "${pkey}" IN (${placeholders.join(", ")})
173
+ `;
174
+ const result = await this.pool.query(sql, arr);
175
+ return { count: result.rowCount ?? 0 };
176
+ }
177
+ /* ---- schema ops ---- */
178
+ /**
179
+ * Add a field to the index schema.
180
+ *
181
+ * Not yet implemented - left as a stub until we have a concrete use case
182
+ * to inform the design (e.g. vector-only vs general columns, index creation).
183
+ */
184
+ async addField(_field, _schema) {
185
+ throw new Error("addField not yet implemented");
186
+ }
187
+ /* ---- internal utils ---- */
188
+ /**
189
+ * Resolve table config: use explicit binding or derive from conventions.
190
+ *
191
+ * Resolution order:
192
+ * - If a PGIndexConfig was provided or bound via PGSearchIndex.createIndex()
193
+ * / bindIndex(), that config is used (schema, table, pkey, fields).
194
+ * - Otherwise we fall back to convention-based parsing of the index id via
195
+ * parseIndexId(id):
196
+ * - "docs" → schema: "public", table: "docs"
197
+ * - "analytics.events" → schema: "analytics", table: "events"
198
+ *
199
+ * This allows callers to:
200
+ * - Point at existing tables without an explicit bindIndex call by using
201
+ * either "table" or "schema.table" ids, and
202
+ * - Rely on explicit configs (from createIndex/bindIndex) when using the
203
+ * higher-level SearchIndex API.
204
+ */
205
+ get table() {
206
+ if (this.config) {
207
+ return this.config;
208
+ }
209
+ // convention-based defaults
210
+ const { schema, table } = parseIndexId(this.id);
211
+ return { schema, table, pkey: "id", fields: {} };
212
+ }
213
+ }
@@ -0,0 +1,10 @@
1
+ import { SearchHit, UnknownDocument } from "@kernl-sdk/retrieval";
2
+ import type { PGIndexConfig } from "./types.js";
3
+ /**
4
+ * Codec for converting DB row ←→ SearchHit.
5
+ */
6
+ export declare const SEARCH_HIT: {
7
+ encode: (_hit: SearchHit) => Record<string, unknown>;
8
+ decode: <TDocument = UnknownDocument>(row: Record<string, unknown>, index: string, config?: PGIndexConfig) => SearchHit<TDocument>;
9
+ };
10
+ //# sourceMappingURL=hit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hit.d.ts","sourceRoot":"","sources":["../../src/pgvector/hit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,SAAS,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,UAAU;mBACN,SAAS,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;aAIzC,SAAS,yBACX,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SACrB,MAAM,WACJ,aAAa,KACrB,SAAS,CAAC,SAAS,CAAC;CAuCxB,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Codec for converting DB row ←→ SearchHit.
3
+ */
4
+ export const SEARCH_HIT = {
5
+ encode: (_hit) => {
6
+ throw new Error("SEARCH_HIT.encode: not implemented");
7
+ },
8
+ decode: (row, index, config) => {
9
+ const { id, score, ...rest } = row;
10
+ const doc = {};
11
+ // Helper to parse pgvector strings like "[0.1,0.2,0.3]" back to arrays
12
+ const parseValue = (val, isVector) => {
13
+ if (isVector && typeof val === "string" && val.startsWith("[")) {
14
+ return JSON.parse(val);
15
+ }
16
+ return val;
17
+ };
18
+ if (config) {
19
+ // map columns back to logical field names
20
+ for (const [field, cfg] of Object.entries(config.fields)) {
21
+ const col = cfg.column;
22
+ if (col in rest) {
23
+ const isVector = cfg.type === "vector";
24
+ doc[field] = parseValue(rest[col], isVector);
25
+ }
26
+ }
27
+ }
28
+ else {
29
+ // no config - parse all values, detecting vectors by format
30
+ for (const [k, v] of Object.entries(rest)) {
31
+ const isVector = typeof v === "string" && v.startsWith("[") && v.endsWith("]");
32
+ doc[k] = parseValue(v, isVector);
33
+ }
34
+ }
35
+ // Always include id in document for consistency
36
+ doc.id = String(id);
37
+ return {
38
+ id: String(id),
39
+ index,
40
+ score: typeof score === "number" ? score : 0,
41
+ document: doc,
42
+ };
43
+ },
44
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * pgvector search index.
3
+ */
4
+ export { PGSearchIndex, type PGSearchIndexConfig } from "./search.js";
5
+ export { PGIndexHandle } from "./handle.js";
6
+ export type { PGIndexConfig, PGFieldBinding } from "./types.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pgvector/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * pgvector search index.
3
+ */
4
+ export { PGSearchIndex } from "./search.js";
5
+ export { PGIndexHandle } from "./handle.js";