@bockit/sequel 0.0.0-stage → 0.2.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 (104) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +227 -2
  3. package/THIRD_PARTY_NOTICES.md +268 -0
  4. package/binaries.json +106 -0
  5. package/dist/binaries/cache.d.ts +6 -0
  6. package/dist/binaries/cache.js +36 -0
  7. package/dist/binaries/ensure-binary.d.ts +10 -0
  8. package/dist/binaries/ensure-binary.js +85 -0
  9. package/dist/binaries/manifest.d.ts +30 -0
  10. package/dist/binaries/manifest.js +112 -0
  11. package/dist/binaries/platform.d.ts +3 -0
  12. package/dist/binaries/platform.js +10 -0
  13. package/dist/binaries/unpack.d.ts +2 -0
  14. package/dist/binaries/unpack.js +35 -0
  15. package/dist/binaries/verify.d.ts +2 -0
  16. package/dist/binaries/verify.js +16 -0
  17. package/dist/check.d.ts +9 -0
  18. package/dist/check.js +178 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +339 -0
  21. package/dist/config/define-config.d.ts +24 -0
  22. package/dist/config/define-config.js +4 -0
  23. package/dist/config/load-config.d.ts +6 -0
  24. package/dist/config/load-config.js +34 -0
  25. package/dist/config/resolve-config.d.ts +35 -0
  26. package/dist/config/resolve-config.js +159 -0
  27. package/dist/db/connection.d.ts +6 -0
  28. package/dist/db/connection.js +38 -0
  29. package/dist/db/shadow.d.ts +11 -0
  30. package/dist/db/shadow.js +68 -0
  31. package/dist/engine/client.d.ts +24 -0
  32. package/dist/engine/client.js +123 -0
  33. package/dist/engine/diff.d.ts +12 -0
  34. package/dist/engine/diff.js +51 -0
  35. package/dist/errors.d.ts +3 -0
  36. package/dist/errors.js +5 -0
  37. package/dist/extras/snapshot.d.ts +3 -0
  38. package/dist/extras/snapshot.js +99 -0
  39. package/dist/files.d.ts +1 -0
  40. package/dist/files.js +15 -0
  41. package/dist/generate/generate-modules.d.ts +17 -0
  42. package/dist/generate/generate-modules.js +97 -0
  43. package/dist/generate/modules.d.ts +16 -0
  44. package/dist/generate/modules.js +118 -0
  45. package/dist/generate/pg-dump.d.ts +8 -0
  46. package/dist/generate/pg-dump.js +66 -0
  47. package/dist/generate/plugin-wrapper.d.ts +3 -0
  48. package/dist/generate/plugin-wrapper.js +20 -0
  49. package/dist/generate/sqlc.d.ts +27 -0
  50. package/dist/generate/sqlc.js +113 -0
  51. package/dist/generate/watch.d.ts +16 -0
  52. package/dist/generate/watch.js +73 -0
  53. package/dist/generator/generate.d.ts +21 -0
  54. package/dist/generator/generate.js +440 -0
  55. package/dist/generator/keys.d.ts +4 -0
  56. package/dist/generator/keys.js +24 -0
  57. package/dist/generator/lexer.d.ts +12 -0
  58. package/dist/generator/lexer.js +210 -0
  59. package/dist/generator/main.d.ts +1 -0
  60. package/dist/generator/main.js +12 -0
  61. package/dist/generator/pg-array-helper.d.ts +2 -0
  62. package/dist/generator/pg-array-helper.js +53 -0
  63. package/dist/generator/plugin.d.ts +6 -0
  64. package/dist/generator/plugin.js +26 -0
  65. package/dist/generator/request.d.ts +57 -0
  66. package/dist/generator/request.js +96 -0
  67. package/dist/generator/select-list.d.ts +27 -0
  68. package/dist/generator/select-list.js +580 -0
  69. package/dist/generator/strings.d.ts +2 -0
  70. package/dist/generator/strings.js +34 -0
  71. package/dist/generator/types.d.ts +23 -0
  72. package/dist/generator/types.js +216 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +2 -0
  75. package/dist/migrate/adopt-prisma.d.ts +23 -0
  76. package/dist/migrate/adopt-prisma.js +119 -0
  77. package/dist/migrate/deploy.d.ts +7 -0
  78. package/dist/migrate/deploy.js +23 -0
  79. package/dist/migrate/drift.d.ts +10 -0
  80. package/dist/migrate/drift.js +36 -0
  81. package/dist/migrate/files.d.ts +19 -0
  82. package/dist/migrate/files.js +97 -0
  83. package/dist/migrate/history.d.ts +20 -0
  84. package/dist/migrate/history.js +54 -0
  85. package/dist/migrate/lint.d.ts +21 -0
  86. package/dist/migrate/lint.js +153 -0
  87. package/dist/migrate/new.d.ts +13 -0
  88. package/dist/migrate/new.js +41 -0
  89. package/dist/migrate/runner.d.ts +28 -0
  90. package/dist/migrate/runner.js +124 -0
  91. package/dist/migrate/sql-text.d.ts +33 -0
  92. package/dist/migrate/sql-text.js +233 -0
  93. package/dist/schema/schema.d.ts +16 -0
  94. package/dist/schema/schema.js +111 -0
  95. package/dist/schema/unsupported.d.ts +3 -0
  96. package/dist/schema/unsupported.js +36 -0
  97. package/dist/schema/wasm.d.ts +8 -0
  98. package/dist/schema/wasm.js +32 -0
  99. package/package.json +50 -4
  100. package/vendor/prisma-schema-wasm/LICENSE +201 -0
  101. package/vendor/prisma-schema-wasm/README.md +32 -0
  102. package/vendor/prisma-schema-wasm/package.json +3 -0
  103. package/vendor/prisma-schema-wasm/prisma_schema_build.js +539 -0
  104. package/vendor/prisma-schema-wasm/prisma_schema_build_bg.wasm +0 -0
