@dbx-tools/postgres 0.6.62

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/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@dbx-tools/postgres",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git+https://github.com/reggie-db/dbx-tools.git",
6
+ "directory": "packages/node/postgres"
7
+ },
8
+ "scripts": {
9
+ "build": "projen build",
10
+ "compile": "projen compile",
11
+ "default": "projen default",
12
+ "package": "projen package",
13
+ "post-compile": "projen post-compile",
14
+ "pre-compile": "projen pre-compile",
15
+ "prepack": "projen prepack",
16
+ "test": "projen test",
17
+ "watch": "projen watch",
18
+ "projen": "projen"
19
+ },
20
+ "devDependencies": {
21
+ "@databricks/appkit": "^0.43.0",
22
+ "@types/bun": "^1.3.14",
23
+ "@types/node": "^24.6.0",
24
+ "@types/pg": "^8",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "peerDependencies": {
28
+ "@databricks/appkit": "^0.43.0"
29
+ },
30
+ "dependencies": {
31
+ "@dbx-tools/shared-core": "0.6.62",
32
+ "pg": "^8.22.0"
33
+ },
34
+ "main": "./lib/index.js",
35
+ "license": "UNLICENSED",
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "main": "./lib/index.js",
39
+ "types": "./lib/index.d.ts",
40
+ "exports": {
41
+ ".": {
42
+ "types": "./lib/index.d.ts",
43
+ "default": "./lib/index.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ }
47
+ },
48
+ "version": "0.6.62",
49
+ "types": "./lib/index.d.ts",
50
+ "type": "module",
51
+ "exports": {
52
+ ".": {
53
+ "types": "./lib/index.d.ts",
54
+ "default": "./lib/index.js"
55
+ },
56
+ "./package.json": "./package.json"
57
+ },
58
+ "files": [
59
+ "index.ts",
60
+ "src",
61
+ "lib"
62
+ ],
63
+ "peerDependenciesMeta": {
64
+ "@databricks/appkit": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "dbxToolsConfig": {
69
+ "tags": [
70
+ "node"
71
+ ]
72
+ },
73
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"bunx projen\"."
74
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Advisory-lock helpers for any `pg.Pool`-compatible pool.
3
+ *
4
+ * PostgreSQL advisory locks belong to a connection, not a pool. These helpers
5
+ * reserve one pooled client for the full callback, acquire the lock on that
6
+ * client, and release both in the correct order.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { createHash } from "node:crypto";
12
+ import { object } from "@dbx-tools/shared-core";
13
+ import type { Pool, PoolClient, QueryResult, QueryResultRow } from "pg";
14
+
15
+ const SIGNED_BIGINT_BITS = 64;
16
+
17
+ /**
18
+ * What names a lock. Anything reducible to a stable identity: a string, an id, a
19
+ * `["invoice", id]` pair, a config object, or an explicit `bigint` to interoperate
20
+ * with another implementation's published lock id.
21
+ *
22
+ * One value or many: an array is read as multiple parts, anything else as a single
23
+ * part. So `["invoice", 7]` and `"invoice_7"` are different locks, since the
24
+ * canonical form sees different structure.
25
+ */
26
+ export type AdvisoryLockKey = unknown;
27
+
28
+ /** Structural pool shape accepted by the lock helpers. */
29
+ export type PgPoolLike = Pick<Pool, "connect">;
30
+
31
+ /** Structural query shape shared by `pg.PoolClient` and AppKit Lakebase. */
32
+ export interface PgQueryable {
33
+ query<T extends QueryResultRow = QueryResultRow>(
34
+ text: string,
35
+ values?: unknown[],
36
+ ): Promise<QueryResult<T>>;
37
+ }
38
+
39
+ type UnlockRow = QueryResultRow & { unlocked: boolean };
40
+
41
+ /**
42
+ * Convert an arbitrary structured key into PostgreSQL's signed 64-bit advisory
43
+ * lock namespace. A bigint is preserved directly so callers can interoperate
44
+ * with another implementation that publishes its lock ID.
45
+ *
46
+ * Everything else is canonicalized with `object.toStableKey` and hashed, so key
47
+ * order in an object does not matter while a `1` and a `"1"` stay different locks.
48
+ * A cycle, a non-finite number, or a function/symbol key throws `TypeError`
49
+ * rather than yielding an identity two callers could disagree about.
50
+ */
51
+ export function advisoryLockId(key: AdvisoryLockKey): bigint {
52
+ if (typeof key === "bigint") return BigInt.asIntN(SIGNED_BIGINT_BITS, key);
53
+ const parts = object.toOneOrMany(key);
54
+ const digest = createHash("sha256")
55
+ .update(parts.map((part) => object.toStableKey(part)).join("\u0000"))
56
+ .digest();
57
+ return digest.readBigInt64BE(0);
58
+ }
59
+
60
+ async function acquire(client: PgQueryable, id: bigint, transaction: boolean): Promise<void> {
61
+ const fn = transaction ? "pg_advisory_xact_lock" : "pg_advisory_lock";
62
+ await client.query(`SELECT ${fn}($1::bigint)`, [id.toString()]);
63
+ }
64
+
65
+ async function unlock(client: PgQueryable, id: bigint): Promise<void> {
66
+ const result = await client.query<UnlockRow>(
67
+ "SELECT pg_advisory_unlock($1::bigint) AS unlocked",
68
+ [id.toString()],
69
+ );
70
+ if (result.rows[0]?.unlocked !== true) {
71
+ throw new Error(`Postgres advisory lock ${id} was not held by this connection`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Hold a session advisory lock for the duration of `fn`.
77
+ *
78
+ * The callback receives the dedicated `PoolClient` that owns the lock. Use it
79
+ * for any operation that must be protected by the lock.
80
+ */
81
+ export async function withAdvisoryLock<T>(
82
+ pool: PgPoolLike,
83
+ key: AdvisoryLockKey,
84
+ fn: (client: PoolClient) => Promise<T> | T,
85
+ ): Promise<T> {
86
+ const id = advisoryLockId(key);
87
+ const client = await pool.connect();
88
+ let acquired = false;
89
+ let failed = false;
90
+ let failure: unknown;
91
+ let value: T | undefined;
92
+
93
+ try {
94
+ await acquire(client, id, false);
95
+ acquired = true;
96
+ value = await fn(client);
97
+ } catch (error) {
98
+ failed = true;
99
+ failure = error;
100
+ }
101
+
102
+ let unlockFailure: unknown;
103
+ if (acquired) {
104
+ try {
105
+ await unlock(client, id);
106
+ } catch (error) {
107
+ unlockFailure = error;
108
+ }
109
+ }
110
+ client.release(unlockFailure instanceof Error ? unlockFailure : undefined);
111
+
112
+ if (failed) throw failure;
113
+ if (unlockFailure !== undefined) throw unlockFailure;
114
+ return value as T;
115
+ }
116
+
117
+ /**
118
+ * Run `fn` in a transaction while holding a transaction advisory lock.
119
+ *
120
+ * The lock is released atomically by `COMMIT` or `ROLLBACK`, making this the
121
+ * right primitive for one-time schema installation and migrations.
122
+ */
123
+ export async function withAdvisoryTransactionLock<T>(
124
+ pool: PgPoolLike,
125
+ key: AdvisoryLockKey,
126
+ fn: (client: PoolClient) => Promise<T> | T,
127
+ ): Promise<T> {
128
+ const id = advisoryLockId(key);
129
+ const client = await pool.connect();
130
+ let releaseError: Error | undefined;
131
+ try {
132
+ await client.query("BEGIN");
133
+ await acquire(client, id, true);
134
+ const value = await fn(client);
135
+ await client.query("COMMIT");
136
+ return value;
137
+ } catch (error) {
138
+ try {
139
+ await client.query("ROLLBACK");
140
+ } catch (rollbackError) {
141
+ releaseError =
142
+ rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError));
143
+ }
144
+ throw error;
145
+ } finally {
146
+ client.release(releaseError);
147
+ }
148
+ }