@uniflowed/sql 0.14.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/better-sqlite3.js +80 -0
- package/adapters/bun-sqlite.js +73 -0
- package/adapters/d1.js +71 -0
- package/adapters/mysql2.js +140 -0
- package/adapters/node-sqlite.js +93 -0
- package/adapters/pg.js +88 -0
- package/adapters/pglite.js +72 -0
- package/adapters/postgres.js +111 -0
- package/index.js +562 -0
- package/internal/shared.js +191 -0
- package/internal/statements.js +55 -0
- package/mysql.js +231 -0
- package/package.json +33 -0
- package/postgresql.js +558 -0
- package/sqlite.js +178 -0
package/postgresql.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/sql/postgresql`: PostgreSQL's text format, both ways.
|
|
4
|
+
//
|
|
5
|
+
// Every PostgreSQL adapter returns column values exactly as the server printed
|
|
6
|
+
// them — `pg` with its type parsers switched off, `postgres` through `.raw()`,
|
|
7
|
+
// PGlite with identity parsers — and sends parameters as text with no declared
|
|
8
|
+
// type, so the server infers each one from the statement. That makes this
|
|
9
|
+
// module the single place a value's representation is decided, and the same
|
|
10
|
+
// for every driver.
|
|
11
|
+
//
|
|
12
|
+
// The text format assumes the server's defaults for `DateStyle` (ISO) and
|
|
13
|
+
// `bytea_output` (hex, though escape is read too). `IntervalStyle` does not
|
|
14
|
+
// matter: an interval stays a string.
|
|
15
|
+
|
|
16
|
+
import type { JsonValue } from "./index.js";
|
|
17
|
+
import { decodeError, encodeError } from "./index.js";
|
|
18
|
+
import {
|
|
19
|
+
bigintFrom,
|
|
20
|
+
enumName,
|
|
21
|
+
integerFromText,
|
|
22
|
+
jsonFromText,
|
|
23
|
+
jsonParam,
|
|
24
|
+
label,
|
|
25
|
+
labelParam,
|
|
26
|
+
stringParam,
|
|
27
|
+
} from "./internal/shared.js";
|
|
28
|
+
|
|
29
|
+
/** A node of a parsed array literal: an element's text, `NULL`, or a sub-array. */
|
|
30
|
+
export type ArrayNode = string | null | $ReadOnlyArray<ArrayNode>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How one SQL type becomes a Flow value and back.
|
|
34
|
+
*
|
|
35
|
+
* `decode` reads a column; `encode` writes a parameter. `element` and
|
|
36
|
+
* `literal` are the same two directions for a value inside an array literal,
|
|
37
|
+
* which is how [`array`] nests without re-parsing.
|
|
38
|
+
*/
|
|
39
|
+
export type Codec<T> = {|
|
|
40
|
+
/** For messages: `int8`, `text[]`. */
|
|
41
|
+
readonly sqlType: string,
|
|
42
|
+
readonly decode: (value: mixed) => T,
|
|
43
|
+
readonly encode: (value: T) => string,
|
|
44
|
+
readonly element: (node: ArrayNode) => T,
|
|
45
|
+
/** The value as it appears inside `{…}`: quoted, or a nested literal. */
|
|
46
|
+
readonly literal: (value: T) => string,
|
|
47
|
+
/** How many array dimensions this codec reads; 0 for a scalar. */
|
|
48
|
+
readonly depth: number,
|
|
49
|
+
|};
|
|
50
|
+
|
|
51
|
+
function quote(text: string): string {
|
|
52
|
+
return `"${text.replace(/[\\"]/g, "\\$&")}"`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A scalar codec from its two text conversions. */
|
|
56
|
+
export function scalar<T>(
|
|
57
|
+
sqlType: string,
|
|
58
|
+
fromText: (text: string) => T,
|
|
59
|
+
toText: (value: T) => string,
|
|
60
|
+
): Codec<T> {
|
|
61
|
+
const decode = (value: mixed): T => {
|
|
62
|
+
if (typeof value !== "string") {
|
|
63
|
+
return decodeError(sqlType, value);
|
|
64
|
+
}
|
|
65
|
+
return fromText(value);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
sqlType,
|
|
69
|
+
decode,
|
|
70
|
+
encode: toText,
|
|
71
|
+
element: (node) => {
|
|
72
|
+
if (node === null) {
|
|
73
|
+
return decodeError(`a non-null ${sqlType} array element`, node);
|
|
74
|
+
}
|
|
75
|
+
if (typeof node !== "string") {
|
|
76
|
+
return decodeError(`${sqlType}, not a nested array`, node);
|
|
77
|
+
}
|
|
78
|
+
return fromText(node);
|
|
79
|
+
},
|
|
80
|
+
literal: (value) => quote(toText(value)),
|
|
81
|
+
depth: 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Numbers.
|
|
87
|
+
|
|
88
|
+
const INT_RANGE = {
|
|
89
|
+
int2: [-32768, 32767],
|
|
90
|
+
int4: [-2147483648, 2147483647],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function boundedInteger(sqlType: "int2" | "int4"): Codec<number> {
|
|
94
|
+
const [min, max] = INT_RANGE[sqlType];
|
|
95
|
+
return scalar(
|
|
96
|
+
sqlType,
|
|
97
|
+
(text) => integerFromText(sqlType, text),
|
|
98
|
+
(value) => {
|
|
99
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
100
|
+
return encodeError(`${sqlType} (an integer from ${min} to ${max})`, value);
|
|
101
|
+
}
|
|
102
|
+
return String(value);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `smallint`, `smallserial`. */
|
|
108
|
+
export const int2: Codec<number> = boundedInteger("int2");
|
|
109
|
+
|
|
110
|
+
/** `integer`, `serial`, `oid` and the other 32-bit integers. */
|
|
111
|
+
export const int4: Codec<number> = boundedInteger("int4");
|
|
112
|
+
|
|
113
|
+
/** `oid`, `xid`, `cid` and the `reg*` types: unsigned 32-bit. */
|
|
114
|
+
export const oid: Codec<number> = scalar(
|
|
115
|
+
"oid",
|
|
116
|
+
(text) => integerFromText("oid", text),
|
|
117
|
+
(value) => {
|
|
118
|
+
if (!Number.isInteger(value) || value < 0 || value > 4294967295) {
|
|
119
|
+
return encodeError("oid (an integer from 0 to 4294967295)", value);
|
|
120
|
+
}
|
|
121
|
+
return String(value);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/** `bigint`, `bigserial`: every value, exactly. */
|
|
126
|
+
export const int8: Codec<bigint> = scalar(
|
|
127
|
+
"int8",
|
|
128
|
+
(text) => bigintFrom("int8", text),
|
|
129
|
+
(value) => {
|
|
130
|
+
if (typeof value !== "bigint") {
|
|
131
|
+
return encodeError("int8 (a bigint)", value);
|
|
132
|
+
}
|
|
133
|
+
return String(value);
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/** `bigint` as a `number`, for a table that opted in with `int8: "number"`; throws past 2^53. */
|
|
138
|
+
export const int8AsNumber: Codec<number> = scalar(
|
|
139
|
+
"int8",
|
|
140
|
+
(text) => integerFromText("int8", text),
|
|
141
|
+
(value) => {
|
|
142
|
+
if (!Number.isSafeInteger(value)) {
|
|
143
|
+
return encodeError("int8 (an integer within ±2^53)", value);
|
|
144
|
+
}
|
|
145
|
+
return String(value);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const DIGITS = /^[-+]?\d+$/;
|
|
150
|
+
|
|
151
|
+
/** `bigint` as its digits, for `int8: "string"`. */
|
|
152
|
+
export const int8AsString: Codec<string> = scalar(
|
|
153
|
+
"int8",
|
|
154
|
+
(text) => text,
|
|
155
|
+
(value) => {
|
|
156
|
+
if (typeof value !== "string" || !DIGITS.test(value)) {
|
|
157
|
+
return encodeError("int8 (a string of digits)", value);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const FLOAT = /^(?:[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?|NaN|-?Infinity)$/;
|
|
164
|
+
|
|
165
|
+
function floatCodec(sqlType: string): Codec<number> {
|
|
166
|
+
return scalar(
|
|
167
|
+
sqlType,
|
|
168
|
+
(text) => {
|
|
169
|
+
if (!FLOAT.test(text)) {
|
|
170
|
+
return decodeError(sqlType, text);
|
|
171
|
+
}
|
|
172
|
+
return Number(text);
|
|
173
|
+
},
|
|
174
|
+
(value) => {
|
|
175
|
+
if (typeof value !== "number") {
|
|
176
|
+
return encodeError(`${sqlType} (a number)`, value);
|
|
177
|
+
}
|
|
178
|
+
return String(value);
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** `real`. */
|
|
184
|
+
export const float4: Codec<number> = floatCodec("float4");
|
|
185
|
+
|
|
186
|
+
/** `double precision`. */
|
|
187
|
+
export const float8: Codec<number> = floatCodec("float8");
|
|
188
|
+
|
|
189
|
+
const NUMERIC = /^(?:[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?|NaN|-?Infinity)$/;
|
|
190
|
+
|
|
191
|
+
/** `numeric`/`decimal` as its exact decimal string. */
|
|
192
|
+
export const numeric: Codec<string> = scalar(
|
|
193
|
+
"numeric",
|
|
194
|
+
(text) => text,
|
|
195
|
+
(value) => {
|
|
196
|
+
if (typeof value !== "string" || !NUMERIC.test(value)) {
|
|
197
|
+
return encodeError("numeric (a decimal string)", value);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
/** `numeric` as a `number`, for `numeric: "number"`. Rounds; that is what opting in means. */
|
|
204
|
+
export const numericAsNumber: Codec<number> = floatCodec("numeric");
|
|
205
|
+
|
|
206
|
+
/** `money`, in the server's `lc_monetary` format (`$1,234.50`), unchanged. */
|
|
207
|
+
export const money: Codec<string> = scalar(
|
|
208
|
+
"money",
|
|
209
|
+
(text) => text,
|
|
210
|
+
(value) => stringParam("money (a string)", value),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Everything else scalar.
|
|
215
|
+
|
|
216
|
+
/** `boolean`. */
|
|
217
|
+
export const bool: Codec<boolean> = scalar(
|
|
218
|
+
"bool",
|
|
219
|
+
(text) => {
|
|
220
|
+
if (text === "t") {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (text === "f") {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return decodeError("bool", text);
|
|
227
|
+
},
|
|
228
|
+
(value) => {
|
|
229
|
+
if (typeof value !== "boolean") {
|
|
230
|
+
return encodeError("bool (a boolean)", value);
|
|
231
|
+
}
|
|
232
|
+
return value ? "t" : "f";
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Any type whose text is the value: `text`, `varchar`, `uuid`, `inet`,
|
|
238
|
+
* `interval`, `time`, ranges, and types uf has no better representation for.
|
|
239
|
+
*/
|
|
240
|
+
export function text(sqlType: string): Codec<string> {
|
|
241
|
+
return scalar(
|
|
242
|
+
sqlType,
|
|
243
|
+
(value) => value,
|
|
244
|
+
(value) => stringParam(`${sqlType} (a string)`, value),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** `text` and the character types. */
|
|
249
|
+
export const string: Codec<string> = text("text");
|
|
250
|
+
|
|
251
|
+
const HEX = /^[0-9a-fA-F]*$/;
|
|
252
|
+
|
|
253
|
+
function bytesFromText(value: string): Uint8Array {
|
|
254
|
+
if (value.startsWith("\\x")) {
|
|
255
|
+
const hex = value.slice(2);
|
|
256
|
+
if (hex.length % 2 !== 0 || !HEX.test(hex)) {
|
|
257
|
+
return decodeError("bytea in hex format", value);
|
|
258
|
+
}
|
|
259
|
+
const out = new Uint8Array(hex.length / 2);
|
|
260
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
261
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
// `bytea_output = escape`: printable bytes as themselves, a backslash as two,
|
|
266
|
+
// and everything else as a backslash and three octal digits.
|
|
267
|
+
const out: Array<number> = [];
|
|
268
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
269
|
+
const code = value.charCodeAt(i);
|
|
270
|
+
if (code === 92) {
|
|
271
|
+
if (value[i + 1] === "\\") {
|
|
272
|
+
out.push(92);
|
|
273
|
+
i += 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const octal = value.slice(i + 1, i + 4);
|
|
277
|
+
if (!/^[0-3][0-7][0-7]$/.test(octal)) {
|
|
278
|
+
return decodeError("bytea in escape format", value);
|
|
279
|
+
}
|
|
280
|
+
out.push(parseInt(octal, 8));
|
|
281
|
+
i += 3;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (code > 255) {
|
|
285
|
+
return decodeError("bytea", value);
|
|
286
|
+
}
|
|
287
|
+
out.push(code);
|
|
288
|
+
}
|
|
289
|
+
return Uint8Array.from(out);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function bytesToText(value: Uint8Array): string {
|
|
293
|
+
if (!(value instanceof Uint8Array)) {
|
|
294
|
+
return encodeError("bytea (a Uint8Array)", value);
|
|
295
|
+
}
|
|
296
|
+
let hex = "\\x";
|
|
297
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
298
|
+
hex += value[i].toString(16).padStart(2, "0");
|
|
299
|
+
}
|
|
300
|
+
return hex;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** `bytea`. */
|
|
304
|
+
export const bytea: Codec<Uint8Array> = scalar("bytea", bytesFromText, bytesToText);
|
|
305
|
+
|
|
306
|
+
/** `json` and `jsonb`. */
|
|
307
|
+
export const json: Codec<JsonValue> = scalar(
|
|
308
|
+
"json",
|
|
309
|
+
(value) => jsonFromText(value),
|
|
310
|
+
(value) => jsonParam(value),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
/** `date`: `YYYY-MM-DD`, or `infinity`. A calendar date has no time zone, so it is not a `Date`. */
|
|
314
|
+
export const date: Codec<string> = text("date");
|
|
315
|
+
|
|
316
|
+
/** `timestamp without time zone`: the wall-clock time as the server printed it. */
|
|
317
|
+
export const timestamp: Codec<string> = text("timestamp");
|
|
318
|
+
|
|
319
|
+
/** `timestamptz` as the server printed it, microseconds and all, for `timestamptz: "string"`. */
|
|
320
|
+
export const timestamptzAsString: Codec<string> = text("timestamptz");
|
|
321
|
+
|
|
322
|
+
const TIMESTAMPTZ =
|
|
323
|
+
/^(\d{4,})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d{1,6}))?([+-])(\d\d)(?::?(\d\d))?(?::?(\d\d))?( BC)?$/;
|
|
324
|
+
|
|
325
|
+
function instantFromText(text: string): Date {
|
|
326
|
+
const parts = TIMESTAMPTZ.exec(text);
|
|
327
|
+
if (parts === null) {
|
|
328
|
+
return decodeError(
|
|
329
|
+
text === "infinity" || text === "-infinity"
|
|
330
|
+
? 'a finite timestamptz (use timestamptz: "string" to read infinity)'
|
|
331
|
+
: "timestamptz in ISO DateStyle",
|
|
332
|
+
text,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const year = Number(parts[1]);
|
|
336
|
+
const fraction = parts[7] ?? "";
|
|
337
|
+
const millis = fraction === "" ? 0 : Number(fraction.padEnd(3, "0").slice(0, 3));
|
|
338
|
+
const sign = parts[8] === "-" ? -1 : 1;
|
|
339
|
+
const offsetSeconds =
|
|
340
|
+
sign * (Number(parts[9]) * 3600 + Number(parts[10] ?? "0") * 60 + Number(parts[11] ?? "0"));
|
|
341
|
+
const instant = new Date(0);
|
|
342
|
+
// `setUTCFullYear` rather than `Date.UTC`, which reads years 0 to 99 as
|
|
343
|
+
// 1900 to 1999. Year 1 BC is year 0 in ISO 8601.
|
|
344
|
+
instant.setUTCFullYear(
|
|
345
|
+
parts[12] === undefined ? year : 1 - year,
|
|
346
|
+
Number(parts[2]) - 1,
|
|
347
|
+
Number(parts[3]),
|
|
348
|
+
);
|
|
349
|
+
instant.setUTCHours(Number(parts[4]), Number(parts[5]), Number(parts[6]), millis);
|
|
350
|
+
const time = instant.getTime() - offsetSeconds * 1000;
|
|
351
|
+
if (!Number.isFinite(time)) {
|
|
352
|
+
return decodeError("timestamptz within the range of a Date", text);
|
|
353
|
+
}
|
|
354
|
+
return new Date(time);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function instantToText(value: Date): string {
|
|
358
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
359
|
+
return encodeError("timestamptz (a valid Date)", value);
|
|
360
|
+
}
|
|
361
|
+
return value.toISOString();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** `timestamp with time zone`: an instant. Millisecond precision, which is a `Date`'s. */
|
|
365
|
+
export const timestamptz: Codec<Date> = scalar("timestamptz", instantFromText, instantToText);
|
|
366
|
+
|
|
367
|
+
/** A value sqlc could not type (`any`, `anyelement`, …): whatever the server printed. */
|
|
368
|
+
export const unknown: Codec<mixed> = {
|
|
369
|
+
sqlType: "unknown",
|
|
370
|
+
decode: (value) => value,
|
|
371
|
+
encode: (value) => {
|
|
372
|
+
if (typeof value === "string") {
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
376
|
+
return String(value);
|
|
377
|
+
}
|
|
378
|
+
if (value instanceof Date) {
|
|
379
|
+
return instantToText(value);
|
|
380
|
+
}
|
|
381
|
+
return jsonParam(value);
|
|
382
|
+
},
|
|
383
|
+
element: (node) => node,
|
|
384
|
+
literal: (value) => (value === null ? "NULL" : quote(String(value))),
|
|
385
|
+
depth: 0,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Enums and arrays.
|
|
390
|
+
|
|
391
|
+
/** A codec for a `CREATE TYPE … AS ENUM`, from its labels in declaration order. */
|
|
392
|
+
export function enumeration<T extends string>(
|
|
393
|
+
sqlType: string,
|
|
394
|
+
values: $ReadOnlyArray<T>,
|
|
395
|
+
): Codec<T> {
|
|
396
|
+
const expected = `${sqlType} (${enumName(values)})`;
|
|
397
|
+
return scalar(
|
|
398
|
+
sqlType,
|
|
399
|
+
(value) => label(values, expected, value),
|
|
400
|
+
(value) => labelParam(values, expected, value),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Parse an array literal: `{1,2,NULL}`, `{{a,b},{c,d}}`, `{"a b","x\"y"}`.
|
|
406
|
+
*
|
|
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.
|
|
409
|
+
*/
|
|
410
|
+
export function parseArray(literal: string): $ReadOnlyArray<ArrayNode> {
|
|
411
|
+
let at = 0;
|
|
412
|
+
if (literal.startsWith("[")) {
|
|
413
|
+
const equals = literal.indexOf("=");
|
|
414
|
+
if (equals < 0) {
|
|
415
|
+
return decodeError("an array literal", literal);
|
|
416
|
+
}
|
|
417
|
+
at = equals + 1;
|
|
418
|
+
}
|
|
419
|
+
const fail = (): empty => decodeError("an array literal", literal);
|
|
420
|
+
const parseLevel = (): $ReadOnlyArray<ArrayNode> => {
|
|
421
|
+
if (literal.charAt(at) !== "{") {
|
|
422
|
+
return fail();
|
|
423
|
+
}
|
|
424
|
+
at += 1;
|
|
425
|
+
const items: Array<ArrayNode> = [];
|
|
426
|
+
if (literal.charAt(at) === "}") {
|
|
427
|
+
at += 1;
|
|
428
|
+
return items;
|
|
429
|
+
}
|
|
430
|
+
for (;;) {
|
|
431
|
+
while (literal.charAt(at) === " ") {
|
|
432
|
+
at += 1;
|
|
433
|
+
}
|
|
434
|
+
const head = literal.charAt(at);
|
|
435
|
+
if (head === "{") {
|
|
436
|
+
items.push(parseLevel());
|
|
437
|
+
} else if (head === '"') {
|
|
438
|
+
at += 1;
|
|
439
|
+
let value = "";
|
|
440
|
+
for (;;) {
|
|
441
|
+
const ch = literal.charAt(at);
|
|
442
|
+
if (at >= literal.length) {
|
|
443
|
+
return fail();
|
|
444
|
+
}
|
|
445
|
+
if (ch === "\\") {
|
|
446
|
+
value += literal.charAt(at + 1);
|
|
447
|
+
at += 2;
|
|
448
|
+
} else if (ch === '"') {
|
|
449
|
+
at += 1;
|
|
450
|
+
break;
|
|
451
|
+
} else {
|
|
452
|
+
value += ch;
|
|
453
|
+
at += 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
items.push(value);
|
|
457
|
+
} else {
|
|
458
|
+
const start = at;
|
|
459
|
+
while (at < literal.length && literal.charAt(at) !== "," && literal.charAt(at) !== "}") {
|
|
460
|
+
at += 1;
|
|
461
|
+
}
|
|
462
|
+
const raw = literal.slice(start, at).trim();
|
|
463
|
+
if (raw === "") {
|
|
464
|
+
return fail();
|
|
465
|
+
}
|
|
466
|
+
items.push(raw.toUpperCase() === "NULL" ? null : raw);
|
|
467
|
+
}
|
|
468
|
+
while (literal.charAt(at) === " ") {
|
|
469
|
+
at += 1;
|
|
470
|
+
}
|
|
471
|
+
if (literal.charAt(at) === ",") {
|
|
472
|
+
at += 1;
|
|
473
|
+
} else if (literal.charAt(at) === "}") {
|
|
474
|
+
at += 1;
|
|
475
|
+
return items;
|
|
476
|
+
} else {
|
|
477
|
+
return fail();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return fail();
|
|
481
|
+
};
|
|
482
|
+
const root = parseLevel();
|
|
483
|
+
if (at !== literal.length) {
|
|
484
|
+
return fail();
|
|
485
|
+
}
|
|
486
|
+
return root;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* An array of `element`. Nest it for more dimensions: `array(array(int4))` is
|
|
491
|
+
* `int4[][]`.
|
|
492
|
+
*
|
|
493
|
+
* A `NULL` inside the array throws. sqlc does not tell a plugin whether a
|
|
494
|
+
* column's elements may be null, and typing them as `T | null` everywhere would
|
|
495
|
+
* make every `text[] NOT NULL` of tags awkward to use to guard against a case
|
|
496
|
+
* most schemas never produce; typing them `T` and returning `null` would be a
|
|
497
|
+
* lie. So the type is `T`, and the lie is refused at the boundary.
|
|
498
|
+
*/
|
|
499
|
+
export function array<T>(element: Codec<T>): Codec<$ReadOnlyArray<T>> {
|
|
500
|
+
const sqlType = `${element.sqlType}[]`;
|
|
501
|
+
const fromNode = (node: ArrayNode): $ReadOnlyArray<T> => {
|
|
502
|
+
if (node === null || typeof node === "string") {
|
|
503
|
+
return decodeError(`${sqlType} with ${element.depth + 1} dimension(s)`, node);
|
|
504
|
+
}
|
|
505
|
+
return node.map(element.element);
|
|
506
|
+
};
|
|
507
|
+
const toLiteral = (value: $ReadOnlyArray<T>): string => {
|
|
508
|
+
if (!Array.isArray(value)) {
|
|
509
|
+
return encodeError(`${sqlType} (an array)`, value);
|
|
510
|
+
}
|
|
511
|
+
return `{${value.map(element.literal).join(",")}}`;
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
sqlType,
|
|
515
|
+
decode: (value) => {
|
|
516
|
+
if (typeof value !== "string") {
|
|
517
|
+
return decodeError(sqlType, value);
|
|
518
|
+
}
|
|
519
|
+
const root = parseArray(value);
|
|
520
|
+
// An empty array has no dimensions at all, whatever the column says.
|
|
521
|
+
return root.length === 0 ? [] : fromNode(root);
|
|
522
|
+
},
|
|
523
|
+
encode: toLiteral,
|
|
524
|
+
element: (node) => (Array.isArray(node) && node.length === 0 ? [] : fromNode(node)),
|
|
525
|
+
literal: toLiteral,
|
|
526
|
+
depth: element.depth + 1,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* An override's codec: `decode` narrows what `codec` read, `encode` widens a
|
|
532
|
+
* value back to what `codec` writes. Generated for `overrides` in the plugin
|
|
533
|
+
* options, where `decode` is the application's own check.
|
|
534
|
+
*/
|
|
535
|
+
export function map<T, U>(
|
|
536
|
+
codec: Codec<T>,
|
|
537
|
+
decode: (value: T) => U,
|
|
538
|
+
encode: (value: U) => T,
|
|
539
|
+
): Codec<U> {
|
|
540
|
+
return {
|
|
541
|
+
sqlType: codec.sqlType,
|
|
542
|
+
decode: (value) => decode(codec.decode(value)),
|
|
543
|
+
encode: (value) => codec.encode(encode(value)),
|
|
544
|
+
element: (node) => decode(codec.element(node)),
|
|
545
|
+
literal: (value) => codec.literal(encode(value)),
|
|
546
|
+
depth: codec.depth,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** A `NULL`able value: the generated code's `x === null ? null : codec.decode(x)`, for arrays of codecs that need it. */
|
|
551
|
+
export function nullable<T>(codec: Codec<T>, value: mixed): T | null {
|
|
552
|
+
return value === null ? null : codec.decode(value);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Encode a nullable parameter. */
|
|
556
|
+
export function param<T>(codec: Codec<T>, value: T | null): string | null {
|
|
557
|
+
return value === null ? null : codec.encode(value);
|
|
558
|
+
}
|