kern-orm 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,28 @@
1
- import { A as TestFixture, M as TestHooks, N as TestTransactionOptions, O as OpenedTestFixture, P as createTestHelpers, j as TestHelpers, k as PendingTestFixture } from "./index-CNqYlONX.mjs";
2
- export { type OpenedTestFixture, type PendingTestFixture, type TestFixture, type TestHelpers, type TestHooks, type TestTransactionOptions, createTestHelpers };
1
+ import { Fr as Driver, G as TestHooks, H as PendingTestFixture, K as TestTransactionOptions, U as TestFixture, V as OpenedTestFixture, W as TestHelpers, q as createTestHelpers } from "./index-Cag6_erK.mjs";
2
+ /** Options of `describeDriverContract`: how to create the driver under test. */
3
+ interface DriverContractOptions {
4
+ /** Creates a driver over a new client of the test database with `max` connections. */
5
+ createDriver(options: {
6
+ max: number;
7
+ }): Driver;
8
+ /** Creates a driver over a new client of the test database whose parsers of result values throw; for a client without parsers, the same as `createDriver`. */
9
+ createDriverWithFailingParsers(): Driver;
10
+ /** Whether the client can close a reserved connection, so that the driver gets rid of a connection left in a transaction or lost; `false` leaves out the tests of it. */
11
+ closesReservedConnections: boolean;
12
+ }
13
+ /**
14
+ * Functions of a test runner that declare the tests of `describeDriverContract`.
15
+ *
16
+ * They are `describe`, `it`, `before`, and `after` of `node:test`, and `describe`, `it`, `beforeAll`, and `afterAll` of `bun:test` and Vitest.
17
+ */
18
+ interface DriverContractRunner extends TestHooks {
19
+ describe(name: string, body: () => void): unknown;
20
+ it(name: string, test: () => Promise<void>): unknown;
21
+ }
22
+ /**
23
+ * Declares the tests of the contract of `Driver` for a driver.
24
+ *
25
+ * They run in the empty database of its clients and leave it unchanged.
26
+ */
27
+ export declare function describeDriverContract(options: DriverContractOptions, runner: DriverContractRunner): void;
28
+ export { type DriverContractOptions, type DriverContractRunner, type OpenedTestFixture, type PendingTestFixture, type TestFixture, type TestHelpers, type TestHooks, type TestTransactionOptions, createTestHelpers };
package/dist/testing.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import { n as text, t as char } from "./text-BTFErHIi.mjs";
2
+ import { a as defineSchema, g as defineTable, i as sql, m as createStatement, r as param, t as createDatabase, x as and } from "./database-BJ0hNYrF.mjs";
3
+ import { _ as timestamptz, a as numeric, b as array, d as jsonb, f as enumOf, g as timestamp, h as time, i as integer, l as inet, m as interval, n as bigint, p as date, r as doublePrecision, t as uuid, v as boolean, y as bytea } from "./types-D9LtZ9W-.mjs";
4
+ import assert from "node:assert/strict";
1
5
  // src/execution/testing.ts
2
6
  /** Creates the test helpers of a database created with `test: true`, registering fixtures with the hooks of a test runner. */
3
7
  function createTestHelpers(database, hooks) {
@@ -31,4 +35,520 @@ function createTestHelpers(database, hooks) {
31
35
  }
32
36
  };
33
37
  }
