pgsql-test 5.16.2 → 5.17.1

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/README.md CHANGED
@@ -131,6 +131,7 @@ The `PgTestClient` returned by `getConnections()` wraps a `pg.Client` and provid
131
131
  * `beforeEach()` – Begins a transaction and sets a savepoint (called at the start of each test)
132
132
  * `afterEach()` – Rolls back to the savepoint and commits the outer transaction (cleans up test state)
133
133
  * `setContext({ key: value })` – Sets PostgreSQL config variables (like `role`) to simulate RLS contexts
134
+ * `checkConstraints()` – Runs the commit-time checks for any pending deferred constraints now, without committing (see [Deferred constraints](#deferred-constraints-under-rollback-isolation))
134
135
  * `any`, `one`, `oneOrNone`, `many`, `manyOrNone`, `none`, `result` – Typed query helpers for specific result expectations
135
136
 
136
137
  These methods make it easier to build expressive and isolated integration tests with strong typing and error handling.
@@ -601,6 +602,34 @@ This table documents the available options for the `getConnections` function. Th
601
602
  | `db.template` | `string` | `undefined` | Template database used for faster test DB creation |
602
603
  | `db.rootDb` | `string` | `'postgres'` | Root database used for administrative operations (e.g., creating databases) |
603
604
  | `db.prefix` | `string` | `'db-'` | Prefix used when generating test database names |
605
+ | `db.deferredConstraints` | `'off' \| 'check' \| 'immediate'` | `'off'` | How `DEFERRABLE INITIALLY DEFERRED` constraints are handled under rollback isolation (env: `DB_DEFERRED_CONSTRAINTS`). See below. |
606
+
607
+ ### Deferred constraints under rollback isolation
608
+
609
+ `beforeEach()`/`afterEach()` isolate tests by rolling back instead of committing. Anything Postgres only does **at COMMIT** therefore never happens inside a test. The one that bites is `DEFERRABLE INITIALLY DEFERRED` constraints (FK / UNIQUE / EXCLUDE / `CONSTRAINT TRIGGER`): a test can leave a dangling deferred foreign key and still pass, because the check that would have failed the commit is discarded with the rollback. This is inherent to every rollback-based test harness, not specific to pgsql-test.
610
+
611
+ `db.deferredConstraints` controls what to do about it:
612
+
613
+ | Mode | What happens | Trade-off |
614
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
615
+ | `'off'` | Nothing (default, previous behaviour). | Deferred violations pass silently. |
616
+ | `'check'` | `afterEach()` runs `SET CONSTRAINTS ALL IMMEDIATE` as the last statement before rolling back. Postgres defines this as running exactly the checks COMMIT would have run, so deferral still works *inside* the test and a pending violation fails the test. | The failure surfaces in `afterEach`, not on the offending line (the message names the constraint and table). If the test body already raised a Postgres error, the check is skipped so the original error is not masked. |
617
+ | `'immediate'` | `beforeEach()` runs `SET CONSTRAINTS ALL IMMEDIATE`, so every deferred constraint is checked per statement and a violation fails on the exact line. | Changes semantics: code that legitimately relies on deferral (insert child before parent, swap two values under a deferred UNIQUE) fails in tests but works in production. |
618
+
619
+ ```ts
620
+ const { db, teardown } = await getConnections({ db: { deferredConstraints: 'check' } });
621
+ ```
622
+
623
+ A test that wants to *assert* that a deferred constraint is enforced can call `checkConstraints()` itself; once it has thrown, the pending events are consumed and `afterEach()` is clean:
624
+
625
+ ```ts
626
+ it('rejects orphan children at commit', async () => {
627
+ await db.query(`INSERT INTO children (parent_id) VALUES (999)`);
628
+ await expect(db.checkConstraints()).rejects.toThrow(/children_parent_id_fkey/);
629
+ });
630
+ ```
631
+
632
+ Other commit-only effects — `NOTIFY` delivery and visibility to other sessions — are not covered by any mode; use `publish()` (which really commits) when a test needs them.
604
633
 
605
634
  ### `pg` Options (PgConfig)
606
635
 
package/connect.js CHANGED
@@ -53,7 +53,9 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.pgpm()]) => {
53
53
  }
