@uniflowed/sql 0.17.0 → 0.19.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 (3) hide show
  1. package/index.js +16 -2
  2. package/package.json +1 -1
  3. package/postgresql.js +39 -9
package/index.js CHANGED
@@ -85,7 +85,9 @@ export type SqlFailure =
85
85
  | {| readonly kind: "encode", readonly expected: string, readonly value: mixed |}
86
86
  | {| readonly kind: "shape", readonly expected: number, readonly actual: number |}
87
87
  | {| readonly kind: "closed" |}
88
- | {| readonly kind: "unsupported", readonly feature: string |};
88
+ | {| readonly kind: "unsupported", readonly feature: string |}
89
+ | {| readonly kind: "params", readonly width: number, readonly max: number |}
90
+ | {| readonly kind: "row", readonly index: number, readonly length: number |};
89
91
 
90
92
  /**
91
93
  * A value that did not fit its column, a row of the wrong width, or a
@@ -135,6 +137,10 @@ function explain(failure: SqlFailure, query: string | null): string {
135
137
  {kind: "closed"} => "the transaction has already committed or rolled back",
136
138
  {kind: "unsupported", feature: const feature} =>
137
139
  `${feature} is not supported by this adapter`,
140
+ {kind: "params", width: const width, max: const max} =>
141
+ `a :copyfrom row binds ${width} parameters and this adapter allows ${max}`,
142
+ {kind: "row", index: const index, length: const length} =>
143
+ `a :copyfrom row has ${length} fields and the statement reads index ${index}`,
138
144
  }
139
145
  );
140
146
  }
@@ -375,6 +381,10 @@ export async function copyFrom(
375
381
  return 0;
376
382
  }
377
383
  const width = plan.refs.length;
384
+ // One row is the smallest statement. Wider than `maxParams`, it cannot be sent.
385
+ if (width > db.maxParams) {
386
+ throw new SqlError({ kind: "params", width, max: db.maxParams }, name);
387
+ }
378
388
  const perStatement =
379
389
  width === 0 ? COPY_ROWS_WITHOUT_PARAMS : Math.max(1, Math.floor(db.maxParams / width));
380
390
  const insert = async (q: Queryable): Promise<number> => {
@@ -386,7 +396,11 @@ export async function copyFrom(
386
396
  for (const row of chunk) {
387
397
  let tuple = plan.tuple[0];
388
398
  for (let i = 0; i < width; i += 1) {
389
- params.push(row[plan.refs[i]]);
399
+ const index = plan.refs[i];
400
+ if (index >= row.length) {
401
+ throw new SqlError({ kind: "row", index, length: row.length }, name);
402
+ }
403
+ params.push(row[index]);
390
404
  tuple += (q.engine === "postgresql" ? `$${params.length}` : "?") + plan.tuple[i + 1];
391
405
  }
392
406
  tuples.push(tuple);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/sql",
3
- "version": "0.17.0",
3
+ "version": "0.19.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",
package/postgresql.js CHANGED
@@ -358,7 +358,21 @@ function instantToText(value: Date): string {
358
358
  if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
359
359
  return encodeError("timestamptz (a valid Date)", value);
360
360
  }
361
- return value.toISOString();
361
+ // ISO DateStyle. `toISOString()` is `T`/`Z` with a leading `+` past year
362
+ // 9999, and PostgreSQL rejects that form for BC, year 0, and year >= 10000.
363
+ // Year 0 is `0001 … BC` and year -43 is `0044 … BC`.
364
+ const year = value.getUTCFullYear();
365
+ const bc = year <= 0;
366
+ const printed = String(bc ? 1 - year : year);
367
+ const y = printed.length < 4 ? printed.padStart(4, "0") : printed;
368
+ const pad = (part: number): string => String(part).padStart(2, "0");
369
+ const millis = value.getUTCMilliseconds();
370
+ const fraction = millis === 0 ? "" : `.${String(millis).padStart(3, "0")}`;
371
+ return (
372
+ `${y}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())} ` +
373
+ `${pad(value.getUTCHours())}:${pad(value.getUTCMinutes())}:${pad(value.getUTCSeconds())}` +
374
+ `${fraction}+00${bc ? " BC" : ""}`
375
+ );
362
376
  }
363
377
 
364
378
  /** `timestamp with time zone`: an instant. Millisecond precision, which is a `Date`'s. */