@@ -0,0 +1,73 @@
1
+ import { existsSync, watch } from 'node:fs';
2
+ // TECH-SPEC §9's watch: reruns are debounced, and never overlap. A change during a run
3
+ // schedules one more run after it.
4
+ export function createRerunScheduler(options) {
5
+ const debounceMs = options.debounceMs ?? 200;
6
+ let timer;
7
+ let running;
8
+ let again = false;
9
+ const fire = async () => {
10
+ if (running !== undefined) {
11
+ again = true;
12
+ return;
13
+ }
14
+ running = (async () => {
15
+ try {
16
+ await options.run();
17
+ }
18
+ catch (error) {
19
+ options.onError(error);
20
+ }
21
+ })();
22
+ await running;
23
+ running = undefined;
24
+ if (again) {
25
+ again = false;
26
+ trigger();
27
+ }
28
+ };
29
+ const trigger = () => {
30
+ clearTimeout(timer);
31
+ timer = setTimeout(() => void fire(), debounceMs);
32
+ };
33
+ return {
34
+ trigger,
35
+ // Resolves once nothing is scheduled or running.
36
+ async settled() {
37
+ while (running !== undefined || again)
38
+ await (running ?? Promise.resolve());
39
+ },
40
+ stop() {
41
+ clearTimeout(timer);
42
+ },
43
+ };
44
+ }
45
+ // Runs once, then again whenever a `.sql` file under the folders changes, until `signal`
46
+ // aborts. Only `.sql` changes count, so the modules a run writes don't trigger another.
47
+ export async function watchAndRerun(options) {
48
+ const scheduler = createRerunScheduler(options);
49
+ try {
50
+ await options.run();
51
+ }
52
+ catch (error) {
53
+ options.onError(error);
54
+ }
55
+ const watchers = [];
56
+ for (const folder of options.folders) {
57
+ if (!existsSync(folder))
58
+ continue;
59
+ watchers.push(watch(folder, { recursive: true }, (_event, filename) => {
60
+ if (filename !== null && filename.endsWith('.sql'))
61
+ scheduler.trigger();
62
+ }));
63
+ }
64
+ await new Promise((resolve) => {
65
+ if (options.signal.aborted)
66
+ resolve();
67
+ options.signal.addEventListener('abort', () => resolve(), { once: true });
68
+ });
69
+ scheduler.stop();
70
+ for (const watcher of watchers)
71
+ watcher.close();
72
+ await scheduler.settled();
73
+ }
@@ -0,0 +1,21 @@
1
+ import type { Catalog, GenerateRequest, Options, Query } from './request.ts';
2
+ export type OutputFile = {
3
+ name: string;
4
+ contents: string;
5
+ };
6
+ export type GenerateResult = {
7
+ ok: true;
8
+ files: OutputFile[];
9
+ } | {
10
+ ok: false;
11
+ failures: string[];
12
+ };
13
+ export declare function readOptions(encoded: string): {
14
+ options: Options;
15
+ failures: string[];
16
+ };
17
+ export declare function generate(request: GenerateRequest): GenerateResult;
18
+ export declare function correctNullability(query: Pick<Query, 'text' | 'columns'>, catalog: Catalog | null): {
19
+ notNull: boolean;
20
+ elementsNotNull: boolean;
21
+ }[];
@@ -0,0 +1,440 @@
1
+ import { camelCase, propertyAccess, propertyName } from './keys.js';
2
+ import { HELPER_NAME, PG_ARRAY_HELPER } from './pg-array-helper.js';
3
+ import { arrayAggElementsNotNull, coalesce, isNeverNullCall, isNullTest, selectList, } from './select-list.js';
4
+ import { templateLiteral } from './strings.js';
5
+ import { isJson, needsParse, paramType, resolveType, rowType } from './types.js';
6
+ // R17: names a function can't have.
7
+ const RESERVED = new Set([
8
+ 'await',
9
+ 'break',
10
+ 'case',
11
+ 'catch',
12
+ 'class',
13
+ 'const',
14
+ 'continue',
15
+ 'debugger',
16
+ 'default',
17
+ 'delete',
18
+ 'do',
19
+ 'else',
20
+ 'enum',
21
+ 'export',
22
+ 'extends',
23
+ 'false',
24
+ 'finally',
25
+ 'for',
26
+ 'function',
27
+ 'if',
28
+ 'implements',
29
+ 'import',
30
+ 'in',
31
+ 'instanceof',
32
+ 'interface',
33
+ 'let',
34
+ 'new',
35
+ 'null',
36
+ 'package',
37
+ 'private',
38
+ 'protected',
39
+ 'public',
40
+ 'return',
41
+ 'static',
42
+ 'super',
43
+ 'switch',
44
+ 'this',
45
+ 'throw',
46
+ 'true',
47
+ 'try',
48
+ 'typeof',
49
+ 'var',
50
+ 'void',
51
+ 'while',
52
+ 'with',
53
+ 'yield',
54
+ 'arguments',
55
+ 'eval',
56
+ ]);
57
+ const SUPPORTED_KINDS = new Set([':one', ':many', ':exec', ':execrows']);
58
+ // R22: the fix for each kind sqlc accepts and sequel doesn't.
59
+ const UNSUPPORTED_KINDS = {
60
+ ':execresult': 'Use :exec or :execrows.',
61
+ ':execlastid': 'MySQL only. Use :one with RETURNING.',
62
+ ':copyfrom': 'Use :exec with INSERT … SELECT … FROM unnest(…) AS u(…).',
63
+ ':batchexec': 'pg has no pipelining. Use :exec, :many or :one.',
64
+ ':batchmany': 'pg has no pipelining. Use :exec, :many or :one.',
65
+ ':batchone': 'pg has no pipelining. Use :exec, :many or :one.',
66
+ };
67
+ // R3: plugin options are the base64 of a JSON object, or empty for the defaults.
68
+ export function readOptions(encoded) {
69
+ const options = { bigint: 'string' };
70
+ if (encoded === '')
71
+ return { options, failures: [] };
72
+ let value;
73
+ try {
74
+ value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
75
+ }
76
+ catch {
77
+ return { options, failures: ['invalid plugin options: not a JSON object'] };
78
+ }
79
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
80
+ return { options, failures: ['invalid plugin options: not a JSON object'] };
81
+ }
82
+ const failures = [];
83
+ for (const [key, setting] of Object.entries(value).sort(([a], [b]) => compare(a, b))) {
84
+ if (key === 'bigint' &&
85
+ (setting === 'string' || setting === 'number' || setting === 'bigint')) {
86
+ options.bigint = setting;
87
+ }
88
+ else {
89
+ failures.push(`invalid plugin option ${key}`);
90
+ }
91
+ }
92
+ return { options, failures };
93
+ }
94
+ export function generate(request) {
95
+ const { options, failures: optionFailures } = readOptions(request.plugin_options);
96
+ const failures = optionFailures.map((message) => ({ file: '', order: -1, message }));
97
+ // R7: one module per query file, in the order the request lists the queries (R9).
98
+ const groups = new Map();
99
+ request.queries.forEach((query, order) => {
100
+ const group = groups.get(query.filename) ?? [];
101
+ group.push({ query, order });
102
+ groups.set(query.filename, group);
103
+ });
104
+ const files = [];
105
+ for (const [filename, group] of groups) {
106
+ const fail = (order, message) => failures.push({ file: filename, order, message });
107
+ if (!filename.endsWith('.sql')) {
108
+ fail(-1, `${filename}: query file name must end in .sql`);
109
+ }
110
+ const prepared = [];
111
+ const functions = new Map();
112
+ for (const { query, order } of group) {
113
+ const queryFailures = [];
114
+ const result = prepare(query, request.catalog, options.bigint, functions, queryFailures);
115
+ for (const message of queryFailures)
116
+ fail(order, `${filename}: ${query.name}: ${message}`);
117
+ if (result !== null && queryFailures.length === 0)
118
+ prepared.push(result);
119
+ }
120
+ if (filename.endsWith('.sql')) {
121
+ files.push({ name: `${filename.slice(0, -4)}.ts`, contents: emitModule(filename, prepared) });
122
+ }
123
+ }
124
+ if (failures.length > 0) {
125
+ // R56: by file name, then the query's order in the request, then position, which is the
126
+ // order each query's checks report in.
127
+ const sorted = failures
128
+ .map((failure, index) => ({ ...failure, index }))
129
+ .sort((a, b) => compare(a.file, b.file) || a.order - b.order || a.index - b.index);
130
+ return { ok: false, failures: sorted.map((failure) => failure.message) };
131
+ }
132
+ files.sort((a, b) => compare(a.name, b.name));
133
+ return { ok: true, files };
134
+ }
135
+ // Code-unit order, as R4 asks.
136
+ function compare(a, b) {
137
+ return a < b ? -1 : a > b ? 1 : 0;
138
+ }
139
+ function prepare(query, catalog, bigint, functions, failures) {
140
+ // R16, R17: names.
141
+ const fn = firstChar(query.name, (c) => c.toLowerCase());
142
+ const base = firstChar(query.name, (c) => c.toUpperCase());
143
+ if (RESERVED.has(fn)) {
144
+ failures.push(`the function name ${fn} is reserved. Rename the query.`);
145
+ }
146
+ else if (fn === HELPER_NAME) {
147
+ failures.push(`the function name ${fn} is reserved. Rename the query.`);
148
+ }
149
+ else {
150
+ const other = functions.get(fn);
151
+ if (other !== undefined) {
152
+ failures.push(`gives the same function name as ${other}. Rename the query.`);
153
+ }
154
+ else {
155
+ functions.set(fn, query.name);
156
+ }
157
+ }
158
+ // R22, R23: kinds.
159
+ const cmd = query.cmd;
160
+ if (!SUPPORTED_KINDS.has(cmd)) {
161
+ const fix = UNSUPPORTED_KINDS[cmd] ?? 'Use :one, :many, :exec or :execrows.';
162
+ failures.push(`${cmd} is not supported. ${fix}`);
163
+ }
164
+ const returnsRows = cmd === ':one' || cmd === ':many';
165
+ if (returnsRows && query.columns.length === 0) {
166
+ failures.push('returns no columns. Use :exec or :execrows.');
167
+ }
168
+ const params = prepareParams(query, catalog, failures);
169
+ const columns = prepareColumns(query, catalog, bigint, returnsRows, failures);
170
+ const doc = jsdoc(query.comments);
171
+ if (failures.length > 0 || params === null || columns === null)
172
+ return null;
173
+ return { name: query.name, fn, base, cmd, text: query.text, doc, params, columns };
174
+ }
175
+ function firstChar(name, change) {
176
+ const [first = '', ...rest] = Array.from(name);
177
+ return change(first) + rest.join('');
178
+ }
179
+ // R27–R34.
180
+ function prepareParams(query, catalog, failures) {
181
+ const sorted = [...query.params].sort((a, b) => a.number - b.number);
182
+ const numbered = sorted.every((param, i) => param.number === i + 1);
183
+ if (!numbered) {
184
+ failures.push(`parameter numbers are not 1 to ${sorted.length}.`);
185
+ }
186
+ if (sorted.some((param) => param.column?.is_sqlc_slice === true)) {
187
+ failures.push('sqlc.slice is not supported for Postgres. Use = ANY(@name::type[]).');
188
+ }
189
+ const result = [];
190
+ const keys = new Map();
191
+ for (const param of sorted) {
192
+ const column = param.column;
193
+ const name = column?.name ?? '';
194
+ if (name === '') {
195
+ failures.push(`parameter $${param.number} has no name. Name it with @name or sqlc.arg(name).`);
196
+ continue;
197
+ }
198
+ const key = camelCase(name);
199
+ if (key === '__proto__') {
200
+ failures.push('the key __proto__ is not allowed. Alias it.');
201
+ continue;
202
+ }
203
+ const earlier = keys.get(key);
204
+ if (earlier !== undefined) {
205
+ failures.push(`parameters $${earlier} and $${param.number} both give the key ${key}. Name them with @name.`);
206
+ continue;
207
+ }
208
+ keys.set(key, param.number);
209
+ const nullable = column?.not_null !== true;
210
+ const type = resolveType(column ?? emptyColumn(), catalog);
211
+ const access = propertyAccess('params', key);
212
+ let value = access;
213
+ if (isJson(type)) {
214
+ let inner = 'JSON.stringify(v)';
215
+ for (let level = 1; level < type.dims; level++)
216
+ inner = `v.map((v) => ${inner})`;
217
+ const stringified = type.dims === 0 ? `JSON.stringify(${access})` : `${access}.map((v) => ${inner})`;
218
+ value = nullable ? `${access} === null ? null : ${stringified}` : stringified;
219
+ }
220
+ result.push({ key, type: paramType(type, nullable), value });
221
+ }
222
+ return numbered ? result : null;
223
+ }
224
+ // R35–R43 and R59–R65.
225
+ function prepareColumns(query, catalog, bigint, returnsRows, failures) {
226
+ if (query.columns.some((column) => column.embed_table !== null && column.embed_table.name !== '')) {
227
+ failures.push('sqlc.embed is not supported. List the columns instead.');
228
+ return null;
229
+ }
230
+ const keys = new Map();
231
+ let ok = true;
232
+ query.columns.forEach((column, i) => {
233
+ if (column.name === '' || column.name === '?column?') {
234
+ failures.push(`column ${i + 1} has no name. Give it an alias.`);
235
+ ok = false;
236
+ return;
237
+ }
238
+ const key = camelCase(column.name);
239
+ if (key === '__proto__') {
240
+ failures.push('the key __proto__ is not allowed. Alias it.');
241
+ ok = false;
242
+ return;
243
+ }
244
+ const earlier = keys.get(key);
245
+ if (earlier !== undefined) {
246
+ failures.push(`columns ${earlier} and ${i + 1} both give the key ${key}. Alias one of them.`);
247
+ ok = false;
248
+ return;
249
+ }
250
+ keys.set(key, i + 1);
251
+ });
252
+ const asserted = notNullAssertions(query, catalog, failures);
253
+ if (!ok || asserted === null || !returnsRows)
254
+ return ok && asserted !== null ? [] : null;
255
+ const nullability = correctNullability(query, catalog);
256
+ return query.columns.map((column, i) => {
257
+ const type = resolveType(column, catalog);
258
+ const corrected = nullability[i];
259
+ const notNull = asserted.columns.has(column.name) || corrected.notNull;
260
+ const elementsNotNull = asserted.elements.has(column.name) || corrected.elementsNotNull;
261
+ const parse = needsParse(type);
262
+ return {
263
+ key: camelCase(column.name),
264
+ type: rowType(type, !notNull, elementsNotNull, bigint),
265
+ value: parse ? `${HELPER_NAME}(row[${i}])` : `row[${i}]`,
266
+ parse,
267
+ };
268
+ });
269
+ }
270
+ // R65: `-- @notnull name name[]` lines in the query's comments.
271
+ function notNullAssertions(query, catalog, failures) {
272
+ const columns = new Set();
273
+ const elements = new Set();
274
+ let ok = true;
275
+ for (const comment of query.comments) {
276
+ const words = comment.trim().split(/\s+/);
277
+ if (words[0] !== '@notnull')
278
+ continue;
279
+ const names = words.slice(1);
280
+ if (names.length === 0) {
281
+ failures.push('@notnull names no columns.');
282
+ ok = false;
283
+ continue;
284
+ }
285
+ for (const name of names) {
286
+ const arrayElements = name.endsWith('[]');
287
+ const columnName = arrayElements ? name.slice(0, -2) : name;
288
+ const matches = query.columns.filter((column) => column.name === columnName);
289
+ if (matches.length === 0) {
290
+ failures.push(`@notnull names ${columnName}, which is not a column of this query.`);
291
+ ok = false;
292
+ }
293
+ else if (arrayElements) {
294
+ if (matches.some((column) => resolveType(column, catalog).dims === 0)) {
295
+ failures.push(`@notnull names ${columnName}[], but ${columnName} is not an array.`);
296
+ ok = false;
297
+ }
298
+ else {
299
+ elements.add(columnName);
300
+ }
301
+ }
302
+ else {
303
+ columns.add(columnName);
304
+ }
305
+ }
306
+ }
307
+ return ok ? { columns, elements } : null;
308
+ }
309
+ // R59–R64: sqlc's `not_null`, corrected for function calls, coalesce, null tests and the
310
+ // elements of array_agg.
311
+ export function correctNullability(query, catalog) {
312
+ const { items, from } = selectList(query.text, query.columns.length);
313
+ const lookup = columnLookup(catalog);
314
+ return query.columns.map((column, i) => {
315
+ let notNull = column.not_null;
316
+ const item = items?.[i];
317
+ if (column.is_func_call)
318
+ notNull = item !== undefined && isNeverNullCall(item);
319
+ let elementsNotNull = false;
320
+ if (item !== undefined) {
321
+ const coalesced = coalesce(item);
322
+ if (coalesced !== null)
323
+ notNull = coalesced === 'never-null';
324
+ if (isNullTest(item))
325
+ notNull = true;
326
+ elementsNotNull = arrayAggElementsNotNull(item, from, lookup);
327
+ }
328
+ return { notNull, elementsNotNull };
329
+ });
330
+ }
331
+ function columnLookup(catalog) {
332
+ return (table, columnName) => {
333
+ if (catalog === null)
334
+ return null;
335
+ const schemaName = table.schema ?? catalog.default_schema;
336
+ const found = catalog.schemas
337
+ .find((schema) => schema.name === schemaName)
338
+ ?.tables.find((t) => t.rel?.name === table.name);
339
+ if (found === undefined)
340
+ return null;
341
+ const column = found.columns.find((c) => c.name === columnName);
342
+ if (column === undefined)
343
+ return 'missing';
344
+ return column.not_null ? 'not-null' : 'nullable';
345
+ };
346
+ }
347
+ function emptyColumn() {
348
+ return {
349
+ name: '',
350
+ not_null: false,
351
+ is_array: false,
352
+ is_func_call: false,
353
+ is_sqlc_slice: false,
354
+ type: null,
355
+ embed_table: null,
356
+ array_dims: 0,
357
+ };
358
+ }
359
+ // R26: the JSDoc lines for a query's comments.
360
+ function jsdoc(comments) {
361
+ const lines = comments
362
+ .map((comment) => (comment.startsWith(' ') ? comment.slice(1) : comment).trimEnd())
363
+ .filter((line) => !line.trimStart().startsWith('@'));
364
+ while (lines.length > 0 && lines[0] === '')
365
+ lines.shift();
366
+ while (lines.length > 0 && lines[lines.length - 1] === '')
367
+ lines.pop();
368
+ return lines.map((line) => line.replaceAll('*/', '*\\/'));
369
+ }
370
+ // --- Emitting -----------------------------------------------------------------------------
371
+ // R10–R14, R52, R54.
372
+ function emitModule(filename, queries) {
373
+ const blocks = [
374
+ `// Generated by sequel from ${filename}. Do not edit.`,
375
+ "import type { QueryArrayConfig, QueryArrayResult } from 'pg'",
376
+ [
377
+ 'export interface Queryable {',
378
+ ' query(config: QueryArrayConfig): Promise<QueryArrayResult>',
379
+ '}',
380
+ ].join('\n'),
381
+ ];
382
+ let usesHelper = false;
383
+ for (const query of queries) {
384
+ if (query.params.length > 0) {
385
+ blocks.push(interfaceBlock(`${query.base}Params`, query.params));
386
+ }
387
+ const returnsRows = query.cmd === ':one' || query.cmd === ':many';
388
+ if (returnsRows) {
389
+ blocks.push(interfaceBlock(`${query.base}Row`, query.columns));
390
+ if (query.columns.some((column) => column.parse))
391
+ usesHelper = true;
392
+ }
393
+ blocks.push(functionBlock(query));
394
+ }
395
+ if (usesHelper)
396
+ blocks.push(PG_ARRAY_HELPER.trimEnd());
397
+ return `${blocks.join('\n\n')}\n`;
398
+ }
399
+ function interfaceBlock(name, properties) {
400
+ const lines = properties.map((p) => ` ${propertyName(p.key)}: ${p.type}`);
401
+ return [`export interface ${name} {`, ...lines, '}'].join('\n');
402
+ }
403
+ function functionBlock(query) {
404
+ const lines = [];
405
+ if (query.doc.length > 0) {
406
+ lines.push('/**', ...query.doc.map((line) => (line === '' ? ' *' : ` * ${line}`)), ' */');
407
+ }
408
+ const args = query.params.length > 0 ? `client: Queryable, params: ${query.base}Params` : 'client: Queryable';
409
+ const returns = {
410
+ ':one': `${query.base}Row | null`,
411
+ ':many': `${query.base}Row[]`,
412
+ ':exec': 'void',
413
+ ':execrows': 'number',
414
+ }[query.cmd];
415
+ lines.push(`export async function ${query.fn}(${args}): Promise<${returns}> {`);
416
+ const call = [
417
+ 'client.query({',
418
+ ` text: ${templateLiteral(`-- name: ${query.name} ${query.cmd}\n${query.text}`)},`,
419
+ ` values: [${query.params.map((p) => p.value).join(', ')}],`,
420
+ " rowMode: 'array',",
421
+ ' })',
422
+ ].join('\n');
423
+ const properties = (indent) => query.columns.map((c) => `${indent}${propertyName(c.key)}: ${c.value},`);
424
+ switch (query.cmd) {
425
+ case ':one':
426
+ lines.push(` const result = await ${call}`, ' const row = result.rows[0]', ' if (row === undefined) {', ' return null', ' }', ' return {', ...properties(' '), ' }');
427
+ break;
428
+ case ':many':
429
+ lines.push(` const result = await ${call}`, ' return result.rows.map((row) => ({', ...properties(' '), ' }))');
430
+ break;
431
+ case ':exec':
432
+ lines.push(` await ${call}`);
433
+ break;
434
+ case ':execrows':
435
+ lines.push(` const result = await ${call}`, ' return result.rowCount ?? 0');
436
+ break;
437
+ }
438
+ lines.push('}');
439
+ return lines.join('\n');
440
+ }
@@ -0,0 +1,4 @@
1
+ export declare function camelCase(name: string): string;
2
+ export declare function propertyName(key: string): string;
3
+ export declare function propertyAccess(object: string, key: string): string;
4
+ export declare function isIdentifier(text: string): boolean;
@@ -0,0 +1,24 @@
1
+ import { singleQuoted } from './strings.js';
2
+ // R41: a name with no `_` is kept exactly. Otherwise leading and trailing underscores are
3
+ // kept, and in between each run of `_` is removed and the character after it upper-cased.
4
+ export function camelCase(name) {
5
+ if (!name.includes('_'))
6
+ return name;
7
+ const match = /^(_*)(.*?)(_*)$/su.exec(name);
8
+ if (match === null)
9
+ return name;
10
+ const [, leading = '', middle = '', trailing = ''] = match;
11
+ return leading + middle.replace(/_+(.)/gsu, (_, next) => next.toUpperCase()) + trailing;
12
+ }
13
+ const IDENTIFIER = /^[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*$/u;
14
+ // R42: a valid identifier is written bare, and any other key as a single-quoted string.
15
+ export function propertyName(key) {
16
+ return IDENTIFIER.test(key) ? key : singleQuoted(key);
17
+ }
18
+ // `params.k`, or `params['k']` for a key that must be quoted (R34).
19
+ export function propertyAccess(object, key) {
20
+ return IDENTIFIER.test(key) ? `${object}.${key}` : `${object}[${singleQuoted(key)}]`;
21
+ }
22
+ export function isIdentifier(text) {
23
+ return IDENTIFIER.test(text);
24
+ }
@@ -0,0 +1,12 @@
1
+ export type TokenKind = 'word' | 'qident' | 'string' | 'number' | 'param' | 'op' | 'punct';
2
+ export type Token = {
3
+ kind: TokenKind;
4
+ text: string;
5
+ upper: string;
6
+ depth: number;
7
+ };
8
+ export type Scan = {
9
+ tokens: Token[];
10
+ closed: boolean;
11
+ };
12
+ export declare function scan(text: string): Scan;