@kernl-sdk/pg 0.1.10 → 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.
- package/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-check-types.log +36 -0
- package/CHANGELOG.md +41 -0
- package/README.md +124 -0
- package/dist/__tests__/integration.test.js +81 -1
- package/dist/__tests__/memory-integration.test.d.ts +2 -0
- package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
- package/dist/__tests__/memory-integration.test.js +287 -0
- package/dist/__tests__/memory.test.d.ts +2 -0
- package/dist/__tests__/memory.test.d.ts.map +1 -0
- package/dist/__tests__/memory.test.js +357 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/memory/sql.d.ts +30 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +100 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +114 -0
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +9 -3
- package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
- package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/handle.test.js +277 -0
- package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
- package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/hit.test.js +134 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
- package/dist/pgvector/__tests__/search.test.d.ts +2 -0
- package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/search.test.js +279 -0
- package/dist/pgvector/handle.d.ts +60 -0
- package/dist/pgvector/handle.d.ts.map +1 -0
- package/dist/pgvector/handle.js +213 -0
- package/dist/pgvector/hit.d.ts +10 -0
- package/dist/pgvector/hit.d.ts.map +1 -0
- package/dist/pgvector/hit.js +44 -0
- package/dist/pgvector/index.d.ts +7 -0
- package/dist/pgvector/index.d.ts.map +1 -0
- package/dist/pgvector/index.js +5 -0
- package/dist/pgvector/search.d.ts +60 -0
- package/dist/pgvector/search.d.ts.map +1 -0
- package/dist/pgvector/search.js +227 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/order.test.js +218 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/query.test.js +392 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/select.test.js +293 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/where.test.js +488 -0
- package/dist/pgvector/sql/index.d.ts +7 -0
- package/dist/pgvector/sql/index.d.ts.map +1 -0
- package/dist/pgvector/sql/index.js +6 -0
- package/dist/pgvector/sql/limit.d.ts +8 -0
- package/dist/pgvector/sql/limit.d.ts.map +1 -0
- package/dist/pgvector/sql/limit.js +20 -0
- package/dist/pgvector/sql/order.d.ts +9 -0
- package/dist/pgvector/sql/order.d.ts.map +1 -0
- package/dist/pgvector/sql/order.js +47 -0
- package/dist/pgvector/sql/query.d.ts +46 -0
- package/dist/pgvector/sql/query.d.ts.map +1 -0
- package/dist/pgvector/sql/query.js +54 -0
- package/dist/pgvector/sql/schema.d.ts +16 -0
- package/dist/pgvector/sql/schema.d.ts.map +1 -0
- package/dist/pgvector/sql/schema.js +47 -0
- package/dist/pgvector/sql/select.d.ts +11 -0
- package/dist/pgvector/sql/select.d.ts.map +1 -0
- package/dist/pgvector/sql/select.js +87 -0
- package/dist/pgvector/sql/where.d.ts +8 -0
- package/dist/pgvector/sql/where.d.ts.map +1 -0
- package/dist/pgvector/sql/where.js +137 -0
- package/dist/pgvector/types.d.ts +20 -0
- package/dist/pgvector/types.d.ts.map +1 -0
- package/dist/pgvector/types.js +1 -0
- package/dist/pgvector/utils.d.ts +18 -0
- package/dist/pgvector/utils.d.ts.map +1 -0
- package/dist/pgvector/utils.js +22 -0
- package/dist/postgres.d.ts +19 -26
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +15 -27
- package/dist/storage.d.ts +62 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -10
- package/dist/thread/sql.d.ts +38 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +7 -3
- package/dist/thread/store.d.ts.map +1 -1
- package/dist/thread/store.js +46 -105
- package/package.json +8 -5
- package/src/__tests__/integration.test.ts +114 -15
- package/src/__tests__/memory-integration.test.ts +355 -0
- package/src/__tests__/memory.test.ts +428 -0
- package/src/index.ts +19 -3
- package/src/memory/sql.ts +141 -0
- package/src/memory/store.ts +166 -0
- package/src/migrations.ts +13 -3
- package/src/pgvector/README.md +50 -0
- package/src/pgvector/__tests__/handle.test.ts +335 -0
- package/src/pgvector/__tests__/hit.test.ts +165 -0
- package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
- package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
- package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
- package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
- package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
- package/src/pgvector/__tests__/search.test.ts +366 -0
- package/src/pgvector/handle.ts +285 -0
- package/src/pgvector/hit.ts +56 -0
- package/src/pgvector/index.ts +7 -0
- package/src/pgvector/search.ts +330 -0
- package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
- package/src/pgvector/sql/__tests__/order.test.ts +248 -0
- package/src/pgvector/sql/__tests__/query.test.ts +548 -0
- package/src/pgvector/sql/__tests__/select.test.ts +367 -0
- package/src/pgvector/sql/__tests__/where.test.ts +554 -0
- package/src/pgvector/sql/index.ts +14 -0
- package/src/pgvector/sql/limit.ts +29 -0
- package/src/pgvector/sql/order.ts +55 -0
- package/src/pgvector/sql/query.ts +112 -0
- package/src/pgvector/sql/schema.ts +61 -0
- package/src/pgvector/sql/select.ts +100 -0
- package/src/pgvector/sql/where.ts +152 -0
- package/src/pgvector/types.ts +21 -0
- package/src/pgvector/utils.ts +24 -0
- package/src/postgres.ts +31 -33
- package/src/storage.ts +102 -11
- package/src/thread/sql.ts +159 -0
- package/src/thread/store.ts +58 -127
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.test.d.ts","sourceRoot":"","sources":["../../../../src/pgvector/sql/__tests__/query.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { sqlize } from "../query.js";
|
|
3
|
+
import { SQL_SELECT } from "../select.js";
|
|
4
|
+
import { SQL_WHERE } from "../where.js";
|
|
5
|
+
import { SQL_ORDER } from "../order.js";
|
|
6
|
+
import { SQL_LIMIT } from "../limit.js";
|
|
7
|
+
describe("sqlize", () => {
|
|
8
|
+
it("converts query with vector signal", () => {
|
|
9
|
+
const vector = [0.1, 0.2, 0.3];
|
|
10
|
+
const result = sqlize({ query: [{ embedding: vector }] }, { pkey: "id", schema: "public", table: "docs" });
|
|
11
|
+
expect(result.select).toEqual({
|
|
12
|
+
pkey: "id",
|
|
13
|
+
signals: [{ embedding: vector }],
|
|
14
|
+
binding: undefined,
|
|
15
|
+
include: undefined,
|
|
16
|
+
});
|
|
17
|
+
expect(result.where).toEqual({ filter: undefined });
|
|
18
|
+
expect(result.order).toEqual({
|
|
19
|
+
signals: [{ embedding: vector }],
|
|
20
|
+
orderBy: undefined,
|
|
21
|
+
binding: undefined,
|
|
22
|
+
schema: "public",
|
|
23
|
+
table: "docs",
|
|
24
|
+
});
|
|
25
|
+
expect(result.limit).toEqual({ topK: 10, offset: 0 });
|
|
26
|
+
});
|
|
27
|
+
it("throws on multi-signal fusion (not supported by pgvector)", () => {
|
|
28
|
+
expect(() => sqlize({
|
|
29
|
+
max: [
|
|
30
|
+
{ content: "search", weight: 0.3 },
|
|
31
|
+
{ embedding: [0.1, 0.2], weight: 0.7 },
|
|
32
|
+
],
|
|
33
|
+
}, { pkey: "doc_id", schema: "public", table: "docs" })).toThrow("pgvector does not support multi-signal fusion");
|
|
34
|
+
});
|
|
35
|
+
it("converts query with filter", () => {
|
|
36
|
+
const result = sqlize({
|
|
37
|
+
query: [{ embedding: [0.1, 0.2] }],
|
|
38
|
+
filter: { status: "active", views: { $gt: 100 } },
|
|
39
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
40
|
+
expect(result.where.filter).toEqual({
|
|
41
|
+
status: "active",
|
|
42
|
+
views: { $gt: 100 },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it("converts query with orderBy", () => {
|
|
46
|
+
const result = sqlize({
|
|
47
|
+
filter: { status: "active" },
|
|
48
|
+
orderBy: { field: "created_at", direction: "desc" },
|
|
49
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
50
|
+
expect(result.order.orderBy).toEqual({
|
|
51
|
+
field: "created_at",
|
|
52
|
+
direction: "desc",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it("converts query with pagination", () => {
|
|
56
|
+
const result = sqlize({
|
|
57
|
+
query: [{ embedding: [0.1, 0.2] }],
|
|
58
|
+
topK: 25,
|
|
59
|
+
offset: 50,
|
|
60
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
61
|
+
expect(result.limit).toEqual({ topK: 25, offset: 50 });
|
|
62
|
+
});
|
|
63
|
+
it("uses default pagination values", () => {
|
|
64
|
+
const result = sqlize({ query: [{ embedding: [0.1, 0.2] }] }, { pkey: "id", schema: "public", table: "docs" });
|
|
65
|
+
expect(result.limit).toEqual({ topK: 10, offset: 0 });
|
|
66
|
+
});
|
|
67
|
+
it("passes binding through to all inputs", () => {
|
|
68
|
+
const binding = {
|
|
69
|
+
schema: "public",
|
|
70
|
+
table: "docs",
|
|
71
|
+
pkey: "id",
|
|
72
|
+
fields: {
|
|
73
|
+
embedding: {
|
|
74
|
+
column: "vec_col",
|
|
75
|
+
type: "vector",
|
|
76
|
+
dimensions: 3,
|
|
77
|
+
similarity: "cosine",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const result = sqlize({ query: [{ embedding: [0.1, 0.2, 0.3] }] }, { pkey: "id", schema: "public", table: "docs", binding });
|
|
82
|
+
expect(result.select.binding).toBe(binding);
|
|
83
|
+
expect(result.order.binding).toBe(binding);
|
|
84
|
+
});
|
|
85
|
+
it("uses custom pkey", () => {
|
|
86
|
+
const result = sqlize({ query: [{ embedding: [0.1, 0.2] }] }, { pkey: "document_id", schema: "public", table: "docs" });
|
|
87
|
+
expect(result.select.pkey).toBe("document_id");
|
|
88
|
+
});
|
|
89
|
+
it("handles empty query (filter-only)", () => {
|
|
90
|
+
const result = sqlize({
|
|
91
|
+
filter: { status: "active" },
|
|
92
|
+
orderBy: { field: "created_at", direction: "desc" },
|
|
93
|
+
topK: 100,
|
|
94
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
95
|
+
expect(result.select.signals).toEqual([]);
|
|
96
|
+
expect(result.order.signals).toEqual([]);
|
|
97
|
+
expect(result.where.filter).toEqual({ status: "active" });
|
|
98
|
+
});
|
|
99
|
+
describe("signal priority", () => {
|
|
100
|
+
it("prefers query over max when both present", () => {
|
|
101
|
+
const querySignals = [{ embedding: [0.1, 0.2, 0.3] }];
|
|
102
|
+
const maxSignals = [{ embedding: [0.4, 0.5, 0.6] }];
|
|
103
|
+
const result = sqlize({ query: querySignals, max: maxSignals }, { pkey: "id", schema: "public", table: "docs" });
|
|
104
|
+
// query takes precedence over max
|
|
105
|
+
expect(result.select.signals).toEqual(querySignals);
|
|
106
|
+
expect(result.order.signals).toEqual(querySignals);
|
|
107
|
+
});
|
|
108
|
+
it("uses max when query is undefined", () => {
|
|
109
|
+
const maxSignals = [{ embedding: [0.4, 0.5, 0.6] }];
|
|
110
|
+
const result = sqlize({ max: maxSignals }, { pkey: "id", schema: "public", table: "docs" });
|
|
111
|
+
expect(result.select.signals).toEqual(maxSignals);
|
|
112
|
+
expect(result.order.signals).toEqual(maxSignals);
|
|
113
|
+
});
|
|
114
|
+
it("uses empty array when both query and max are undefined", () => {
|
|
115
|
+
const result = sqlize({ filter: { status: "active" } }, { pkey: "id", schema: "public", table: "docs" });
|
|
116
|
+
expect(result.select.signals).toEqual([]);
|
|
117
|
+
expect(result.order.signals).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("filter-only with binding", () => {
|
|
121
|
+
it("passes binding through for filter-only queries", () => {
|
|
122
|
+
const binding = {
|
|
123
|
+
schema: "public",
|
|
124
|
+
table: "docs",
|
|
125
|
+
pkey: "id",
|
|
126
|
+
fields: {
|
|
127
|
+
embedding: {
|
|
128
|
+
column: "vec_col",
|
|
129
|
+
type: "vector",
|
|
130
|
+
dimensions: 3,
|
|
131
|
+
similarity: "cosine",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const result = sqlize({ filter: { status: "active" }, topK: 50 }, { pkey: "id", schema: "public", table: "docs", binding });
|
|
136
|
+
expect(result.select.binding).toBe(binding);
|
|
137
|
+
expect(result.order.binding).toBe(binding);
|
|
138
|
+
expect(result.select.signals).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe("orderBy-only queries", () => {
|
|
142
|
+
it("handles orderBy without query signals", () => {
|
|
143
|
+
const result = sqlize({
|
|
144
|
+
orderBy: { field: "created_at", direction: "asc" },
|
|
145
|
+
topK: 20,
|
|
146
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
147
|
+
expect(result.select.signals).toEqual([]);
|
|
148
|
+
expect(result.order.signals).toEqual([]);
|
|
149
|
+
expect(result.order.orderBy).toEqual({
|
|
150
|
+
field: "created_at",
|
|
151
|
+
direction: "asc",
|
|
152
|
+
});
|
|
153
|
+
expect(result.limit).toEqual({ topK: 20, offset: 0 });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe("edge cases", () => {
|
|
157
|
+
it("handles query with empty signals array", () => {
|
|
158
|
+
const result = sqlize({ query: [] }, { pkey: "id", schema: "public", table: "docs" });
|
|
159
|
+
expect(result.select.signals).toEqual([]);
|
|
160
|
+
expect(result.order.signals).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
it("handles all options together", () => {
|
|
163
|
+
const binding = {
|
|
164
|
+
schema: "public",
|
|
165
|
+
table: "docs",
|
|
166
|
+
pkey: "id",
|
|
167
|
+
fields: {
|
|
168
|
+
embedding: {
|
|
169
|
+
column: "vec_col",
|
|
170
|
+
type: "vector",
|
|
171
|
+
dimensions: 3,
|
|
172
|
+
similarity: "euclidean",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const result = sqlize({
|
|
177
|
+
query: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
178
|
+
filter: { status: "active", views: { $gt: 100 } },
|
|
179
|
+
orderBy: { field: "created_at", direction: "desc" },
|
|
180
|
+
topK: 50,
|
|
181
|
+
offset: 25,
|
|
182
|
+
}, { pkey: "doc_id", schema: "public", table: "documents", binding });
|
|
183
|
+
expect(result.select.pkey).toBe("doc_id");
|
|
184
|
+
expect(result.select.signals).toHaveLength(1);
|
|
185
|
+
expect(result.select.binding).toBe(binding);
|
|
186
|
+
expect(result.where.filter).toEqual({
|
|
187
|
+
status: "active",
|
|
188
|
+
views: { $gt: 100 },
|
|
189
|
+
});
|
|
190
|
+
expect(result.order.orderBy).toEqual({
|
|
191
|
+
field: "created_at",
|
|
192
|
+
direction: "desc",
|
|
193
|
+
});
|
|
194
|
+
expect(result.order.binding).toBe(binding);
|
|
195
|
+
expect(result.limit).toEqual({ topK: 50, offset: 25 });
|
|
196
|
+
});
|
|
197
|
+
it("handles undefined filter", () => {
|
|
198
|
+
const result = sqlize({ query: [{ embedding: [0.1, 0.2] }] }, { pkey: "id", schema: "public", table: "docs" });
|
|
199
|
+
expect(result.where.filter).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
it("handles undefined orderBy", () => {
|
|
202
|
+
const result = sqlize({ query: [{ embedding: [0.1, 0.2] }] }, { pkey: "id", schema: "public", table: "docs" });
|
|
203
|
+
expect(result.order.orderBy).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
/**
|
|
208
|
+
* Full pipeline integration tests.
|
|
209
|
+
* These tests verify that sqlize → all codecs → assembled SQL works correctly.
|
|
210
|
+
*/
|
|
211
|
+
describe("full pipeline integration", () => {
|
|
212
|
+
it("assembles complete SQL with vector search, filter, and pagination", () => {
|
|
213
|
+
const vector = [0.1, 0.2, 0.3];
|
|
214
|
+
const binding = {
|
|
215
|
+
schema: "public",
|
|
216
|
+
table: "documents",
|
|
217
|
+
pkey: "id",
|
|
218
|
+
fields: {
|
|
219
|
+
embedding: {
|
|
220
|
+
column: "vec_col",
|
|
221
|
+
type: "vector",
|
|
222
|
+
dimensions: 3,
|
|
223
|
+
similarity: "cosine",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
// Step 1: sqlize the query
|
|
228
|
+
const query = sqlize({
|
|
229
|
+
query: [{ embedding: vector }],
|
|
230
|
+
filter: { status: "active", views: { $gt: 100 } },
|
|
231
|
+
topK: 25,
|
|
232
|
+
offset: 50,
|
|
233
|
+
}, { pkey: "doc_id", schema: "public", table: "documents", binding });
|
|
234
|
+
// Step 2: encode each clause (include: false to exclude extra columns)
|
|
235
|
+
const select = SQL_SELECT.encode({ ...query.select, include: false });
|
|
236
|
+
const whereStartIdx = 1 + select.params.length;
|
|
237
|
+
const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
|
|
238
|
+
const order = SQL_ORDER.encode(query.order);
|
|
239
|
+
const limitStartIdx = whereStartIdx + where.params.length;
|
|
240
|
+
const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
|
|
241
|
+
// Step 3: verify individual clauses
|
|
242
|
+
expect(select.sql).toBe('"doc_id" as id, 1 - ("vec_col" <=> $1::vector) as score');
|
|
243
|
+
expect(select.params).toEqual([JSON.stringify(vector)]);
|
|
244
|
+
expect(where.sql).toBe('"status" = $2 AND "views" > $3');
|
|
245
|
+
expect(where.params).toEqual(["active", 100]);
|
|
246
|
+
expect(order.sql).toBe('"vec_col" <=> $1::vector');
|
|
247
|
+
expect(limit.sql).toBe("LIMIT $4 OFFSET $5");
|
|
248
|
+
expect(limit.params).toEqual([25, 50]);
|
|
249
|
+
// Step 4: verify assembled params array
|
|
250
|
+
const allParams = [...select.params, ...where.params, ...limit.params];
|
|
251
|
+
expect(allParams).toEqual([
|
|
252
|
+
JSON.stringify(vector), // $1
|
|
253
|
+
"active", // $2
|
|
254
|
+
100, // $3
|
|
255
|
+
25, // $4
|
|
256
|
+
50, // $5
|
|
257
|
+
]);
|
|
258
|
+
// Step 5: verify param indices are correct
|
|
259
|
+
expect(whereStartIdx).toBe(2);
|
|
260
|
+
expect(limitStartIdx).toBe(4);
|
|
261
|
+
});
|
|
262
|
+
it("assembles SQL with filter-only query (no vector)", () => {
|
|
263
|
+
const query = sqlize({
|
|
264
|
+
filter: {
|
|
265
|
+
status: "published",
|
|
266
|
+
$or: [{ featured: true }, { views: { $gte: 1000 } }],
|
|
267
|
+
},
|
|
268
|
+
orderBy: { field: "created_at", direction: "desc" },
|
|
269
|
+
topK: 10,
|
|
270
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
271
|
+
const select = SQL_SELECT.encode(query.select);
|
|
272
|
+
const whereStartIdx = 1 + select.params.length;
|
|
273
|
+
const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
|
|
274
|
+
const order = SQL_ORDER.encode(query.order);
|
|
275
|
+
const limitStartIdx = whereStartIdx + where.params.length;
|
|
276
|
+
const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
|
|
277
|
+
// No vector signal, so no params from SELECT
|
|
278
|
+
expect(select.sql).toBe('"id" as id, 1 as score');
|
|
279
|
+
expect(select.params).toEqual([]);
|
|
280
|
+
// WHERE starts at $1 since SELECT has no params
|
|
281
|
+
expect(where.sql).toBe('"status" = $1 AND (("featured" = $2) OR ("views" >= $3))');
|
|
282
|
+
expect(where.params).toEqual(["published", true, 1000]);
|
|
283
|
+
// ORDER BY uses explicit field (table-qualified to avoid ambiguity)
|
|
284
|
+
expect(order.sql).toBe('"public"."docs"."created_at" DESC');
|
|
285
|
+
// LIMIT starts at $4
|
|
286
|
+
expect(limit.sql).toBe("LIMIT $4");
|
|
287
|
+
expect(limit.params).toEqual([10]);
|
|
288
|
+
});
|
|
289
|
+
it("assembles SQL with complex nested filter", () => {
|
|
290
|
+
const vector = [0.5, 0.5];
|
|
291
|
+
const query = sqlize({
|
|
292
|
+
query: [{ embedding: vector }],
|
|
293
|
+
filter: {
|
|
294
|
+
$and: [
|
|
295
|
+
{ type: "article" },
|
|
296
|
+
{
|
|
297
|
+
$or: [
|
|
298
|
+
{ status: "published" },
|
|
299
|
+
{ $and: [{ status: "draft" }, { author_id: 123 }] },
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
deleted_at: null,
|
|
304
|
+
},
|
|
305
|
+
topK: 5,
|
|
306
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
307
|
+
const select = SQL_SELECT.encode(query.select);
|
|
308
|
+
const whereStartIdx = 1 + select.params.length;
|
|
309
|
+
const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
|
|
310
|
+
const limitStartIdx = whereStartIdx + where.params.length;
|
|
311
|
+
const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
|
|
312
|
+
// SELECT uses $1
|
|
313
|
+
expect(select.params).toHaveLength(1);
|
|
314
|
+
// WHERE uses $2-$6
|
|
315
|
+
expect(where.sql).toBe('(("type" = $2) AND ((("status" = $3) OR ((("status" = $4) AND ("author_id" = $5)))))) AND "deleted_at" IS NULL');
|
|
316
|
+
expect(where.params).toEqual(["article", "published", "draft", 123]);
|
|
317
|
+
// LIMIT uses $6
|
|
318
|
+
expect(limit.sql).toBe("LIMIT $6");
|
|
319
|
+
expect(limit.params).toEqual([5]);
|
|
320
|
+
});
|
|
321
|
+
it("handles euclidean distance correctly through pipeline", () => {
|
|
322
|
+
const vector = [1.0, 2.0, 3.0];
|
|
323
|
+
const binding = {
|
|
324
|
+
schema: "public",
|
|
325
|
+
table: "docs",
|
|
326
|
+
pkey: "id",
|
|
327
|
+
fields: {
|
|
328
|
+
embedding: {
|
|
329
|
+
column: "embedding",
|
|
330
|
+
type: "vector",
|
|
331
|
+
dimensions: 3,
|
|
332
|
+
similarity: "euclidean",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
const query = sqlize({ query: [{ embedding: vector }], topK: 10 }, { pkey: "id", schema: "public", table: "docs", binding });
|
|
337
|
+
const select = SQL_SELECT.encode({ ...query.select, include: false });
|
|
338
|
+
const order = SQL_ORDER.encode(query.order);
|
|
339
|
+
// Euclidean uses <-> operator
|
|
340
|
+
expect(select.sql).toBe('"id" as id, 1 / (1 + ("embedding" <-> $1::vector)) as score');
|
|
341
|
+
expect(order.sql).toBe('"embedding" <-> $1::vector');
|
|
342
|
+
});
|
|
343
|
+
it("handles dot product correctly through pipeline", () => {
|
|
344
|
+
const vector = [1.0, 2.0, 3.0];
|
|
345
|
+
const binding = {
|
|
346
|
+
schema: "public",
|
|
347
|
+
table: "docs",
|
|
348
|
+
pkey: "id",
|
|
349
|
+
fields: {
|
|
350
|
+
embedding: {
|
|
351
|
+
column: "embedding",
|
|
352
|
+
type: "vector",
|
|
353
|
+
dimensions: 3,
|
|
354
|
+
similarity: "dot_product",
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
const query = sqlize({ query: [{ embedding: vector }], topK: 10 }, { pkey: "id", schema: "public", table: "docs", binding });
|
|
359
|
+
const select = SQL_SELECT.encode({ ...query.select, include: false });
|
|
360
|
+
const order = SQL_ORDER.encode(query.order);
|
|
361
|
+
// Dot product uses <#> operator
|
|
362
|
+
expect(select.sql).toBe('"id" as id, -("embedding" <#> $1::vector) as score');
|
|
363
|
+
expect(order.sql).toBe('"embedding" <#> $1::vector');
|
|
364
|
+
});
|
|
365
|
+
it("guards against parameter index drift", () => {
|
|
366
|
+
// This test ensures that when we chain all codecs together,
|
|
367
|
+
// the parameter indices don't drift or overlap
|
|
368
|
+
const query = sqlize({
|
|
369
|
+
query: [{ embedding: [0.1, 0.2] }],
|
|
370
|
+
filter: { a: 1, b: 2, c: 3 },
|
|
371
|
+
topK: 10,
|
|
372
|
+
offset: 20,
|
|
373
|
+
}, { pkey: "id", schema: "public", table: "docs" });
|
|
374
|
+
const select = SQL_SELECT.encode(query.select);
|
|
375
|
+
const whereStartIdx = 1 + select.params.length;
|
|
376
|
+
const where = SQL_WHERE.encode({ ...query.where, startIdx: whereStartIdx });
|
|
377
|
+
const limitStartIdx = whereStartIdx + where.params.length;
|
|
378
|
+
const limit = SQL_LIMIT.encode({ ...query.limit, startIdx: limitStartIdx });
|
|
379
|
+
// Verify no gaps in indices
|
|
380
|
+
// SELECT: $1 (vector)
|
|
381
|
+
// WHERE: $2, $3, $4 (a, b, c)
|
|
382
|
+
// LIMIT: $5, $6 (topK, offset)
|
|
383
|
+
expect(select.params).toHaveLength(1); // $1
|
|
384
|
+
expect(whereStartIdx).toBe(2);
|
|
385
|
+
expect(where.params).toHaveLength(3); // $2, $3, $4
|
|
386
|
+
expect(limitStartIdx).toBe(5);
|
|
387
|
+
expect(limit.params).toHaveLength(2); // $5, $6
|
|
388
|
+
// Total params should be 6
|
|
389
|
+
const allParams = [...select.params, ...where.params, ...limit.params];
|
|
390
|
+
expect(allParams).toHaveLength(6);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.test.d.ts","sourceRoot":"","sources":["../../../../src/pgvector/sql/__tests__/select.test.ts"],"names":[],"mappings":""}
|