@onreza/sqlx-js 0.4.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,18 +1,148 @@
1
- import { existsSync } from "node:fs";
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
3
  import { join } from "node:path";
3
- export async function loadConfig(root) {
4
- for (const name of ["sqlx-js.config.ts", "sqlx-js.config.js", "sqlx-js.config.mjs"]) {
4
+ import { pathToFileURL } from "node:url";
5
+ import { parseEnv } from "node:util";
6
+ export function defineConfig(config) {
7
+ return config;
8
+ }
9
+ export function loadRootEnv(root) {
10
+ const path = join(root, ".env");
11
+ if (!existsSync(path))
12
+ return undefined;
13
+ const parsed = parseEnv(readFileSync(path, "utf8"));
14
+ for (const [key, value] of Object.entries(parsed)) {
15
+ if (process.env[key] === undefined)
16
+ process.env[key] = value;
17
+ }
18
+ return path;
19
+ }
20
+ export function configPath(root) {
21
+ for (const name of ["sqlx-js.config.mts", "sqlx-js.config.ts", "sqlx-js.config.mjs", "sqlx-js.config.js"]) {
5
22
  const p = join(root, name);
6
- if (!existsSync(p))
7
- continue;
8
- const mod = await import(p);
9
- const cfg = (mod.default ?? mod);
10
- if (typeof cfg !== "object" || cfg === null) {
11
- throw new Error(`sqlx-js: ${name} must default-export a config object`);
23
+ if (existsSync(p))
24
+ return p;
25
+ }
26
+ return undefined;
27
+ }
28
+ export async function loadConfig(root) {
29
+ const path = configPath(root);
30
+ if (!path)
31
+ return {};
32
+ const url = pathToFileURL(path);
33
+ url.searchParams.set("mtime", String(statSync(path).mtimeMs));
34
+ const mod = await import(url.href);
35
+ if (!("default" in mod)) {
36
+ throw new Error(`sqlx-js: ${path} must default-export a config object`);
37
+ }
38
+ return validateConfig(mod.default, path);
39
+ }
40
+ function validateStringRecord(value, name, path) {
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
42
+ throw new Error(`sqlx-js: ${path} ${name} must be an object of string values`);
43
+ }
44
+ for (const [key, item] of Object.entries(value)) {
45
+ if (typeof item !== "string") {
46
+ throw new Error(`sqlx-js: ${path} ${name}.${key} must be a string`);
47
+ }
48
+ }
49
+ }
50
+ function validateStringArray(value, name, path) {
51
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
52
+ throw new Error(`sqlx-js: ${path} ${name} must be an array of strings`);
53
+ }
54
+ }
55
+ function validateConfig(value, path) {
56
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
57
+ throw new Error(`sqlx-js: ${path} must default-export a config object`);
58
+ }
59
+ const config = value;
60
+ if (config.jsonbTypes !== undefined)
61
+ validateStringRecord(config.jsonbTypes, "jsonbTypes", path);
62
+ if (config.customTypes !== undefined)
63
+ validateStringRecord(config.customTypes, "customTypes", path);
64
+ if (config.scan !== undefined) {
65
+ if (!config.scan || typeof config.scan !== "object" || Array.isArray(config.scan)) {
66
+ throw new Error(`sqlx-js: ${path} scan must be an object`);
12
67
  }
13
- return cfg;
68
+ const scan = config.scan;
69
+ if (scan.include !== undefined)
70
+ validateStringArray(scan.include, "scan.include", path);
71
+ if (scan.exclude !== undefined)
72
+ validateStringArray(scan.exclude, "scan.exclude", path);
14
73
  }
15
- return {};
74
+ if (config.schema !== undefined) {
75
+ if (!config.schema || typeof config.schema !== "object" || Array.isArray(config.schema)) {
76
+ throw new Error(`sqlx-js: ${path} schema must be an object`);
77
+ }
78
+ const schema = config.schema;
79
+ if (schema.provider !== undefined && schema.provider !== "builtin" && schema.provider !== "pgschema") {
80
+ throw new Error(`sqlx-js: ${path} schema.provider must be builtin or pgschema`);
81
+ }
82
+ for (const key of ["file", "command"]) {
83
+ if (schema[key] !== undefined && typeof schema[key] !== "string") {
84
+ throw new Error(`sqlx-js: ${path} schema.${key} must be a string`);
85
+ }
86
+ }
87
+ if (schema.schemas !== undefined)
88
+ validateStringArray(schema.schemas, "schema.schemas", path);
89
+ }
90
+ return value;
91
+ }
92
+ export function prepareConfigHash(cfg) {
93
+ const value = stableValue({
94
+ jsonbTypes: cfg.jsonbTypes ?? {},
95
+ customTypes: cfg.customTypes ?? {},
96
+ });
97
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
98
+ }
99
+ export function configHash(cfg) {
100
+ return createHash("sha256").update(JSON.stringify(stableValue(cfg))).digest("hex").slice(0, 16);
101
+ }
102
+ function stableValue(value) {
103
+ if (Array.isArray(value))
104
+ return value.map(stableValue);
105
+ if (!value || typeof value !== "object")
106
+ return value;
107
+ return Object.fromEntries(Object.entries(value)
108
+ .sort(([a], [b]) => a.localeCompare(b))
109
+ .map(([key, item]) => [key, stableValue(item)]));
110
+ }
111
+ export async function loadConfigInfo(root) {
112
+ const path = configPath(root);
113
+ if (!path)
114
+ return { config: {} };
115
+ return { config: await loadConfig(root), path };
116
+ }
117
+ export function assertSupportedRuntime() {
118
+ const bun = process.versions.bun;
119
+ if (bun) {
120
+ if (majorMinorLessThan(bun, 1, 3)) {
121
+ throw new Error(`sqlx-js requires Bun >=1.3, current ${bun}`);
122
+ }
123
+ return;
124
+ }
125
+ const node = process.versions.node;
126
+ if (majorMinorLessThan(node, 24, 0)) {
127
+ throw new Error(`sqlx-js requires Node.js >=24, current ${node}`);
128
+ }
129
+ }
130
+ function majorMinorLessThan(version, minMajor, minMinor) {
131
+ const [major = 0, minor = 0] = version.split(".").map(Number);
132
+ return major < minMajor || (major === minMajor && minor < minMinor);
133
+ }
134
+ export function runtimeVersion() {
135
+ if (process.versions.bun)
136
+ return { runtime: "bun", version: process.versions.bun };
137
+ return { runtime: "node", version: process.versions.node };
138
+ }
139
+ export function nativeTypeScriptEnabled() {
140
+ if (process.versions.bun)
141
+ return true;
142
+ return process.features.typescript;
143
+ }
144
+ export function envFilePath(root) {
145
+ return join(root, ".env");
16
146
  }