34
- export { createTestHelpers };
38
+ // src/execution/driver-contract.ts
39
+ /** Creates helpers that run raw statements on `database`. */
40
+ function createStatementRunners(database) {
41
+ return {
42
+ async runStatement(text) {
43
+ await database.execute(createStatement(text));
44
+ },
45
+ async selectValue(text, parameters = []) {
46
+ const { rows } = await database.execute(createStatement(text, parameters));
47
+ return rows[0][0];
48
+ }
49
+ };
50
+ }
51
+ /**
52
+ * Declares the tests of the contract of `Driver` for a driver.
53
+ *
54
+ * They run in the empty database of its clients and leave it unchanged.
55
+ */
56
+ function describeDriverContract(options, runner) {
57
+ const { createDriver, createDriverWithFailingParsers, closesReservedConnections } = options;
58
+ const { describe, it, before, after } = runner;
59
+ describe("driver contract", () => {
60
+ const database = createDatabase({ driver: createDriver({ max: 2 }) });
61
+ after(() => database.close());
62
+ const { runStatement, selectValue } = createStatementRunners(database);
63
+ it("executes a query in a transaction", async () => {
64
+ const tag = defineTable("pg_tag_test", {
65
+ id: integer,
66
+ label: text
67
+ });
68
+ const db = database.bind(defineSchema({ tag }));
69
+ const rows = await database.transaction(async () => {
70
+ await runStatement("CREATE TEMP TABLE pg_tag_test (id integer, label text) ON COMMIT DROP");
71
+ await runStatement("INSERT INTO pg_tag_test VALUES (1, 'first'), (2, 'second')");
72
+ return await db.tag.select("label").where({ id: 2 });
73
+ });
74
+ assert.deepEqual(rows, [{ label: "second" }]);
75
+ });
76
+ it("executes a script of several statements on the connection of a transaction", async () => {
77
+ const labels = await database.transaction(async () => {
78
+ await database.script(`
79
+ CREATE TEMP TABLE pg_script_test (label text) ON COMMIT DROP;
80
+ INSERT INTO pg_script_test VALUES ('first'), ('second');
81
+ `);
82
+ return await selectValue("SELECT string_agg(label, ',' ORDER BY label) FROM pg_script_test");
83
+ });
84
+ assert.equal(labels, "first,second");
85
+ });
86
+ it("rejects a text of several statements without parameters before running any", async () => {
87
+ try {
88
+ await assert.rejects(database.execute(createStatement("CREATE TABLE pg_statement_test (id integer); SELECT 1")), /cannot insert multiple commands into a prepared statement/);
89
+ assert.equal(await selectValue("SELECT to_regclass('pg_statement_test')::text"), null);
90
+ } finally {
91
+ await database.script("SET LOCAL client_min_messages = warning; DROP TABLE IF EXISTS pg_statement_test");
92
+ }
93
+ });
94
+ it("keeps the values of a statement as text without the parsers of the client", async () => {
95
+ const parsed = createDatabase({ driver: createDriverWithFailingParsers() });
96
+ try {
97
+ const { rows } = await parsed.execute(createStatement("SELECT 1"));
98
+ assert.deepEqual(rows, [["1"]]);
99
+ } finally {
100
+ await parsed.close();
101
+ }
102
+ });
103
+ it("executes a script without the parsers of the client", async () => {
104
+ const parsed = createDatabase({ driver: createDriverWithFailingParsers() });
105
+ try {
106
+ await parsed.script("SELECT 1; SELECT 2");
107
+ } finally {
108
+ await parsed.close();
109
+ }
110
+ });
111
+ it("executes a script on the pool in one implicit transaction", async () => {
112
+ try {
113
+ await assert.rejects(database.script("CREATE TABLE pg_script_pool_test (id integer); SELECT 1/0"), /division by zero/);
114
+ assert.equal(await selectValue("SELECT to_regclass('pg_script_pool_test')::text"), null);
115
+ } finally {
116
+ await database.script("SET LOCAL client_min_messages = warning; DROP TABLE IF EXISTS pg_script_pool_test");
117
+ }
118
+ });
119
+ it("reports the number of rows of the command tag", async () => {
120
+ const counts = await database.transaction(async () => {
121
+ const created = await database.execute(createStatement("CREATE TEMP TABLE pg_count_test (id integer) ON COMMIT DROP"));
122
+ const inserted = await database.execute(createStatement("INSERT INTO pg_count_test VALUES (1), (2), (3)"));
123
+ const updated = await database.execute(createStatement("UPDATE pg_count_test SET id = id + 1 WHERE id > 1"));
124
+ return [
125
+ created.count,
126
+ inserted.count,
127
+ updated.count
128
+ ];
129
+ });
130
+ assert.deepEqual(counts, [
131
+ 0,
132
+ 3,
133
+ 2
134
+ ]);
135
+ });
136
+ it("reports the names of the result columns, also without rows", async () => {
137
+ const { created, selected } = await database.transaction(async () => ({
138
+ created: await database.execute(createStatement("CREATE TEMP TABLE pg_columns_test (id integer, label text) ON COMMIT DROP")),
139
+ selected: await database.execute(createStatement("SELECT id, label FROM pg_columns_test"))
140
+ }));
141
+ assert.deepEqual(created.columns, []);
142
+ assert.deepEqual(selected.columns, ["id", "label"]);
143
+ assert.deepEqual(selected.rows, []);
144
+ });
145
+ it("infers the types of parameters from the statement", async () => {
146
+ assert.equal(await selectValue("SELECT $1::integer + $2", ["1", "2"]), "3");
147
+ assert.equal(await selectValue("SELECT $1", ["text"]), "text");
148
+ assert.equal(await selectValue("SELECT $1::text IS NULL", [null]), "t");
149
+ });
150
+ it("runs concurrent statements of a transaction in the order of calls", async () => {
151
+ const values = await database.transaction(() => Promise.all([
152
+ selectValue("SELECT set_config('kern.test', '1', true) FROM pg_sleep(0.05)"),
153
+ selectValue("SELECT current_setting('kern.test')"),
154
+ selectValue("SELECT set_config('kern.test', '2', true)"),
155
+ selectValue("SELECT current_setting('kern.test')")
156
+ ]));
157
+ assert.deepEqual(values, [
158
+ "1",
159
+ "1",
160
+ "2",
161
+ "2"
162
+ ]);
163
+ });
164
+ it("decodes selected expressions with their types", async () => {
165
+ const tag = defineTable("pg_tag_test", {
166
+ id: integer,
167
+ label: text.nullable()
168
+ });
169
+ const db = database.bind(defineSchema({ tag }));
170
+ const rows = await database.transaction(async () => {
171
+ await runStatement("CREATE TEMP TABLE pg_tag_test (id integer, label text) ON COMMIT DROP");
172
+ await runStatement("INSERT INTO pg_tag_test VALUES (1, 'first'), (2, NULL)");
173
+ return await db.tag.select((columns) => ({
174
+ key: columns.id,
175
+ name: columns.label,
176
+ isFirst: columns.label.eq("first"),
177
+ total: sql(bigint)`count(*) OVER ()`,
178
+ length: sql(integer.nullable())`length(${columns.label})`
179
+ }));
180
+ });
181
+ assert.deepEqual(rows.toSorted((a, b) => a.key - b.key), [{
182
+ key: 1,
183
+ name: "first",
184
+ isFirst: true,
185
+ total: 2n,
186
+ length: 5
187
+ }, {
188
+ key: 2,
189
+ name: null,
190
+ isFirst: null,
191
+ total: 2n,
192
+ length: null
193
+ }]);
194
+ });
195
+ it("rejects a transaction whose failed statement the callback caught", async () => {
196
+ const transaction = database.transaction(async () => {
197
+ await runStatement("SELECT 1/0").catch(() => {});
198
+ return "done";
199
+ });
200
+ await assert.rejects(transaction, /Transaction was rolled back/);
201
+ });
202
+ it("releases a clean connection after a failed commit", async () => {
203
+ const single = createDatabase({ driver: createDriver({ max: 1 }) });
204
+ try {
205
+ const transaction = single.transaction(async () => {
206
+ await single.execute(createStatement("CREATE TEMP TABLE pg_commit_test (id integer UNIQUE DEFERRABLE INITIALLY DEFERRED) ON COMMIT DROP"));
207
+ await single.execute(createStatement("INSERT INTO pg_commit_test VALUES (1), (1)"));
208
+ });
209
+ await assert.rejects(transaction, /duplicate key/);
210
+ await assert.rejects(single.execute(createStatement("SAVEPOINT probe")), /transaction blocks/);
211
+ } finally {
212
+ await single.close();
213
+ }
214
+ });
215
+ it("leaves no failed transaction on the pool after a release with its error", async () => {
216
+ const driver = createDriver({ max: 1 });
217
+ try {
218
+ const connection = await driver.acquire();
219
+ await connection.execute(createStatement("BEGIN"));
220
+ const error = await connection.execute(createStatement("SELECT 1/0")).catch((error) => error);
221
+ await connection.release({ error });
222
+ await assert.rejects(driver.execute(createStatement("SAVEPOINT probe")), /transaction blocks/);
223
+ } finally {
224
+ await driver.close();
225
+ }
226
+ });
227
+ if (closesReservedConnections) {
228
+ for (const [state, text] of [["an open", "SELECT 1"], ["a failed", "SELECT 1/0"]]) it(`does not return a connection in ${state} transaction to the pool`, async () => {
229
+ const driver = createDriver({ max: 1 });
230
+ try {
231
+ const connection = await driver.acquire();
232
+ await connection.execute(createStatement("BEGIN"));
233
+ await connection.execute(createStatement(text)).catch(() => {});
234
+ await connection.release({ error: /* @__PURE__ */ new Error("failure") });
235
+ await assert.rejects(driver.execute(createStatement("SAVEPOINT probe")), /transaction blocks/);
236
+ } finally {
237
+ await driver.close();
238
+ }
239
+ });
240
+ it("rejects a transaction whose connection is lost and keeps the pool usable", async () => {
241
+ const single = createDatabase({ driver: createDriver({ max: 1 }) });
242
+ const { selectValue: selectValueOnSingle } = createStatementRunners(single);
243
+ try {
244
+ const transaction = single.transaction(async () => {
245
+ const pid = await selectValueOnSingle("SELECT pg_backend_pid()");
246
+ await selectValue("SELECT pg_terminate_backend($1)", [pid]);
247
+ await selectValueOnSingle("SELECT 1");
248
+ });
249
+ await assert.rejects(transaction, /Transaction rollback failed/);
250
+ assert.equal(await single.transaction(() => selectValueOnSingle("SELECT 'usable'")), "usable");
251
+ } finally {
252
+ await single.close();
253
+ }
254
+ });
255
+ }
256
+ it("isolates concurrent transactions on their own connections", async () => {
257
+ const bothStarted = Promise.withResolvers();
258
+ let startedCount = 0;
259
+ function runTransaction(label) {
260
+ return database.transaction(async () => {
261
+ const startPid = await selectValue("SELECT pg_backend_pid()");
262
+ await selectValue("SELECT set_config('kern.test', $1, true)", [label]);
263
+ startedCount += 1;
264
+ if (startedCount === 2) bothStarted.resolve();
265
+ await bothStarted.promise;
266
+ return {
267
+ startPid,
268
+ endPid: await selectValue("SELECT pg_backend_pid()"),
269
+ label: await selectValue("SELECT current_setting('kern.test')")
270
+ };
271
+ });
272
+ }
273
+ const [result1, result2] = await Promise.all([runTransaction("1"), runTransaction("2")]);
274
+ assert.equal(result1.endPid, result1.startPid);
275
+ assert.equal(result2.endPid, result2.startPid);
276
+ assert.notEqual(result1.startPid, result2.startPid);
277
+ assert.deepEqual([result1.label, result2.label], ["1", "2"]);
278
+ });
279
+ it("runs queries outside an open transaction on the pool", async () => {
280
+ const transactionStarted = Promise.withResolvers();
281
+ const outsideFinished = Promise.withResolvers();
282
+ const transactionPid = database.transaction(async () => {
283
+ await selectValue("SELECT set_config('kern.test', 'inside', true)");
284
+ transactionStarted.resolve();
285
+ await outsideFinished.promise;
286
+ return selectValue("SELECT pg_backend_pid()");
287
+ });
288
+ await transactionStarted.promise;
289
+ const outsidePid = await selectValue("SELECT pg_backend_pid()");
290
+ const outsideLabel = await selectValue("SELECT current_setting('kern.test', true)");
291
+ outsideFinished.resolve();
292
+ assert.notEqual(outsidePid, await transactionPid);
293
+ assert.notEqual(outsideLabel, "inside");
294
+ });
295
+ describe("closing", () => {
296
+ /** Creates a database whose client has one connection, so that a transaction waits for the connection of another one. */
297
+ function createClosableDatabase() {
298
+ return createDatabase({ driver: createDriver({ max: 1 }) });
299
+ }
300
+ it("waits for a running transaction and for a transaction waiting for a connection", async () => {
301
+ const database = createClosableDatabase();
302
+ const { promise: started, resolve: start } = Promise.withResolvers();
303
+ const { promise: closeRequested, resolve: requestClose } = Promise.withResolvers();
304
+ const { selectValue } = createStatementRunners(database);
305
+ const running = database.transaction(async () => {
306
+ start();
307
+ await closeRequested;
308
+ return selectValue("SELECT 'running'");
309
+ });
310
+ await started;
311
+ const waiting = database.transaction(() => selectValue("SELECT 'waiting'"));
312
+ const closing = database.close();
313
+ requestClose();
314
+ assert.equal(await running, "running");
315
+ assert.equal(await waiting, "waiting");
316
+ await closing;
317
+ });
318
+ it("waits for a statement on the pool waiting for a connection", async () => {
319
+ const database = createClosableDatabase();
320
+ const { promise: started, resolve: start } = Promise.withResolvers();
321
+ const { promise: closeRequested, resolve: requestClose } = Promise.withResolvers();
322
+ const { selectValue } = createStatementRunners(database);
323
+ const running = database.transaction(async () => {
324
+ start();
325
+ await closeRequested;
326
+ });
327
+ await started;
328
+ const waiting = selectValue("SELECT 'waiting'");
329
+ const closing = database.close();
330
+ requestClose();
331
+ await running;
332
+ assert.equal(await waiting, "waiting");
333
+ await closing;
334
+ });
335
+ it("waits for an in-flight pool query before closing", async () => {
336
+ const database = createClosableDatabase();
337
+ const { selectValue } = createStatementRunners(database);
338
+ await selectValue("SELECT 1");
339
+ const settled = [];
340
+ const query = selectValue("SELECT 'slept' FROM pg_sleep(0.2)");
341
+ query.then(() => settled.push("query"), () => {});
342
+ await database.close();
343
+ settled.push("database");
344
+ assert.equal(await query, "slept");
345
+ assert.deepEqual(settled, ["query", "database"]);
346
+ });
347
+ });
348
+ describe("in test transactions", () => {
349
+ const database = createDatabase({
350
+ driver: createDriver({ max: 2 }),
351
+ test: true
352
+ });
353
+ after(() => database.close());
354
+ const { testTransaction } = createTestHelpers(database, {
355
+ before,
356
+ after
357
+ });
358
+ const { runStatement, selectValue } = createStatementRunners(database);
359
+ it("discards only the rows of a rolled-back nested transaction", async () => {
360
+ const tag = defineTable("pg_nested_test", { id: integer });
361
+ const db = database.bind(defineSchema({ tag }));
362
+ const rows = await testTransaction(async () => {
363
+ await runStatement("CREATE TABLE pg_nested_test (id integer)");
364
+ await runStatement("INSERT INTO pg_nested_test VALUES (1)");
365
+ const failure = /* @__PURE__ */ new Error("failure");
366
+ const failed = database.transaction(async () => {
367
+ await runStatement("INSERT INTO pg_nested_test VALUES (2)");
368
+ throw failure;
369
+ });
370
+ await assert.rejects(failed, (error) => error === failure);
371
+ await database.transaction(() => runStatement("INSERT INTO pg_nested_test VALUES (3)"));
372
+ return await db.tag.select("id");
373
+ });
374
+ assert.deepEqual(rows.map(({ id }) => id).toSorted(), [1, 3]);
375
+ });
376
+ it("keeps the transaction usable after a failed statement in a nested transaction", async () => {
377
+ const value = await testTransaction(async () => {
378
+ await assert.rejects(database.transaction(() => runStatement("SELECT 1/0")), /division by zero/);
379
+ return await selectValue("SELECT 'usable'");
380
+ });
381
+ assert.equal(value, "usable");
382
+ });
383
+ it("keeps the changes of open fixtures for test transactions until each fixture closes", async () => {
384
+ const entry = defineTable("pg_fixture_test", { label: text });
385
+ const db = database.bind(defineSchema({ entry }));
386
+ const selectLabels = () => testTransaction(async () => {
387
+ return (await db.entry.select("label")).map(({ label }) => label).toSorted();
388
+ });
389
+ {
390
+ await using _run = await database.coordinator.createTestFixture(async () => {
391
+ await runStatement("CREATE TABLE pg_fixture_test (label text)");
392
+ await runStatement("INSERT INTO pg_fixture_test VALUES ('run')");
393
+ });
394
+ {
395
+ await using file = await database.coordinator.createTestFixture(async () => {
396
+ await runStatement("INSERT INTO pg_fixture_test VALUES ('file')");
397
+ return await db.entry.where({ label: "file" }).select("label");
398
+ });
399
+ assert.deepEqual(file.data, [{ label: "file" }]);
400
+ const inTest = await testTransaction(async () => {
401
+ await runStatement("INSERT INTO pg_fixture_test VALUES ('test')");
402
+ return (await db.entry.select("label")).map(({ label }) => label).toSorted();
403
+ });
404
+ assert.deepEqual(inTest, [
405
+ "file",
406
+ "run",
407
+ "test"
408
+ ]);
409
+ assert.deepEqual(await selectLabels(), ["file", "run"]);
410
+ }
411
+ assert.deepEqual(await selectLabels(), ["run"]);
412
+ }
413
+ assert.equal(await testTransaction(() => selectValue("SELECT to_regclass('pg_fixture_test')")), null);
414
+ });
415
+ it("closes a database with an open fixture", async () => {
416
+ const closed = createDatabase({
417
+ driver: createDriver({ max: 1 }),
418
+ test: true
419
+ });
420
+ const fixture = await closed.coordinator.createTestFixture(() => closed.execute(createStatement("SELECT 1")));
421
+ await closed.close();
422
+ assert.throws(() => fixture.data, /Test fixture is closed/);
423
+ });
424
+ it("round-trips the built-in types", async () => {
425
+ const sample = defineTable("pg_type_test", {
426
+ bigint,
427
+ numeric: numeric.with(20, 6),
428
+ price: numeric.with(10, 2).transform({ decode: Number }),
429
+ double: doublePrecision,
430
+ flag: boolean,
431
+ id: uuid,
432
+ bytes: bytea,
433
+ at: timestamptz,
434
+ local: timestamp,
435
+ day: date,
436
+ clock: time,
437
+ span: interval,
438
+ doc: jsonb.of(),
439
+ labels: array(text.nullable()),
440
+ address: inet,
441
+ status: enumOf("pg_status_test", ["draft", "published"])
442
+ });
443
+ const db = database.bind(defineSchema({ sample }));
444
+ const values = {
445
+ bigint: 9007199254740993n,
446
+ numeric: "12345678901234.123456",
447
+ price: 12.5,
448
+ double: .1,
449
+ flag: true,
450
+ id: "0193d6b6-7c43-7000-8000-000000000001",
451
+ bytes: new Uint8Array([0, 255]),
452
+ at: "2026-01-15T07:00:00.123456Z",
453
+ local: "2026-01-15T10:00:00.000000",
454
+ day: "2026-01-15",
455
+ clock: "10:00:00.500000",
456
+ span: "1 day 02:00:00",
457
+ doc: { tags: ["a", "b"] },
458
+ labels: [
459
+ "a",
460
+ "b c",
461
+ null,
462
+ "NULL"
463
+ ],
464
+ address: "192.168.0.1",
465
+ status: "published"
466
+ };
467
+ const rows = await testTransaction(async () => {
468
+ await runStatement("SET LOCAL TIME ZONE 'Asia/Bangkok'");
469
+ await runStatement("CREATE TYPE pg_status_test AS ENUM ('draft', 'published')");
470
+ await runStatement(`
471
+ CREATE TABLE pg_type_test (
472
+ bigint bigint, numeric numeric(20, 6), price numeric(10, 2), double double precision, flag boolean, id uuid,
473
+ bytes bytea, at timestamptz, local timestamp, day date, clock time, span interval,
474
+ doc jsonb, labels text[], address inet, status pg_status_test
475
+ )
476
+ `);
477
+ await runStatement(`
478
+ INSERT INTO pg_type_test VALUES (
479
+ 9007199254740993, 12345678901234.123456, 12.50, 0.1, true, '0193d6b6-7c43-7000-8000-000000000001',
480
+ '\\x00ff', '2026-01-15 14:00:00.123456+07', '2026-01-15 10:00', '2026-01-15', '10:00:00.5',
481
+ '1 day 2 hours', '{"tags": ["a", "b"]}', '{a,"b c",NULL,"NULL"}', '192.168.0.1', 'published'
482
+ )
483
+ `);
484
+ return {
485
+ selected: await db.sample.select("*"),
486
+ matched: await db.sample.where(values).select("id")
487
+ };
488
+ });
489
+ assert.deepEqual(rows.selected, [values]);
490
+ assert.deepEqual(rows.matched, [{ id: values.id }]);
491
+ });
492
+ it("compares parameters of param with the operators of their types", async () => {
493
+ const probe = defineTable("pg_param_test", { id: integer });
494
+ const db = database.bind(defineSchema({ probe }));
495
+ const rows = await testTransaction(async () => {
496
+ await runStatement("CREATE TABLE pg_param_test (id integer)");
497
+ await runStatement("INSERT INTO pg_param_test VALUES (1)");
498
+ return await db.probe.where(() => and(param(integer, 2).lt(10), param(numeric.with(3, 1), "1.04").ne("1.0"), param(char, "ab").eq("ab"))).select("id");
499
+ });
500
+ assert.deepEqual(rows, [{ id: 1 }]);
501
+ });
502
+ it("round-trips date and time values of years outside 1 to 9999", async () => {
503
+ const moment = defineTable("pg_moment_test", {
504
+ id: integer,
505
+ at: timestamptz,
506
+ local: timestamp,
507
+ day: date
508
+ });
509
+ const db = database.bind(defineSchema({ moment }));
510
+ const values = [
511
+ {
512
+ id: 1,
513
+ at: "0000-01-01T00:00:00.000000Z",
514
+ local: "-000001-06-15T10:00:00.000000",
515
+ day: "-004712-01-01"
516
+ },
517
+ {
518
+ id: 2,
519
+ at: "+294276-12-31T23:59:59.999999Z",
520
+ local: "+010000-01-01T00:00:00.000000",
521
+ day: "+010000-01-01"
522
+ },
523
+ {
524
+ id: 3,
525
+ at: "9999-12-31T23:59:59.000000Z",
526
+ local: "9999-12-31T23:59:59.000000",
527
+ day: "+5874897-12-31"
528
+ }
529
+ ];
530
+ const rows = await testTransaction(async () => {
531
+ await runStatement("SET LOCAL TIME ZONE 'Asia/Bangkok'");
532
+ await runStatement("CREATE TABLE pg_moment_test (id integer, at timestamptz, local timestamp, day date)");
533
+ await runStatement(`
534
+ INSERT INTO pg_moment_test VALUES
535
+ (1, '0001-01-01 00:00:00+00 BC', '0002-06-15 10:00 BC', '4713-01-01 BC'),
536
+ (2, '294276-12-31 23:59:59.999999+00', '10000-01-01', '10000-01-01'),
537
+ (3, '9999-12-31 23:59:59+00', '9999-12-31 23:59:59', '5874897-12-31')
538
+ `);
539
+ return {
540
+ selected: await db.moment.select("*"),
541
+ matched: await Promise.all(values.map((value) => db.moment.where(value).select("id")))
542
+ };
543
+ });
544
+ assert.deepEqual(rows.selected, values);
545
+ assert.deepEqual(rows.matched, [
546
+ [{ id: 1 }],
547
+ [{ id: 2 }],
548
+ [{ id: 3 }]
549
+ ]);
550
+ });
551
+ });
552
+ });
553
+ }
554
+ export { createTestHelpers, describeDriverContract };
@@ -346,6 +346,20 @@ function toValueNode(value, type) {
346
346
  type
347
347
  });
