@uniflowed/sql 0.34.0 → 0.36.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.
@@ -22,7 +22,7 @@ export interface BetterSqlite3Statement {
22
22
  readonly reader: boolean;
23
23
  all(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
24
24
  run(...params: $ReadOnlyArray<SqlParam>): {
25
- readonly changes: number,
25
+ readonly changes : number,
26
26
  readonly lastInsertRowid: number | bigint,
27
27
  ...
28
28
  };
@@ -36,15 +36,15 @@ export interface BetterSqlite3Database {
36
36
  }
37
37
 
38
38
  export type BetterSqlite3Options = {|
39
- readonly begin?: string,
39
+ readonly begin? : string,
40
40
  readonly statementCache?: number,
41
- readonly maxParams?: number,
41
+ readonly maxParams? : number,
42
42
  |};
43
43
 
44
44
  /** A [`Queryable`] and a [`SyncQueryable`] over one better-sqlite3 `Database`. */
45
45
  export function fromBetterSqlite3(
46
46
  database: BetterSqlite3Database,
47
- options: BetterSqlite3Options = {},
47
+ options : BetterSqlite3Options = {},
48
48
  ): Queryable & SyncQueryable {
49
49
  const statement = statementCache((text) => {
50
50
  const prepared = database.prepare(text);
@@ -70,16 +70,16 @@ export function fromBetterSqlite3(
70
70
  }
71
71
  const result = prepared.run(...params);
72
72
  return {
73
- rows: [],
73
+ rows : [],
74
74
  rowsAffected: count(result.changes),
75
75
  lastInsertId: rowId(result.lastInsertRowid),
76
76
  };
77
77
  };
78
78
  return singleConnectionSync({
79
- engine: "sqlite",
79
+ engine : "sqlite",
80
80
  maxParams: options.maxParams ?? 32766,
81
- begin: options.begin,
82
- run: async (text, params, mode) => runSync(text, params, mode),
81
+ begin : options.begin,
82
+ run : async (text, params, mode) => runSync(text, params, mode),
83
83
  runSync,
84
84
  });
85
85
  }
@@ -21,7 +21,7 @@ import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../intern
21
21
  export interface BunSqliteStatement {
22
22
  values(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
23
23
  run(...params: $ReadOnlyArray<SqlParam>): {
24
- readonly changes: number | bigint,
24
+ readonly changes : number | bigint,
25
25
  readonly lastInsertRowid: number | bigint,
26
26
  ...
27
27
  };
@@ -34,15 +34,15 @@ export interface BunSqliteDatabase {
34
34
  }
35
35
 
36
36
  export type BunSqliteOptions = {|
37
- readonly begin?: string,
37
+ readonly begin? : string,
38
38
  readonly statementCache?: number,
39
- readonly maxParams?: number,
39
+ readonly maxParams? : number,
40
40
  |};
41
41
 
42
42
  /** A [`Queryable`] and a [`SyncQueryable`] over one `bun:sqlite` `Database`. */
43
43
  export function fromBunSqlite(
44
44
  database: BunSqliteDatabase,
45
- options: BunSqliteOptions = {},
45
+ options : BunSqliteOptions = {},
46
46
  ): Queryable & SyncQueryable {
47
47
  const statement = statementCache((text) => {
48
48
  const prepared = database.prepare(text);
@@ -63,16 +63,16 @@ export function fromBunSqlite(
63
63
  }
64
64
  const result = prepared.run(...params);
65
65
  return {
66
- rows: [],
66
+ rows : [],
67
67
  rowsAffected: count(result.changes),
68
68
  lastInsertId: rowId(result.lastInsertRowid),
69
69
  };
70
70
  };
71
71
  return singleConnectionSync({
72
- engine: "sqlite",
72
+ engine : "sqlite",
73
73
  maxParams: options.maxParams ?? 32766,
74
- begin: options.begin,
75
- run: async (text, params, mode) => runSync(text, params, mode),
74
+ begin : options.begin,
75
+ run : async (text, params, mode) => runSync(text, params, mode),
76
76
  runSync,
77
77
  });
78
78
  }
package/adapters/d1.js CHANGED
@@ -47,7 +47,7 @@ function toD1(param: SqlParam): mixed {
47
47
  /** A [`Queryable`] over a D1 binding. */
48
48
  export function fromD1(database: D1Database): Queryable {
49
49
  return {
50
- engine: "sqlite",
50
+ engine : "sqlite",
51
51
  maxParams: 100,
52
52
  query: async (text, params, mode) => {
53
53
  const statement = database.prepare(text).bind(...params.map(toD1));
@@ -63,7 +63,7 @@ export function fromD1(database: D1Database): Queryable {
63
63
  }
64
64
  const { meta } = await statement.run();
65
65
  return {
66
- rows: [],
66
+ rows : [],
67
67
  rowsAffected: count(meta.changes ?? 0),
68
68
  lastInsertId: rowId(meta.last_row_id),
69
69
  };
@@ -34,13 +34,13 @@ export type Mysql2Field = {
34
34
  };
35
35
 
36
36
  type Mysql2Options = {|
37
- readonly sql: string,
38
- readonly values: $ReadOnlyArray<mixed>,
39
- readonly rowsAsArray: true,
37
+ readonly sql : string,
38
+ readonly values : $ReadOnlyArray<mixed>,
39
+ readonly rowsAsArray : true,
40
40
  readonly supportBigNumbers: true,
41
- readonly bigNumberStrings: true,
42
- readonly dateStrings: true,
43
- readonly typeCast: (field: Mysql2Field, next: () => mixed) => mixed,
41
+ readonly bigNumberStrings : true,
42
+ readonly dateStrings : true,
43
+ readonly typeCast : (field: Mysql2Field, next: () => mixed) => mixed,
44
44
  |};
45
45
 
46
46
  type Mysql2Result =
@@ -93,12 +93,12 @@ function lastId(value: number | string): bigint | null {
93
93
  function runOn(connection: Mysql2Connection): Run {
94
94
  return async (text, params, mode): Promise<QueryResult> => {
95
95
  const options: Mysql2Options = {
96
- sql: text,
97
- values: params,
98
- rowsAsArray: true,
96
+ sql : text,
97
+ values : params,
98
+ rowsAsArray : true,
99
99
  supportBigNumbers: true,
100
- bigNumberStrings: true,
101
- dateStrings: true,
100
+ bigNumberStrings : true,
101
+ dateStrings : true,
102
102
  typeCast,
103
103
  };
104
104
  const [result] =
@@ -125,9 +125,9 @@ function connection(client: Mysql2Connection): Connection {
125
125
  /** A [`Queryable`] over a `mysql2/promise` pool. */
126
126
  export function fromMysql2Pool(pool: Mysql2Pool): Queryable {
127
127
  return {
128
- engine: "mysql",
128
+ engine : "mysql",
129
129
  maxParams: 65535,
130
- query: runOn(pool),
130
+ query : runOn(pool),
131
131
  transaction: async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
132
132
  const client = await pool.getConnection();
133
133
  try {
@@ -22,7 +22,7 @@ import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../intern
22
22
  export interface NodeSqliteStatement {
23
23
  all(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
24
24
  run(...params: $ReadOnlyArray<SqlParam>): {
25
- readonly changes: number | bigint,
25
+ readonly changes : number | bigint,
26
26
  readonly lastInsertRowid: number | bigint,
27
27
  ...
28
28
  };
@@ -51,7 +51,7 @@ function prepared(database: NodeSqliteDatabase, text: string): NodeSqliteStateme
51
51
  // failing on the call below.
52
52
  if (!("setReturnArrays" in statement)) {
53
53
  throw new SqlError({
54
- kind: "unsupported",
54
+ kind : "unsupported",
55
55
  feature: "node:sqlite without StatementSync#setReturnArrays (Node 24.0 or 22.16 and newer)",
56
56
  });
57
57
  }
@@ -62,7 +62,7 @@ function prepared(database: NodeSqliteDatabase, text: string): NodeSqliteStateme
62
62
  /** A [`Queryable`] and a [`SyncQueryable`] over one `DatabaseSync`. */
63
63
  export function fromNodeSqlite(
64
64
  database: NodeSqliteDatabase,
65
- options: NodeSqliteOptions = {},
65
+ options : NodeSqliteOptions = {},
66
66
  ): Queryable & SyncQueryable {
67
67
  const statement = statementCache(
68
68
  (text) => prepared(database, text),
@@ -83,16 +83,16 @@ export function fromNodeSqlite(
83
83
  }
84
84
  const result = prepared.run(...params);
85
85
  return {
86
- rows: [],
86
+ rows : [],
87
87
  rowsAffected: count(result.changes),
88
88
  lastInsertId: rowId(result.lastInsertRowid),
89
89
  };
90
90
  };
91
91
  return singleConnectionSync({
92
- engine: "sqlite",
92
+ engine : "sqlite",
93
93
  maxParams: options.maxParams ?? 32766,
94
- begin: options.begin,
95
- run: async (text, params, mode) => runSync(text, params, mode),
94
+ begin : options.begin,
95
+ run : async (text, params, mode) => runSync(text, params, mode),
96
96
  runSync,
97
97
  });
98
98
  }
package/adapters/pg.js CHANGED
@@ -11,22 +11,48 @@
11
11
  // with the identity for that query alone, so the pool's own configuration is
12
12
  // left as the application set it, and generated code still sees the server's
13
13
  // text. A transaction checks a client out of the pool for its duration.
14
+ // `:copyfrom` is a `COPY FROM STDIN`: node-postgres runs it when the query
15
+ // object supplies `submit` and `handleCopyInResponse`, which is the protocol
16
+ // hook it already has and does not implement for a string query.
14
17
 
15
- import type { Connection, QueryResult, Queryable, Run } from "../index.js";
18
+ import type { Connection, CopyIn, QueryResult, Queryable, Run } from "../index.js";
16
19
  import { singleConnection, transactionOn } from "../index.js";
17
20
 
18
21
  type PgResult = { readonly rows: $ReadOnlyArray<mixed>, readonly rowCount: number | null, ... };
19
22
 
20
23
  type PgQuery = {|
21
- readonly text: string,
22
- readonly values: $ReadOnlyArray<mixed>,
24
+ readonly text : string,
25
+ readonly values : $ReadOnlyArray<mixed>,
23
26
  readonly rowMode: "array",
24
- readonly types: {| readonly getTypeParser: () => (value: string) => string |},
27
+ readonly types : {| readonly getTypeParser: () => (value: string) => string |},
25
28
  |};
26
29
 
30
+ /** The connection methods a `COPY FROM STDIN` uses. `pg` passes its own. */
31
+ type PgCopyConnection = {
32
+ query(text: string): mixed,
33
+ sendCopyFromChunk(chunk: Uint8Array): mixed,
34
+ endCopyFrom(): mixed,
35
+ };
36
+
37
+ /** A query object `pg` runs when `submit` is a function. */
38
+ type PgCopyRequest = {
39
+ text : string,
40
+ callback: ?(?mixed, ?PgResult) => mixed,
41
+ submit(connection: PgCopyConnection): null,
42
+ handleCopyInResponse(connection: PgCopyConnection): void,
43
+ handleCommandComplete(message: { text?: string, ... }): void,
44
+ handleReadyForQuery(): void,
45
+ handleError(error: mixed): void,
46
+ handleCopyData(): void,
47
+ handleRowDescription(): void,
48
+ handleDataRow(): void,
49
+ handleEmptyQuery(): void,
50
+ handlePortalSuspended(): void,
51
+ };
52
+
27
53
  /** A connected `pg.Client` or a client checked out of a pool. */
28
54
  export interface PgClient {
29
- query(config: PgQuery): Promise<PgResult>;
55
+ query(config: PgQuery | PgCopyRequest): Promise<PgResult> | void;
30
56
  }
31
57
 
32
58
  /** A client checked out of a `pg.Pool`. */
@@ -36,15 +62,99 @@ export interface PgPoolClient extends PgClient {
36
62
 
37
63
  /** A `pg.Pool`. */
38
64
  export interface PgPool {
39
- query(config: PgQuery): Promise<PgResult>;
65
+ query(config: PgQuery | PgCopyRequest): Promise<PgResult>;
40
66
  connect(): Promise<PgPoolClient>;
41
67
  }
42
68
 
69
+ const COPY_ROWS = /COPY (\d+)/;
70
+
71
+ /**
72
+ * One `COPY FROM STDIN` on `runner`.
73
+ *
74
+ * The callback is left unset when `runner` is a pool: the pool sets it, and
75
+ * setting it here would skip the pool's own, which is what releases the
76
+ * client. A client sets it in the wrapper below, because `client.query` only
77
+ * returns a promise when the object does not already have one and nobody
78
+ * passed a callback.
79
+ */
80
+ function copyRequest(statement: string, payload: string): PgCopyRequest {
81
+ let rowCount = 0;
82
+ let settled = false;
83
+ const request: PgCopyRequest = {
84
+ text : statement,
85
+ callback: undefined,
86
+ submit(connection) {
87
+ connection.query(statement);
88
+ return null;
89
+ },
90
+ handleCopyInResponse(connection) {
91
+ // pg-protocol copies the chunk with `Buffer#copy`. A `Uint8Array` has
92
+ // no `copy`, and the throw leaves the backend waiting for `CopyData`.
93
+ connection.sendCopyFromChunk(Buffer.from(payload, "utf8"));
94
+ connection.endCopyFrom();
95
+ },
96
+ handleCommandComplete(message) {
97
+ const text = typeof message.text === "string" ? message.text : "";
98
+ const match = COPY_ROWS.exec(text);
99
+ if (match?.[1] !== undefined) {
100
+ rowCount = Number(match[1]);
101
+ }
102
+ },
103
+ handleReadyForQuery() {
104
+ if (settled) {
105
+ return;
106
+ }
107
+ settled = true;
108
+ request.callback?.(null, { rows: [], rowCount });
109
+ },
110
+ handleError(error) {
111
+ if (settled) {
112
+ return;
113
+ }
114
+ settled = true;
115
+ request.callback?.(error, undefined);
116
+ },
117
+ handleCopyData() {},
118
+ handleRowDescription() {},
119
+ handleDataRow() {},
120
+ handleEmptyQuery() {},
121
+ handlePortalSuspended() {},
122
+ };
123
+ return request;
124
+ }
125
+
126
+ function copyOn(runner: PgClient): CopyIn {
127
+ return (statement, payload) =>
128
+ new Promise((resolve, reject) => {
129
+ const request = copyRequest(statement, payload);
130
+ request.callback = (error, result) => {
131
+ if (error != null) {
132
+ reject(error instanceof Error ? error : new Error(String(error)));
133
+ return;
134
+ }
135
+ resolve(result?.rowCount ?? 0);
136
+ };
137
+ runner.query(request);
138
+ });
139
+ }
140
+
141
+ function copyOnPool(pool: PgPool): CopyIn {
142
+ return async (statement, payload) => {
143
+ const result = await pool.query(copyRequest(statement, payload));
144
+ return result.rowCount ?? 0;
145
+ };
146
+ }
147
+
43
148
  const RAW = { getTypeParser: () => (value: string) => value };
44
149
 
45
150
  function runOn(client: PgClient): Run {
46
151
  return async (text, params): Promise<QueryResult> => {
47
152
  const result = await client.query({ text, values: params, rowMode: "array", types: RAW });
153
+ // A `COPY` request returns void. This call is a normal query, which
154
+ // returns a result; the union is what `query` is declared as.
155
+ if (result == null) {
156
+ throw new Error("pg query returned no result");
157
+ }
48
158
  const rows: Array<$ReadOnlyArray<mixed>> = [];
49
159
  for (const row of result.rows) {
50
160
  if (Array.isArray(row)) {
@@ -56,7 +166,7 @@ function runOn(client: PgClient): Run {
56
166
  }
57
167
 
58
168
  function connection(client: PgClient): Connection {
59
- return { engine: "postgresql", maxParams: 65535, run: runOn(client) };
169
+ return { engine: "postgresql", maxParams: 65535, run: runOn(client), copy: copyOn(client) };
60
170
  }
61
171
 
62
172
  /** A [`Queryable`] over one connected `pg.Client`, which it serialises. */
@@ -67,9 +177,10 @@ export function fromPgClient(client: PgClient): Queryable {
67
177
  /** A [`Queryable`] over a `pg.Pool`. */
68
178
  export function fromPgPool(pool: PgPool): Queryable {
69
179
  return {
70
- engine: "postgresql",
180
+ engine : "postgresql",
71
181
  maxParams: 65535,
72
- query: runOn(pool),
182
+ query : runOn(pool),
183
+ copy : copyOnPool(pool),
73
184
  transaction: async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
74
185
  const client = await pool.connect();
75
186
  let broken: mixed = undefined;
@@ -10,7 +10,8 @@
10
10
  // PGlite parses and serialises values itself, by type. This adapter turns both
11
11
  // off — an identity parser and serialiser for every type it knows — so rows
12
12
  // arrive as the server's text and parameters leave as the codecs wrote them,
13
- // exactly as with every other PostgreSQL adapter.
13
+ // exactly as with every other PostgreSQL adapter. `:copyfrom` uses PGlite's
14
+ // `/dev/blob` device, which is how its `COPY FROM` reads a payload.
14
15
 
15
16
  import type { Queryable, SqlParam } from "../index.js";
16
17
  import { singleConnection } from "../index.js";
@@ -18,15 +19,16 @@ import { singleConnection } from "../index.js";
18
19
  /** The part of a `PGlite` instance this uses. */
19
20
  export interface PGliteDatabase {
20
21
  query(
21
- text: string,
22
+ text : string,
22
23
  params: $ReadOnlyArray<SqlParam>,
23
24
  options: {|
24
- readonly rowMode: "array",
25
- readonly parsers: { readonly [string]: (value: string) => mixed },
25
+ readonly rowMode : "array",
26
+ readonly parsers : { readonly [string]: (value: string) => mixed },
26
27
  readonly serializers: { readonly [string]: (value: mixed) => mixed },
28
+ readonly blob? : Blob,
27
29
  |},
28
30
  ): Promise<{ readonly rows: $ReadOnlyArray<mixed>, readonly affectedRows?: number, ... }>;
29
- readonly parsers: { readonly [string]: mixed };
31
+ readonly parsers : { readonly [string]: mixed };
30
32
  readonly serializers: { readonly [string]: mixed };
31
33
  }
32
34
 
@@ -50,8 +52,29 @@ export function fromPGlite(database: PGliteDatabase): Queryable {
50
52
  let parsers: { readonly [string]: (value: mixed) => mixed } = {};
51
53
  let serializers: { readonly [string]: (value: mixed) => mixed } = {};
52
54
  return singleConnection({
53
- engine: "postgresql",
55
+ engine : "postgresql",
54
56
  maxParams: 65535,
57
+ copy: async (statement, payload) => {
58
+ // `COPY … FROM STDIN` is the wire form. PGlite reads the payload from
59
+ // the virtual file named here instead of a frontend COPY stream.
60
+ const text = statement.replace(" FROM STDIN", " FROM '/dev/blob'");
61
+ const result = await database.query(text, [], {
62
+ rowMode : "array",
63
+ parsers : {},
64
+ serializers: {},
65
+ blob : new Blob([payload]),
66
+ });
67
+ if (typeof result.affectedRows === "number") {
68
+ return result.affectedRows;
69
+ }
70
+ let rows = 0;
71
+ for (let i = 0; i < payload.length; i += 1) {
72
+ if (payload[i] === "\n") {
73
+ rows += 1;
74
+ }
75
+ }
76
+ return rows;
77
+ },
55
78
  run: async (text, params) => {
56
79
  const size = Object.keys(database.parsers).length + Object.keys(database.serializers).length;
57
80
  if (size !== known) {
@@ -18,20 +18,36 @@
18
18
  // `bytea`, `x === true ? "t" : "f"` for `bool` — each of which would corrupt a
19
19
  // value the codecs already wrote as text. Declaring every parameter as
20
20
  // `unknown` (OID 705) leaves the server to infer the type, as an undeclared
21
- // one would, and leaves postgres.js nothing to serialise.
21
+ // one would, and leaves postgres.js nothing to serialise. `:copyfrom` writes
22
+ // the text-format payload to the stream `unsafe(statement, []).writable()`
23
+ // opens for `COPY FROM STDIN`.
22
24
 
23
25
  import { SqlError } from "../index.js";
24
- import type { Queryable, SqlParam } from "../index.js";
26
+ import type { CopyIn, Queryable, SqlParam } from "../index.js";
25
27
 
26
28
  type RawRows = Promise<$ReadOnlyArray<mixed> & { readonly count?: number | null, ... }>;
27
29
 
28
- type Unsafe = { raw(): RawRows, ... };
30
+ /** The Node writable postgres.js resolves `COPY FROM STDIN` to. */
31
+ type CopyWritable = {
32
+ write(chunk: string, callback: (error?: mixed) => mixed): mixed,
33
+ end(callback: (error?: mixed) => mixed): mixed,
34
+ once(event: "error", listener: (error: mixed) => mixed): mixed,
35
+ ...
36
+ };
37
+
38
+ type Unsafe = {
39
+ raw(): RawRows,
40
+ writable(): Promise<CopyWritable>,
41
+ /** Rejects the query. postgres.js calls it for a server error during `COPY`. */
42
+ reject: (error: mixed) => mixed,
43
+ ...
44
+ };
29
45
 
30
46
  /** What postgres.js's `Sql` and its transaction handles have in common. */
31
47
  export interface PostgresQueries {
32
48
  unsafe(
33
- text: string,
34
- params: $ReadOnlyArray<mixed>,
49
+ text : string,
50
+ params : $ReadOnlyArray<mixed>,
35
51
  options?: {| readonly prepare?: boolean |},
36
52
  ): Unsafe;
37
53
  typed(value: SqlParam, oid: number): mixed;
@@ -56,6 +72,78 @@ function text(value: mixed): mixed {
56
72
  /** `unknown`: a parameter whose type the server decides. */
57
73
  const UNKNOWN = 705;
58
74
 
75
+ function rowCount(payload: string): number {
76
+ let rows = 0;
77
+ for (let i = 0; i < payload.length; i += 1) {
78
+ if (payload[i] === "\n") {
79
+ rows += 1;
80
+ }
81
+ }
82
+ return rows;
83
+ }
84
+
85
+ /** Write `payload` and wait until the server finishes the `COPY`. */
86
+ function writeCopy(writable: CopyWritable, payload: string): Promise<void> {
87
+ return new Promise((resolve, reject) => {
88
+ let settled = false;
89
+ const fail = (error: mixed) => {
90
+ if (settled) {
91
+ return;
92
+ }
93
+ settled = true;
94
+ reject(error instanceof Error ? error : new Error(String(error)));
95
+ };
96
+ const succeed = () => {
97
+ if (settled) {
98
+ return;
99
+ }
100
+ settled = true;
101
+ resolve();
102
+ };
103
+ writable.once("error", fail);
104
+ writable.write(payload, (error) => {
105
+ if (error != null) {
106
+ fail(error);
107
+ return;
108
+ }
109
+ writable.end((endError) => {
110
+ if (endError != null) {
111
+ fail(endError);
112
+ } else {
113
+ succeed();
114
+ }
115
+ });
116
+ });
117
+ });
118
+ }
119
+
120
+ function copyOn(sql: PostgresQueries): CopyIn {
121
+ return async (statement, payload) => {
122
+ const query = sql.unsafe(statement, []);
123
+ // CopyInResponse resolves the query with the writable, and sending
124
+ // CopyDone clears that stream before the server answers. A failure after
125
+ // the data is in (a foreign key) calls `query.reject` and never reaches
126
+ // the writable, so its end callback would wait forever.
127
+ let fail: (error: mixed) => void = () => {};
128
+ const failed = new Promise((resolve, reject) => {
129
+ void resolve;
130
+ fail = reject;
131
+ });
132
+ failed.catch(() => {});
133
+ const rejectQuery = query.reject.bind(query);
134
+ query.reject = (error) => {
135
+ fail(error);
136
+ return rejectQuery(error);
137
+ };
138
+ const writable = await query.writable();
139
+ await Promise.race([writeCopy(writable, payload), failed]);
140
+ // CommandComplete carries `COPY n`, and postgres.js keeps that count on
141
+ // its internal result. A `COPY` without `WHERE` accepts every row or
142
+ // fails, so the number of lines written is the number the server copied.
143
+ return rowCount(payload);
144
+ };
145
+ }
146
+
59
147
  function run(sql: PostgresQueries): Queryable["query"] {
60
148
  return async (statement, params) => {
61
149
  const typed = params.map((param) => (param === null ? null : sql.typed(param, UNKNOWN)));
@@ -83,10 +171,12 @@ async function scoped<T>(tx: PostgresTransaction, body: (tx: Queryable) => Promi
83
171
  let open = true;
84
172
  const query = run(tx);
85
173
  const queryable: Queryable = {
86
- engine: "postgresql",
174
+ engine : "postgresql",
87
175
  maxParams: 65535,
88
176
  query: (statement, params, mode) =>
89
177
  open ? query(statement, params, mode) : Promise.reject(new SqlError({ kind: "closed" })),
178
+ copy: (statement, payload) =>
179
+ open ? copyOn(tx)(statement, payload) : Promise.reject(new SqlError({ kind: "closed" })),
90
180
  transaction: <U>(inner: (tx: Queryable) => Promise<U>): Promise<U> =>
91
181
  open
92
182
  ? tx.savepoint((savepoint) => scoped(savepoint, inner))
@@ -102,9 +192,10 @@ async function scoped<T>(tx: PostgresTransaction, body: (tx: Queryable) => Promi
102
192
  /** A [`Queryable`] over a postgres.js `Sql`. */
103
193
  export function fromPostgres(sql: PostgresSql): Queryable {
104
194
  return {
105
- engine: "postgresql",
195
+ engine : "postgresql",
106
196
  maxParams: 65535,
107
- query: run(sql),
197
+ query : run(sql),
198
+ copy : copyOn(sql),
108
199
  transaction: <T>(body: (tx: Queryable) => Promise<T>): Promise<T> =>
109
200
  sql.begin((tx) => scoped(tx, body)),
110
201
  };