17
147
  export function lookupJsonbType(cfg, schema, table, column) {
18
148
  const types = cfg.jsonbTypes;
@@ -18,11 +18,13 @@ export type JsonInputObject = {
18
18
  readonly [key: string]: JsonInputValue | undefined;
19
19
  };
20
20
  export type JsonInputArray = readonly JsonInputValue[];
21
- export type { SqlxJsConfig } from "./config.js";
21
+ export { defineConfig } from "./config.js";
22
+ export type { ScanConfig, SqlxJsConfig } from "./config.js";
22
23
  export type { SslMode, ConnConfig } from "./pg/wire.js";
23
24
  export { PgError, ConnectionLostError } from "./pg/wire.js";
24
25
  export { NoRowsError, TooManyRowsError } from "./runtime.js";
25
26
  export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook } from "./runtime.js";
27
+ export type { ExecuteResult, JsonParameter, PgArrayParameter } from "./runtime.js";
26
28
  export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
27
29
  export type TypedFile = TypedFileFor<KnownFileQueries>;
28
30
  export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
@@ -34,4 +36,4 @@ export declare const getClient: typeof rt.getClient;
34
36
  export declare const setClient: typeof rt.setClient;
35
37
  export declare const createClient: typeof rt.createClient;
36
38
  export declare const close: typeof rt.close;
37
- export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";
39
+ export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
package/dist/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as rt from "./postgres-runtime.js";
2
+ export { defineConfig } from "./config.js";
2
3
  export { PgError, ConnectionLostError } from "./pg/wire.js";
3
4
  export { NoRowsError, TooManyRowsError } from "./runtime.js";
4
5
  export const sql = rt.sql;
@@ -7,4 +8,4 @@ export const getClient = rt.getClient;
7
8
  export const setClient = rt.setClient;
8
9
  export const createClient = rt.createClient;
9
10
  export const close = rt.close;
