@onreza/sqlx-js 0.0.0 → 0.2.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.
- package/README.md +111 -22
- package/ROADMAP.md +2 -3
- package/dist/bin/sqlx-js.js +94 -11
- package/dist/src/codegen.js +0 -2
- package/dist/src/commands/init.d.ts +5 -0
- package/dist/src/commands/init.js +57 -0
- package/dist/src/commands/migrate.d.ts +190 -3
- package/dist/src/commands/migrate.js +1383 -46
- package/dist/src/commands/prepare.d.ts +16 -2
- package/dist/src/commands/prepare.js +81 -23
- package/dist/src/commands/schema.js +4 -0
- package/dist/src/commands/watch.js +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/pg/narrow.js +8 -0
- package/dist/src/pg/schema.d.ts +17 -1
- package/dist/src/pg/schema.js +48 -2
- package/dist/src/pg/wire.d.ts +14 -1
- package/dist/src/pg/wire.js +81 -3
- package/dist/src/postgres-runtime.d.ts +9 -2
- package/dist/src/postgres-runtime.js +20 -5
- package/dist/src/runtime.d.ts +12 -0
- package/dist/src/runtime.js +83 -3
- package/dist/src/scan/scanner.js +1 -1
- package/package.json +6 -9
- package/dist/src/bun-runtime.d.ts +0 -7
- package/dist/src/bun-runtime.js +0 -45
- package/dist/src/bun.d.ts +0 -22
- package/dist/src/bun.js +0 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PgClient } from "../pg/wire.js";
|
|
1
|
+
import { PgClient, type ConnConfig, type FieldDescription } from "../pg/wire.js";
|
|
2
2
|
import { SchemaCache } from "../pg/schema.js";
|
|
3
3
|
import { type SqlxJsConfig } from "../config.js";
|
|
4
4
|
export type PrepareOptions = {
|
|
@@ -15,9 +15,23 @@ export type PrepareSession = {
|
|
|
15
15
|
userCfg: SqlxJsConfig;
|
|
16
16
|
};
|
|
17
17
|
export declare function openSession(opts: PrepareOptions): Promise<PrepareSession>;
|
|
18
|
-
|
|
18
|
+
type DescribeOutcome = {
|
|
19
|
+
ok: true;
|
|
20
|
+
paramOids: number[];
|
|
21
|
+
fields: FieldDescription[];
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
error: unknown;
|
|
25
|
+
};
|
|
26
|
+
export declare function defaultPrepareConcurrency(): number;
|
|
27
|
+
export declare function describeAll(cfg: ConnConfig, sessionClient: PgClient, queries: {
|
|
28
|
+
fp: string;
|
|
29
|
+
query: string;
|
|
30
|
+
}[], concurrency: number): Promise<Map<string, DescribeOutcome>>;
|
|
31
|
+
export declare function prepareOnce(opts: PrepareOptions, session: PrepareSession, log?: (msg: string) => void, err?: (msg: string) => void, concurrency?: number): Promise<{
|
|
19
32
|
entries: number;
|
|
20
33
|
failures: number;
|
|
21
34
|
pruned: number;
|
|
22
35
|
}>;
|
|
23
36
|
export declare function runPrepare(opts: PrepareOptions): Promise<void>;
|
|
37
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PgClient, parseDatabaseUrl, PgError } from "../pg/wire.js";
|
|
2
|
-
import { SchemaCache } from "../pg/schema.js";
|
|
2
|
+
import { SchemaCache, compositeLiteral } from "../pg/schema.js";
|
|
3
3
|
import { analyzeQuery } from "../pg/analyze.js";
|
|
4
4
|
import { isBuiltinOid, oidToTs } from "../pg/oids.js";
|
|
5
5
|
import { scanProject } from "../scan/scanner.js";
|
|
@@ -26,6 +26,10 @@ function resolveTs(oid, customLookup) {
|
|
|
26
26
|
return c.tsType;
|
|
27
27
|
if (c.kind === "scalarArray")
|
|
28
28
|
return `(${c.element.tsType})[]`;
|
|
29
|
+
if (c.kind === "composite")
|
|
30
|
+
return compositeLiteral(c);
|
|
31
|
+
if (c.kind === "compositeArray")
|
|
32
|
+
return `(${compositeLiteral(c.element)})[]`;
|
|
29
33
|
}
|
|
30
34
|
return oidToTs(oid).ts;
|
|
31
35
|
}
|
|
@@ -105,7 +109,59 @@ export async function openSession(opts) {
|
|
|
105
109
|
schema.setTypeRegistry(mergeExtensionTypes(userCfg.customTypes));
|
|
106
110
|
return { client, schema, userCfg };
|
|
107
111
|
}
|
|
108
|
-
export
|
|
112
|
+
export function defaultPrepareConcurrency() {
|
|
113
|
+
const raw = process.env.SQLX_JS_PREPARE_CONCURRENCY;
|
|
114
|
+
const n = raw ? Number(raw) : NaN;
|
|
115
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 8;
|
|
116
|
+
}
|
|
117
|
+
// describe() is sequential per PgClient (see wire.ts), so concurrency comes from
|
|
118
|
+
// running several short-lived connections in parallel, each draining a shared
|
|
119
|
+
// cursor. The session connection is reused as one worker; extras are closed after.
|
|
120
|
+
export async function describeAll(cfg, sessionClient, queries, concurrency) {
|
|
121
|
+
const results = new Map();
|
|
122
|
+
if (queries.length === 0)
|
|
123
|
+
return results;
|
|
124
|
+
const workerCount = Math.max(1, Math.min(concurrency, queries.length));
|
|
125
|
+
let cursor = 0;
|
|
126
|
+
const drain = async (client) => {
|
|
127
|
+
while (true) {
|
|
128
|
+
const i = cursor++;
|
|
129
|
+
if (i >= queries.length)
|
|
130
|
+
return;
|
|
131
|
+
const { fp, query } = queries[i];
|
|
132
|
+
try {
|
|
133
|
+
const d = await client.describe(query);
|
|
134
|
+
results.set(fp, { ok: true, paramOids: d.paramOids, fields: d.fields });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
results.set(fp, { ok: false, error });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const extras = [];
|
|
142
|
+
try {
|
|
143
|
+
// Open extra connections best-effort. The session connection alone is enough
|
|
144
|
+
// to drain the queue, so a connection-limited server (low max_connections,
|
|
145
|
+
// PgBouncer) degrades to fewer workers instead of failing the whole prepare.
|
|
146
|
+
for (let i = 1; i < workerCount; i++) {
|
|
147
|
+
const c = new PgClient(cfg);
|
|
148
|
+
try {
|
|
149
|
+
await c.connect();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
await c.end().catch(() => { });
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
extras.push(c);
|
|
156
|
+
}
|
|
157
|
+
await Promise.all([sessionClient, ...extras].map((c) => drain(c)));
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await Promise.all(extras.map((c) => c.end().catch(() => { })));
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency()) {
|
|
109
165
|
const sites = scanProject(opts.root);
|
|
110
166
|
log(`scanned: found ${sites.length} sql() call site(s)`);
|
|
111
167
|
const cache = new Cache(opts.cacheDir);
|
|
@@ -121,30 +177,32 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
121
177
|
const raw = [];
|
|
122
178
|
let failures = 0;
|
|
123
179
|
const { client, schema, userCfg } = session;
|
|
180
|
+
const describeList = [...unique.values()].map((u) => ({ fp: u.fp, query: u.query }));
|
|
181
|
+
const describeResults = await describeAll(parseDatabaseUrl(opts.databaseUrl), client, describeList, concurrency);
|
|
124
182
|
for (const { fp, query, sites: ss } of unique.values()) {
|
|
125
183
|
const site = ss[0];
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
raw.push({ fp, query, sites: ss, paramOids:
|
|
184
|
+
const outcome = describeResults.get(fp);
|
|
185
|
+
if (outcome.ok) {
|
|
186
|
+
raw.push({ fp, query, sites: ss, paramOids: outcome.paramOids, fields: outcome.fields });
|
|
187
|
+
continue;
|
|
129
188
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
189
|
+
failures++;
|
|
190
|
+
const e = outcome.error;
|
|
191
|
+
if (e instanceof PgError) {
|
|
192
|
+
const extras = [];
|
|
193
|
+
if (e.position)
|
|
194
|
+
extras.push(`pos ${e.position}`);
|
|
195
|
+
if (e.code)
|
|
196
|
+
extras.push(`code ${e.code}`);
|
|
197
|
+
const tail = extras.length > 0 ? ` (${extras.join(", ")})` : "";
|
|
198
|
+
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}${tail}`);
|
|
199
|
+
if (e.hint)
|
|
200
|
+
err(` hint: ${e.hint}`);
|
|
201
|
+
err(` query: ${snippet(query)}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
|
|
205
|
+
err(` query: ${snippet(query)}`);
|
|
148
206
|
}
|
|
149
207
|
}
|
|
150
208
|
const allAttrRefs = [];
|
|
@@ -14,6 +14,10 @@ export async function applyShadowMigrations(databaseUrl, migrationsDir, log = co
|
|
|
14
14
|
applied++;
|
|
15
15
|
log(`shadow: applied ${String(e.version).padStart(4, "0")}_${e.name}`);
|
|
16
16
|
}
|
|
17
|
+
else if (e.kind === "adopted") {
|
|
18
|
+
applied++;
|
|
19
|
+
log(`shadow: adopted ${String(e.version).padStart(4, "0")}_${e.name} (${e.replaced} replaced)`);
|
|
20
|
+
}
|
|
17
21
|
else if (e.kind === "tampered") {
|
|
18
22
|
throw new Error(`sqlx-js shadow: ${e.version}_${e.name} hash mismatch (applied ${e.applied.slice(0, 16)} vs current ${e.current.slice(0, 16)})`);
|
|
19
23
|
}
|
|
@@ -21,7 +21,7 @@ export async function prepareWatchedOnce(opts, state, log, err, deps = DEFAULT_D
|
|
|
21
21
|
}
|
|
22
22
|
if (!state.session)
|
|
23
23
|
state.session = await deps.openSession(opts);
|
|
24
|
-
return await deps.prepareOnce(opts, state.session, log, err);
|
|
24
|
+
return await deps.prepareOnce(opts, state.session, log, err, 1);
|
|
25
25
|
}
|
|
26
26
|
export async function runWatch(opts) {
|
|
27
27
|
const stamp = () => new Date().toTimeString().slice(0, 8);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export type { SqlxJsConfig } from "./config.js";
|
|
|
8
8
|
export type { SslMode, ConnConfig } from "./pg/wire.js";
|
|
9
9
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
10
10
|
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
11
|
-
export type { TransactionOptions, MigrateOptions } from "./runtime.js";
|
|
12
|
-
export type { PostgresClient, PostgresOptions } from "./postgres-runtime.js";
|
|
11
|
+
export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook } from "./runtime.js";
|
|
12
|
+
export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
|
|
13
13
|
export type TypedFile = TypedFileFor<KnownFileQueries>;
|
|
14
14
|
export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
|
|
15
15
|
export type Typed = TypedFor<KnownQueries, KnownFileQueries, import("./runtime.js").TransactionOptions>;
|
package/dist/src/pg/narrow.js
CHANGED
|
@@ -78,6 +78,14 @@ function walk(node) {
|
|
|
78
78
|
}
|
|
79
79
|
return forcedInfo(acc ?? []);
|
|
80
80
|
}
|
|
81
|
+
if (op === "NOT_EXPR") {
|
|
82
|
+
const arg = args[0];
|
|
83
|
+
if (arg?.NullTest?.nulltesttype === "IS_NULL") {
|
|
84
|
+
const k = keyOfColumnRef(arg.NullTest.arg);
|
|
85
|
+
return k ? forcedInfo([k]) : emptyInfo();
|
|
86
|
+
}
|
|
87
|
+
return emptyInfo();
|
|
88
|
+
}
|
|
81
89
|
return emptyInfo();
|
|
82
90
|
}
|
|
83
91
|
if (node.A_Expr) {
|
package/dist/src/pg/schema.d.ts
CHANGED
|
@@ -24,7 +24,23 @@ export type ScalarArrayInfo = {
|
|
|
24
24
|
name: string;
|
|
25
25
|
element: ScalarInfo;
|
|
26
26
|
};
|
|
27
|
-
export type
|
|
27
|
+
export type CompositeField = {
|
|
28
|
+
name: string;
|
|
29
|
+
tsType: string;
|
|
30
|
+
nullable: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type CompositeInfo = {
|
|
33
|
+
kind: "composite";
|
|
34
|
+
name: string;
|
|
35
|
+
fields: CompositeField[];
|
|
36
|
+
};
|
|
37
|
+
export type CompositeArrayInfo = {
|
|
38
|
+
kind: "compositeArray";
|
|
39
|
+
name: string;
|
|
40
|
+
element: CompositeInfo;
|
|
41
|
+
};
|
|
42
|
+
export type CustomTypeInfo = EnumInfo | EnumArrayInfo | ScalarInfo | ScalarArrayInfo | CompositeInfo | CompositeArrayInfo;
|
|
43
|
+
export declare function compositeLiteral(info: CompositeInfo): string;
|
|
28
44
|
export declare class SchemaCache {
|
|
29
45
|
private client;
|
|
30
46
|
private byOidNum;
|
package/dist/src/pg/schema.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { decodeText } from "./wire.js";
|
|
2
2
|
import { isBuiltinOid, oidToTs } from "./oids.js";
|
|
3
|
+
export function compositeLiteral(info) {
|
|
4
|
+
if (info.fields.length === 0)
|
|
5
|
+
return "Record<string, unknown>";
|
|
6
|
+
const parts = info.fields.map((f) => {
|
|
7
|
+
const name = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(f.name) ? f.name : JSON.stringify(f.name);
|
|
8
|
+
return `${name}: ${f.tsType}${f.nullable ? " | null" : ""}`;
|
|
9
|
+
});
|
|
10
|
+
return `{ ${parts.join("; ")} }`;
|
|
11
|
+
}
|
|
3
12
|
export class SchemaCache {
|
|
4
13
|
client;
|
|
5
14
|
byOidNum = new Map();
|
|
@@ -130,11 +139,12 @@ export class SchemaCache {
|
|
|
130
139
|
for (const oid of need)
|
|
131
140
|
this.typesProbed.add(oid);
|
|
132
141
|
const list1 = [...new Set(need)].join(",");
|
|
133
|
-
const sql1 = `SELECT oid::int8, typname, typtype, typcategory, typelem::int8, typbasetype::int8 FROM pg_type WHERE oid IN (${list1})`;
|
|
142
|
+
const sql1 = `SELECT oid::int8, typname, typtype, typcategory, typelem::int8, typbasetype::int8, typrelid::int8 FROM pg_type WHERE oid IN (${list1})`;
|
|
134
143
|
const r1 = await this.client.simpleQueryAll(sql1);
|
|
135
144
|
const enumOids = [];
|
|
136
145
|
const arrayInfos = [];
|
|
137
146
|
const domainInfos = [];
|
|
147
|
+
const compositeInfos = [];
|
|
138
148
|
for (const row of r1.rows) {
|
|
139
149
|
const oid = Number(decodeText(row[0]));
|
|
140
150
|
const name = decodeText(row[1]);
|
|
@@ -142,6 +152,7 @@ export class SchemaCache {
|
|
|
142
152
|
const typcategory = decodeText(row[3]);
|
|
143
153
|
const typelem = Number(decodeText(row[4]));
|
|
144
154
|
const typbasetype = Number(decodeText(row[5]));
|
|
155
|
+
const typrelid = Number(decodeText(row[6]));
|
|
145
156
|
if (typtype === "e") {
|
|
146
157
|
enumOids.push(oid);
|
|
147
158
|
this.customTypes.set(oid, { kind: "enum", name, values: [] });
|
|
@@ -155,10 +166,29 @@ export class SchemaCache {
|
|
|
155
166
|
else if (typtype === "d" && typbasetype > 0) {
|
|
156
167
|
domainInfos.push({ oid, name, baseOid: typbasetype });
|
|
157
168
|
}
|
|
169
|
+
else if (typtype === "c" && typrelid > 0) {
|
|
170
|
+
compositeInfos.push({ oid, name, relOid: typrelid });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const compositeAttrs = new Map();
|
|
174
|
+
if (compositeInfos.length > 0) {
|
|
175
|
+
const relList = [...new Set(compositeInfos.map((c) => c.relOid))].join(",");
|
|
176
|
+
const sqlc = `SELECT c.oid::int8, a.attname, a.atttypid::int8, a.attnotnull FROM pg_attribute a JOIN pg_type c ON c.typrelid = a.attrelid WHERE a.attrelid IN (${relList}) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY c.oid, a.attnum`;
|
|
177
|
+
const rc = await this.client.simpleQueryAll(sqlc);
|
|
178
|
+
for (const row of rc.rows) {
|
|
179
|
+
const typoid = Number(decodeText(row[0]));
|
|
180
|
+
const attname = decodeText(row[1]);
|
|
181
|
+
const atttypid = Number(decodeText(row[2]));
|
|
182
|
+
const notNull = decodeText(row[3]) === "t";
|
|
183
|
+
const arr = compositeAttrs.get(typoid) ?? [];
|
|
184
|
+
arr.push({ name: attname, typeOid: atttypid, notNull });
|
|
185
|
+
compositeAttrs.set(typoid, arr);
|
|
186
|
+
}
|
|
158
187
|
}
|
|
159
188
|
const elemsToProbe = arrayInfos.map((a) => a.elemOid).filter((o) => !this.typesProbed.has(o));
|
|
160
189
|
const basesToProbe = domainInfos.map((d) => d.baseOid).filter((o) => !this.typesProbed.has(o) && !isBuiltinOid(o));
|
|
161
|
-
const
|
|
190
|
+
const fieldsToProbe = [...compositeAttrs.values()].flat().map((f) => f.typeOid).filter((o) => !this.typesProbed.has(o) && !isBuiltinOid(o));
|
|
191
|
+
const recurse = [...new Set([...elemsToProbe, ...basesToProbe, ...fieldsToProbe])];
|
|
162
192
|
if (recurse.length > 0)
|
|
163
193
|
await this.loadCustomTypes(recurse);
|
|
164
194
|
for (const { oid, name, baseOid } of domainInfos) {
|
|
@@ -167,6 +197,15 @@ export class SchemaCache {
|
|
|
167
197
|
this.customTypes.set(oid, { kind: "scalar", name, tsType: resolved });
|
|
168
198
|
}
|
|
169
199
|
}
|
|
200
|
+
for (const { oid, name } of compositeInfos) {
|
|
201
|
+
const attrs = compositeAttrs.get(oid) ?? [];
|
|
202
|
+
const fields = attrs.map((a) => ({
|
|
203
|
+
name: a.name,
|
|
204
|
+
tsType: this.resolveBaseTs(a.typeOid) ?? "unknown",
|
|
205
|
+
nullable: !a.notNull,
|
|
206
|
+
}));
|
|
207
|
+
this.customTypes.set(oid, { kind: "composite", name, fields });
|
|
208
|
+
}
|
|
170
209
|
for (const { arrayOid, arrayName, elemOid } of arrayInfos) {
|
|
171
210
|
const elem = this.customTypes.get(elemOid);
|
|
172
211
|
if (elem && elem.kind === "enum") {
|
|
@@ -175,6 +214,9 @@ export class SchemaCache {
|
|
|
175
214
|
else if (elem && elem.kind === "scalar") {
|
|
176
215
|
this.customTypes.set(arrayOid, { kind: "scalarArray", name: arrayName, element: elem });
|
|
177
216
|
}
|
|
217
|
+
else if (elem && elem.kind === "composite") {
|
|
218
|
+
this.customTypes.set(arrayOid, { kind: "compositeArray", name: arrayName, element: elem });
|
|
219
|
+
}
|
|
178
220
|
}
|
|
179
221
|
if (enumOids.length > 0) {
|
|
180
222
|
const list2 = enumOids.join(",");
|
|
@@ -211,6 +253,10 @@ export class SchemaCache {
|
|
|
211
253
|
return "string[]";
|
|
212
254
|
return `(${info.element.values.map((v) => JSON.stringify(v)).join(" | ")})[]`;
|
|
213
255
|
}
|
|
256
|
+
if (info.kind === "composite")
|
|
257
|
+
return compositeLiteral(info);
|
|
258
|
+
if (info.kind === "compositeArray")
|
|
259
|
+
return `(${compositeLiteral(info.element)})[]`;
|
|
214
260
|
return undefined;
|
|
215
261
|
}
|
|
216
262
|
customType(oid) {
|
package/dist/src/pg/wire.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export type ConnConfig = {
|
|
|
9
9
|
sslmode?: SslMode;
|
|
10
10
|
applicationName?: string;
|
|
11
11
|
connectTimeoutMs?: number;
|
|
12
|
+
statementTimeoutMs?: number;
|
|
13
|
+
sslRootCert?: string;
|
|
14
|
+
sslCert?: string;
|
|
15
|
+
sslKey?: string;
|
|
12
16
|
};
|
|
13
17
|
export declare function parseDatabaseUrl(url: string): ConnConfig;
|
|
14
18
|
export type ServerMessage = {
|
|
@@ -91,6 +95,9 @@ export declare class PgClient {
|
|
|
91
95
|
constructor(cfg: ConnConfig);
|
|
92
96
|
get usingTls(): boolean;
|
|
93
97
|
connect(): Promise<void>;
|
|
98
|
+
private destroySocket;
|
|
99
|
+
private connectInner;
|
|
100
|
+
private abortConnect;
|
|
94
101
|
private attachHandlers;
|
|
95
102
|
private deliver;
|
|
96
103
|
private flushWaiters;
|
|
@@ -120,12 +127,18 @@ export declare function computeScramProof(password: string, salt: Uint8Array, it
|
|
|
120
127
|
};
|
|
121
128
|
export declare class PgError extends Error {
|
|
122
129
|
fields: Record<string, string>;
|
|
123
|
-
constructor(fields: Record<string, string
|
|
130
|
+
constructor(fields: Record<string, string>, options?: {
|
|
131
|
+
cause?: unknown;
|
|
132
|
+
});
|
|
124
133
|
get code(): string | undefined;
|
|
125
134
|
get position(): number | undefined;
|
|
126
135
|
get hint(): string | undefined;
|
|
127
136
|
get detail(): string | undefined;
|
|
128
137
|
get severity(): string | undefined;
|
|
138
|
+
get schema(): string | undefined;
|
|
139
|
+
get table(): string | undefined;
|
|
140
|
+
get column(): string | undefined;
|
|
141
|
+
get constraint(): string | undefined;
|
|
129
142
|
}
|
|
130
143
|
export declare class ConnectionLostError extends Error {
|
|
131
144
|
readonly cause: Error;
|
package/dist/src/pg/wire.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, createHmac, pbkdf2Sync, randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { connect as netConnect } from "node:net";
|
|
3
4
|
import { connect as tlsConnect } from "node:tls";
|
|
4
5
|
const textEncoder = new TextEncoder();
|
|
@@ -31,6 +32,21 @@ export function parseDatabaseUrl(url) {
|
|
|
31
32
|
if (Number.isFinite(n) && n > 0)
|
|
32
33
|
cfg.connectTimeoutMs = n * 1000;
|
|
33
34
|
}
|
|
35
|
+
const st = params.get("statement_timeout");
|
|
36
|
+
if (st) {
|
|
37
|
+
const n = Number(st);
|
|
38
|
+
if (Number.isFinite(n) && n >= 0)
|
|
39
|
+
cfg.statementTimeoutMs = n;
|
|
40
|
+
}
|
|
41
|
+
const sslRootCert = params.get("sslrootcert");
|
|
42
|
+
if (sslRootCert)
|
|
43
|
+
cfg.sslRootCert = sslRootCert;
|
|
44
|
+
const sslCert = params.get("sslcert");
|
|
45
|
+
if (sslCert)
|
|
46
|
+
cfg.sslCert = sslCert;
|
|
47
|
+
const sslKey = params.get("sslkey");
|
|
48
|
+
if (sslKey)
|
|
49
|
+
cfg.sslKey = sslKey;
|
|
34
50
|
return cfg;
|
|
35
51
|
}
|
|
36
52
|
export class MessageReader {
|
|
@@ -229,6 +245,14 @@ function frame(tag, body) {
|
|
|
229
245
|
function isTlsRequired(mode) {
|
|
230
246
|
return mode === "require" || mode === "verify-ca" || mode === "verify-full";
|
|
231
247
|
}
|
|
248
|
+
function readCertFile(kind, path) {
|
|
249
|
+
try {
|
|
250
|
+
return readFileSync(path);
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
throw new Error(`sqlx-js: cannot read ${kind} at ${path}: ${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
232
256
|
async function openPlainSocket(host, port, timeoutMs) {
|
|
233
257
|
return new Promise((resolve, reject) => {
|
|
234
258
|
const sock = netConnect({ host, port });
|
|
@@ -275,6 +299,9 @@ async function performSslHandshake(sock, cfg, mode) {
|
|
|
275
299
|
servername: cfg.host,
|
|
276
300
|
rejectUnauthorized: mode === "verify-full" || mode === "verify-ca",
|
|
277
301
|
checkServerIdentity: mode === "verify-full" ? undefined : () => undefined,
|
|
302
|
+
...(cfg.sslRootCert ? { ca: readCertFile("sslrootcert", cfg.sslRootCert) } : {}),
|
|
303
|
+
...(cfg.sslCert ? { cert: readCertFile("sslcert", cfg.sslCert) } : {}),
|
|
304
|
+
...(cfg.sslKey ? { key: readCertFile("sslkey", cfg.sslKey) } : {}),
|
|
278
305
|
});
|
|
279
306
|
t.once("secureConnect", () => resolve(t));
|
|
280
307
|
t.once("error", (err) => {
|
|
@@ -308,9 +335,42 @@ export class PgClient {
|
|
|
308
335
|
}
|
|
309
336
|
get usingTls() { return this.tlsEnabled; }
|
|
310
337
|
async connect() {
|
|
311
|
-
const mode = this.cfg.sslmode ?? "prefer";
|
|
312
338
|
const timeoutMs = this.cfg.connectTimeoutMs ?? 15000;
|
|
339
|
+
let aborted = false;
|
|
340
|
+
let timer;
|
|
341
|
+
const deadline = new Promise((_, reject) => {
|
|
342
|
+
timer = setTimeout(() => {
|
|
343
|
+
aborted = true;
|
|
344
|
+
const err = new Error(`sqlx-js: connect timeout to ${this.cfg.host}:${this.cfg.port} after ${timeoutMs}ms (includes TLS + authentication)`);
|
|
345
|
+
this.closed = true;
|
|
346
|
+
this.closeReason ??= err;
|
|
347
|
+
this.destroySocket();
|
|
348
|
+
reject(err);
|
|
349
|
+
}, timeoutMs);
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
await Promise.race([this.connectInner(timeoutMs, () => aborted), deadline]);
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
if (timer)
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
destroySocket() {
|
|
360
|
+
try {
|
|
361
|
+
this.sock?.destroy();
|
|
362
|
+
}
|
|
363
|
+
catch { /* ignore */ }
|
|
364
|
+
}
|
|
365
|
+
// Cooperative cancellation: a socket can finish connecting after the deadline
|
|
366
|
+
// has already rejected. Re-check `aborted` at each await boundary so a late
|
|
367
|
+
// connection is torn down instead of leaking.
|
|
368
|
+
async connectInner(timeoutMs, aborted) {
|
|
369
|
+
const mode = this.cfg.sslmode ?? "prefer";
|
|
313
370
|
const plain = await openPlainSocket(this.cfg.host, this.cfg.port, timeoutMs);
|
|
371
|
+
this.sock = plain;
|
|
372
|
+
if (aborted())
|
|
373
|
+
return this.abortConnect();
|
|
314
374
|
let socket = plain;
|
|
315
375
|
if (mode !== "disable") {
|
|
316
376
|
const result = await performSslHandshake(plain, this.cfg, mode);
|
|
@@ -318,11 +378,17 @@ export class PgClient {
|
|
|
318
378
|
this.tlsEnabled = result.tls;
|
|
319
379
|
}
|
|
320
380
|
this.sock = socket;
|
|
381
|
+
if (aborted())
|
|
382
|
+
return this.abortConnect();
|
|
321
383
|
this.attachHandlers();
|
|
322
384
|
await this.startup();
|
|
323
385
|
await this.authenticate();
|
|
324
386
|
await this.awaitReady();
|
|
325
387
|
}
|
|
388
|
+
abortConnect() {
|
|
389
|
+
this.destroySocket();
|
|
390
|
+
throw this.closeReason ?? new Error("sqlx-js: connect aborted");
|
|
391
|
+
}
|
|
326
392
|
attachHandlers() {
|
|
327
393
|
const onData = (chunk) => {
|
|
328
394
|
for (const m of this.reader.push(chunk))
|
|
@@ -380,6 +446,9 @@ export class PgClient {
|
|
|
380
446
|
if (this.cfg.applicationName) {
|
|
381
447
|
pairs.push(cstr("application_name"), cstr(this.cfg.applicationName));
|
|
382
448
|
}
|
|
449
|
+
if (this.cfg.statementTimeoutMs !== undefined) {
|
|
450
|
+
pairs.push(cstr("statement_timeout"), cstr(String(this.cfg.statementTimeoutMs)));
|
|
451
|
+
}
|
|
383
452
|
pairs.push(new Uint8Array([0]));
|
|
384
453
|
const body = concat([writeInt32(196608), concat(pairs)]);
|
|
385
454
|
this.write(frame(null, body));
|
|
@@ -678,16 +747,25 @@ function stringifyMessage(m) {
|
|
|
678
747
|
}
|
|
679
748
|
export class PgError extends Error {
|
|
680
749
|
fields;
|
|
681
|
-
constructor(fields) {
|
|
682
|
-
super(fields.M ?? "postgres error");
|
|
750
|
+
constructor(fields, options) {
|
|
751
|
+
super(fields.M ?? "postgres error", options);
|
|
683
752
|
this.fields = fields;
|
|
684
753
|
this.name = "PgError";
|
|
754
|
+
// Bun attaches own `line`/`column` properties to every Error instance, which
|
|
755
|
+
// would shadow the prototype getters below. Drop them so `.column` resolves
|
|
756
|
+
// to the PG column name, not the engine's source position.
|
|
757
|
+
delete this.line;
|
|
758
|
+
delete this.column;
|
|
685
759
|
}
|
|
686
760
|
get code() { return this.fields.C; }
|
|
687
761
|
get position() { return this.fields.P ? Number(this.fields.P) : undefined; }
|
|
688
762
|
get hint() { return this.fields.H; }
|
|
689
763
|
get detail() { return this.fields.D; }
|
|
690
764
|
get severity() { return this.fields.S; }
|
|
765
|
+
get schema() { return this.fields.s; }
|
|
766
|
+
get table() { return this.fields.t; }
|
|
767
|
+
get column() { return this.fields.c; }
|
|
768
|
+
get constraint() { return this.fields.n; }
|
|
691
769
|
}
|
|
692
770
|
export class ConnectionLostError extends Error {
|
|
693
771
|
cause;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
|
+
import { type OnQueryHook } from "./runtime.js";
|
|
2
3
|
export type PostgresClient = postgres.Sql<{
|
|
3
4
|
bigint: bigint;
|
|
4
5
|
}>;
|
|
5
6
|
export type PostgresOptions = postgres.Options<Record<string, postgres.PostgresType>>;
|
|
6
|
-
export
|
|
7
|
+
export type CreateClientOptions = PostgresOptions & {
|
|
8
|
+
onQuery?: OnQueryHook;
|
|
9
|
+
statementTimeoutMs?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
|
|
7
12
|
export declare function getClient(): PostgresClient;
|
|
8
|
-
export declare function setClient(client: PostgresClient
|
|
13
|
+
export declare function setClient(client: PostgresClient, options?: {
|
|
14
|
+
onQuery?: OnQueryHook;
|
|
15
|
+
}): void;
|
|
9
16
|
export declare function close(): Promise<void>;
|
|
10
17
|
export declare const sql: import("./runtime.js").SqlRoot;
|
|
11
18
|
export declare const unsafe: (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
2
|
import { createSqlRuntime, encodeParam, encodePgArrayLiteral, parsePgArrayLiteral } from "./runtime.js";
|
|
3
3
|
import { arrayElementOid, builtinArrayOids } from "./pg/oids.js";
|
|
4
|
+
const HOOKS = Symbol.for("sqlx-js.hooks");
|
|
4
5
|
class PostgresRuntimeClient {
|
|
5
6
|
client;
|
|
6
|
-
|
|
7
|
+
onQuery;
|
|
8
|
+
constructor(client, onQuery) {
|
|
7
9
|
this.client = client;
|
|
10
|
+
this.onQuery = onQuery;
|
|
8
11
|
}
|
|
9
12
|
async query(query, params) {
|
|
10
13
|
return await this.client.unsafe(query, params, { prepare: true });
|
|
@@ -19,7 +22,7 @@ class PostgresRuntimeClient {
|
|
|
19
22
|
if (!("begin" in this.client))
|
|
20
23
|
throw new Error("sqlx-js.transaction: nested transactions are not supported");
|
|
21
24
|
return await this.client.begin(async (tx) => {
|
|
22
|
-
return await fn(new PostgresRuntimeClient(tx));
|
|
25
|
+
return await fn(new PostgresRuntimeClient(tx, this.onQuery));
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
28
|
async close() {
|
|
@@ -124,7 +127,18 @@ function postgresTypes() {
|
|
|
124
127
|
export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
125
128
|
if (!url)
|
|
126
129
|
throw new Error("sqlx-js: DATABASE_URL is not set");
|
|
127
|
-
|
|
130
|
+
const { onQuery, statementTimeoutMs, ...pgOptions } = options;
|
|
131
|
+
const connection = statementTimeoutMs !== undefined
|
|
132
|
+
? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
|
|
133
|
+
: pgOptions.connection;
|
|
134
|
+
const client = postgres(url, {
|
|
135
|
+
...pgOptions,
|
|
136
|
+
...(connection ? { connection } : {}),
|
|
137
|
+
types: { ...postgresTypes(), ...(pgOptions.types ?? {}) },
|
|
138
|
+
});
|
|
139
|
+
if (onQuery)
|
|
140
|
+
client[HOOKS] = { onQuery };
|
|
141
|
+
return client;
|
|
128
142
|
}
|
|
129
143
|
function createDefaultClient() {
|
|
130
144
|
return new PostgresRuntimeClient(createClient());
|
|
@@ -136,8 +150,9 @@ function getRuntimeClient() {
|
|
|
136
150
|
export function getClient() {
|
|
137
151
|
return getRuntimeClient().client;
|
|
138
152
|
}
|
|
139
|
-
export function setClient(client) {
|
|
140
|
-
|
|
153
|
+
export function setClient(client, options) {
|
|
154
|
+
const attached = client[HOOKS];
|
|
155
|
+
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery);
|
|
141
156
|
}
|
|
142
157
|
export async function close() {
|
|
143
158
|
if (defaultClient) {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { PgError } from "./pg/wire.js";
|
|
2
|
+
export type OnQueryEvent = {
|
|
3
|
+
query: string;
|
|
4
|
+
params: unknown[];
|
|
5
|
+
durationMs: number;
|
|
6
|
+
rowCount?: number;
|
|
7
|
+
error?: unknown;
|
|
8
|
+
};
|
|
9
|
+
export type OnQueryHook = (event: OnQueryEvent) => void;
|
|
1
10
|
export type RuntimeClient = {
|
|
2
11
|
query: (query: string, params: unknown[]) => Promise<unknown[]>;
|
|
3
12
|
transformParam?: (param: unknown) => unknown;
|
|
4
13
|
transaction: <R>(fn: (client: RuntimeClient) => Promise<R>) => Promise<R>;
|
|
5
14
|
close: () => Promise<void>;
|
|
15
|
+
onQuery?: OnQueryHook;
|
|
6
16
|
};
|
|
7
17
|
type AnyFn = (...args: unknown[]) => Promise<unknown[]>;
|
|
8
18
|
type AnyOneFn = (...args: unknown[]) => Promise<unknown>;
|
|
@@ -21,6 +31,7 @@ export declare class TooManyRowsError extends Error {
|
|
|
21
31
|
actual: number;
|
|
22
32
|
constructor(actual: number, expected?: "1" | "0 or 1");
|
|
23
33
|
}
|
|
34
|
+
export declare function toPgError(e: unknown): PgError | null;
|
|
24
35
|
export declare const _internal: {
|
|
25
36
|
renameRows: typeof renameRows;
|
|
26
37
|
encodeParam: typeof encodeParam;
|
|
@@ -29,6 +40,7 @@ export declare const _internal: {
|
|
|
29
40
|
loadSqlFile: typeof loadSqlFile;
|
|
30
41
|
buildSetTransaction: typeof buildSetTransaction;
|
|
31
42
|
clearIdentifierCache: typeof clearIdentifierCache;
|
|
43
|
+
toPgError: typeof toPgError;
|
|
32
44
|
};
|
|
33
45
|
declare function loadSqlFile(path: string): string;
|
|
34
46
|
export declare function clearSqlFileCache(): void;
|