@kequtech/tenon-orm 0.1.3 → 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.
Files changed (81) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +16 -20
  3. package/dist/affected-rows.d.ts +1 -1
  4. package/dist/affected-rows.js +6 -6
  5. package/dist/baseline.d.ts +1 -1
  6. package/dist/baseline.js +5 -5
  7. package/dist/cli.js +18 -39
  8. package/dist/client.js +1 -1
  9. package/dist/connection.d.ts +18 -13
  10. package/dist/connection.js +14 -43
  11. package/dist/database.js +4 -2
  12. package/dist/describe.d.ts +3 -0
  13. package/dist/describe.js +21 -0
  14. package/dist/driver/array.d.ts +5 -0
  15. package/dist/driver/array.js +70 -0
  16. package/dist/driver/lifecycle.d.ts +15 -0
  17. package/dist/driver/lifecycle.js +57 -0
  18. package/dist/driver/session.d.ts +8 -0
  19. package/dist/driver/session.js +92 -0
  20. package/dist/driver/types.d.ts +10 -0
  21. package/dist/driver/types.js +49 -0
  22. package/dist/generate.js +7 -7
  23. package/dist/generation/catalog.d.ts +2 -2
  24. package/dist/generation/catalog.js +4 -4
  25. package/dist/generation/output.d.ts +2 -0
  26. package/dist/generation/output.js +35 -0
  27. package/dist/generation/schema-summary.d.ts +3 -3
  28. package/dist/generation/schema-summary.js +11 -11
  29. package/dist/generation/table-types.js +2 -52
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.js +1 -0
  32. package/dist/inspection/catalog.d.ts +25 -0
  33. package/dist/inspection/catalog.js +29 -0
  34. package/dist/inspection/cli.d.ts +22 -0
  35. package/dist/inspection/cli.js +34 -0
  36. package/dist/inspection/connection.d.ts +4 -0
  37. package/dist/inspection/connection.js +44 -0
  38. package/dist/inspection/options.d.ts +15 -0
  39. package/dist/inspection/options.js +49 -0
  40. package/dist/inspection/result.d.ts +4 -0
  41. package/dist/inspection/result.js +90 -0
  42. package/dist/inspection/types.d.ts +74 -0
  43. package/dist/inspection/types.js +1 -0
  44. package/dist/migrations/baseline-sql.d.ts +1 -1
  45. package/dist/migrations/baseline-sql.js +1 -1
  46. package/dist/migrations/connection.d.ts +2 -3
  47. package/dist/migrations/connection.js +15 -13
  48. package/dist/migrations/history.d.ts +4 -4
  49. package/dist/migrations/history.js +5 -5
  50. package/dist/migrations.js +7 -7
  51. package/dist/mutations.d.ts +0 -5
  52. package/dist/mutations.js +33 -41
  53. package/dist/query-options.d.ts +2 -2
  54. package/dist/query-options.js +4 -4
  55. package/dist/query.d.ts +3 -4
  56. package/dist/query.js +2 -2
  57. package/dist/sql.d.ts +1 -1
  58. package/dist/sql.js +1 -1
  59. package/dist/transaction-sql.js +1 -1
  60. package/dist/transaction.d.ts +2 -2
  61. package/dist/transaction.js +14 -13
  62. package/dist/types.d.ts +4 -4
  63. package/docs/adoption-guide.md +39 -76
  64. package/docs/agent-guide.md +82 -111
  65. package/docs/human-guide.md +42 -56
  66. package/docs/mutations.md +13 -13
  67. package/docs/query-inspection.md +128 -0
  68. package/docs/reference.md +81 -294
  69. package/package.json +10 -31
  70. package/dist/safeql/composition.d.ts +0 -9
  71. package/dist/safeql/composition.js +0 -168
  72. package/dist/safeql/eslint.d.ts +0 -2
  73. package/dist/safeql/eslint.js +0 -50
  74. package/dist/safeql/mutations.d.ts +0 -3
  75. package/dist/safeql/mutations.js +0 -74
  76. package/dist/safeql/plugin.d.ts +0 -3
  77. package/dist/safeql/plugin.js +0 -43
  78. package/dist/safeql/static.d.ts +0 -10
  79. package/dist/safeql/static.js +0 -59
  80. package/dist/safeql.d.ts +0 -6
  81. package/dist/safeql.js +0 -7