10
- export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";
11
+ export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
@@ -7,11 +7,14 @@ export type PostgresOptions = postgres.Options<Record<string, postgres.PostgresT
7
7
  export type CreateClientOptions = PostgresOptions & {
8
8
  onQuery?: OnQueryHook;
9
9
  statementTimeoutMs?: number;
10
+ fileRoot?: string;
10
11
  };
11
12
  export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
12
13
  export declare function getClient(): PostgresClient;
13
14
  export declare function setClient(client: PostgresClient, options?: {
14
15
  onQuery?: OnQueryHook;
16
+ prepare?: boolean;
17
+ fileRoot?: string;
15
18
  }): void;
16
19
  export declare function close(): Promise<void>;
17
20
  export declare const sql: import("./runtime.js").SqlRoot;
@@ -1,28 +1,45 @@
1
1
  import postgres from "postgres";
2
- import { createSqlRuntime, encodeParam, encodePgArrayLiteral, parsePgArrayLiteral } from "./runtime.js";
2
+ import { resolve } from "node:path";
3
+ import { createSqlRuntime, encodePgArrayLiteral, isPrimitiveArrayElement, parameterKind, parsePgArrayLiteral, } from "./runtime.js";
3
4
  import { arrayElementOid, builtinArrayOids } from "./pg/oids.js";
4
5
  const HOOKS = Symbol.for("sqlx-js.hooks");
6
+ function resolvedFileRoot(value) {
7
+ return resolve(value ?? process.env.SQLX_JS_FILE_ROOT ?? process.cwd());
8
+ }
5
9
  class PostgresRuntimeClient {
6
10
  client;
7
11
  onQuery;
8
- constructor(client, onQuery) {
12
+ prepare;
13
+ fileRoot;
14
+ constructor(client, onQuery, prepare = true, fileRoot = resolvedFileRoot()) {
9
15
  this.client = client;
10
16
  this.onQuery = onQuery;
17
+ this.prepare = prepare;
18
+ this.fileRoot = fileRoot;
11
19
  }
12
20
  async query(query, params) {
13
- return await this.client.unsafe(query, params, { prepare: true });
21
+ return await this.client.unsafe(query, params, { prepare: this.prepare });
14
22
  }
15
23
  transformParam(param) {
16
- const elementOid = postgresNativeArrayElementOid(param);
17
- if (elementOid !== undefined)
18
- return this.client.array(param, elementOid);
19
- return encodeParam(param);
24
+ const kind = parameterKind(param);
25
+ if (kind === "json")
26
+ return this.client.json(param.value);
27
+ if (kind === "array") {
28
+ const value = [...param.value];
29
+ if (value.every(isPrimitiveArrayElement))
30
+ return encodePgArrayLiteral(value);
31
+ if (value.every((item) => item === null || parameterKind(item) === "json")) {
32
+ return this.client.array(value, 3807);
33
+ }
34
+ return this.client.array(value);
35
+ }
36
+ return param;
20
37
  }
21
38
  async transaction(fn) {
22
39
  if (!("begin" in this.client))
23
40
  throw new Error("sqlx-js.transaction: nested transactions are not supported");
24
41
  return await this.client.begin(async (tx) => {
25
- return await fn(new PostgresRuntimeClient(tx, this.onQuery));
42
+ return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.prepare, this.fileRoot));
26
43
  });
27
44
  }
