@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,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge case integration tests for pgvector.
|
|
3
|
+
*
|
|
4
|
+
* Tests boundary conditions, error handling, special values,
|
|
5
|
+
* and security considerations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
|
9
|
+
import { Pool } from "pg";
|
|
10
|
+
|
|
11
|
+
import { PGSearchIndex } from "../../search";
|
|
12
|
+
import type { IndexHandle } from "@kernl-sdk/retrieval";
|
|
13
|
+
|
|
14
|
+
const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
|
|
15
|
+
const SCHEMA = "kernl_edge_integration_test";
|
|
16
|
+
|
|
17
|
+
describe.sequential("pgvector edge cases integration tests", () => {
|
|
18
|
+
if (!TEST_DB_URL) {
|
|
19
|
+
it.skip("requires KERNL_PG_TEST_URL environment variable", () => {});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let pool: Pool;
|
|
24
|
+
let pgvec: PGSearchIndex;
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
pool = new Pool({ connectionString: TEST_DB_URL });
|
|
28
|
+
|
|
29
|
+
await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);
|
|
30
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
31
|
+
await pool.query(`CREATE SCHEMA "${SCHEMA}"`);
|
|
32
|
+
|
|
33
|
+
pgvec = new PGSearchIndex({ pool });
|
|
34
|
+
}, 30000);
|
|
35
|
+
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
38
|
+
await pool.end();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ============================================================
|
|
42
|
+
// SPECIAL STRING VALUES
|
|
43
|
+
// ============================================================
|
|
44
|
+
|
|
45
|
+
describe("special string values", () => {
|
|
46
|
+
let handle: IndexHandle<any>;
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
await pgvec.createIndex({
|
|
50
|
+
id: "string_edge",
|
|
51
|
+
schema: {
|
|
52
|
+
id: { type: "string", pk: true },
|
|
53
|
+
title: { type: "string" },
|
|
54
|
+
content: { type: "string" },
|
|
55
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
56
|
+
},
|
|
57
|
+
providerOptions: { schema: SCHEMA },
|
|
58
|
+
});
|
|
59
|
+
handle = pgvec.index("string_edge");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."string_edge"`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles SQL injection attempt in value", async () => {
|
|
67
|
+
await handle.upsert({
|
|
68
|
+
id: "injection-1",
|
|
69
|
+
title: "'; DROP TABLE string_edge; --",
|
|
70
|
+
content: "Content",
|
|
71
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Table should still exist and be queryable
|
|
75
|
+
const hits = await handle.query({
|
|
76
|
+
filter: { id: "injection-1" },
|
|
77
|
+
topK: 1,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(hits).toHaveLength(1);
|
|
81
|
+
expect(hits[0].document?.title).toBe("'; DROP TABLE string_edge; --");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("handles SQL injection attempt in filter", async () => {
|
|
85
|
+
await handle.upsert({
|
|
86
|
+
id: "safe-doc",
|
|
87
|
+
title: "Safe Title",
|
|
88
|
+
content: "Content",
|
|
89
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const hits = await handle.query({
|
|
93
|
+
filter: { title: "'; DROP TABLE string_edge; --" },
|
|
94
|
+
topK: 10,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Should return no results, not crash
|
|
98
|
+
expect(hits).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("handles quotes in strings", async () => {
|
|
102
|
+
await handle.upsert({
|
|
103
|
+
id: "quotes",
|
|
104
|
+
title: 'He said "Hello"',
|
|
105
|
+
content: "It's a test",
|
|
106
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const hits = await handle.query({
|
|
110
|
+
filter: { id: "quotes" },
|
|
111
|
+
topK: 1,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(hits[0].document?.title).toBe('He said "Hello"');
|
|
115
|
+
expect(hits[0].document?.content).toBe("It's a test");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("handles backslashes", async () => {
|
|
119
|
+
await handle.upsert({
|
|
120
|
+
id: "backslash",
|
|
121
|
+
title: "Path\\to\\file",
|
|
122
|
+
content: "C:\\Windows\\System32",
|
|
123
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const hits = await handle.query({
|
|
127
|
+
filter: { id: "backslash" },
|
|
128
|
+
topK: 1,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(hits[0].document?.title).toBe("Path\\to\\file");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("handles newlines and tabs", async () => {
|
|
135
|
+
await handle.upsert({
|
|
136
|
+
id: "whitespace",
|
|
137
|
+
title: "Line1\nLine2",
|
|
138
|
+
content: "Col1\tCol2\tCol3",
|
|
139
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const hits = await handle.query({
|
|
143
|
+
filter: { id: "whitespace" },
|
|
144
|
+
topK: 1,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(hits[0].document?.title).toBe("Line1\nLine2");
|
|
148
|
+
expect(hits[0].document?.content).toBe("Col1\tCol2\tCol3");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("handles null byte attempt", async () => {
|
|
152
|
+
// Postgres doesn't allow null bytes in text
|
|
153
|
+
await expect(
|
|
154
|
+
handle.upsert({
|
|
155
|
+
id: "nullbyte",
|
|
156
|
+
title: "Has\x00null",
|
|
157
|
+
content: "Content",
|
|
158
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
159
|
+
}),
|
|
160
|
+
).rejects.toThrow();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("handles unicode edge cases", async () => {
|
|
164
|
+
await handle.upsert({
|
|
165
|
+
id: "unicode-edge",
|
|
166
|
+
title: "\u0000\u0001\uFFFF", // Postgres will reject null byte
|
|
167
|
+
content: "Normal",
|
|
168
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
169
|
+
}).catch(() => {
|
|
170
|
+
// Expected to fail with null byte
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Test without null byte
|
|
174
|
+
await handle.upsert({
|
|
175
|
+
id: "unicode-edge-2",
|
|
176
|
+
title: "\u0001\uFFFE\uFFFF",
|
|
177
|
+
content: "Unicode test",
|
|
178
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const hits = await handle.query({
|
|
182
|
+
filter: { id: "unicode-edge-2" },
|
|
183
|
+
topK: 1,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(hits).toHaveLength(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("handles very long strings", async () => {
|
|
190
|
+
const longString = "x".repeat(100000);
|
|
191
|
+
|
|
192
|
+
await handle.upsert({
|
|
193
|
+
id: "long",
|
|
194
|
+
title: longString,
|
|
195
|
+
content: "Short",
|
|
196
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const hits = await handle.query({
|
|
200
|
+
filter: { id: "long" },
|
|
201
|
+
topK: 1,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(hits[0].document?.title).toHaveLength(100000);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("handles LIKE pattern characters in $contains", async () => {
|
|
208
|
+
await handle.upsert({
|
|
209
|
+
id: "pattern-chars",
|
|
210
|
+
title: "100% match_test",
|
|
211
|
+
content: "Content",
|
|
212
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Should not interpret % and _ as wildcards
|
|
216
|
+
const hits = await handle.query({
|
|
217
|
+
filter: { title: { $contains: "100%" } },
|
|
218
|
+
topK: 10,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(hits).toHaveLength(1);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ============================================================
|
|
226
|
+
// NUMERIC EDGE CASES
|
|
227
|
+
// ============================================================
|
|
228
|
+
|
|
229
|
+
describe("numeric edge cases", () => {
|
|
230
|
+
let handle: IndexHandle<any>;
|
|
231
|
+
|
|
232
|
+
beforeAll(async () => {
|
|
233
|
+
await pgvec.createIndex({
|
|
234
|
+
id: "numeric_edge",
|
|
235
|
+
schema: {
|
|
236
|
+
id: { type: "string", pk: true },
|
|
237
|
+
int_val: { type: "int" },
|
|
238
|
+
float_val: { type: "float" },
|
|
239
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
240
|
+
},
|
|
241
|
+
providerOptions: { schema: SCHEMA },
|
|
242
|
+
});
|
|
243
|
+
handle = pgvec.index("numeric_edge");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
beforeEach(async () => {
|
|
247
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."numeric_edge"`);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("handles integer zero", async () => {
|
|
251
|
+
await handle.upsert({
|
|
252
|
+
id: "zero",
|
|
253
|
+
int_val: 0,
|
|
254
|
+
float_val: 0.0,
|
|
255
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const hits = await handle.query({
|
|
259
|
+
filter: { int_val: 0 },
|
|
260
|
+
topK: 1,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(hits).toHaveLength(1);
|
|
264
|
+
expect(hits[0].document?.int_val).toBe(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("handles negative numbers", async () => {
|
|
268
|
+
await handle.upsert({
|
|
269
|
+
id: "negative",
|
|
270
|
+
int_val: -999,
|
|
271
|
+
float_val: -123.456,
|
|
272
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const hits = await handle.query({
|
|
276
|
+
filter: { int_val: { $lt: 0 } },
|
|
277
|
+
topK: 1,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(hits).toHaveLength(1);
|
|
281
|
+
expect(hits[0].document?.int_val).toBe(-999);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("handles large integers", async () => {
|
|
285
|
+
// PostgreSQL integer max is 2147483647
|
|
286
|
+
await handle.upsert({
|
|
287
|
+
id: "large-int",
|
|
288
|
+
int_val: 2147483647,
|
|
289
|
+
float_val: 0,
|
|
290
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const hits = await handle.query({
|
|
294
|
+
filter: { int_val: 2147483647 },
|
|
295
|
+
topK: 1,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(hits).toHaveLength(1);
|
|
299
|
+
expect(hits[0].document?.int_val).toBe(2147483647);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("handles float precision", async () => {
|
|
303
|
+
await handle.upsert({
|
|
304
|
+
id: "precise",
|
|
305
|
+
int_val: 0,
|
|
306
|
+
float_val: 0.123456789012345,
|
|
307
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const hits = await handle.query({
|
|
311
|
+
filter: { id: "precise" },
|
|
312
|
+
topK: 1,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Double precision maintains about 15 significant digits
|
|
316
|
+
expect(hits[0].document?.float_val).toBeCloseTo(0.123456789012345, 10);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("handles special float values", async () => {
|
|
320
|
+
// Infinity and NaN are not valid JSON, so they shouldn't be used
|
|
321
|
+
// But very small and very large floats should work
|
|
322
|
+
await handle.upsert({
|
|
323
|
+
id: "extreme-float",
|
|
324
|
+
int_val: 0,
|
|
325
|
+
float_val: 1e308, // Near max double
|
|
326
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const hits = await handle.query({
|
|
330
|
+
filter: { id: "extreme-float" },
|
|
331
|
+
topK: 1,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(hits[0].document?.float_val).toBe(1e308);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ============================================================
|
|
339
|
+
// VECTOR EDGE CASES
|
|
340
|
+
// ============================================================
|
|
341
|
+
|
|
342
|
+
describe("vector edge cases", () => {
|
|
343
|
+
let handle: IndexHandle<any>;
|
|
344
|
+
|
|
345
|
+
beforeAll(async () => {
|
|
346
|
+
await pgvec.createIndex({
|
|
347
|
+
id: "vector_edge",
|
|
348
|
+
schema: {
|
|
349
|
+
id: { type: "string", pk: true },
|
|
350
|
+
embedding: { type: "vector", dimensions: 4, similarity: "cosine" },
|
|
351
|
+
},
|
|
352
|
+
providerOptions: { schema: SCHEMA },
|
|
353
|
+
});
|
|
354
|
+
handle = pgvec.index("vector_edge");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
beforeEach(async () => {
|
|
358
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."vector_edge"`);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("handles zero vector", async () => {
|
|
362
|
+
await handle.upsert({
|
|
363
|
+
id: "zero-vec",
|
|
364
|
+
embedding: [0, 0, 0, 0],
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Querying with zero vector - cosine similarity undefined
|
|
368
|
+
// pgvector returns NaN for cosine of zero vectors
|
|
369
|
+
const hits = await handle.query({
|
|
370
|
+
filter: { id: "zero-vec" },
|
|
371
|
+
topK: 1,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
expect(hits).toHaveLength(1);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("handles negative vector components", async () => {
|
|
378
|
+
await handle.upsert({
|
|
379
|
+
id: "negative-vec",
|
|
380
|
+
embedding: [-1, -0.5, 0.5, 1],
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const hits = await handle.query({
|
|
384
|
+
query: [{ embedding: [-1, -0.5, 0.5, 1] }],
|
|
385
|
+
topK: 1,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(hits[0].id).toBe("negative-vec");
|
|
389
|
+
expect(hits[0].score).toBeGreaterThan(0.99);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("handles very small vector components", async () => {
|
|
393
|
+
await handle.upsert({
|
|
394
|
+
id: "small-vec",
|
|
395
|
+
embedding: [1e-10, 1e-10, 1e-10, 1e-10],
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const hits = await handle.query({
|
|
399
|
+
filter: { id: "small-vec" },
|
|
400
|
+
topK: 1,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
expect(hits).toHaveLength(1);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("handles very large vector components", async () => {
|
|
407
|
+
await handle.upsert({
|
|
408
|
+
id: "large-vec",
|
|
409
|
+
embedding: [1e10, 1e10, 1e10, 1e10],
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const hits = await handle.query({
|
|
413
|
+
query: [{ embedding: [1e10, 1e10, 1e10, 1e10] }],
|
|
414
|
+
topK: 1,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(hits[0].id).toBe("large-vec");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("rejects wrong dimension vector", async () => {
|
|
421
|
+
await expect(
|
|
422
|
+
handle.upsert({
|
|
423
|
+
id: "wrong-dim",
|
|
424
|
+
embedding: [1, 2, 3], // 3 dimensions instead of 4
|
|
425
|
+
}),
|
|
426
|
+
).rejects.toThrow();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("handles high-dimensional vectors (1536)", async () => {
|
|
430
|
+
await pgvec.createIndex({
|
|
431
|
+
id: "high_dim_edge",
|
|
432
|
+
schema: {
|
|
433
|
+
id: { type: "string", pk: true },
|
|
434
|
+
embedding: { type: "vector", dimensions: 1536 },
|
|
435
|
+
},
|
|
436
|
+
providerOptions: { schema: SCHEMA },
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const highHandle = pgvec.index("high_dim_edge");
|
|
440
|
+
const vec = new Array(1536).fill(0).map((_, i) => Math.sin(i));
|
|
441
|
+
|
|
442
|
+
await highHandle.upsert({ id: "high-1", embedding: vec });
|
|
443
|
+
|
|
444
|
+
const hits = await highHandle.query({
|
|
445
|
+
query: [{ embedding: vec }],
|
|
446
|
+
topK: 1,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(hits[0].id).toBe("high-1");
|
|
450
|
+
expect(hits[0].score).toBeGreaterThan(0.99);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ============================================================
|
|
455
|
+
// NULL HANDLING
|
|
456
|
+
// ============================================================
|
|
457
|
+
|
|
458
|
+
describe("null handling", () => {
|
|
459
|
+
let handle: IndexHandle<any>;
|
|
460
|
+
|
|
461
|
+
beforeAll(async () => {
|
|
462
|
+
await pgvec.createIndex({
|
|
463
|
+
id: "null_edge",
|
|
464
|
+
schema: {
|
|
465
|
+
id: { type: "string", pk: true },
|
|
466
|
+
required_field: { type: "string" },
|
|
467
|
+
optional_field: { type: "string" },
|
|
468
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
469
|
+
},
|
|
470
|
+
providerOptions: { schema: SCHEMA },
|
|
471
|
+
});
|
|
472
|
+
handle = pgvec.index("null_edge");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
beforeEach(async () => {
|
|
476
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."null_edge"`);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("stores null in optional field", async () => {
|
|
480
|
+
await handle.upsert({
|
|
481
|
+
id: "null-test",
|
|
482
|
+
required_field: "present",
|
|
483
|
+
optional_field: null,
|
|
484
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const hits = await handle.query({
|
|
488
|
+
filter: { optional_field: null },
|
|
489
|
+
topK: 1,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(hits).toHaveLength(1);
|
|
493
|
+
expect(hits[0].document?.optional_field).toBeNull();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("$exists works with null values", async () => {
|
|
497
|
+
await handle.upsert([
|
|
498
|
+
{
|
|
499
|
+
id: "has-value",
|
|
500
|
+
required_field: "yes",
|
|
501
|
+
optional_field: "present",
|
|
502
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: "has-null",
|
|
506
|
+
required_field: "yes",
|
|
507
|
+
optional_field: null,
|
|
508
|
+
embedding: [0.2, 0.2, 0.2, 0.2],
|
|
509
|
+
},
|
|
510
|
+
]);
|
|
511
|
+
|
|
512
|
+
const existsTrue = await handle.query({
|
|
513
|
+
filter: { optional_field: { $exists: true } },
|
|
514
|
+
topK: 10,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const existsFalse = await handle.query({
|
|
518
|
+
filter: { optional_field: { $exists: false } },
|
|
519
|
+
topK: 10,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(existsTrue).toHaveLength(1);
|
|
523
|
+
expect(existsTrue[0].id).toBe("has-value");
|
|
524
|
+
|
|
525
|
+
expect(existsFalse).toHaveLength(1);
|
|
526
|
+
expect(existsFalse[0].id).toBe("has-null");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("patches field to null", async () => {
|
|
530
|
+
await handle.upsert({
|
|
531
|
+
id: "patch-null",
|
|
532
|
+
required_field: "yes",
|
|
533
|
+
optional_field: "will be nulled",
|
|
534
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await handle.patch({
|
|
538
|
+
id: "patch-null",
|
|
539
|
+
optional_field: null,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const hits = await handle.query({
|
|
543
|
+
filter: { id: "patch-null" },
|
|
544
|
+
topK: 1,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(hits[0].document?.optional_field).toBeNull();
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ============================================================
|
|
552
|
+
// ID EDGE CASES
|
|
553
|
+
// ============================================================
|
|
554
|
+
|
|
555
|
+
describe("id edge cases", () => {
|
|
556
|
+
let handle: IndexHandle<any>;
|
|
557
|
+
|
|
558
|
+
beforeAll(async () => {
|
|
559
|
+
await pgvec.createIndex({
|
|
560
|
+
id: "id_edge",
|
|
561
|
+
schema: {
|
|
562
|
+
id: { type: "string", pk: true },
|
|
563
|
+
name: { type: "string" },
|
|
564
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
565
|
+
},
|
|
566
|
+
providerOptions: { schema: SCHEMA },
|
|
567
|
+
});
|
|
568
|
+
handle = pgvec.index("id_edge");
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
beforeEach(async () => {
|
|
572
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."id_edge"`);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("handles UUID-style ids", async () => {
|
|
576
|
+
const uuid = "550e8400-e29b-41d4-a716-446655440000";
|
|
577
|
+
|
|
578
|
+
await handle.upsert({
|
|
579
|
+
id: uuid,
|
|
580
|
+
name: "UUID Doc",
|
|
581
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const hits = await handle.query({
|
|
585
|
+
filter: { id: uuid },
|
|
586
|
+
topK: 1,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
expect(hits[0].id).toBe(uuid);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("handles very long ids", async () => {
|
|
593
|
+
const longId = "x".repeat(255);
|
|
594
|
+
|
|
595
|
+
await handle.upsert({
|
|
596
|
+
id: longId,
|
|
597
|
+
name: "Long ID Doc",
|
|
598
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const hits = await handle.query({
|
|
602
|
+
filter: { id: longId },
|
|
603
|
+
topK: 1,
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
expect(hits[0].id).toBe(longId);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("handles ids with special characters", async () => {
|
|
610
|
+
const specialId = "doc/with:special@chars#and?query=params";
|
|
611
|
+
|
|
612
|
+
await handle.upsert({
|
|
613
|
+
id: specialId,
|
|
614
|
+
name: "Special ID Doc",
|
|
615
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const hits = await handle.query({
|
|
619
|
+
filter: { id: specialId },
|
|
620
|
+
topK: 1,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(hits[0].id).toBe(specialId);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("handles numeric string ids", async () => {
|
|
627
|
+
await handle.upsert({
|
|
628
|
+
id: "12345",
|
|
629
|
+
name: "Numeric ID",
|
|
630
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const hits = await handle.query({
|
|
634
|
+
filter: { id: "12345" },
|
|
635
|
+
topK: 1,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
expect(hits[0].id).toBe("12345");
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// ============================================================
|
|
643
|
+
// ERROR HANDLING
|
|
644
|
+
// ============================================================
|
|
645
|
+
|
|
646
|
+
describe("error handling", () => {
|
|
647
|
+
it("throws on non-existent table query", async () => {
|
|
648
|
+
const badHandle = pgvec.index("nonexistent_table");
|
|
649
|
+
|
|
650
|
+
await expect(
|
|
651
|
+
badHandle.query({
|
|
652
|
+
query: [{ embedding: [0.1, 0.1, 0.1, 0.1] }],
|
|
653
|
+
topK: 10,
|
|
654
|
+
}),
|
|
655
|
+
).rejects.toThrow();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("throws on non-existent column in filter", async () => {
|
|
659
|
+
await pgvec.createIndex({
|
|
660
|
+
id: "error_test",
|
|
661
|
+
schema: {
|
|
662
|
+
id: { type: "string", pk: true },
|
|
663
|
+
name: { type: "string" },
|
|
664
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
665
|
+
},
|
|
666
|
+
providerOptions: { schema: SCHEMA },
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const handle = pgvec.index("error_test");
|
|
670
|
+
|
|
671
|
+
await expect(
|
|
672
|
+
handle.query({
|
|
673
|
+
filter: { nonexistent_column: "value" } as any,
|
|
674
|
+
topK: 10,
|
|
675
|
+
}),
|
|
676
|
+
).rejects.toThrow();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("throws on deleteIndex for non-bound index", async () => {
|
|
680
|
+
await expect(pgvec.deleteIndex("not_bound")).rejects.toThrow(
|
|
681
|
+
'Index "not_bound" not bound',
|
|
682
|
+
);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("throws on describeIndex for non-bound index", async () => {
|
|
686
|
+
await expect(pgvec.describeIndex("not_bound")).rejects.toThrow(
|
|
687
|
+
'Index "not_bound" not bound',
|
|
688
|
+
);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("throws on createIndex without primary key", async () => {
|
|
692
|
+
await expect(
|
|
693
|
+
pgvec.createIndex({
|
|
694
|
+
id: "no_pk",
|
|
695
|
+
schema: {
|
|
696
|
+
name: { type: "string" },
|
|
697
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
698
|
+
},
|
|
699
|
+
providerOptions: { schema: SCHEMA },
|
|
700
|
+
}),
|
|
701
|
+
).rejects.toThrow("schema must have a field with pk: true");
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// ============================================================
|
|
706
|
+
// CONCURRENT OPERATIONS
|
|
707
|
+
// ============================================================
|
|
708
|
+
|
|
709
|
+
describe("concurrent operations", () => {
|
|
710
|
+
let handle: IndexHandle<any>;
|
|
711
|
+
|
|
712
|
+
beforeAll(async () => {
|
|
713
|
+
await pgvec.createIndex({
|
|
714
|
+
id: "concurrent_test",
|
|
715
|
+
schema: {
|
|
716
|
+
id: { type: "string", pk: true },
|
|
717
|
+
counter: { type: "int" },
|
|
718
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
719
|
+
},
|
|
720
|
+
providerOptions: { schema: SCHEMA },
|
|
721
|
+
});
|
|
722
|
+
handle = pgvec.index("concurrent_test");
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
beforeEach(async () => {
|
|
726
|
+
await pool.query(`DELETE FROM "${SCHEMA}"."concurrent_test"`);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("handles concurrent upserts to same document", async () => {
|
|
730
|
+
// Insert initial doc
|
|
731
|
+
await handle.upsert({
|
|
732
|
+
id: "concurrent-1",
|
|
733
|
+
counter: 0,
|
|
734
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Concurrent updates
|
|
738
|
+
const updates = Array.from({ length: 10 }, (_, i) =>
|
|
739
|
+
handle.upsert({
|
|
740
|
+
id: "concurrent-1",
|
|
741
|
+
counter: i + 1,
|
|
742
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
743
|
+
}),
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
await Promise.all(updates);
|
|
747
|
+
|
|
748
|
+
// Should have completed without errors
|
|
749
|
+
const hits = await handle.query({
|
|
750
|
+
filter: { id: "concurrent-1" },
|
|
751
|
+
topK: 1,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
expect(hits).toHaveLength(1);
|
|
755
|
+
// Counter should be one of the values
|
|
756
|
+
expect(hits[0].document?.counter).toBeGreaterThanOrEqual(1);
|
|
757
|
+
expect(hits[0].document?.counter).toBeLessThanOrEqual(10);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it("handles concurrent upserts to different documents", async () => {
|
|
761
|
+
const upserts = Array.from({ length: 50 }, (_, i) =>
|
|
762
|
+
handle.upsert({
|
|
763
|
+
id: `parallel-${i}`,
|
|
764
|
+
counter: i,
|
|
765
|
+
embedding: [i / 50, (50 - i) / 50, 0.5, 0.5],
|
|
766
|
+
}),
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
await Promise.all(upserts);
|
|
770
|
+
|
|
771
|
+
// Increase ef_search to allow HNSW to explore more candidates
|
|
772
|
+
// Default is 40, which limits results to ~40 even with topK=100
|
|
773
|
+
await pool.query("SET hnsw.ef_search = 200");
|
|
774
|
+
|
|
775
|
+
const hits = await handle.query({
|
|
776
|
+
query: [{ embedding: [0.5, 0.5, 0.5, 0.5] }],
|
|
777
|
+
topK: 100,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
expect(hits).toHaveLength(50);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("handles query during upsert", async () => {
|
|
784
|
+
// Start upserting
|
|
785
|
+
const upsertPromise = handle.upsert(
|
|
786
|
+
Array.from({ length: 100 }, (_, i) => ({
|
|
787
|
+
id: `during-${i}`,
|
|
788
|
+
counter: i,
|
|
789
|
+
embedding: [0.1, 0.1, 0.1, 0.1],
|
|
790
|
+
})),
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
// Query while upserting
|
|
794
|
+
const queryPromise = handle.query({
|
|
795
|
+
query: [{ embedding: [0.1, 0.1, 0.1, 0.1] }],
|
|
796
|
+
topK: 1000,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const [, hits] = await Promise.all([upsertPromise, queryPromise]);
|
|
800
|
+
|
|
801
|
+
// Query should succeed (may see partial or full results)
|
|
802
|
+
expect(hits).toBeDefined();
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// ============================================================
|
|
807
|
+
// SCHEMA NAME EDGE CASES
|
|
808
|
+
// ============================================================
|
|
809
|
+
|
|
810
|
+
describe("schema name handling", () => {
|
|
811
|
+
it("creates index in custom schema", async () => {
|
|
812
|
+
await pool.query(`CREATE SCHEMA IF NOT EXISTS "custom_schema"`);
|
|
813
|
+
|
|
814
|
+
await pgvec.createIndex({
|
|
815
|
+
id: "custom_schema_test",
|
|
816
|
+
schema: {
|
|
817
|
+
id: { type: "string", pk: true },
|
|
818
|
+
embedding: { type: "vector", dimensions: 4 },
|
|
819
|
+
},
|
|
820
|
+
providerOptions: { schema: "custom_schema" },
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// Verify table is in correct schema
|
|
824
|
+
const result = await pool.query(
|
|
825
|
+
`SELECT 1 FROM information_schema.tables
|
|
826
|
+
WHERE table_schema = 'custom_schema' AND table_name = 'custom_schema_test'`,
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
expect(result.rows).toHaveLength(1);
|
|
830
|
+
|
|
831
|
+
await pgvec.deleteIndex("custom_schema_test");
|
|
832
|
+
await pool.query(`DROP SCHEMA IF EXISTS "custom_schema" CASCADE`);
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
});
|