@@ -404,10 +418,12 @@ export function enumeration<T extends string>(
404
418
  /**
405
419
  * Parse an array literal: `{1,2,NULL}`, `{{a,b},{c,d}}`, `{"a b","x\"y"}`.
406
420
  *
407
- * A literal with non-default bounds starts with a dimension decoration
408
- * (`[0:2]={…}`); the bounds are not part of the value and are dropped.
421
+ * `delimiter` is the type's typdelim: `,` for almost every type, `;` for
422
+ * `box`, whose literal contains commas. A literal with non-default bounds
423
+ * starts with a dimension decoration (`[0:2]={…}`); the bounds are not part
424
+ * of the value and are dropped.
409
425
  */
410
- export function parseArray(literal: string): $ReadOnlyArray<ArrayNode> {
426
+ export function parseArray(literal: string, delimiter: string = ","): $ReadOnlyArray<ArrayNode> {
411
427
  let at = 0;
412
428
  if (literal.startsWith("[")) {
413
429
  const equals = literal.indexOf("=");
@@ -456,7 +472,11 @@ export function parseArray(literal: string): $ReadOnlyArray<ArrayNode> {
456
472
  items.push(value);
457
473
  } else {
458
474
  const start = at;
459
- while (at < literal.length && literal.charAt(at) !== "," && literal.charAt(at) !== "}") {
475
+ while (
476
+ at < literal.length &&
477
+ literal.charAt(at) !== "}" &&
478
+ !literal.startsWith(delimiter, at)
479
+ ) {
460
480
  at += 1;
461
481
  }
462
482
  const raw = literal.slice(start, at).trim();
@@ -468,8 +488,8 @@ export function parseArray(literal: string): $ReadOnlyArray<ArrayNode> {
468
488
  while (literal.charAt(at) === " ") {
469
489
  at += 1;
470
490
  }
471
- if (literal.charAt(at) === ",") {
472
- at += 1;
491
+ if (literal.startsWith(delimiter, at)) {
492
+ at += delimiter.length;
473
493
  } else if (literal.charAt(at) === "}") {
474
494
  at += 1;
475
495
  return items;
@@ -486,6 +506,15 @@ export function parseArray(literal: string): $ReadOnlyArray<ArrayNode> {
486
506
  return root;
487
507
  }
488
508
 
509
+ // `box` literals contain commas, so PostgreSQL delimits `box[]` (every dimension) with `;`.
510
+ function arrayDelimiter(sqlType: string): string {
511
+ let base = sqlType;
512
+ while (base.endsWith("[]")) {
513
+ base = base.slice(0, -2);
514
+ }
515
+ return base === "box" ? ";" : ",";
516
+ }
517
+
489
518
  /**
490
519
  * An array of `element`. Nest it for more dimensions: `array(array(int4))` is
491
520
  * `int4[][]`.
@@ -498,6 +527,7 @@ export function parseArray(literal: string): $ReadOnlyArray<ArrayNode> {
498
527
  */
499
528
  export function array<T>(element: Codec<T>): Codec<$ReadOnlyArray<T>> {
500
529
  const sqlType = `${element.sqlType}[]`;
530
+ const delimiter = arrayDelimiter(element.sqlType);
501
531
  const fromNode = (node: ArrayNode): $ReadOnlyArray<T> => {
502
532
  if (node === null || typeof node === "string") {
503
533
  return decodeError(`${sqlType} with ${element.depth + 1} dimension(s)`, node);
@@ -508,7 +538,7 @@ export function array<T>(element: Codec<T>): Codec<$ReadOnlyArray<T>> {
508
538
  if (!Array.isArray(value)) {
509
539
  return encodeError(`${sqlType} (an array)`, value);
510
540
  }
511
- return `{${value.map(element.literal).join(",")}}`;
541
+ return `{${value.map(element.literal).join(delimiter)}}`;
512
542
  };
513
543
  return {
514
544
  sqlType,
@@ -516,7 +546,7 @@ export function array<T>(element: Codec<T>): Codec<$ReadOnlyArray<T>> {
516
546
  if (typeof value !== "string") {
517
547
  return decodeError(sqlType, value);
518
548
  }
519
- const root = parseArray(value);
549
+ const root = parseArray(value, delimiter);
520
550
  // An empty array has no dimensions at all, whatever the column says.
521
551
  return root.length === 0 ? [] : fromNode(root);
522
552
  },