28
45
  async close() {
@@ -74,6 +91,7 @@ const STRING_ARRAY_ELEMENT_OIDS = new Set([
74
91
  4451,
75
92
  4536,
76
93
  ]);
94
+ const JSON_ELEMENT_OIDS = new Set([114, 3802]);
77
95
  function parseSimpleArrayElement(oid) {
78
96
  switch (oid) {
79
97
  case 16:
@@ -90,28 +108,21 @@ function parseSimpleArrayElement(oid) {
90
108
  return STRING_ARRAY_ELEMENT_OIDS.has(oid) ? (value) => value : undefined;
91
109
  }
92
110
  }
93
- function postgresNativeArrayElementOid(value) {
94
- if (!Array.isArray(value) || value.length === 0)
95
- return undefined;
96
- let oid;
97
- for (const item of value) {
98
- if (item === null)
99
- continue;
100
- const itemOid = item instanceof Uint8Array ? 17 : undefined;
101
- if (itemOid === undefined)
102
- return undefined;
103
- if (oid !== undefined && oid !== itemOid)
104
- return undefined;
105
- oid = itemOid;
106
- }
107
- return oid;
108
- }
109
111
  function postgresTypes() {
110
112
  const types = { bigint: postgres.BigInt };
111
113
  for (const oid of builtinArrayOids()) {
112
114
  const elementOid = arrayElementOid(oid);
113
115
  if (elementOid === undefined)
114
116
  continue;
117
+ if (JSON_ELEMENT_OIDS.has(elementOid)) {
118
+ types[`array_${oid}`] = {
119
+ to: oid,
120
+ from: [oid],
121
+ serialize: (value) => Array.isArray(value) ? encodePgArrayLiteral(value) : String(value),
122
+ parse: (value) => parsePgArrayLiteral(value, JSON.parse),
123
+ };
124
+ continue;
125
+ }
115
126
  const parseElement = parseSimpleArrayElement(elementOid);
116
127
  if (!parseElement)
117
128
  continue;
@@ -124,10 +135,30 @@ function postgresTypes() {
124
135
  }
125
136
  return types;
126
137
  }
138
+ function typeOids(value) {
139
+ if (value === null || value === undefined)
140
+ return [];
141
+ return Array.isArray(value) ? value : [value];
142
+ }
143
+ function installJsonArrayCodecs(client) {
144
+ const options = client.options;
145
+ options.parsers ??= {};
146
+ options.serializers ??= {};
147
+ const configured = Object.values(options.types ?? {});
148
+ for (const oid of [199, 3807]) {
149
+ const hasParser = configured.some((type) => typeOids(type.from).includes(oid));
150
+ const hasSerializer = configured.some((type) => type.to === oid);
151
+ if (!hasParser)
152
+ options.parsers[oid] = (value) => parsePgArrayLiteral(value, JSON.parse);
153
+ if (!hasSerializer) {
154
+ options.serializers[oid] = (value) => Array.isArray(value) ? encodePgArrayLiteral(value) : String(value);
155
+ }
156
+ }
157
+ }
127
158
  export function createClient(url = process.env.DATABASE_URL, options = {}) {
128
159
  if (!url)
129
160
  throw new Error("sqlx-js: DATABASE_URL is not set");
130
- const { onQuery, statementTimeoutMs, ...pgOptions } = options;
161
+ const { onQuery, statementTimeoutMs, fileRoot, ...pgOptions } = options;
131
162
  const connection = statementTimeoutMs !== undefined
132
163
  ? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
133
164
  : pgOptions.connection;
@@ -136,12 +167,17 @@ export function createClient(url = process.env.DATABASE_URL, options = {}) {
136
167
  ...(connection ? { connection } : {}),
137
168
  types: { ...postgresTypes(), ...(pgOptions.types ?? {}) },
138
169
  });
139
- if (onQuery)
140
- client[HOOKS] = { onQuery };
170
+ client[HOOKS] = {
171
+ onQuery,
172
+ prepare: pgOptions.prepare ?? true,
173
+ fileRoot: resolvedFileRoot(fileRoot),
174
+ };
141
175
  return client;
142
176
  }
143
177
  function createDefaultClient() {
144
- return new PostgresRuntimeClient(createClient());
178
+ const client = createClient();
179
+ const attached = client[HOOKS];
180
+ return new PostgresRuntimeClient(client, attached.onQuery, attached.prepare, attached.fileRoot);
145
181
  }
146
182
  function getRuntimeClient() {
147
183
  defaultClient ??= createDefaultClient();
@@ -151,8 +187,9 @@ export function getClient() {
151
187
  return getRuntimeClient().client;
152
188
  }
153
189
  export function setClient(client, options) {
190
+ installJsonArrayCodecs(client);
154
191
  const attached = client[HOOKS];
155
- defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery);
192
+ defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot));
156
193
  }
