@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.
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
 
@@ -91,7 +104,7 @@ export type Queryable = {
91
104
  * body would deadlock the same way `await db.query` does inside `transaction`.
92
105
  */
93
106
  export type SyncQueryable = {
94
- readonly engine: Engine,
107
+ readonly engine : Engine,
95
108
  readonly maxParams: number,
96
109
  querySync(text: string, params: $ReadOnlyArray<SqlParam>, mode: QueryMode): QueryResult,
97
110
  readonly transactionSync?: <T>(body: (tx: SyncQueryable) => T) => T,
@@ -155,19 +168,19 @@ function explain(failure: SqlFailure, query: string | null): string {
155
168
  return (
156
169
  where +
157
170
  match (failure) {
158
- {kind: "decode", expected: const expected, value: const value} =>
171
+ {kind: "decode", expected: const expected, value: const value} =>
159
172
  `expected ${expected} from the database, got ${show(value)}`,
160
- {kind: "encode", expected: const expected, value: const value} =>
173
+ {kind: "encode", expected: const expected, value: const value} =>
161
174
  `expected ${expected} as a parameter, got ${show(value)}`,
162
175
  {kind: "shape", expected: const expected, actual: const actual} =>
163
176
  `the adapter returned ${actual} columns where the query has ${expected}; ` +
164
177
  "regenerate with `uf sqlc generate`, or check that the adapter returns rows as arrays",
165
- {kind: "closed"} => "the transaction has already committed or rolled back",
166
- {kind: "unsupported", feature: const feature} =>
178
+ {kind: "closed"} => "the transaction has already committed or rolled back",
179
+ {kind: "unsupported", feature: const feature} =>
167
180
  `${feature} is not supported by this adapter`,
168
- {kind: "params", width: const width, max: const max} =>
181
+ {kind: "params", width: const width, max: const max} =>
169
182
  `a :copyfrom row binds ${width} parameters and this adapter allows ${max}`,
170
- {kind: "row", index: const index, length: const length} =>
183
+ {kind: "row", index: const index, length: const length} =>
171
184
  `a :copyfrom row has ${length} fields and the statement reads index ${index}`,
172
185
  }
173
186
  );
@@ -229,11 +242,11 @@ function allRows<T>(rows: $ReadOnlyArray<Row>, width: number, decode: (row: Row)
229
242
 
230
243
  /** `:one` — the first row, or `null`. */
231
244
  export async function one<T>(
232
- db: Queryable,
233
- name: string,
234
- text: string,
245
+ db : Queryable,
246
+ name : string,
247
+ text : string,
235
248
  params: $ReadOnlyArray<SqlParam>,
236
- width: number,
249
+ width : number,
237
250
  decode: (row: Row) => T,
238
251
  ): Promise<T | null> {
239
252
  try {
@@ -246,11 +259,11 @@ export async function one<T>(
246
259
 
247
260
  /** `:one` on a [`SyncQueryable`]. */
248
261
  export function oneSync<T>(
249
- db: SyncQueryable,
250
- name: string,
251
- text: string,
262
+ db : SyncQueryable,
263
+ name : string,
264
+ text : string,
252
265
  params: $ReadOnlyArray<SqlParam>,
253
- width: number,
266
+ width : number,
254
267
  decode: (row: Row) => T,
255
268
  ): T | null {
256
269
  try {
@@ -263,11 +276,11 @@ export function oneSync<T>(
263
276
 
264
277
  /** `:many` — every row. */
265
278
  export async function many<T>(
266
- db: Queryable,
267
- name: string,
268
- text: string,
279
+ db : Queryable,
280
+ name : string,
281
+ text : string,
269
282
  params: $ReadOnlyArray<SqlParam>,
270
- width: number,
283
+ width : number,
271
284
  decode: (row: Row) => T,
272
285
  ): Promise<Array<T>> {
273
286
  try {
@@ -280,11 +293,11 @@ export async function many<T>(
280
293
 
281
294
  /** `:many` on a [`SyncQueryable`]. */
282
295
  export function manySync<T>(
283
- db: SyncQueryable,
284
- name: string,
285
- text: string,
296
+ db : SyncQueryable,
297
+ name : string,
298
+ text : string,
286
299
  params: $ReadOnlyArray<SqlParam>,
287
- width: number,
300
+ width : number,
288
301
  decode: (row: Row) => T,
289
302
  ): Array<T> {
290
303
  try {
@@ -296,9 +309,9 @@ export function manySync<T>(
296
309
  }
297
310
 
298
311
  async function run(
299
- db: Queryable,
300
- name: string,
301
- text: string,
312
+ db : Queryable,
313
+ name : string,
314
+ text : string,
302
315
  params: $ReadOnlyArray<SqlParam>,
303
316
  ): Promise<QueryResult> {
304
317
  try {
@@ -309,9 +322,9 @@ async function run(
309
322
  }
310
323
 
311
324
  function executed(
312
- db: SyncQueryable,
313
- name: string,
314
- text: string,
325
+ db : SyncQueryable,
326
+ name : string,
327
+ text : string,
315
328
  params: $ReadOnlyArray<SqlParam>,
316
329
  ): QueryResult {
317
330
  try {
@@ -323,9 +336,9 @@ function executed(
323
336
 
324
337
  /** `:exec`. */
325
338
  export async function exec(
326
- db: Queryable,
327
- name: string,
328
- text: string,
339
+ db : Queryable,
340
+ name : string,
341
+ text : string,
329
342
  params: $ReadOnlyArray<SqlParam>,
330
343
  ): Promise<void> {
331
344
  await run(db, name, text, params);
@@ -333,9 +346,9 @@ export async function exec(
333
346
 
334
347
  /** `:exec` on a [`SyncQueryable`]. */
335
348
  export function execSync(
336
- db: SyncQueryable,
337
- name: string,
338
- text: string,
349
+ db : SyncQueryable,
350
+ name : string,
351
+ text : string,
339
352
  params: $ReadOnlyArray<SqlParam>,
340
353
  ): void {
341
354
  executed(db, name, text, params);
@@ -343,9 +356,9 @@ export function execSync(
343
356
 
344
357
  /** `:execrows` — how many rows the statement changed. */
345
358
  export async function execRows(
346
- db: Queryable,
347
- name: string,
348
- text: string,
359
+ db : Queryable,
360
+ name : string,
361
+ text : string,
349
362
  params: $ReadOnlyArray<SqlParam>,
350
363
  ): Promise<number> {
351
364
  return (await run(db, name, text, params)).rowsAffected;
@@ -353,9 +366,9 @@ export async function execRows(
353
366
 
354
367
  /** `:execrows` on a [`SyncQueryable`]. */
355
368
  export function execRowsSync(
356
- db: SyncQueryable,
357
- name: string,
358
- text: string,
369
+ db : SyncQueryable,
370
+ name : string,
371
+ text : string,
359
372
  params: $ReadOnlyArray<SqlParam>,
360
373
  ): number {
361
374
  return executed(db, name, text, params).rowsAffected;
@@ -369,9 +382,9 @@ export type ExecResult = {|
369
382
 
370
383
  /** `:execresult` — the affected-row count and, on MySQL and SQLite, the last id. */
371
384
  export async function execResult(
372
- db: Queryable,
373
- name: string,
374
- text: string,
385
+ db : Queryable,
386
+ name : string,
387
+ text : string,
375
388
  params: $ReadOnlyArray<SqlParam>,
376
389
  ): Promise<ExecResult> {
377
390
  const { rowsAffected, lastInsertId } = await run(db, name, text, params);
@@ -380,9 +393,9 @@ export async function execResult(
380
393
 
381
394
  /** `:execresult` on a [`SyncQueryable`]. */
382
395
  export function execResultSync(
383
- db: SyncQueryable,
384
- name: string,
385
- text: string,
396
+ db : SyncQueryable,
397
+ name : string,
398
+ text : string,
386
399
  params: $ReadOnlyArray<SqlParam>,
387
400
  ): ExecResult {
388
401
  const { rowsAffected, lastInsertId } = executed(db, name, text, params);
@@ -403,9 +416,9 @@ function insertedId(result: QueryResult, name: string): bigint {
403
416
  * `:one`, and this throws rather than resolve to a made-up zero.
404
417
  */
405
418
  export async function execLastId(
406
- db: Queryable,
407
- name: string,
408
- text: string,
419
+ db : Queryable,
420
+ name : string,
421
+ text : string,
409
422
  params: $ReadOnlyArray<SqlParam>,
410
423
  ): Promise<bigint> {
411
424
  return insertedId(await run(db, name, text, params), name);
@@ -413,9 +426,9 @@ export async function execLastId(
413
426
 
414
427
  /** `:execlastid` on a [`SyncQueryable`]. */
415
428
  export function execLastIdSync(
416
- db: SyncQueryable,
417
- name: string,
418
- text: string,
429
+ db : SyncQueryable,
430
+ name : string,
431
+ text : string,
419
432
  params: $ReadOnlyArray<SqlParam>,
420
433
  ): bigint {
421
434
  return insertedId(executed(db, name, text, params), name);
@@ -453,7 +466,7 @@ export function slice(values: $ReadOnlyArray<SqlParam>): Slice {
453
466
  * should match.
454
467
  */
455
468
  export function expand(
456
- parts: $ReadOnlyArray<string>,
469
+ parts : $ReadOnlyArray<string>,
457
470
  values: $ReadOnlyArray<SqlParam | Slice>,
458
471
  ): {| readonly text: string, readonly params: $ReadOnlyArray<SqlParam> |} {
459
472
  const params: Array<SqlParam> = [];
@@ -485,20 +498,110 @@ export function expand(
485
498
  * `VALUES ($1, $2)` is `tuple: ["(", ", ", ")"]`, `refs: [0, 1]`.
486
499
  */
487
500
  export type CopyPlan = {|
488
- readonly head: string,
501
+ readonly head : string,
489
502
  readonly tuple: $ReadOnlyArray<string>,
490
- readonly refs: $ReadOnlyArray<number>,
491
- readonly tail: string,
503
+ readonly refs : $ReadOnlyArray<number>,
504
+ readonly tail : string,
492
505
  |};
493
506
 
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
 
497
- function copyChunk(
498
- engine: Engine,
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(
499
578
  plan: CopyPlan,
500
- chunk: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
579
+ rows: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
501
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
+
600
+ function copyChunk(
601
+ engine: Engine,
602
+ plan : CopyPlan,
603
+ chunk : $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
604
+ name : string,
502
605
  ): {| readonly text: string, readonly params: $ReadOnlyArray<SqlParam> |} {
503
606
  const width = plan.refs.length;
504
607
  const params: Array<SqlParam> = [];
@@ -528,15 +631,18 @@ 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
- db: Queryable,
645
+ db : Queryable,
540
646
  name: string,
541
647
  plan: CopyPlan,
542
648
  rows: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
@@ -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;
@@ -562,7 +675,7 @@ export async function copyFrom(
562
675
 
563
676
  /** `:copyfrom` on a [`SyncQueryable`]. */
564
677
  export function copyFromSync(
565
- db: SyncQueryable,
678
+ db : SyncQueryable,
566
679
  name: string,
567
680
  plan: CopyPlan,
568
681
  rows: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
@@ -598,9 +711,9 @@ export function copyFromSync(
598
711
  * order without one, and the docs say so.
599
712
  */
600
713
  export async function batch<A, R>(
601
- db: Queryable,
714
+ db : Queryable,
602
715
  items: $ReadOnlyArray<A>,
603
- body: (q: Queryable, item: A) => Promise<R>,
716
+ body : (q: Queryable, item: A) => Promise<R>,
604
717
  ): Promise<Array<R>> {
605
718
  const each = async (q: Queryable): Promise<Array<R>> => {
606
719
  const out: Array<R> = [];
@@ -623,9 +736,9 @@ export async function batch<A, R>(
623
736
  * items still run in order, on this thread.
624
737
  */
625
738
  export function batchSync<A, R>(
626
- db: SyncQueryable,
739
+ db : SyncQueryable,
627
740
  items: $ReadOnlyArray<A>,
628
- body: (q: SyncQueryable, item: A) => R,
741
+ body : (q: SyncQueryable, item: A) => R,
629
742
  ): Array<R> {
630
743
  const each = (q: SyncQueryable): Array<R> => {
631
744
  const out: Array<R> = [];
@@ -646,27 +759,29 @@ export function batchSync<A, R>(
646
759
 
647
760
  /** How an adapter runs one statement on one connection. */
648
761
  export type Run = (
649
- text: string,
762
+ text : string,
650
763
  params: $ReadOnlyArray<SqlParam>,
651
- mode: QueryMode,
764
+ mode : QueryMode,
652
765
  ) => Promise<QueryResult>;
653
766
 
654
767
  /** [`Run`] for a driver that finishes the statement before returning. */
655
768
  export type RunSync = (
656
- text: string,
769
+ text : string,
657
770
  params: $ReadOnlyArray<SqlParam>,
658
- mode: QueryMode,
771
+ mode : QueryMode,
659
772
  ) => QueryResult;
660
773
 
661
774
  /** What [`transactionOn`] needs from an adapter. */
662
775
  export type Connection = {|
663
- readonly engine: Engine,
776
+ readonly engine : Engine,
664
777
  readonly maxParams: number,
665
- readonly run: Run,
778
+ readonly run : Run,
666
779
  /** Present when the driver can also run a statement on the calling thread. */
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
  /**
@@ -681,7 +796,7 @@ export type Connection = {|
681
796
  */
682
797
  export async function transactionOn<T>(
683
798
  connection: Connection,
684
- body: (tx: Queryable) => Promise<T>,
799
+ body : (tx: Queryable) => Promise<T>,
685
800
  ): Promise<T> {
686
801
  await connection.run(connection.begin ?? "BEGIN", [], "exec");
687
802
  return scoped(connection, 0, body, "COMMIT", "ROLLBACK");
@@ -689,10 +804,10 @@ export async function transactionOn<T>(
689
804
 
690
805
  async function scoped<T>(
691
806
  connection: Connection,
692
- depth: number,
693
- body: (tx: Queryable) => Promise<T>,
694
- commit: string,
695
- rollback: string,
807
+ depth : number,
808
+ body : (tx: Queryable) => Promise<T>,
809
+ commit : string,
810
+ rollback : string,
696
811
  ): Promise<T> {
697
812
  let open = true;
698
813
  const guarded: Run = (text, params, mode) => {
@@ -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
- engine: connection.engine,
826
+ engine : connection.engine,
706
827
  maxParams: connection.maxParams,
707
- query: guarded,
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");
@@ -773,12 +895,12 @@ export function transactionOnSync<T>(connection: Connection, body: (tx: SyncQuer
773
895
  }
774
896
 
775
897
  function scopedSync<T>(
776
- runSync: RunSync,
898
+ runSync : RunSync,
777
899
  connection: Connection,
778
- depth: number,
779
- body: (tx: SyncQueryable) => T,
780
- commit: string,
781
- rollback: string,
900
+ depth : number,
901
+ body : (tx: SyncQueryable) => T,
902
+ commit : string,
903
+ rollback : string,
782
904
  ): T {
783
905
  let open = true;
784
906
  const guarded: RunSync = (text, params, mode) => {
@@ -788,7 +910,7 @@ function scopedSync<T>(
788
910
  return runSync(text, params, mode);
789
911
  };
790
912
  const tx: SyncQueryable = {
791
- engine: connection.engine,
913
+ engine : connection.engine,
792
914
  maxParams: connection.maxParams,
793
915
  querySync: guarded,
794
916
  transactionSync: <U>(inner: (tx: SyncQueryable) => U): U => {
@@ -863,9 +985,9 @@ function connect(connection: Connection): Handle {
863
985
  const runSync = connection.runSync;
864
986
 
865
987
  const query = async (
866
- text: string,
988
+ text : string,
867
989
  params: $ReadOnlyArray<SqlParam>,
868
- mode: QueryMode,
990
+ mode : QueryMode,
869
991
  ): Promise<QueryResult> => {
870
992
  if (syncOpen) {
871
993
  unsupported("an asynchronous query while a synchronous transaction is open");
@@ -894,9 +1016,9 @@ function connect(connection: Connection): Handle {
894
1016
  }
895
1017
  };
896
1018
  const querySync = (
897
- text: string,
1019
+ text : string,
898
1020
  params: $ReadOnlyArray<SqlParam>,
899
- mode: QueryMode,
1021
+ mode : QueryMode,
900
1022
  ): QueryResult => {
901
1023
  const run =
902
1024
  connection.runSync ?? unsupported("a synchronous query on an asynchronous connection");
@@ -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");
@@ -930,12 +1065,13 @@ function connect(connection: Connection): Handle {
930
1065
  }
931
1066
  };
932
1067
  return {
933
- engine: connection.engine,
1068
+ engine : connection.engine,
934
1069
  maxParams: connection.maxParams,
935
1070
  query,
936
1071
  transaction,
937
1072
  querySync,
938
1073
  transactionSync,
1074
+ ...(copy === undefined ? null : { copy }),
939
1075
  };
940
1076
  }
941
1077
 
@@ -161,9 +161,9 @@ export function bytesParam(expected: string, value: mixed): Uint8Array {
161
161
 
162
162
  /** One of an enum's labels. */
163
163
  export function label<T extends string>(
164
- values: $ReadOnlyArray<T>,
164
+ values : $ReadOnlyArray<T>,
165
165
  expected: string,
166
- value: mixed,
166
+ value : mixed,
167
167
  ): T {
168
168
  const index = typeof value === "string" ? values.findIndex((item) => item === value) : -1;
169
169
  if (index < 0) {
@@ -174,9 +174,9 @@ export function label<T extends string>(
174
174
 
175
175
  /** Encode one of an enum's labels, refusing a string that is not one. */
176
176
  export function labelParam<T extends string>(
177
- values: $ReadOnlyArray<T>,
177
+ values : $ReadOnlyArray<T>,
178
178
  expected: string,
179
- value: mixed,
179
+ value : mixed,
180
180
  ): T {
181
181
  const index = typeof value === "string" ? values.findIndex((item) => item === value) : -1;
182
182
  if (index < 0) {
@@ -13,7 +13,7 @@ export const DEFAULT_STATEMENT_CACHE = 256;
13
13
 
14
14
  export function statementCache<S>(
15
15
  prepare: (text: string) => S,
16
- limit: number,
16
+ limit : number,
17
17
  ): (text: string) => S {
18
18
  const cache: Map<string, S> = new Map();
19
19
  return (text) => {