54
54
  await admin.grantConnect(connOpts.connections.app.user, config.database);
55
55
  manager = manager_1.PgTestConnector.getInstance(config);
56
- const pg = manager.getClient(config);
56
+ const pg = manager.getClient(config, {
57
+ deferredConstraints: connOpts.deferredConstraints
58
+ });
57
59
  let teardownPromise = null;
58
60
  let teardownOpts = {};
59
61
  const teardown = async (opts = {}) => {
@@ -97,7 +99,8 @@ const getConnections = async (cn = {}, seedAdapters = [seed_1.seed.pgpm()]) => {
97
99
  };
98
100
  const db = manager.getClient(dbConfig, {
99
101
  auth: connOpts.auth,
100
- roles: connOpts.roles
102
+ roles: connOpts.roles,
103
+ deferredConstraints: connOpts.deferredConstraints
101
104
  });
102
105
  db.setContext({ role: (0, pgsql_client_1.getDefaultRole)(connOpts) });
103
106
  return { pg, db, teardown, manager, admin };
package/esm/connect.js CHANGED
@@ -49,7 +49,9 @@ export const getConnections = async (cn = {}, seedAdapters = [seed.pgpm()]) => {
49
49
  }
50
50
  await admin.grantConnect(connOpts.connections.app.user, config.database);
51
51
  manager = PgTestConnector.getInstance(config);
52
- const pg = manager.getClient(config);
52
+ const pg = manager.getClient(config, {
53
+ deferredConstraints: connOpts.deferredConstraints
54
+ });
53
55
  let teardownPromise = null;
54
56
  let teardownOpts = {};
55
57
  const teardown = async (opts = {}) => {
@@ -93,7 +95,8 @@ export const getConnections = async (cn = {}, seedAdapters = [seed.pgpm()]) => {
93
95
  };
94
96
  const db = manager.getClient(dbConfig, {
95
97
  auth: connOpts.auth,
96
- roles: connOpts.roles
98
+ roles: connOpts.roles,
99
+ deferredConstraints: connOpts.deferredConstraints
97
100
  });
98
101
  db.setContext({ role: getDefaultRole(connOpts) });
99
102
  return { pg, db, teardown, manager, admin };
@@ -4,6 +4,7 @@ import { loadCsvMap } from 'pgsql-seed';
4
4
  import { loadSqlFiles } from 'pgsql-seed';
5
5
  import { deployPgpm } from 'pgsql-seed';
6
6
  import { formatPgError } from './utils';
7
+ const IN_FAILED_SQL_TRANSACTION = '25P02';
7
8
  export class PgTestClient extends PgClient {
8
9
  testOpts;
9
10
  constructor(config, opts = {}) {
@@ -34,13 +35,51 @@ export class PgTestClient extends PgClient {
34
35
  throw err;
35
36
  }
36
37
  }
38
+ get deferredConstraintsMode() {
39
+ return this.testOpts.deferredConstraints ?? 'off';
40
+ }
37
41
  async beforeEach() {
38
42
  await this.begin();
39
43
  await this.savepoint();
44
+ if (this.deferredConstraintsMode === 'immediate') {
45
+ await this.setConstraintsImmediate();
46
+ }
40
47
  }
41
48
  async afterEach() {
49
+ let violation;
50
+ if (this.deferredConstraintsMode === 'check') {
51
+ try {
52
+ await this.checkConstraints();
53
+ }
54
+ catch (err) {
55
+ if (err?.code !== IN_FAILED_SQL_TRANSACTION)
56
+ violation = err;
57
+ }
58
+ }
42
59
  await this.rollback();
43
60
  await this.commit();
61
+ if (violation)
62
+ throw violation;
63
+ }
64
+ /**
65
+ * Run the commit-time checks for every pending deferred constraint now, without committing.
66
+ * Postgres checks all outstanding deferred constraint events when a constraint switches from
67
+ * DEFERRED to IMMEDIATE, so this fails exactly where a real COMMIT would have failed.
68
+ * Once it passes (or throws), the pending events are consumed.
69
+ */
70
+ async checkConstraints() {
71
+ try {
72
+ await this.setConstraintsImmediate();
73
+ }
74
+ catch (err) {
75
+ if (err?.code !== IN_FAILED_SQL_TRANSACTION) {
76
+ err.message = `[pgsql-test] deferred constraint violated at end of test (a real COMMIT would have failed here):\n${err.message}`;
77
+ }
78
+ throw err;
79
+ }
80
+ }
81
+ async setConstraintsImmediate() {
82
+ await this.query('SET CONSTRAINTS ALL IMMEDIATE');
44
83
  }
45
84
  /**
46
85
  * Commit current transaction to make data visible to other connections, then start fresh transaction.
@@ -50,6 +89,9 @@ export class PgTestClient extends PgClient {
50
89
  await this.commit(); // make data visible to other sessions
51
90
  await this.begin(); // fresh tx
52
91
  await this.savepoint(); // keep rollback harness
92
+ if (this.deferredConstraintsMode === 'immediate') {
93
+ await this.setConstraintsImmediate();
94
+ }
53
95
  await this.ctxQuery(); // reapply all setContext()
54
96
  }
55
97
  async loadJson(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgsql-test",
3
- "version": "5.16.2",
3
+ "version": "5.17.1",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "pgsql-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution",
6
6
  "main": "index.js",
@@ -55,21 +55,21 @@
55
55
  "test:watch": "jest --watch"
56
56
  },
57
57
  "devDependencies": {
58
- "@pgpmjs/core": "7.32.1",
58
+ "@pgpmjs/core": "7.32.3",
59
59
  "@types/pg": "^8.20.4",
60
60
  "@types/pg-copy-streams": "^1.2.5",
61
61
  "makage": "^0.8.0"
62
62
  },
63
63
  "dependencies": {
64
- "@pgpmjs/env": "^2.45.2",
64
+ "@pgpmjs/env": "^2.46.1",
65
65
  "@pgpmjs/logger": "^2.26.0",
66
- "@pgpmjs/server-utils": "^3.29.2",
67
- "@pgpmjs/types": "^2.55.2",
66
+ "@pgpmjs/server-utils": "^3.29.4",
67
+ "@pgpmjs/types": "^2.56.1",
68
68
  "pg": "^8.21.0",
69
- "pg-cache": "^3.29.2",
69
+ "pg-cache": "^3.29.4",
70
70
  "pg-env": "^1.32.0",
71
- "pgsql-client": "^4.16.2",
72
- "pgsql-seed": "^3.15.2"
71
+ "pgsql-client": "^4.16.4",
72
+ "pgsql-seed": "^3.15.4"
73
73
  },
74
- "gitHead": "e05686fb94c5e22c954436f18c25aa4439b9e83e"
74
+ "gitHead": "34e91da5b3dd9c0cae14875afe609993acf53a84"
75
75
  }
package/test-client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DeferredConstraintsMode } from '@pgpmjs/types';
1
2
  import { QueryResult } from 'pg';
2
3
  import { PgConfig } from 'pg-env';
3
4
  import { PgClient, PgClientOpts } from 'pgsql-client';
@@ -10,6 +11,11 @@ export type PgTestClientOpts = PgClientOpts & {
10
11
  * Can be disabled by setting enhancedErrors: false.
11
12
  */
12
13
  enhancedErrors?: boolean;
14
+ /**
15
+ * How DEFERRABLE INITIALLY DEFERRED constraints are handled under rollback isolation.
16
+ * Defaults to 'off'. See {@link DeferredConstraintsMode}.
17
+ */
18
+ deferredConstraints?: DeferredConstraintsMode;
13
19
  };
14
20
  export declare class PgTestClient extends PgClient {
15
21
  protected testOpts: PgTestClientOpts;
@@ -24,8 +30,17 @@ export declare class PgTestClient extends PgClient {
24
30
  * When enhancedErrors is enabled, errors will include detail, hint, where, position, etc.
25
31
  */
26
32
  query<T = any>(query: string, values?: any[]): Promise<QueryResult<T>>;
33
+ private get deferredConstraintsMode();
27
34
  beforeEach(): Promise<void>;
28
35
  afterEach(): Promise<void>;
36
+ /**
37
+ * Run the commit-time checks for every pending deferred constraint now, without committing.
38
+ * Postgres checks all outstanding deferred constraint events when a constraint switches from
39
+ * DEFERRED to IMMEDIATE, so this fails exactly where a real COMMIT would have failed.
40
+ * Once it passes (or throws), the pending events are consumed.
41
+ */
42
+ checkConstraints(): Promise<void>;
43
+ private setConstraintsImmediate;
29
44
  /**
30
45
  * Commit current transaction to make data visible to other connections, then start fresh transaction.
31
46
  * Maintains test isolation by creating a savepoint and reapplying session context.
package/test-client.js CHANGED
@@ -7,6 +7,7 @@ const pgsql_seed_2 = require("pgsql-seed");
7
7
  const pgsql_seed_3 = require("pgsql-seed");
8
8
  const pgsql_seed_4 = require("pgsql-seed");
9
9
  const utils_1 = require("./utils");
10
+ const IN_FAILED_SQL_TRANSACTION = '25P02';
10
11
  class PgTestClient extends pgsql_client_1.PgClient {
11
12
  testOpts;
12
13
  constructor(config, opts = {}) {
@@ -37,13 +38,51 @@ class PgTestClient extends pgsql_client_1.PgClient {
37
38
  throw err;
38
39
  }
39
40
  }
41
+ get deferredConstraintsMode() {
42
+ return this.testOpts.deferredConstraints ?? 'off';
43
+ }
40
44
  async beforeEach() {
41
45
  await this.begin();
42
46
  await this.savepoint();
47
+ if (this.deferredConstraintsMode === 'immediate') {
48
+ await this.setConstraintsImmediate();
49
+ }
43
50
  }
44
51
  async afterEach() {
52
+ let violation;
53
+ if (this.deferredConstraintsMode === 'check') {
54
+ try {
55
+ await this.checkConstraints();
56
+ }
57
+ catch (err) {
58
+ if (err?.code !== IN_FAILED_SQL_TRANSACTION)
59
+ violation = err;
60
+ }
61
+ }
45
62
  await this.rollback();
46
63
  await this.commit();
64
+ if (violation)
65
+ throw violation;
66
+ }
67
+ /**
68
+ * Run the commit-time checks for every pending deferred constraint now, without committing.
69
+ * Postgres checks all outstanding deferred constraint events when a constraint switches from
70
+ * DEFERRED to IMMEDIATE, so this fails exactly where a real COMMIT would have failed.
71
+ * Once it passes (or throws), the pending events are consumed.
72
+ */
73
+ async checkConstraints() {
74
+ try {
75
+ await this.setConstraintsImmediate();
76
+ }
77
+ catch (err) {
78
+ if (err?.code !== IN_FAILED_SQL_TRANSACTION) {
79
+ err.message = `[pgsql-test] deferred constraint violated at end of test (a real COMMIT would have failed here):\n${err.message}`;
80
+ }
81
+ throw err;
82
+ }
83
+ }
84
+ async setConstraintsImmediate() {
85
+ await this.query('SET CONSTRAINTS ALL IMMEDIATE');
47
86
  }
48
87
  /**
49
88
  * Commit current transaction to make data visible to other connections, then start fresh transaction.
@@ -53,6 +92,9 @@ class PgTestClient extends pgsql_client_1.PgClient {
53
92
  await this.commit(); // make data visible to other sessions
54
93
  await this.begin(); // fresh tx
55
94
  await this.savepoint(); // keep rollback harness
95
+ if (this.deferredConstraintsMode === 'immediate') {
96
+ await this.setConstraintsImmediate();
97
+ }
56
98
  await this.ctxQuery(); // reapply all setContext()
57
99
  }
58
100
  async loadJson(data) {