@onreza/sqlx-js 0.1.0 → 0.3.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.
@@ -24,7 +24,23 @@ export type ScalarArrayInfo = {
24
24
  name: string;
25
25
  element: ScalarInfo;
26
26
  };
27
- export type CustomTypeInfo = EnumInfo | EnumArrayInfo | ScalarInfo | ScalarArrayInfo;
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;
@@ -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 recurse = [...new Set([...elemsToProbe, ...basesToProbe])];
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) {
@@ -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;
@@ -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 declare function createClient(url?: string | undefined, options?: PostgresOptions): PostgresClient;
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): void;
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
- constructor(client) {
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
- return postgres(url, { ...options, types: { ...postgresTypes(), ...(options.types ?? {}) } });
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
- defaultClient = new PostgresRuntimeClient(client);
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) {
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { PgClient, parseDatabaseUrl } from "./pg/wire.js";
3
+ import { PgClient, parseDatabaseUrl, PgError } from "./pg/wire.js";
4
4
  import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./commands/migrate.js";
5
5
  const SUFFIX = /[!?]$/;
6
6
  function renameRows(rows) {
@@ -148,6 +148,63 @@ export class TooManyRowsError extends Error {
148
148
  this.actual = actual;
149
149
  }
150
150
  }
151
+ // SQLSTATE is exactly five characters from [0-9A-Z]; lowercase or other shapes
152
+ // are never valid, so transport codes like "EPIPE" must not match on shape alone.
153
+ const SQLSTATE = /^[0-9A-Z]{5}$/;
154
+ function firstString(...candidates) {
155
+ for (const value of candidates) {
156
+ if (typeof value === "string" && value.length > 0)
157
+ return value;
158
+ }
159
+ return undefined;
160
+ }
161
+ export function toPgError(e) {
162
+ if (e instanceof PgError)
163
+ return e;
164
+ if (e === null || typeof e !== "object")
165
+ return null;
166
+ const o = e;
167
+ const code = typeof o.code === "string" ? o.code : undefined;
168
+ // `_name` variants are postgres.js; bare forms are node-postgres. Bare
169
+ // `column`/`schema`/`table` can also collide with runtime-added Error
170
+ // properties, so we read namespaced variants first and only accept strings.
171
+ const severity = firstString(o.severity, o.severity_local);
172
+ // A genuine database error is identified by the driver's branded name
173
+ // (Postgres.js) or by a SQLSTATE-shaped code paired with a severity. Transport
174
+ // and system errors (EPIPE, ECONNREFUSED, CONNECTION_ENDED) carry neither, so
175
+ // they pass through untouched instead of masquerading as a PgError.
176
+ const isDatabaseError = o.name === "PostgresError" ||
177
+ (code !== undefined && SQLSTATE.test(code) && severity !== undefined);
178
+ if (!isDatabaseError)
179
+ return null;
180
+ const fields = {};
181
+ if (typeof o.message === "string" && o.message.length > 0)
182
+ fields.M = o.message;
183
+ if (code)
184
+ fields.C = code;
185
+ if (typeof o.detail === "string" && o.detail.length > 0)
186
+ fields.D = o.detail;
187
+ if (typeof o.hint === "string" && o.hint.length > 0)
188
+ fields.H = o.hint;
189
+ const position = firstString(o.position) ?? (typeof o.position === "number" && Number.isFinite(o.position) ? String(o.position) : undefined);
190
+ if (position)
191
+ fields.P = position;
192
+ if (severity)
193
+ fields.S = severity;
194
+ const table = firstString(o.table_name, o.table);
195
+ if (table)
196
+ fields.t = table;
197
+ const column = firstString(o.column_name, o.column);
198
+ if (column)
199
+ fields.c = column;
200
+ const constraint = firstString(o.constraint_name, o.constraint);
201
+ if (constraint)
202
+ fields.n = constraint;
203
+ const schema = firstString(o.schema_name, o.schema);
204
+ if (schema)
205
+ fields.s = schema;
206
+ return new PgError(fields, { cause: e });
207
+ }
151
208
  export const _internal = {
152
209
  renameRows,
153
210
  encodeParam,
@@ -156,13 +213,32 @@ export const _internal = {
156
213
  loadSqlFile,
157
214
  buildSetTransaction,
158
215
  clearIdentifierCache,
216
+ toPgError,
159
217
  };
160
218
  async function runQuery(client, query, params) {
161
219
  const encoded = params.length === 0
162
220
  ? params
163
221
  : params.map((p) => client.transformParam ? client.transformParam(p) : encodeParam(p));
164
- const rows = await client.query(query, encoded);
165
- return renameRows(rows);
222
+ const onQuery = client.onQuery;
223
+ if (!onQuery) {
224
+ try {
225
+ return renameRows(await client.query(query, encoded));
226
+ }
227
+ catch (e) {
228
+ throw toPgError(e) ?? e;
229
+ }
230
+ }
231
+ const start = performance.now();
232
+ try {
233
+ const rows = await client.query(query, encoded);
234
+ onQuery({ query, params, durationMs: performance.now() - start, rowCount: rows.length });
235
+ return renameRows(rows);
236
+ }
237
+ catch (e) {
238
+ const error = toPgError(e) ?? e;
239
+ onQuery({ query, params, durationMs: performance.now() - start, error });
240
+ throw error;
241
+ }
166
242
  }
167
243
  async function runOne(client, query, params) {
168
244
  const rows = await runQuery(client, query, params);
@@ -422,6 +498,10 @@ export async function migrate(opts = {}) {
422
498
  log(`migrate: applied ${String(e.version).padStart(4, "0")}_${e.name}`);
423
499
  appliedAny = true;
424
500
  }
501
+ else if (e.kind === "adopted") {
502
+ log(`migrate: adopted ${String(e.version).padStart(4, "0")}_${e.name} (${e.replaced} replaced)`);
503
+ appliedAny = true;
504
+ }
425
505
  else if (e.kind === "tampered") {
426
506
  throw new Error(`sqlx-js.migrate: ${e.version}_${e.name} hash mismatch (applied ${e.applied.slice(0, 16)}… vs current ${e.current.slice(0, 16)}…)`);
427
507
  }
@@ -2,7 +2,7 @@ import ts from "typescript";
2
2
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve } from "node:path";
4
4
  const EXCLUDE_DIRS = new Set(["node_modules", ".git", ".sqlx-js", "dist", "build", ".next"]);
5
- const SQLX_MODULES = new Set(["@onreza/sqlx-js", "@onreza/sqlx-js/bun"]);
5
+ const SQLX_MODULES = new Set(["@onreza/sqlx-js"]);
6
6
  const EXT = /\.(ts|tsx|mts|cts)$/;
7
7
  function walk(dir, out) {
8
8
  for (const name of readdirSync(dir)) {
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@onreza/sqlx-js",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by sqlx.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
7
10
  "sideEffects": false,
8
11
  "main": "./dist/src/index.js",
9
12
  "types": "./dist/src/index.d.ts",
@@ -12,12 +15,6 @@
12
15
  "types": "./dist/src/index.d.ts",
13
16
  "import": "./dist/src/index.js",
14
17
  "default": "./dist/src/index.js"
15
- },
16
- "./bun": {
17
- "types": "./dist/src/bun.d.ts",
18
- "bun": "./dist/src/bun.js",
19
- "import": "./dist/src/bun.js",
20
- "default": "./dist/src/bun.js"
21
18
  }
22
19
  },
23
20
  "bin": {
@@ -52,7 +49,7 @@
52
49
  "migrations"
53
50
  ],
54
51
  "scripts": {
55
- "prepare": "[ -d .git ] && lefthook install >/dev/null 2>&1 || true",
52
+ "prepare": "(test -f dist/bin/sqlx-js.js || bun run build) && ([ ! -d .git ] || lefthook install >/dev/null 2>&1 || true)",
56
53
  "test": "bun test tests",
57
54
  "test:typecheck": "tsc --noEmit",
58
55
  "test:example": "bunx tsc -p example --noEmit",
@@ -1,7 +0,0 @@
1
- import { SQL } from "bun";
2
- export type BunClient = SQL;
3
- export declare function getClient(): BunClient;
4
- export declare function setClient(client: BunClient): void;
5
- export declare function close(): Promise<void>;
6
- export declare const sql: import("./runtime.js").SqlRoot;
7
- export declare const unsafe: (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
@@ -1,45 +0,0 @@
1
- import { SQL } from "bun";
2
- import { createSqlRuntime } from "./runtime.js";
3
- class BunRuntimeClient {
4
- client;
5
- constructor(client) {
6
- this.client = client;
7
- }
8
- async query(query, params) {
9
- return await this.client.unsafe(query, params);
10
- }
11
- async transaction(fn) {
12
- return await this.client.begin(async (tx) => {
13
- return await fn(new BunRuntimeClient(tx));
14
- });
15
- }
16
- async close() {
17
- await this.client.close();
18
- }
19
- }
20
- let defaultClient = null;
21
- function createDefaultClient() {
22
- const url = process.env.DATABASE_URL;
23
- if (!url)
24
- throw new Error("sqlx-js: DATABASE_URL is not set");
25
- return new BunRuntimeClient(new SQL({ url, bigint: true }));
26
- }
27
- function getRuntimeClient() {
28
- defaultClient ??= createDefaultClient();
29
- return defaultClient;
30
- }
31
- export function getClient() {
32
- return getRuntimeClient().client;
33
- }
34
- export function setClient(client) {
35
- defaultClient = new BunRuntimeClient(client);
36
- }
37
- export async function close() {
38
- if (defaultClient) {
39
- await defaultClient.close();
40
- defaultClient = null;
41
- }
42
- }
43
- const runtime = createSqlRuntime(getRuntimeClient);
44
- export const sql = runtime.sql;
45
- export const unsafe = runtime.unsafe;
package/dist/src/bun.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import * as rt from "./bun-runtime.js";
2
- import type { Typed as TypedFor, TypedFile as TypedFileFor, TypedSql as TypedSqlFor } from "./typed.js";
3
- export interface KnownQueries {
4
- }
5
- export interface KnownFileQueries {
6
- }
7
- export type { SqlxJsConfig } from "./config.js";
8
- export type { SslMode, ConnConfig } from "./pg/wire.js";
9
- export { PgError, ConnectionLostError } from "./pg/wire.js";
10
- export { NoRowsError, TooManyRowsError } from "./runtime.js";
11
- export type { TransactionOptions, MigrateOptions } from "./runtime.js";
12
- export type { BunClient } from "./bun-runtime.js";
13
- export type TypedFile = TypedFileFor<KnownFileQueries>;
14
- export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
15
- export type Typed = TypedFor<KnownQueries, KnownFileQueries, import("./runtime.js").TransactionOptions>;
16
- export declare const sql: Typed;
17
- export type Unsafe = (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
18
- export declare const unsafe: Unsafe;
19
- export declare const getClient: typeof rt.getClient;
20
- export declare const setClient: typeof rt.setClient;
21
- export declare const close: typeof rt.close;
22
- export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";