@@ -0,0 +1,49 @@
1
+ import { plainObject } from '../validation.js';
2
+ /** Validate and capture caller-owned settings before opening a connection. */
3
+ export function inspectionOptions(options) {
4
+ if (typeof options.connection !== 'string' || !options.connection.trim()) {
5
+ throw new Error('Describe requires an explicit nonempty connection string.');
6
+ }
7
+ plainObject(options.session ?? {}, 'Inspection session');
8
+ const session = { ...options.session };
9
+ for (const [key, value] of Object.entries(session)) {
10
+ if (!['role', 'searchPath', 'timeZone'].includes(key)) {
11
+ throw new Error(`Unknown inspection session option: ${key}`);
12
+ }
13
+ if (typeof value !== 'string' || !value.trim() || value.includes('\0')) {
14
+ throw new Error(`Inspection session ${key} requires a nonempty string without NUL.`);
15
+ }
16
+ }
17
+ plainObject(options.timeouts ?? {}, 'Inspection timeouts');
18
+ const timeouts = { ...options.timeouts };
19
+ const defaults = { connectMs: 3000, statementMs: 5000, lockMs: 1000 };
20
+ for (const [key, value] of Object.entries(timeouts)) {
21
+ if (!Object.hasOwn(defaults, key))
22
+ throw new Error(`Unknown inspection timeout: ${key}`);
23
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 2147483647) {
24
+ throw new Error(`Inspection timeout ${key} must be a positive integer in milliseconds.`);
25
+ }
26
+ }
27
+ const limits = { ...defaults, ...timeouts };
28
+ // URL startup options must not disable the inspection's server-side bounds.
29
+ let url;
30
+ try {
31
+ url = new URL(options.connection);
32
+ }
33
+ catch {
34
+ throw new Error('Describe requires a PostgreSQL connection URI.');
35
+ }
36
+ if (!['postgres:', 'postgresql:'].includes(url.protocol)) {
37
+ throw new Error('Describe requires a PostgreSQL connection URI.');
38
+ }
39
+ for (const [name, value] of [
40
+ ['statement_timeout', limits.statementMs],
41
+ ['lock_timeout', limits.lockMs],
42
+ ]) {
43
+ url.searchParams.set(name, String(value));
44
+ }
45
+ if (!url.searchParams.has('application_name')) {
46
+ url.searchParams.set('application_name', 'tenon-describe');
47
+ }
48
+ return { url, limits, session };
49
+ }
@@ -0,0 +1,4 @@
1
+ import type { StatementMetadata } from './catalog.js';
2
+ import type { InspectionContext, QueryDescription } from './types.js';
3
+ /** Convert database metadata to conservative Postgres.js types and diagnostics. */
4
+ export declare function queryDescription({ statement, types, sources }: StatementMetadata, context: InspectionContext): QueryDescription;
@@ -0,0 +1,90 @@
1
+ import { arrayTypes, scalarTypes, valueTypes } from '../driver/types.js';
2
+ /** Convert database metadata to conservative Postgres.js types and diagnostics. */
3
+ export function queryDescription({ statement, types, sources }, context) {
4
+ const catalog = new Map(types.map((t) => [t.oid, t]));
5
+ const issues = new Map();
6
+ function describeType(oid) {
7
+ const type = catalog.get(oid);
8
+ if (!type)
9
+ throw new Error(`PostgreSQL type OID ${oid} was not found.`);
10
+ let readType;
11
+ if (type.base) {
12
+ readType = describeType(type.base).readType;
13
+ }
14
+ else if (type.kind === 'e') {
15
+ readType = type.choices?.map((v) => JSON.stringify(v)).join(' | ') || 'never';
16
+ }
17
+ else if (Object.hasOwn(arrayTypes, String(oid))) {
18
+ readType = `PgArray<${valueTypes(arrayTypes[String(oid)]).read}>`;
19
+ }
20
+ else if (type.schema === 'pg_catalog' && Object.hasOwn(scalarTypes, type.name)) {
21
+ readType = valueTypes(scalarTypes[type.name]).read;
22
+ }
23
+ else {
24
+ readType = 'unknown';
25
+ issues.set(`type:${oid}`, {
26
+ code: 'unmapped-type',
27
+ oid,
28
+ postgres: `${type.schema}.${type.name}`,
29
+ message: `No verified Postgres.js mapping for ${type.schema}.${type.name} (OID ${oid}).`,
30
+ });
31
+ }
32
+ return { oid, postgres: `${type.schema}.${type.name}`, readType };
33
+ }
34
+ const sourceMap = new Map(sources.map((s) => [`${s.oid}:${s.number}`, s]));
35
+ const names = new Set();
36
+ const columns = statement.columns.map((column) => {
37
+ if (names.has(column.name)) {
38
+ issues.set(`column:${column.name}`, {
39
+ code: 'duplicate-column',
40
+ column: column.name,
41
+ message: `Duplicate result column ${JSON.stringify(column.name)}; object rows overwrite earlier values.`,
42
+ });
43
+ }
44
+ names.add(column.name);
45
+ const source = sourceMap.get(`${column.table}:${column.number}`);
46
+ return {
47
+ name: column.name,
48
+ ...describeType(column.type),
49
+ nullability: 'unknown',
50
+ source: source
51
+ ? {
52
+ schema: source.schema,
53
+ table: source.table,
54
+ column: source.column,
55
+ notNull: source.notNull,
56
+ }
57
+ : null,
58
+ };
59
+ });
60
+ const parameters = statement.types.map(describeType);
61
+ if (columns.length) {
62
+ issues.set('nullability', {
63
+ code: 'nullability-unknown',
64
+ message: 'Result nullability is not established by Describe.',
65
+ });
66
+ }
67
+ const unknown = columns
68
+ .filter((c) => c.readType === 'unknown' || c.readType === 'PgArray<unknown>')
69
+ .map((c) => c.name);
70
+ if (unknown.length) {
71
+ issues.set('shape', {
72
+ code: 'shape-unknown',
73
+ columns: unknown,
74
+ message: 'An unknown read type does not verify JSON structure or an application-specific result type.',
75
+ });
76
+ }
77
+ const members = columns.map((c) => ` ${JSON.stringify(c.name)}: ${c.readType}${c.readType === 'unknown' ? '' : ' | null'};`);
78
+ const array = columns.some((c) => c.readType.startsWith('PgArray<'))
79
+ ? 'type PgArray<T> = (T | null | PgArray<T>)[];\n'
80
+ : '';
81
+ return {
82
+ context,
83
+ parameters,
84
+ columns,
85
+ rowType: columns.length > 0 && names.size === columns.length
86
+ ? `${array}type DescribedRow = {\n${members.join('\n')}\n};`
87
+ : null,
88
+ issues: [...issues.values()],
89
+ };
90
+ }
@@ -0,0 +1,74 @@
1
+ import type { SqlQuery } from '../query.js';
2
+ export interface InspectionSession {
3
+ readonly role?: string;
4
+ /** PostgreSQL's search_path setting, as used in the application's connection options. */
5
+ readonly searchPath?: string;
6
+ readonly timeZone?: string;
7
+ }
8
+ export interface InspectionTimeouts {
9
+ readonly connectMs?: number;
10
+ readonly statementMs?: number;
11
+ readonly lockMs?: number;
12
+ }
13
+ export interface InspectionContext {
14
+ readonly mode: 'separate-session';
15
+ readonly database: string;
16
+ readonly sessionUser: string;
17
+ readonly currentUser: string;
18
+ readonly searchPath: string;
19
+ readonly resolvedSearchPath: readonly string[];
20
+ readonly timeZone: string;
21
+ readonly statementTimeoutMs: number;
22
+ readonly lockTimeoutMs: number;
23
+ readonly readMappings: 'postgres';
24
+ }
25
+ export interface DescribeOptions {
26
+ /** PostgreSQL connection URI. Inspection owns a separate, short-lived connection. */
27
+ readonly connection: string;
28
+ /** Finished SQL; query values are neither bound nor executed. */
29
+ readonly query: string | Pick<SqlQuery, 'text'>;
30
+ readonly session?: InspectionSession;
31
+ readonly timeouts?: InspectionTimeouts;
32
+ }
33
+ export type DescriptionIssue = {
34
+ readonly code: 'nullability-unknown';
35
+ readonly message: string;
36
+ } | {
37
+ readonly code: 'shape-unknown';
38
+ readonly message: string;
39
+ readonly columns: readonly string[];
40
+ } | {
41
+ readonly code: 'unmapped-type';
42
+ readonly message: string;
43
+ readonly postgres: string;
44
+ readonly oid: number;
45
+ } | {
46
+ readonly code: 'duplicate-column';
47
+ readonly message: string;
48
+ readonly column: string;
49
+ };
50
+ export interface DescribedType {
51
+ readonly oid: number;
52
+ readonly postgres: string;
53
+ /** Postgres.js read representation, before considering result nullability. */
54
+ readonly readType: string;
55
+ }
56
+ export interface DescribedColumn extends DescribedType {
57
+ readonly name: string;
58
+ readonly nullability: 'unknown';
59
+ /** Source constraint only: outer joins and grouping can still introduce NULL. */
60
+ readonly source: {
61
+ readonly schema: string;
62
+ readonly table: string;
63
+ readonly column: string;
64
+ readonly notNull: boolean;
65
+ } | null;
66
+ }
67
+ export interface QueryDescription {
68
+ readonly context: InspectionContext;
69
+ readonly parameters: readonly DescribedType[];
70
+ readonly columns: readonly DescribedColumn[];
71
+ /** Conservative result declaration. This does not verify an existing annotation. */
72
+ readonly rowType: string | null;
73
+ readonly issues: readonly DescriptionIssue[];
74
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,3 @@
1
1
  export declare const baselineHeader = "-- Tenon baseline captured by pg_dump.\n";
2
- /** Bridge pg_dump's plain-text script to pg's transactional SQL execution. */
2
+ /** Bridge pg_dump's plain-text script to transactional SQL execution. */
3
3
  export declare function prepareBaselineSql(dump: string, namespace: string): string;
@@ -1,6 +1,6 @@
1
1
  import { quote } from '../sql.js';
2
2
  export const baselineHeader = '-- Tenon baseline captured by pg_dump.\n';
3
- /** Bridge pg_dump's plain-text script to pg's transactional SQL execution. */
3
+ /** Bridge pg_dump's plain-text script to transactional SQL execution. */
4
4
  export function prepareBaselineSql(dump, namespace) {
5
5
  // Recent pg_dump versions bracket psql scripts with a random restrict key.
6
6
  // Remove only the outer pair; SQL bodies containing similar text stay intact.
@@ -1,9 +1,8 @@
1
- import type { PoolClient } from 'pg';
2
- import type { Connection } from '../connection.js';
1
+ import type { Connection, SqlConnection } from '../connection.js';
3
2
  export interface DatabaseToolOptions {
4
3
  readonly connection: Connection;
5
4
  readonly namespace?: string;
6
5
  readonly directory?: string;
7
6
  }
8
7
  /** Coordinate tools independently of application sessions; PostgreSQL owns the wait. */
9
- export declare function withMigrationLock<T>(options: DatabaseToolOptions, mode: 'shared' | 'exclusive', work: (client: PoolClient, namespace: string) => Promise<T>): Promise<T>;
8
+ export declare function withMigrationLock<T>(options: DatabaseToolOptions, mode: 'shared' | 'exclusive', work: (client: SqlConnection, namespace: string) => Promise<T>): Promise<T>;
@@ -1,21 +1,21 @@
1
- import { createPool, watchClient } from '../connection.js';
1
+ import { reserveConnection } from '../driver/session.js';
2
+ import { createConnection } from '../connection.js';
2
3
  import { validateName } from '../sql.js';
3
4
  /** Coordinate tools independently of application sessions; PostgreSQL owns the wait. */
4
5
  export async function withMigrationLock(options, mode, work) {
5
6
  const namespace = options.namespace ?? 'public';
6
7
  validateName(namespace);
7
- const { pool, owned } = createPool(options.connection);
8
+ const { sql: pool, owned } = createConnection(options.connection);
8
9
  const suffix = mode === 'shared' ? '_shared' : '';
9
10
  const lockKey = `tenon:migrations:${namespace}`;
10
11
  const errors = [];
11
- let client;
12
12
  let connection;
13
13
  let locked = false;
14
14
  let result;
15
15
  try {
16
- client = await pool.connect();
17
- connection = watchClient(client);
18
- await client.query(`SELECT pg_advisory_lock${suffix}(hashtextextended($1, 0))`, [lockKey]);
16
+ connection = await reserveConnection(pool);
17
+ const client = connection.sql;
18
+ await client.unsafe(`SELECT pg_advisory_lock${suffix}(hashtextextended($1, 0))`, [lockKey]);
19
19
  locked = true;
20
20
  result = await work(client, namespace);
21
21
  connection.check();
@@ -24,23 +24,25 @@ export async function withMigrationLock(options, mode, work) {
24
24
  errors.push(error);
25
25
  }
26
26
  finally {
27
- if (client) {
28
- let releaseError;
27
+ if (connection) {
28
+ const client = connection.sql;
29
+ let discard = false;
29
30
  try {
30
- await client.query('ROLLBACK');
31
+ connection.check();
32
+ if (errors.length)
33
+ await client.unsafe('ROLLBACK');
31
34
  if (locked) {
32
- await client.query(`SELECT pg_advisory_unlock${suffix}(hashtextextended($1, 0))`, [
35
+ await client.unsafe(`SELECT pg_advisory_unlock${suffix}(hashtextextended($1, 0))`, [
33
36
  lockKey,
34
37
  ]);
35
38
  }
36
39
  }
37
40
  catch (error) {
38
41
  errors.push(error);
39
- releaseError =
40
- error instanceof Error ? error : new Error('Tool connection cleanup failed.');
42
+ discard = true;
41
43
  }
42
44
  try {
43
- connection.release(releaseError);
45
+ await connection.release(discard);
44
46
  }
45
47
  catch (error) {
46
48
  errors.push(error);
@@ -1,4 +1,4 @@
1
- import type { PoolClient } from 'pg';
1
+ import type { SqlConnection } from '../connection.js';
2
2
  import type { SqlMigration } from './files.js';
3
3
  export declare const migrationTable = "__tenon_migrations";
4
4
  interface AppliedMigration {
@@ -6,7 +6,7 @@ interface AppliedMigration {
6
6
  name: string;
7
7
  checksum: string;
8
8
  }
9
- export declare function migrationState(client: PoolClient, namespace: string, migrations: readonly SqlMigration[]): Promise<readonly AppliedMigration[]>;
10
- export declare function createHistoryTable(client: PoolClient, namespace: string): Promise<void>;
11
- export declare function recordMigration(client: PoolClient, namespace: string, migration: SqlMigration): Promise<void>;
9
+ export declare function migrationState(client: SqlConnection, namespace: string, migrations: readonly SqlMigration[]): Promise<readonly AppliedMigration[]>;
10
+ export declare function createHistoryTable(client: SqlConnection, namespace: string): Promise<void>;
11
+ export declare function recordMigration(client: SqlConnection, namespace: string, migration: SqlMigration): Promise<void>;
12
12
  export {};
@@ -1,11 +1,11 @@
1
1
  import { qualified } from '../sql.js';
2
2
  export const migrationTable = '__tenon_migrations';
3
3
  export async function migrationState(client, namespace, migrations) {
4
- const exists = await client.query('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
5
- if (exists.rows[0].name === null) {
4
+ const exists = await client.unsafe('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
5
+ if (exists[0].name === null) {
6
6
  return [];
7
7
  }
8
- const { rows: applied } = await client.query(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`);
8
+ const applied = await client.unsafe(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`);
9
9
  for (const [i, record] of applied.entries()) {
10
10
  const file = migrations[i];
11
11
  if (!file ||
@@ -18,7 +18,7 @@ export async function migrationState(client, namespace, migrations) {
18
18
  return applied;
19
19
  }
20
20
  export async function createHistoryTable(client, namespace) {
21
- await client.query(`CREATE TABLE IF NOT EXISTS ${qualified(namespace, migrationTable)} (
21
+ await client.unsafe(`CREATE TABLE IF NOT EXISTS ${qualified(namespace, migrationTable)} (
22
22
  id TEXT PRIMARY KEY CHECK (id ~ '^[0-9]+$'),
23
23
  name TEXT NOT NULL,
24
24
  checksum TEXT NOT NULL,
@@ -26,5 +26,5 @@ export async function createHistoryTable(client, namespace) {
26
26
  )`);
27
27
  }
28
28
  export async function recordMigration(client, namespace, migration) {
29
- await client.query(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
29
+ await client.unsafe(`INSERT INTO ${qualified(namespace, migrationTable)} (id, name, checksum) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.checksum]);
30
30
  }
@@ -28,25 +28,25 @@ async function execute(options, direction, steps) {
28
28
  if (!selected.length) {
29
29
  return [];
30
30
  }
31
- await client.query(`CREATE SCHEMA IF NOT EXISTS ${quote(namespace)}`);
31
+ await client.unsafe(`CREATE SCHEMA IF NOT EXISTS ${quote(namespace)}`);
32
32
  await createHistoryTable(client, namespace);
33
33
  const completed = [];
34
34
  for (const file of selected) {
35
35
  try {
36
- await client.query('BEGIN');
36
+ await client.unsafe('BEGIN');
37
37
  // Match the string-literal rules used by the authored migration scanner.
38
- await client.query('SET LOCAL standard_conforming_strings TO on');
39
- await client.query(`SET LOCAL search_path TO ${quote(namespace)}, pg_catalog`);
40
- await client.query(direction === 'up' ? file.up : file.down);
38
+ await client.unsafe('SET LOCAL standard_conforming_strings TO on');
39
+ await client.unsafe(`SET LOCAL search_path TO ${quote(namespace)}, pg_catalog`);
40
+ await client.unsafe(direction === 'up' ? file.up : file.down);
41
41
  if (direction === 'up') {
42
42
  await recordMigration(client, namespace, file);
43
43
  }
44
44
  else {
45
- await client.query(`DELETE FROM ${qualified(namespace, migrationTable)} WHERE id = $1`, [
45
+ await client.unsafe(`DELETE FROM ${qualified(namespace, migrationTable)} WHERE id = $1`, [
46
46
  file.id,
47
47
  ]);
48
48
  }
49
- const result = await client.query('COMMIT');
49
+ const result = await client.unsafe('COMMIT');
50
50
  if (result.command !== 'COMMIT') {
51
51
  throw new Error('The migration transaction was rolled back.');
52
52
  }
@@ -2,10 +2,5 @@ export type MutationPart = string | Readonly<{
2
2
  value: unknown;
3
3
  }>;
4
4
  export type MutationColumns<Input> = Input extends object ? Extract<keyof Input, string> : never;
5
- /** Validate and quote one explicit column list, shared by runtime and SQL checking. */
6
- export declare function mutationColumns(columns: readonly string[]): string[];
7
- /** SQL structure depends only on these columns and cells, never on JavaScript source. */
8
- export declare function insertLayout(columns: readonly string[], rows: readonly (readonly unknown[])[]): MutationPart[];
9
- export declare function assignmentLayout(columns: readonly string[], values: readonly unknown[]): MutationPart[];
10
5
  export declare function insertParts(input: object | readonly object[], selected?: readonly string[]): MutationPart[];
11
6
  export declare function assignmentParts(input: object, selected?: readonly string[]): MutationPart[];
package/dist/mutations.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { quote } from './sql.js';
2
2
  import { plainObject } from './validation.js';
3
- /** Validate and quote one explicit column list, shared by runtime and SQL checking. */
4
- export function mutationColumns(columns) {
3
+ /** Validate and quote one explicit column list for mutation fragments. */
4
+ function mutationColumns(columns) {
5
5
  if (!Array.isArray(columns) || !columns.length)
6
6
  throw new Error('Mutation columns must be a nonempty array.');
7
7
  const seen = new Set();
@@ -15,43 +15,6 @@ export function mutationColumns(columns) {
15
15
  return column;
16
16
  });
17
17
  }
18
- /** SQL structure depends only on these columns and cells, never on JavaScript source. */
19
- export function insertLayout(columns, rows) {
20
- if (!rows.length)
21
- throw new Error('sql.insertValues requires at least one row.');
22
- if (!columns.length) {
23
- if (rows.length !== 1)
24
- throw new Error('Default-only inserts require exactly one row.');
25
- return ['DEFAULT VALUES'];
26
- }
27
- const parts = [`(${columns.map(quote).join(', ')}) VALUES `];
28
- for (const [index, row] of rows.entries()) {
29
- if (index)
30
- parts.push(', ');
31
- parts.push('(');
32
- for (const [column, value] of row.entries()) {
33
- if (column)
34
- parts.push(', ');
35
- parts.push(value === undefined ? 'DEFAULT' : Object.freeze({ value }));
36
- }
37
- parts.push(')');
38
- }
39
- return parts;
40
- }
41
- export function assignmentLayout(columns, values) {
42
- const parts = [];
43
- for (const [index, column] of columns.entries()) {
44
- const value = values[index];
45
- if (value === undefined)
46
- continue;
47
- if (parts.length)
48
- parts.push(', ');
49
- parts.push(`${quote(column)} = `, Object.freeze({ value }));
50
- }
51
- if (!parts.length)
52
- throw new Error('sql.assignments requires at least one defined field.');
53
- return parts;
54
- }
55
18
  function record(value, helper, index) {
56
19
  return plainObject(value, `${helper} row ${index + 1}`);
57
20
  }
@@ -78,10 +41,39 @@ export function insertParts(input, selected) {
78
41
  ...new Set(rows.flatMap((row, index) => Object.keys(row).filter((column) => cell(row, column, 'sql.insertValues', index) !== undefined))),
79
42
  ]
80
43
  : mutationColumns(selected);
81
- return insertLayout(columns, rows.map((row, index) => columns.map((column) => cell(row, column, 'sql.insertValues', index))));
44
+ if (!columns.length) {
45
+ if (rows.length !== 1)
46
+ throw new Error('Default-only inserts require exactly one row.');
47
+ return ['DEFAULT VALUES'];
48
+ }
49
+ const parts = [`(${columns.map(quote).join(', ')}) VALUES `];
50
+ for (const [index, row] of rows.entries()) {
51
+ if (index)
52
+ parts.push(', ');
53
+ parts.push('(');
54
+ for (const [position, column] of columns.entries()) {
55
+ if (position)
56
+ parts.push(', ');
57
+ const value = cell(row, column, 'sql.insertValues', index);
58
+ parts.push(value === undefined ? 'DEFAULT' : Object.freeze({ value }));
59
+ }
60
+ parts.push(')');
61
+ }
62
+ return parts;
82
63
  }
83
64
  export function assignmentParts(input, selected) {
84
65
  const row = record(input, 'sql.assignments', 0);
85
66
  const columns = selected === undefined ? Object.keys(row) : mutationColumns(selected);
86
- return assignmentLayout(columns, columns.map((column) => cell(row, column, 'sql.assignments', 0)));
67
+ const parts = [];
68
+ for (const column of columns) {
69
+ const value = cell(row, column, 'sql.assignments', 0);
70
+ if (value === undefined)
71
+ continue;
72
+ if (parts.length)
73
+ parts.push(', ');
74
+ parts.push(`${quote(column)} = `, Object.freeze({ value }));
75
+ }
76
+ if (!parts.length)
77
+ throw new Error('sql.assignments requires at least one defined field.');
78
+ return parts;
87
79
  }
@@ -1,4 +1,4 @@
1
- import type { QueryResult, QueryResultRow } from 'pg';
1
+ import type { QueryResult } from './connection.js';
2
2
  import type { AffectedRowsConstraint } from './affected-rows.js';
3
3
  import type { UnknownRow } from './query.js';
4
4
  export type QueryReturnMode = 'native' | 'many' | 'oneOrNone' | 'one';
@@ -8,7 +8,7 @@ export interface QueryOptions {
8
8
  /** Optional diagnostic label. No SQL or input values are sent to observers. */
9
9
  readonly label?: string;
10
10
  }
11
- export type QueryOutput<Row extends QueryResultRow, Mode extends QueryReturnMode> = Mode extends 'native' ? QueryResult<Row> : Mode extends 'many' ? Row[] : Mode extends 'oneOrNone' ? Row | undefined : Row;
11
+ export type QueryOutput<Row extends object, Mode extends QueryReturnMode> = Mode extends 'native' ? QueryResult<Row> : Mode extends 'many' ? Row[] : Mode extends 'oneOrNone' ? Row | undefined : Row;
12
12
  export declare function validateOptions(options?: QueryOptions): Readonly<QueryOptions & {
13
13
  returns: QueryReturnMode;
14
14
  }>;
@@ -26,10 +26,10 @@ export function queryOutput(options, result) {
26
26
  if (options.returns === 'native')
27
27
  return result;
28
28
  if (options.returns === 'many')
29
- return result.rows;
29
+ return [...result];
30
30
  const expected = options.returns === 'oneOrNone' ? 'at most one row' : 'exactly one row';
31
- if (result.rows.length > 1 || (options.returns === 'one' && result.rows.length === 0)) {
32
- throw new Error(`${options.label ?? 'Query'} expected ${expected}, but returned ${result.rows.length} rows.`);
31
+ if (result.length > 1 || (options.returns === 'one' && result.length === 0)) {
32
+ throw new Error(`${options.label ?? 'Query'} expected ${expected}, but returned ${result.length} rows.`);
33
33
  }
34
- return result.rows[0];
34
+ return result[0];
35
35
  }
package/dist/query.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import type { QueryResultRow } from 'pg';
2
1
  import { type MutationColumns } from './mutations.js';
3
2
  export type UnknownRow = Record<string, unknown>;
4
3
  /** A parameterized statement. The row type describes one returned record. */
5
- export interface SqlQuery<Row extends QueryResultRow = UnknownRow> {
4
+ export interface SqlQuery<Row extends object = UnknownRow> {
6
5
  readonly text: string;
7
6
  readonly values: readonly unknown[];
8
7
  /** Type-only field; a stable key preserves row inference across package copies. */
@@ -24,7 +23,7 @@ declare function join(fragments: readonly SqlFragment[], separator: 'AND' | 'OR'
24
23
  declare function insertValues<Input extends object>(input: Input | readonly Input[], columns?: readonly NoInfer<MutationColumns<Input>>[]): SqlFragment;
25
24
  /** Omit undefined fields; null remains a value. Empty assignments reject. */
26
25
  declare function assignments<Input extends object>(input: Input, columns?: readonly NoInfer<MutationColumns<Input>>[]): SqlFragment;
27
- declare function createQuery<Row extends QueryResultRow = UnknownRow>(strings: TemplateStringsArray, ...values: readonly unknown[]): NoInfer<SqlQuery<Row>>;
26
+ declare function createQuery<Row extends object = UnknownRow>(strings: TemplateStringsArray, ...values: readonly unknown[]): NoInfer<SqlQuery<Row>>;
28
27
  /** Values bind parameters; identifiers quote names; fragments compose authored SQL. */
29
28
  export declare const sql: typeof createQuery & {
30
29
  fragment: typeof fragment;
@@ -33,6 +32,6 @@ export declare const sql: typeof createQuery & {
33
32
  assignments: typeof assignments;
34
33
  };
35
34
  /** Escape hatch for externally authored SQL. Never concatenate application input into text. */
36
- export declare function unsafeSql<Row extends QueryResultRow = UnknownRow>(text: string, values: readonly unknown[]): NoInfer<SqlQuery<Row>>;
35
+ export declare function unsafeSql<Row extends object = UnknownRow>(text: string, values: readonly unknown[]): NoInfer<SqlQuery<Row>>;
37
36
  export declare function assertQuery(value: unknown): asserts value is SqlQuery;
38
37
  export {};
package/dist/query.js CHANGED
@@ -110,8 +110,8 @@ export function unsafeSql(text, values) {
110
110
  throw new Error('SQL must be a nonempty string.');
111
111
  if (!Array.isArray(values))
112
112
  throw new Error('SQL input must be an explicit array.');
113
- if (values.length > 65535)
114
- throw new Error(`A PostgreSQL query supports at most 65535 bound parameters; received ${values.length}. Split large batches explicitly.`);
113
+ if (values.length > 65533)
114
+ throw new Error(`Postgres.js 3.4.9 supports at most 65533 bound parameters; received ${values.length}. Split large batches explicitly.`);
115
115
  return Object.freeze({ [queryTag]: true, text, values: Object.freeze([...values]) });
116
116
  }
117
117
  export function assertQuery(value) {
package/dist/sql.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare function validateName(value: string): void;
2
2
  export declare function quote(name: string): string;
3
3
  export declare function qualified(namespace: string, table: string): string;
4
- /** Shared by query assembly and static checking so separators produce identical SQL. */
4
+ /** Assemble query parts with a validated SQL separator. */
5
5
  export declare function joinSeparator(value: unknown): string;
package/dist/sql.js CHANGED
@@ -13,7 +13,7 @@ export function quote(name) {
13
13
  export function qualified(namespace, table) {
14
14
  return `${quote(namespace)}.${quote(table)}`;
15
15
  }
16
- /** Shared by query assembly and static checking so separators produce identical SQL. */
16
+ /** Assemble query parts with a validated SQL separator. */
17
17
  export function joinSeparator(value) {
18
18
  switch (value) {
19
19
  case 'AND':
@@ -1,4 +1,4 @@
1
- /** Read only the command prefix, leaving strings, bodies and SQL syntax to pg. */
1
+ /** Read only the command prefix, leaving strings, bodies and SQL syntax to PostgreSQL. */
2
2
  function commandPrefix(sql) {
3
3
  const words = [];
4
4
  let i = 0;
@@ -1,4 +1,4 @@
1
- import type { Pool } from 'pg';
1
+ import type postgres from 'postgres';
2
2
  import type { QueryExecutor } from './connection.js';
3
3
  /** The callback owns one connection and expires before commit or rollback begins. */
4
- export declare function withTransaction<T>(pool: Pool, work: (query: QueryExecutor) => Promise<T>): Promise<T>;
4
+ export declare function withTransaction<T>(pool: postgres.Sql, work: (query: QueryExecutor) => Promise<T>): Promise<T>;