348
348
  }
349
+ /** Converts an operand into a node as `toValueNode` does, casting a plain value to `type`, so that its parameter has the type even where nothing else in the statement determines it. */
350
+ function toCastValueNode(value, type) {
351
+ const node = toValueNode(value, type);
352
+ return node.kind === "param" ? createNode({
353
+ kind: "cast",
354
+ operand: node,
355
+ type
356
+ }) : node;
357
+ }
358
+ /** Converts an operand of the function `name` that skips `NULL`, such as the fallback of `coalesce`, into a node as `toValueNode` does, rejecting a plain `null` and `undefined`, which would have no effect or no value. */
359
+ function toFunctionOperandNode(value, type, name) {
360
+ if (value === null || value === void 0) throw new TypeError(`${name} takes a value other than ${String(value)}`);
361
+ return toValueNode(value, type);
362
+ }
349
363
  /** Creates an `IS NULL` node directly, so that resolving an object condition never calls an `isNull` operator replaced by a type. */
350
364
  function createIsNullNode(operand) {
351
365
  return createNode({
@@ -378,19 +392,58 @@ const orderingOperators = {
378
392
  function createComparison(operator) {
379
393
  return defineGenericOperator((self, value) => compare(self, operator, value));
380
394
  }
395
+ // src/type/number.ts
396
+ /** Defines a type of numbers with the operators of the built-in ones. */
397
+ function defineNumberType(sqlType) {
398
+ return defineType({ sqlType }).transform({
399
+ decode: Number,
400
+ encode: (value) => String(value)
401
+ }).withOperators(orderingOperators);
402
+ }
403
+ let integerType;
404
+ /** Returns the shared `integer` type used by built-in expressions and columns. */
405
+ function getIntegerType() {
406
+ integerType ??= defineNumberType("integer");
407
+ return integerType;
408
+ }
409
+ // src/type/text.ts
410
+ /** Defines a text type with the operators of the built-in text types. */
411
+ function defineTextType(sqlType, castType) {
412
+ return defineType({
413
+ sqlType,
414
+ castType
415
+ }).withOperators(orderingOperators).withOperators(textOperators);
416
+ }
417
+ let textType;
418
+ /** Returns the shared `text` type used by built-in expressions and columns. */
419
+ function getTextType() {
420
+ textType ??= defineTextType("text");
421
+ return textType;
422
+ }
381
423
  // src/expression/operators/text.ts
382
424
  /**
383
- * Operators of text types: pattern matching with `LIKE` and `ILIKE`.
425
+ * Operators of text types: pattern matching with `LIKE` and `ILIKE`, `lower` and `upper`, which convert the case of the text, `trim`, which removes spaces from both of its ends, and `length`, which counts its characters.
384
426
  *
385
427
  * A string pattern is sent as it is; `param(type, pattern)` encodes it with a type.
386
428
  */
387
429
  const textOperators = {
388
430
  like: /* @__PURE__ */ createPatternComparison("LIKE"),
389
- ilike: /* @__PURE__ */ createPatternComparison("ILIKE")
431
+ ilike: /* @__PURE__ */ createPatternComparison("ILIKE"),
432
+ lower: /* @__PURE__ */ createTextConversion("lower"),
433
+ upper: /* @__PURE__ */ createTextConversion("upper"),
434
+ trim: /* @__PURE__ */ createTextConversion("btrim"),
435
+ length: /* @__PURE__ */ defineGenericOperator((self) => callTextFunction("length", self, getIntegerType()))
390
436
  };
391
437
  function createPatternComparison(operator) {
392
438
  return defineGenericOperator((self, pattern) => toBooleanExpression(compareNodes(operator, toNode(self), pattern, void 0)));
393
439
  }
440
+ function createTextConversion(name) {
441
+ return defineGenericOperator((self) => callTextFunction(name, self, getTextType()));
442
+ }
443
+ /** Calls a SQL function of text whose result has `type`, whatever the type of the expression: a decoder of a transformed type expects values that it encoded rather than results of the function. */
444
+ function callTextFunction(name, self, type) {
445
+ return toExpression(createList(`${name}(`, [toNode(self)], ")"), type);
446
+ }
394
447
  // src/type/codecs/bytea.ts
395
448
  const hexDigits = "0123456789abcdef";
396
449
  /** Encodes bytes as a hex `bytea` literal, such as `\\x00ff`. */
@@ -425,16 +478,10 @@ function formatTypeName(name, ...parameters) {
425
478
  return defined.length === 0 ? name : `${name}(${defined.join(",")})`;
426
479
  }
427
480
  // src/type/builtins/text.ts
428
- function defineTextType(sqlType, castType) {
429
- return defineType({
430
- sqlType,
431
- castType
432
- }).withOperators(orderingOperators).withOperators(textOperators);
433
- }
434
481
  /** Variable-length string without a limit. */
435
- const text = /* @__PURE__ */ defineTextType("text");
482
+ const text = /* @__PURE__ */ getTextType();
436
483
  /** Variable-length string; `varchar.with(length)` limits its length. */
437
484
  const varchar = /* @__PURE__ */ defineTextType("varchar").withParameters((length) => formatTypeName("varchar", length));
438
485
  /** Fixed-length string that PostgreSQL pads with spaces; `char` alone has length 1, and `char.with(length)` sets the length. */
439
486
  const char = /* @__PURE__ */ defineTextType("char", "bpchar").withParameters((length) => formatTypeName("char", length));
440
- export { createList as A, isType as C, isEqualityCompatible as D, hasEquality as E, oneNode as F, trueNode as I, createSelectNode as M, falseNode as N, valuesQueryNode as O, noFilters as P, defineType as S, equalityOperators as T, toBooleanExpression as _, stringifyJson as a, toValueNode as b, textOperators as c, compareNodes as d, createIsNullNode as f, operandNode as g, isOperandSource as h, formatTypeName as i, createNode as j, defineGenericOperator as k, orderingOperators as l, isExpression as m, text as n, decodeBytea as o, getType as p, varchar as r, encodeBytea as s, char as t, compare as u, toExpression as v, Column as w, getBooleanType as x, toNode as y };
487
+ export { hasEquality as A, trueNode as B, toNode as C, isType as D, defineType as E, createNode as F, createSelectNode as I, falseNode as L, valuesQueryNode as M, defineGenericOperator as N, Column as O, createList as P, noFilters as R, toFunctionOperandNode as S, getBooleanType as T, isOperandSource as _, stringifyJson as a, toCastValueNode as b, textOperators as c, orderingOperators as d, compare as f, isExpression as g, getType as h, formatTypeName as i, isEqualityCompatible as j, equalityOperators as k, defineNumberType as l, createIsNullNode as m, text as n, decodeBytea as o, compareNodes as p, varchar as r, encodeBytea as s, char as t, getIntegerType as u, operandNode as v, toValueNode as w, toExpression as x, toBooleanExpression as y, oneNode as z };