@uniflowed/sql 0.35.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.
package/adapters/pg.js CHANGED
@@ -11,8 +11,11 @@
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, ... };
@@ -24,9 +27,32 @@ type PgQuery = {|
24
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. */
@@ -70,6 +180,7 @@ export function fromPgPool(pool: PgPool): Queryable {
70
180
  engine : "postgresql",
71
181
  maxParams: 65535,
72
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";
@@ -24,6 +25,7 @@ export interface PGliteDatabase {
24
25
  readonly rowMode : "array",
25
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
31
  readonly parsers : { readonly [string]: mixed };
@@ -52,6 +54,27 @@ export function fromPGlite(database: PGliteDatabase): Queryable {
52
54
  return singleConnection({
53
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,14 +18,30 @@
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 {
@@ -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)));
@@ -87,6 +175,8 @@ async function scoped<T>(tx: PostgresTransaction, body: (tx: Queryable) => Promi
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))
@@ -105,6 +195,7 @@ export function fromPostgres(sql: PostgresSql): Queryable {
105
195
  engine : "postgresql",
106
196
  maxParams: 65535,
107
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
  };
package/index.js CHANGED
@@ -16,8 +16,9 @@
16
16
  // type. So an `int8` is a `bigint` under `pg` and under `postgres` alike,
17
17
  // rather than whatever each driver's own parser decided.
18
18
  // * **The parts of sqlc's semantics a driver does not have**: `sqlc.slice`
19
- // expansion, `:copyfrom` as chunked multi-row inserts, `:batch*` in one
20
- // transaction, and nested transactions as savepoints.
19
+ // expansion, `:copyfrom` (PostgreSQL `COPY` when the adapter can send it,
20
+ // otherwise chunked multi-row inserts), `:batch*` in one transaction, and
21
+ // nested transactions as savepoints.
21
22
  // * **A synchronous path** for drivers that run a statement on the calling
22
23
  // thread (`node:sqlite`, `better-sqlite3`, `bun:sqlite`). Generated code
23
24
  // with `sync: true` calls it, so a transaction can stay inside `BEGIN`
@@ -65,12 +66,24 @@ export type QueryMode = "rows" | "exec";
65
66
  * `transaction` is absent where the platform has no interactive transactions
66
67
  * (Cloudflare D1). Inside a transaction it opens a savepoint.
67
68
  */
69
+ /**
70
+ * Send one `COPY … FROM STDIN` payload and resolve to the number of rows the
71
+ * server accepted.
72
+ *
73
+ * Present on a PostgreSQL adapter that can speak `COPY`. Absent everywhere
74
+ * else, where `:copyfrom` stays a chunked `INSERT`. The payload is PostgreSQL
75
+ * text format: tab-separated fields, one row per line.
76
+ */
77
+ export type CopyIn = (statement: string, payload: string) => Promise<number>;
78
+
68
79
  export type Queryable = {
69
80
  readonly engine: Engine,
70
81
  /** The most bound parameters one statement may carry; `:copyfrom` chunks under it. */
71
82
  readonly maxParams: number,
72
83
  query(text: string, params: $ReadOnlyArray<SqlParam>, mode: QueryMode): Promise<QueryResult>,
73
84
  readonly transaction?: <T>(body: (tx: Queryable) => Promise<T>) => Promise<T>,
85
+ /** PostgreSQL `COPY`, when this adapter can send one. */
86
+ readonly copy?: CopyIn,
74
87
  ...
75
88
  };
76
89
 
@@ -494,6 +507,96 @@ export type CopyPlan = {|
494
507
  /** The most rows one statement carries when a row has no parameters at all. */
495
508
  const COPY_ROWS_WITHOUT_PARAMS = 1000;
496
509
 
510
+ /**
511
+ * `COPY <table> (<columns>) FROM STDIN` for an `INSERT … VALUES` plan, or
512
+ * `null` when the plan is not that shape.
513
+ *
514
+ * sqlc only accepts `INSERT INTO t (…) VALUES (…)` for `:copyfrom`, and the
515
+ * generator puts the table and the column list in `head`. Anything else keeps
516
+ * the chunked `INSERT`, which is the path every engine can run.
517
+ */
518
+ function copyStatement(plan: CopyPlan): string | null {
519
+ const prefix = "INSERT INTO ";
520
+ const suffix = " VALUES ";
521
+ if (!plan.head.startsWith(prefix) || !plan.head.endsWith(suffix)) {
522
+ return null;
523
+ }
524
+ const target = plan.head.slice(prefix.length, plan.head.length - suffix.length);
525
+ return `COPY ${target} FROM STDIN WITH (FORMAT text)`;
526
+ }
527
+
528
+ /** One field of PostgreSQL's text `COPY` format. */
529
+ function copyField(value: SqlParam): string {
530
+ if (value === null) {
531
+ return "\\N";
532
+ }
533
+ return escapeCopyText(copyText(value));
534
+ }
535
+
536
+ function copyText(value: string | number | bigint | Uint8Array): string {
537
+ if (typeof value === "string") {
538
+ return value;
539
+ }
540
+ if (typeof value === "number" || typeof value === "bigint") {
541
+ return String(value);
542
+ }
543
+ let hex = "\\x";
544
+ for (let i = 0; i < value.length; i += 1) {
545
+ hex += value[i].toString(16).padStart(2, "0");
546
+ }
547
+ return hex;
548
+ }
549
+
550
+ /** `\`, newline, carriage return and tab, the four escapes text `COPY` requires. */
551
+ function escapeCopyText(text: string): string {
552
+ let out = "";
553
+ for (let i = 0; i < text.length; i += 1) {
554
+ const character = text[i];
555
+ if (character === "\\") {
556
+ out += "\\\\";
557
+ } else if (character === "\n") {
558
+ out += "\\n";
559
+ } else if (character === "\r") {
560
+ out += "\\r";
561
+ } else if (character === "\t") {
562
+ out += "\\t";
563
+ } else {
564
+ out += character;
565
+ }
566
+ }
567
+ return out;
568
+ }
569
+
570
+ /**
571
+ * Every row of a `:copyfrom` as one text-format payload.
572
+ *
573
+ * A short row throws before anything is sent, the same way the `INSERT` path
574
+ * does. A null field is `\N`. The payload ends each row with a newline, which
575
+ * is the row terminator `COPY` counts.
576
+ */
577
+ function copyPayload(
578
+ plan: CopyPlan,
579
+ rows: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
580
+ name: string,
581
+ ): string {
582
+ const width = plan.refs.length;
583
+ let payload = "";
584
+ for (const row of rows) {
585
+ for (let i = 0; i < width; i += 1) {
586
+ const index = plan.refs[i];
587
+ if (index >= row.length) {
588
+ throw new SqlError({ kind: "row", index, length: row.length }, name);
589
+ }
590
+ if (i > 0) {
591
+ payload += "\t";
592
+ }
593
+ payload += copyField(row[index]);
594
+ }
595
+ payload += "\n";
596
+ }
597
+ return payload;
598
+ }
599
+
497
600
  function copyChunk(
498
601
  engine: Engine,
499
602
  plan : CopyPlan,
@@ -528,12 +631,15 @@ function copyWidth(db: { readonly maxParams: number, ... }, plan: CopyPlan, name
528
631
  }
529
632
 
530
633
  /**
531
- * `:copyfrom` — insert every row, as few statements as `maxParams` allows.
634
+ * `:copyfrom` — insert every row.
532
635
  *
533
- * Inside one transaction when the `Queryable` can open one, so a failure in
534
- * the third chunk does not leave the first two behind: pgx's `CopyFrom` is one
535
- * `COPY`, and a caller of the generated function should not have to know it is
536
- * now several statements. Resolves to the number of rows inserted.
636
+ * A PostgreSQL adapter that implements `copy` receives one `COPY … FROM STDIN`
637
+ * in text format, which is one statement and so one success or one failure.
638
+ * Every other adapter, and a plan that is not an `INSERT … VALUES`, stays on
639
+ * chunked multi-row `INSERT`s. Those run inside one transaction when the
640
+ * `Queryable` can open one and the rows do not fit in a single statement, so
641
+ * a failure in the third chunk does not leave the first two behind. Resolves
642
+ * to the number of rows inserted.
537
643
  */
538
644
  export async function copyFrom(
539
645
  db : Queryable,
@@ -544,6 +650,13 @@ export async function copyFrom(
544
650
  if (rows.length === 0) {
545
651
  return 0;
546
652
  }
653
+ const copy = db.copy;
654
+ if (db.engine === "postgresql" && typeof copy === "function") {
655
+ const statement = copyStatement(plan);
656
+ if (statement !== null) {
657
+ return copy(statement, copyPayload(plan, rows, name));
658
+ }
659
+ }
547
660
  const perStatement = copyWidth(db, plan, name);
548
661
  const insert = async (q: Queryable): Promise<number> => {
549
662
  let total = 0;
@@ -667,6 +780,8 @@ export type Connection = {|
667
780
  readonly runSync?: RunSync,
668
781
  /** The statement that opens a transaction; `BEGIN` unless the adapter knows better. */
669
782
  readonly begin?: string,
783
+ /** PostgreSQL `COPY` on this connection, when the driver can send one. */
784
+ readonly copy?: CopyIn,
670
785
  |};
671
786
 
672
787
  /**
@@ -701,10 +816,17 @@ async function scoped<T>(
701
816
  }
702
817
  return connection.run(text, params, mode);
703
818
  };
819
+ const copy = connection.copy;
820
+ const guardedCopy: CopyIn | void =
821
+ copy === undefined
822
+ ? undefined
823
+ : (statement, payload) =>
824
+ open ? copy(statement, payload) : Promise.reject(new SqlError({ kind: "closed" }));
704
825
  const tx: Queryable = {
705
826
  engine : connection.engine,
706
827
  maxParams: connection.maxParams,
707
828
  query : guarded,
829
+ ...(guardedCopy === undefined ? null : { copy: guardedCopy }),
708
830
  transaction: async <U>(inner: (tx: Queryable) => Promise<U>): Promise<U> => {
709
831
  const savepoint = `uf_sp_${depth + 1}`;
710
832
  await guarded(`SAVEPOINT ${savepoint}`, [], "exec");
@@ -910,6 +1032,19 @@ function connect(connection: Connection): Handle {
910
1032
  }
911
1033
  return run(text, params, mode);
912
1034
  };
1035
+ const rawCopy = connection.copy;
1036
+ const copy =
1037
+ rawCopy === undefined
1038
+ ? undefined
1039
+ : async (statement: string, payload: string): Promise<number> => {
1040
+ if (syncOpen) {
1041
+ unsupported("a COPY while a synchronous transaction is open");
1042
+ }
1043
+ while (gate !== null) {
1044
+ await gate;
1045
+ }
1046
+ return rawCopy(statement, payload);
1047
+ };
913
1048
  const transactionSync = <T>(body: (tx: SyncQueryable) => T): T => {
914
1049
  if (runSync == null) {
915
1050
  unsupported("a synchronous transaction on an asynchronous connection");
@@ -936,6 +1071,7 @@ function connect(connection: Connection): Handle {
936
1071
  transaction,
937
1072
  querySync,
938
1073
  transactionSync,
1074
+ ...(copy === undefined ? null : { copy }),
939
1075
  };
940
1076
  }
941
1077
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/sql",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "The runtime for sqlc's Flow target: exact codecs and driver adapters, part of the Unified Toolchain for Flow.",
5
5
  "type": "module",
6
6
  "license": "MIT",