157
194
  export async function close() {
158
195
  if (defaultClient) {
@@ -7,19 +7,47 @@ export type OnQueryEvent = {
7
7
  error?: unknown;
8
8
  };
9
9
  export type OnQueryHook = (event: OnQueryEvent) => void;
10
+ export type RuntimeQueryResult = unknown[] & {
11
+ count?: number | null;
12
+ command?: string | null;
13
+ };
10
14
  export type RuntimeClient = {
11
- query: (query: string, params: unknown[]) => Promise<unknown[]>;
15
+ query: (query: string, params: unknown[]) => Promise<RuntimeQueryResult>;
12
16
  transformParam?: (param: unknown) => unknown;
13
17
  transaction: <R>(fn: (client: RuntimeClient) => Promise<R>) => Promise<R>;
14
18
  close: () => Promise<void>;
15
19
  onQuery?: OnQueryHook;
20
+ fileRoot?: string;
16
21
  };
17
22
  type AnyFn = (...args: unknown[]) => Promise<unknown[]>;
18
23
  type AnyOneFn = (...args: unknown[]) => Promise<unknown>;
19
24
  type AnyOptionalFn = (...args: unknown[]) => Promise<unknown | null>;
25
+ type AnyExecuteFn = (...args: unknown[]) => Promise<ExecuteResult>;
20
26
  type IdentifierFn = (...parts: string[]) => string;
27
+ declare const PARAMETER_KIND: unique symbol;
28
+ export type JsonParameter<T = JsonInputValue> = {
29
+ readonly [PARAMETER_KIND]: "json";
30
+ readonly value: T;
31
+ };
32
+ export type PgArrayParameter<T = unknown> = {
33
+ readonly [PARAMETER_KIND]: "array";
34
+ readonly value: readonly (T | null)[];
35
+ };
36
+ export type JsonPrimitive = string | number | boolean | null;
37
+ export type JsonInputValue = JsonPrimitive | JsonInputObject | JsonInputArray;
38
+ export type JsonInputObject = {
39
+ readonly [key: string]: JsonInputValue | undefined;
40
+ };
41
+ export type JsonInputArray = readonly JsonInputValue[];
42
+ export type ExecuteResult = {
43
+ rowCount: number;
44
+ command: string;
45
+ };
46
+ export declare function json<T extends JsonInputValue>(value: T): JsonParameter<T>;
47
+ export declare function array<T>(value: readonly (T | null)[]): PgArrayParameter<T>;
48
+ export declare function parameterKind(value: unknown): "json" | "array" | undefined;
21
49
  declare function renameRows(rows: unknown[]): unknown[];
22
- declare function isPrimitiveArrayElement(v: unknown): boolean;
50
+ export declare function isPrimitiveArrayElement(v: unknown): boolean;
23
51
  export declare function encodePgArrayLiteral(arr: unknown[]): string;
24
52
  type PgArrayValue<T> = T | null | PgArrayValue<T>[];
25
53
  export declare function parsePgArrayLiteral<T = string>(input: string, parseElement?: (value: string) => T): PgArrayValue<T>[];
@@ -40,21 +68,26 @@ export declare const _internal: {
40
68
  loadSqlFile: typeof loadSqlFile;
41
69
  buildSetTransaction: typeof buildSetTransaction;
42
70
  clearIdentifierCache: typeof clearIdentifierCache;
71
+ parameterKind: typeof parameterKind;
43
72
  toPgError: typeof toPgError;
44
73
  };
45
- declare function loadSqlFile(path: string): string;
74
+ declare function loadSqlFile(path: string, fileRoot?: string): string;
46
75
  export declare function clearSqlFileCache(): void;
47
76
  declare function clearIdentifierCache(): void;
48
77
  export declare function id(...parts: string[]): string;
49
78
  type FileCallable = AnyFn & {
50
79
  one: AnyOneFn;
51
80
  optional: AnyOptionalFn;
81
+ execute: AnyExecuteFn;
52
82
  };
53
83
  type SqlCallable = AnyFn & {
54
84
  file: FileCallable;
55
85
  one: AnyOneFn;
56
86
  optional: AnyOptionalFn;
87
+ execute: AnyExecuteFn;
57
88
  id: IdentifierFn;
89
+ json: typeof json;
90
+ array: typeof array;
58
91
  };
59
92
  export type TransactionOptions = {
60
93
  isolation?: "read uncommitted" | "read committed" | "repeatable read" | "serializable";
@@ -1,7 +1,19 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { PgClient, parseDatabaseUrl, PgError } from "./pg/wire.js";
4
4
  import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./commands/migrate.js";
5
+ const PARAMETER_KIND = Symbol("sqlx-js.parameter");
6
+ export function json(value) {
7
+ return { [PARAMETER_KIND]: "json", value };
8
+ }
9
+ export function array(value) {
10
+ return { [PARAMETER_KIND]: "array", value };
11
+ }
12
+ export function parameterKind(value) {
13
+ if (!value || typeof value !== "object")
14
+ return undefined;
15
+ return value[PARAMETER_KIND];
16
+ }
5
17
  const SUFFIX = /[!?]$/;
6
18
  function renameRows(rows) {
7
19
  if (rows.length === 0)
@@ -28,9 +40,11 @@ function renameRows(rows) {
28
40
  }
29
41
  return out;
30
42
  }
31
- function isPrimitiveArrayElement(v) {
43
+ export function isPrimitiveArrayElement(v) {
32
44
  if (v === null || v === undefined)
33
45
  return true;
46
+ if (v instanceof Date || v instanceof Uint8Array)
47
+ return true;
34
48
  const t = typeof v;
35
49
  return t === "string" || t === "number" || t === "bigint" || t === "boolean";
36
50
  }
@@ -44,6 +58,18 @@ export function encodePgArrayLiteral(arr) {
44
58
  parts.push("NULL");
45
59
  continue;
46
60
  }
61
+ if (parameterKind(v) === "json") {
62
+ parts.push(quoteArrayElement(JSON.stringify(v.value)));
63
+ continue;
64
+ }
65
+ if (v instanceof Date) {
66
+ parts.push(quoteArrayElement(v.toISOString()));
67
+ continue;
68
+ }
69
+ if (v instanceof Uint8Array) {
70
+ parts.push(quoteArrayElement(`\\x${Buffer.from(v).toString("hex")}`));
71
+ continue;
72
+ }
47
73
  if (typeof v === "bigint") {
48
74
  parts.push(v.toString());
49
75
  continue;
@@ -126,13 +152,12 @@ export function parsePgArrayLiteral(input, parseElement = (value) => value) {
126
152
  return parsed;
127
153
  }
128
154
  export function encodeParam(p) {
129
- if (!Array.isArray(p))
130
- return p;
131
- if (p.length === 0)
132
- return p;
133
- if (!p.every(isPrimitiveArrayElement))
134
- return p;
135
- return encodePgArrayLiteral(p);
155
+ const kind = parameterKind(p);
156
+ if (kind === "json")
157
+ return JSON.stringify(p.value);
158
+ if (kind === "array")
159
+ return encodePgArrayLiteral([...p.value]);
160
+ return p;
136
161
  }
137
162
  export class NoRowsError extends Error {
138
163
  constructor(message = "expected exactly 1 row, got 0") {
@@ -213,16 +238,17 @@ export const _internal = {
213
238
  loadSqlFile,
214
239
  buildSetTransaction,
215
240
  clearIdentifierCache,
241
+ parameterKind,
216
242
  toPgError,
217
243
  };
218
- async function runQuery(client, query, params) {
244
+ async function runRawQuery(client, query, params) {
219
245
  const encoded = params.length === 0
220
246
  ? params
221
247
  : params.map((p) => client.transformParam ? client.transformParam(p) : encodeParam(p));
222
248
  const onQuery = client.onQuery;
223
249
  if (!onQuery) {
224
250
  try {
225
- return renameRows(await client.query(query, encoded));
251
+ return await client.query(query, encoded);
226
252
  }
227
253
  catch (e) {
228
254
  throw toPgError(e) ?? e;
@@ -230,9 +256,14 @@ async function runQuery(client, query, params) {
230
256
  }
231
257
  const start = performance.now();
232
258
  try {
233
- const rows = await client.query(query, encoded);
234
- onQuery({ query, params, durationMs: performance.now() - start, rowCount: rows.length });
235
- return renameRows(rows);
259
+ const result = await client.query(query, encoded);
260
+ onQuery({
261
+ query,
262
+ params,
263
+ durationMs: performance.now() - start,
264
+ rowCount: result.count ?? result.length,
265
+ });
266
+ return result;
236
267
  }
237
268
  catch (e) {
238
269
  const error = toPgError(e) ?? e;
@@ -240,6 +271,16 @@ async function runQuery(client, query, params) {
240
271
  throw error;
241
272
  }
242
273
  }
274
+ async function runQuery(client, query, params) {
275
+ return renameRows(await runRawQuery(client, query, params));
276
+ }
277
+ async function runExecute(client, query, params) {
278
+ const result = await runRawQuery(client, query, params);
279
+ return {
280
+ rowCount: result.count ?? result.length,
281
+ command: result.command ?? "",
282
+ };
283
+ }
243
284
  async function runOne(client, query, params) {
244
285
  const rows = await runQuery(client, query, params);
245
286
  if (rows.length === 1)
@@ -257,8 +298,16 @@ async function runOptional(client, query, params) {
257
298
  throw new TooManyRowsError(rows.length, "0 or 1");
258
299
  }
259
300
  const sqlFileCache = new Map();
260
- function loadSqlFile(path) {
261
- const full = resolve(process.cwd(), path);
301
+ function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd()) {
302
+ const root = resolve(fileRoot);
303
+ if (isAbsolute(path)) {
304
+ throw new Error(`sqlx-js.sql.file: path must be relative to fileRoot: ${path}`);
305
+ }
306
+ const full = resolve(root, path);
307
+ const rel = relative(root, full);
308
+ if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
309
+ throw new Error(`sqlx-js.sql.file: path escapes fileRoot: ${path}`);
310
+ }
262
311
  try {
263
312
  const st = statSync(full);
264
313
  const cached = sqlFileCache.get(full);
@@ -393,13 +442,16 @@ function makeBoundCallable(client) {
393
442
  return runQuery(client, query, params);
394
443
  });
395
444
  const file = (async (path, ...params) => {
396
- return runQuery(client, loadSqlFile(path), params);
445
+ return runQuery(client, loadSqlFile(path, client.fileRoot), params);
397
446
  });
398
447
  file.one = (async (path, ...params) => {
399
- return runOne(client, loadSqlFile(path), params);
448
+ return runOne(client, loadSqlFile(path, client.fileRoot), params);
400
449
  });
401
450
  file.optional = (async (path, ...params) => {
402
- return runOptional(client, loadSqlFile(path), params);
451
+ return runOptional(client, loadSqlFile(path, client.fileRoot), params);
452
+ });
453
+ file.execute = (async (path, ...params) => {
454
+ return runExecute(client, loadSqlFile(path, client.fileRoot), params);
403
455
  });
404
456
  fn.file = file;
405
457
  fn.one = (async (query, ...params) => {
@@ -408,7 +460,12 @@ function makeBoundCallable(client) {
408
460
  fn.optional = (async (query, ...params) => {
409
461
  return runOptional(client, query, params);
410
462
  });
463
+ fn.execute = (async (query, ...params) => {
464
+ return runExecute(client, query, params);
465
+ });
411
466
  fn.id = id;
467
+ fn.json = json;
468
+ fn.array = array;
412
469
  return fn;
413
470
  }
414
471
  function buildSetTransaction(opts) {
@@ -428,13 +485,20 @@ export function createSqlRuntime(getClient) {
428
485
  return runQuery(getClient(), query, params);
429
486
  });
430
487
  const rootFile = (async (path, ...params) => {
431
- return runQuery(getClient(), loadSqlFile(path), params);
488
+ const client = getClient();
489
+ return runQuery(client, loadSqlFile(path, client.fileRoot), params);
432
490
  });
433
491
  rootFile.one = (async (path, ...params) => {
434
- return runOne(getClient(), loadSqlFile(path), params);
492
+ const client = getClient();
493
+ return runOne(client, loadSqlFile(path, client.fileRoot), params);
435
494
  });
436
495
  rootFile.optional = (async (path, ...params) => {
437
- return runOptional(getClient(), loadSqlFile(path), params);
496
+ const client = getClient();
497
+ return runOptional(client, loadSqlFile(path, client.fileRoot), params);
498
+ });
499
+ rootFile.execute = (async (path, ...params) => {
500
+ const client = getClient();
501
+ return runExecute(client, loadSqlFile(path, client.fileRoot), params);
438
502
  });
439
503
  root.file = rootFile;
440
504
  root.one = (async (query, ...params) => {
@@ -443,7 +507,12 @@ export function createSqlRuntime(getClient) {
443
507
  root.optional = (async (query, ...params) => {
444
508
  return runOptional(getClient(), query, params);
445
509
  });
510
+ root.execute = (async (query, ...params) => {
511
+ return runExecute(getClient(), query, params);
512
+ });
446
513
  root.id = id;
514
+ root.json = json;
515
+ root.array = array;
447
516
  root.transaction = (async (fnOrOpts, maybeFn) => {
448
517
  const c = getClient();
449
518
  let opts = {};