@zmdb/sql 1.0.0-beta.1

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 (98) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +35 -0
  3. package/dist/aggregations/index.d.ts +30 -0
  4. package/dist/aggregations/index.d.ts.map +1 -0
  5. package/dist/aggregations/index.js +52 -0
  6. package/dist/aggregations/index.js.map +1 -0
  7. package/dist/clauses.d.ts +112 -0
  8. package/dist/clauses.d.ts.map +1 -0
  9. package/dist/clauses.js +267 -0
  10. package/dist/clauses.js.map +1 -0
  11. package/dist/comments/index.d.ts +26 -0
  12. package/dist/comments/index.d.ts.map +1 -0
  13. package/dist/comments/index.js +28 -0
  14. package/dist/comments/index.js.map +1 -0
  15. package/dist/compiled-query.d.ts +12 -0
  16. package/dist/compiled-query.d.ts.map +1 -0
  17. package/dist/compiled-query.js +2 -0
  18. package/dist/compiled-query.js.map +1 -0
  19. package/dist/dialects/index.d.ts +19 -0
  20. package/dist/dialects/index.d.ts.map +1 -0
  21. package/dist/dialects/index.js +43 -0
  22. package/dist/dialects/index.js.map +1 -0
  23. package/dist/dialects/protocol.d.ts +230 -0
  24. package/dist/dialects/protocol.d.ts.map +1 -0
  25. package/dist/dialects/protocol.js +230 -0
  26. package/dist/dialects/protocol.js.map +1 -0
  27. package/dist/errors.d.ts +9 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +17 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/expressions/index.d.ts +50 -0
  32. package/dist/expressions/index.d.ts.map +1 -0
  33. package/dist/expressions/index.js +77 -0
  34. package/dist/expressions/index.js.map +1 -0
  35. package/dist/extensions/index.d.ts +85 -0
  36. package/dist/extensions/index.d.ts.map +1 -0
  37. package/dist/extensions/index.js +120 -0
  38. package/dist/extensions/index.js.map +1 -0
  39. package/dist/fts/index.d.ts +28 -0
  40. package/dist/fts/index.d.ts.map +1 -0
  41. package/dist/fts/index.js +110 -0
  42. package/dist/fts/index.js.map +1 -0
  43. package/dist/index.d.ts +128 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +364 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/introspect/types.d.ts +58 -0
  48. package/dist/introspect/types.d.ts.map +1 -0
  49. package/dist/introspect/types.js +2 -0
  50. package/dist/introspect/types.js.map +1 -0
  51. package/dist/joins/index.d.ts +20 -0
  52. package/dist/joins/index.d.ts.map +1 -0
  53. package/dist/joins/index.js +24 -0
  54. package/dist/joins/index.js.map +1 -0
  55. package/dist/migrations/types.d.ts +100 -0
  56. package/dist/migrations/types.d.ts.map +1 -0
  57. package/dist/migrations/types.js +2 -0
  58. package/dist/migrations/types.js.map +1 -0
  59. package/dist/quoting.d.ts +40 -0
  60. package/dist/quoting.d.ts.map +1 -0
  61. package/dist/quoting.js +98 -0
  62. package/dist/quoting.js.map +1 -0
  63. package/dist/schema-objects/extensions.d.ts +5 -0
  64. package/dist/schema-objects/extensions.d.ts.map +1 -0
  65. package/dist/schema-objects/extensions.js +9 -0
  66. package/dist/schema-objects/extensions.js.map +1 -0
  67. package/dist/schema-objects/index.d.ts +25 -0
  68. package/dist/schema-objects/index.d.ts.map +1 -0
  69. package/dist/schema-objects/index.js +86 -0
  70. package/dist/schema-objects/index.js.map +1 -0
  71. package/dist/schema-objects/types.d.ts +62 -0
  72. package/dist/schema-objects/types.d.ts.map +1 -0
  73. package/dist/schema-objects/types.js +2 -0
  74. package/dist/schema-objects/types.js.map +1 -0
  75. package/dist/set-ops/index.d.ts +16 -0
  76. package/dist/set-ops/index.d.ts.map +1 -0
  77. package/dist/set-ops/index.js +41 -0
  78. package/dist/set-ops/index.js.map +1 -0
  79. package/package.json +70 -0
  80. package/src/aggregations/index.ts +127 -0
  81. package/src/clauses.ts +387 -0
  82. package/src/comments/index.ts +49 -0
  83. package/src/compiled-query.ts +12 -0
  84. package/src/dialects/index.ts +103 -0
  85. package/src/dialects/protocol.ts +478 -0
  86. package/src/errors.ts +18 -0
  87. package/src/expressions/index.ts +127 -0
  88. package/src/extensions/index.ts +235 -0
  89. package/src/fts/index.ts +157 -0
  90. package/src/index.ts +685 -0
  91. package/src/introspect/types.ts +60 -0
  92. package/src/joins/index.ts +69 -0
  93. package/src/migrations/types.ts +93 -0
  94. package/src/quoting.ts +111 -0
  95. package/src/schema-objects/extensions.ts +12 -0
  96. package/src/schema-objects/index.ts +125 -0
  97. package/src/schema-objects/types.ts +73 -0
  98. package/src/set-ops/index.ts +48 -0
package/src/index.ts ADDED
@@ -0,0 +1,685 @@
1
+ import type { CompiledQuery } from './compiled-query.js';
2
+ // @zmdb/sql — implementation.
3
+ import {
4
+ dialectName,
5
+ dialectTraits,
6
+ type DialectReturningSql,
7
+ type DialectTarget,
8
+ type ReturningStatement,
9
+ type SqlDialect,
10
+ } from './dialects/index.js';
11
+ import { UnsupportedFeatureError } from './errors.js';
12
+
13
+ export { QueryCompilerError, UnsupportedFeatureError } from './errors.js';
14
+ export type { CompiledQuery, QueryTelemetry } from './compiled-query.js';
15
+ export {
16
+ defineSqlDialect,
17
+ dialectCapabilities,
18
+ dialectFamily,
19
+ dialectName,
20
+ dialectSupportsReturning,
21
+ dialectTraits,
22
+ extendSqlDialect,
23
+ isSqlDialect,
24
+ } from './dialects/index.js';
25
+ export type {
26
+ AppliedMigration,
27
+ DatabaseCapabilities,
28
+ DialectCompiler,
29
+ DialectFeature,
30
+ DialectOutbox,
31
+ DialectReturningColumn,
32
+ DialectReturningContext,
33
+ DialectReturningSql,
34
+ DialectSqlType,
35
+ DialectTarget,
36
+ DialectTypeMap,
37
+ DialectUpsertConflict,
38
+ DialectUpsertContext,
39
+ DialectUpsertReferences,
40
+ IntrospectionDriver,
41
+ Introspector,
42
+ IntrospectOptions,
43
+ MigrationConnection,
44
+ MigrationDialect,
45
+ MigrationDriver,
46
+ MigrationPlan,
47
+ MigrationTableOptions,
48
+ PaginationTail,
49
+ PlaceholderStyle,
50
+ ResolvedDialectTraits,
51
+ ReturningCapability,
52
+ ReturningStatement,
53
+ ReturningStyle,
54
+ SchemaObjectOperation,
55
+ SqlDialect,
56
+ SqlDialectDefinition,
57
+ SqlDialectExtension,
58
+ } from './dialects/index.js';
59
+ export type {
60
+ ChangeOp,
61
+ ColumnSnapshot,
62
+ ExtensionSnapshot,
63
+ ExtensionType,
64
+ ForeignKeySnapshot,
65
+ ReferentialAction,
66
+ SchemaSnapshot,
67
+ TableOptions,
68
+ TableSnapshot,
69
+ } from './migrations/types.js';
70
+ export type {
71
+ CatalogColumnSnapshot,
72
+ CatalogForeignKeySnapshot,
73
+ CatalogIndexColumn,
74
+ CatalogIndexSnapshot,
75
+ CatalogSchemaSnapshot,
76
+ CatalogSelection,
77
+ CatalogTableSnapshot,
78
+ CatalogWarning,
79
+ } from './introspect/types.js';
80
+
81
+ // #17 SELECT compilation implemented (+ shared dialect quoting/placeholders,
82
+ // which also satisfies the SELECT-based dialect tests of #19). Write builders
83
+ // (#18 INSERT/UPDATE/DELETE) remain unimplemented; their tests stay red.
84
+
85
+ import {
86
+ frozenQuery,
87
+ queryTelemetry,
88
+ tailClause,
89
+ tailMethods,
90
+ whereClause,
91
+ type ComparisonPredicate,
92
+ type Predicate,
93
+ type PredicateGroup,
94
+ } from './clauses.js';
95
+ import { emitColumnExpr, isColumnExpr } from './expressions/index.js';
96
+ import {
97
+ isAliasedDistanceExpression,
98
+ isDistanceExpression,
99
+ isSpatialPredicate,
100
+ renderAliasedDistanceExpression,
101
+ renderDistanceExpression,
102
+ type AliasedDistanceExpression,
103
+ type DistanceExpression,
104
+ type SpatialPredicate,
105
+ } from './extensions/index.js';
106
+ import { formatPlaceholder, quoteColumn, quoteIdentifier, quoteTable, renumberPlaceholders } from './quoting.js';
107
+
108
+ export { EXPR, coalesce, concat, dec, inc, mul, not, proposed } from './expressions/index.js';
109
+ export type { ColumnExpr, SetValue } from './expressions/index.js';
110
+ export { DISTANCE_OPERATORS, distance, stContains, stDWithin } from './extensions/index.js';
111
+ export type {
112
+ AliasedDistanceExpression,
113
+ DistanceExpression,
114
+ DistanceOp,
115
+ ExtensionColumnOf,
116
+ GeoJsonGeometry,
117
+ GeometryColumnOf,
118
+ GeometryValueOf,
119
+ SpatialPredicate,
120
+ VectorColumnOf,
121
+ } from './extensions/index.js';
122
+ export { formatPlaceholder, quoteColumn, quoteIdentifier, quoteTable, renumberPlaceholders };
123
+ export type Operator =
124
+ | '='
125
+ | '!='
126
+ | '<'
127
+ | '<='
128
+ | '>'
129
+ | '>='
130
+ | 'like'
131
+ | 'ilike'
132
+ | 'in'
133
+ | 'not in'
134
+ | 'nin'
135
+ | 'exists'
136
+ | 'not exists'
137
+ | 'is null'
138
+ | 'is not null'
139
+ | (string & {});
140
+
141
+ export { OP_MAP } from './clauses.js';
142
+ export { renderPredicate } from './clauses.js';
143
+ export type { ComparisonPredicate, Predicate, PredicateGroup } from './clauses.js';
144
+
145
+ export type Direction = 'asc' | 'desc';
146
+
147
+ /** A database column projected under an application-facing result key. */
148
+ export interface AliasedColumn {
149
+ readonly column: string;
150
+ readonly alias: string;
151
+ }
152
+
153
+ type SelectedColumn = string | AliasedColumn | AliasedDistanceExpression;
154
+ type ReturningColumn = string | AliasedColumn;
155
+
156
+ function isAliasedColumn(column: SelectedColumn | ReturningColumn): column is AliasedColumn {
157
+ return typeof column === 'object' && 'column' in column && 'alias' in column;
158
+ }
159
+
160
+ /**
161
+ * Collection utility that deduplicates keys while preserving insertion order AND
162
+ * filtering out `null` and `undefined` key values.
163
+ *
164
+ * Note: Dropping null/undefined key values is a semantic choice designed for batch key loading
165
+ * (parent rows with a null foreign key are silently omitted from relationship loading).
166
+ */
167
+ export function sanitizeKeys<T>(keys: readonly T[]): T[] {
168
+ const result: T[] = [];
169
+ const seen = new Set<T>();
170
+ for (const k of keys) {
171
+ if (k !== null && k !== undefined && !seen.has(k)) {
172
+ seen.add(k);
173
+ result.push(k);
174
+ }
175
+ }
176
+ return result;
177
+ }
178
+
179
+ /**
180
+ * Collection utility that partitions an array into contiguous chunks of at most `chunkSize` elements.
181
+ * Used to split large batch parameter lists into parameter-safe sub-queries.
182
+ */
183
+ export function chunkArray<T>(array: readonly T[], chunkSize: number): T[][] {
184
+ if (chunkSize <= 0) throw new Error('chunkSize must be greater than 0');
185
+ const chunks: T[][] = [];
186
+ for (let i = 0; i < array.length; i += chunkSize) {
187
+ chunks.push(array.slice(i, i + chunkSize));
188
+ }
189
+ return chunks;
190
+ }
191
+
192
+ export { appendComment, serializeComment, withComments } from './comments/index.js';
193
+ export { type CommentKey, type CommentKeys, type CommentPairs } from './comments/index.js';
194
+
195
+ /** Optional compiler features that would otherwise change every query shape. */
196
+ export interface QueryCompilerOptions {
197
+ readonly telemetry?: true;
198
+ }
199
+
200
+ interface SelectState {
201
+ readonly table: string;
202
+ readonly columns?: readonly SelectedColumn[];
203
+ readonly wheres: readonly Predicate[];
204
+ readonly orderBys: readonly { col: string | DistanceExpression; dir: Direction }[];
205
+ readonly limitN?: number;
206
+ readonly offsetN?: number;
207
+ }
208
+
209
+ export interface SelectBuilder<T = unknown> {
210
+ select(columns?: readonly SelectedColumn[]): SelectBuilder<T>;
211
+ where(predicate: SpatialPredicate): SelectBuilder<T>;
212
+ where(col: string, op: Operator, value: unknown): SelectBuilder<T>;
213
+ andWhere(predicate: SpatialPredicate): SelectBuilder<T>;
214
+ andWhere(col: string, op: Operator, value: unknown): SelectBuilder<T>;
215
+ orWhere(predicate: SpatialPredicate): SelectBuilder<T>;
216
+ orWhere(col: string, op: Operator, value: unknown): SelectBuilder<T>;
217
+ whereGroup(predicates: readonly ComparisonPredicate[]): SelectBuilder<T>;
218
+ orWhereGroup(predicates: readonly ComparisonPredicate[]): SelectBuilder<T>;
219
+ whereIn(col: string, values: readonly unknown[]): SelectBuilder<T>;
220
+ andWhereIn(col: string, values: readonly unknown[]): SelectBuilder<T>;
221
+ orWhereIn(col: string, values: readonly unknown[]): SelectBuilder<T>;
222
+ whereNotIn(col: string, values: readonly unknown[]): SelectBuilder<T>;
223
+ andWhereNotIn(col: string, values: readonly unknown[]): SelectBuilder<T>;
224
+ orWhereNotIn(col: string, values: readonly unknown[]): SelectBuilder<T>;
225
+ whereExists(subquery: SelectBuilder<unknown> | { compile(): CompiledQuery }): SelectBuilder<T>;
226
+ andWhereExists(subquery: SelectBuilder<unknown> | { compile(): CompiledQuery }): SelectBuilder<T>;
227
+ orWhereExists(subquery: SelectBuilder<unknown> | { compile(): CompiledQuery }): SelectBuilder<T>;
228
+ whereNotExists(subquery: SelectBuilder<unknown> | { compile(): CompiledQuery }): SelectBuilder<T>;
229
+ andWhereNotExists(subquery: SelectBuilder<unknown> | { compile(): CompiledQuery }): SelectBuilder<T>;
230
+ orWhereNotExists(subquery: SelectBuilder<unknown> | { compile(): CompiledQuery }): SelectBuilder<T>;
231
+ orderBy(col: string | DistanceExpression, dir: Direction): SelectBuilder<T>;
232
+ limit(n: number): SelectBuilder<T>;
233
+ offset(n: number): SelectBuilder<T>;
234
+ compile(): CompiledQuery;
235
+ readonly dialect: DialectTarget;
236
+ readonly _type?: T;
237
+ }
238
+
239
+ function makeSelect<T = unknown>(d: DialectTarget, state: SelectState, telemetry: boolean): SelectBuilder<T> {
240
+ const next = (patch: Partial<SelectState>): SelectBuilder<T> => makeSelect(d, { ...state, ...patch }, telemetry);
241
+ const addWhere = (connector: 'AND' | 'OR', col: string, op: Operator, value: unknown) =>
242
+ next({ wheres: [...state.wheres, { col, op, value, connector }] });
243
+ const addSpatial = (connector: 'AND' | 'OR', predicate: SpatialPredicate) =>
244
+ next({ wheres: [...state.wheres, { ...predicate, connector }] });
245
+ const addGroup = (connector: 'AND' | 'OR', predicates: readonly ComparisonPredicate[]) =>
246
+ next({ wheres: [...state.wheres, { kind: 'group', predicates, connector } satisfies PredicateGroup] });
247
+
248
+ function where(predicate: SpatialPredicate): SelectBuilder<T>;
249
+ function where(col: string, op: Operator, value: unknown): SelectBuilder<T>;
250
+ function where(first: string | SpatialPredicate, op?: Operator, value?: unknown): SelectBuilder<T> {
251
+ if (isSpatialPredicate(first)) return addSpatial('AND', first);
252
+ if (op === undefined) throw new TypeError('where(column, operator, value) requires an operator');
253
+ return addWhere('AND', first, op, value);
254
+ }
255
+
256
+ function andWhere(predicate: SpatialPredicate): SelectBuilder<T>;
257
+ function andWhere(col: string, op: Operator, value: unknown): SelectBuilder<T>;
258
+ function andWhere(first: string | SpatialPredicate, op?: Operator, value?: unknown): SelectBuilder<T> {
259
+ if (isSpatialPredicate(first)) return addSpatial('AND', first);
260
+ if (op === undefined) throw new TypeError('andWhere(column, operator, value) requires an operator');
261
+ return addWhere('AND', first, op, value);
262
+ }
263
+
264
+ function orWhere(predicate: SpatialPredicate): SelectBuilder<T>;
265
+ function orWhere(col: string, op: Operator, value: unknown): SelectBuilder<T>;
266
+ function orWhere(first: string | SpatialPredicate, op?: Operator, value?: unknown): SelectBuilder<T> {
267
+ if (isSpatialPredicate(first)) return addSpatial('OR', first);
268
+ if (op === undefined) throw new TypeError('orWhere(column, operator, value) requires an operator');
269
+ return addWhere('OR', first, op, value);
270
+ }
271
+
272
+ return {
273
+ ...tailMethods(state, next),
274
+ dialect: d,
275
+ select: columns => (columns === undefined ? next({}) : next({ columns })),
276
+ where,
277
+ andWhere,
278
+ orWhere,
279
+ whereGroup: predicates => addGroup('AND', predicates),
280
+ orWhereGroup: predicates => addGroup('OR', predicates),
281
+ whereIn: (col, values) => addWhere('AND', col, 'in', values),
282
+ andWhereIn: (col, values) => addWhere('AND', col, 'in', values),
283
+ orWhereIn: (col, values) => addWhere('OR', col, 'in', values),
284
+ whereNotIn: (col, values) => addWhere('AND', col, 'not in', values),
285
+ andWhereNotIn: (col, values) => addWhere('AND', col, 'not in', values),
286
+ orWhereNotIn: (col, values) => addWhere('OR', col, 'not in', values),
287
+ whereExists: subquery => addWhere('AND', '', 'EXISTS', subquery),
288
+ andWhereExists: subquery => addWhere('AND', '', 'EXISTS', subquery),
289
+ orWhereExists: subquery => addWhere('OR', '', 'EXISTS', subquery),
290
+ whereNotExists: subquery => addWhere('AND', '', 'NOT EXISTS', subquery),
291
+ andWhereNotExists: subquery => addWhere('AND', '', 'NOT EXISTS', subquery),
292
+ orWhereNotExists: subquery => addWhere('OR', '', 'NOT EXISTS', subquery),
293
+ compile: () => {
294
+ const params: unknown[] = [];
295
+ const cols =
296
+ state.columns && state.columns.length > 0
297
+ ? state.columns
298
+ .map(column =>
299
+ isAliasedColumn(column)
300
+ ? `${quoteColumn(d, column.column)} AS ${quoteIdentifier(d, column.alias)}`
301
+ : isAliasedDistanceExpression(column)
302
+ ? renderAliasedDistanceExpression(d, column, params)
303
+ : quoteColumn(d, column),
304
+ )
305
+ .join(', ')
306
+ : '*';
307
+ const predicates = whereClause(d, state.wheres, params);
308
+ const orderBy =
309
+ state.orderBys.length === 0
310
+ ? ''
311
+ : ` ORDER BY ${state.orderBys
312
+ .map(order => {
313
+ const expression = isDistanceExpression(order.col)
314
+ ? renderDistanceExpression(d, order.col, params)
315
+ : quoteColumn(d, order.col);
316
+ return `${expression} ${order.dir.toUpperCase()}`;
317
+ })
318
+ .join(', ')}`;
319
+ const text =
320
+ `SELECT ${cols} FROM ${quoteTable(d, state.table)}` +
321
+ predicates +
322
+ orderBy +
323
+ tailClause(d, {
324
+ limitN: state.limitN,
325
+ offsetN: state.offsetN,
326
+ ordered: state.orderBys.length > 0,
327
+ });
328
+ return frozenQuery(text, params, queryTelemetry(d, 'SELECT', state.table, telemetry));
329
+ },
330
+ };
331
+ }
332
+
333
+ export interface OnConflictBuilder {
334
+ doUpdate(updateFields?: readonly string[] | Record<string, unknown>): InsertBuilder;
335
+ doNothing(): InsertBuilder;
336
+ }
337
+
338
+ export interface InsertBuilder {
339
+ values(row: Record<string, unknown>): InsertBuilder;
340
+ onConflict(target?: string | readonly string[]): OnConflictBuilder;
341
+ returning(cols?: readonly ReturningColumn[]): InsertBuilder;
342
+ compile(): CompiledQuery;
343
+ }
344
+ export interface UpdateBuilder {
345
+ set(row: Record<string, unknown>): UpdateBuilder;
346
+ where(col: string, op: Operator, value: unknown): UpdateBuilder;
347
+ orWhere(col: string, op: Operator, value: unknown): UpdateBuilder;
348
+ whereGroup(predicates: readonly ComparisonPredicate[]): UpdateBuilder;
349
+ whereIn(col: string, values: readonly unknown[]): UpdateBuilder;
350
+ whereNotIn(col: string, values: readonly unknown[]): UpdateBuilder;
351
+ returning(cols?: readonly ReturningColumn[]): UpdateBuilder;
352
+ compile(): CompiledQuery;
353
+ }
354
+ export interface DeleteBuilder {
355
+ where(col: string, op: Operator, value: unknown): DeleteBuilder;
356
+ orWhere(col: string, op: Operator, value: unknown): DeleteBuilder;
357
+ whereGroup(predicates: readonly ComparisonPredicate[]): DeleteBuilder;
358
+ whereIn(col: string, values: readonly unknown[]): DeleteBuilder;
359
+ whereNotIn(col: string, values: readonly unknown[]): DeleteBuilder;
360
+ returning(cols?: readonly ReturningColumn[]): DeleteBuilder;
361
+ compile(): CompiledQuery;
362
+ }
363
+
364
+ export interface QueryCompiler {
365
+ selectFrom(table: string): SelectBuilder;
366
+ insertInto(table: string): InsertBuilder;
367
+ updateTable(table: string): UpdateBuilder;
368
+ deleteFrom(table: string): DeleteBuilder;
369
+ callFunction(name: string, args: readonly unknown[]): CompiledQuery;
370
+ callTableFunction(name: string, args: readonly unknown[]): CompiledQuery;
371
+ callProcedure(name: string, args: readonly unknown[]): CompiledQuery;
372
+ }
373
+
374
+ function routineCall(
375
+ dialect: DialectTarget,
376
+ name: string,
377
+ args: readonly unknown[],
378
+ kind: 'function' | 'table-function' | 'procedure',
379
+ ): CompiledQuery {
380
+ const nameOfDialect = dialectName(dialect);
381
+ const traits = dialectTraits(dialect);
382
+ const supported =
383
+ kind === 'function' ? traits.functions : kind === 'procedure' ? traits.procedures : traits.tableFunctions;
384
+ if (!supported) {
385
+ throw new UnsupportedFeatureError(`stored routine "${name}"`, nameOfDialect);
386
+ }
387
+
388
+ const placeholders = args.map((_, index) => formatPlaceholder(dialect, index + 1)).join(', ');
389
+ const routine = quoteIdentifier(dialect, name);
390
+ const text =
391
+ kind === 'procedure'
392
+ ? `CALL ${routine}(${placeholders})`
393
+ : kind === 'table-function'
394
+ ? `SELECT * FROM ${routine}(${placeholders})`
395
+ : `SELECT ${routine}(${placeholders}) AS ${quoteIdentifier(dialect, 'result')}`;
396
+ return frozenQuery(text, args);
397
+ }
398
+
399
+ function returningColumn(d: DialectTarget, column: ReturningColumn): string {
400
+ if (typeof column === 'string') return column === '*' ? '*' : quoteColumn(d, column);
401
+ return `${quoteColumn(d, column.column)} AS ${quoteIdentifier(d, column.alias)}`;
402
+ }
403
+
404
+ const NO_RETURNING_SQL: DialectReturningSql = Object.freeze({ inline: '', suffix: '' });
405
+
406
+ function returningSql(
407
+ d: DialectTarget,
408
+ statement: ReturningStatement,
409
+ row: 'new' | 'old',
410
+ cols?: readonly ReturningColumn[],
411
+ ): DialectReturningSql {
412
+ if (!cols || cols.length === 0) return NO_RETURNING_SQL;
413
+ const style = dialectTraits(d).returning[statement];
414
+ if (style === 'none') {
415
+ const name = dialectName(d);
416
+ throw new UnsupportedFeatureError(
417
+ 'returning',
418
+ name,
419
+ `returning is not supported for ${statement.toUpperCase()} on dialect "${name}"; ` +
420
+ 'omit returning() and perform an explicit read',
421
+ );
422
+ }
423
+ if (style === 'suffix') {
424
+ return Object.freeze({
425
+ inline: '',
426
+ suffix: ` RETURNING ${cols.map(column => returningColumn(d, column)).join(', ')}`,
427
+ });
428
+ }
429
+ if (typeof d === 'string' || d.compiler === undefined) {
430
+ throw new UnsupportedFeatureError(
431
+ `${style} returning strategy`,
432
+ dialectName(d),
433
+ `dialect "${dialectName(d)}" must provide its package-owned compiler strategy`,
434
+ );
435
+ }
436
+ return d.compiler.returning({
437
+ dialect: d,
438
+ statement,
439
+ row,
440
+ columns: cols,
441
+ });
442
+ }
443
+
444
+ interface ConflictState {
445
+ readonly action: 'update' | 'ignore';
446
+ readonly target?: readonly string[] | undefined;
447
+ readonly updateFields?: readonly string[] | Record<string, unknown> | undefined;
448
+ }
449
+
450
+ function normalizeTarget(target?: string | readonly string[]): readonly string[] | undefined {
451
+ if (!target) return undefined;
452
+ if (typeof target === 'string') return [target];
453
+ return target;
454
+ }
455
+
456
+ /** ` (a, b)` for an explicit conflict target; '' when the server infers it. */
457
+ function conflictTarget(d: DialectTarget, target?: readonly string[]): string {
458
+ if (!target || target.length === 0) return '';
459
+ return ` (${target.map(t => quoteIdentifier(d, t)).join(', ')})`;
460
+ }
461
+
462
+ /**
463
+ * `col = <the value this INSERT tried to write>` for each column. MySQL spells
464
+ * that VALUES(col) where postgres and sqlite say EXCLUDED.col. VALUES() is
465
+ * deprecated in MySQL 8.0.20+ in favour of a row alias (`AS new`), but keeping
466
+ * it means servers older than that still work.
467
+ */
468
+ function upsertSetSql(d: DialectTarget, cols: readonly string[], upsert: 'onConflict' | 'onDuplicateKey'): string {
469
+ const value = (c: string) =>
470
+ upsert === 'onDuplicateKey' ? `VALUES(${quoteIdentifier(d, c)})` : `EXCLUDED.${quoteIdentifier(d, c)}`;
471
+ return cols.map(c => `${quoteIdentifier(d, c)} = ${value(c)}`).join(', ');
472
+ }
473
+
474
+ function setValueSql(
475
+ d: DialectTarget,
476
+ table: string,
477
+ column: string,
478
+ value: unknown,
479
+ params: unknown[],
480
+ scope: 'update' | 'upsert',
481
+ references?: {
482
+ readonly current: string;
483
+ readonly proposed: string;
484
+ },
485
+ ): string {
486
+ if (!isColumnExpr(value)) {
487
+ params.push(value);
488
+ return formatPlaceholder(d, params.length);
489
+ }
490
+
491
+ const emitted = emitColumnExpr(value, {
492
+ dialect: d,
493
+ table,
494
+ column,
495
+ parameterIndex: params.length + 1,
496
+ scope,
497
+ ...(references === undefined
498
+ ? {}
499
+ : {
500
+ currentReference: references.current,
501
+ proposedReference: references.proposed,
502
+ }),
503
+ });
504
+ params.push(...emitted.params);
505
+ return emitted.sql;
506
+ }
507
+
508
+ function makeInsert(
509
+ d: DialectTarget,
510
+ table: string,
511
+ row?: Record<string, unknown>,
512
+ ret?: readonly ReturningColumn[],
513
+ conflict?: ConflictState,
514
+ telemetry = false,
515
+ ): InsertBuilder {
516
+ const setConflict = (c: ConflictState) => makeInsert(d, table, row, ret, c, telemetry);
517
+ return {
518
+ values: r => makeInsert(d, table, r, ret, conflict, telemetry),
519
+ returning: cols => makeInsert(d, table, row, cols ?? [], conflict, telemetry),
520
+ onConflict: target => {
521
+ const normTarget = normalizeTarget(target);
522
+ return {
523
+ doUpdate: updateFields => {
524
+ if (Array.isArray(updateFields) && updateFields.length === 0) {
525
+ throw new Error(
526
+ 'Empty updateFields array is not allowed in doUpdate(). Omit updateFields (or pass undefined) to update all non-target columns, or use doNothing().',
527
+ );
528
+ }
529
+ return setConflict({ action: 'update', target: normTarget, updateFields });
530
+ },
531
+ doNothing: () => setConflict({ action: 'ignore', target: normTarget }),
532
+ };
533
+ },
534
+ compile: () => {
535
+ if (!row) throw new Error('insertInto requires values()');
536
+ const keys = Object.keys(row);
537
+ const params = keys.map(k => row[k]);
538
+ const cols = keys.map(k => quoteIdentifier(d, k)).join(', ');
539
+ const placeholders = keys.map((_, i) => formatPlaceholder(d, i + 1));
540
+ const placeholderList = placeholders.join(', ');
541
+ const returning = returningSql(d, conflict === undefined ? 'insert' : 'upsert', 'new', ret);
542
+ const insert = `INSERT INTO ${quoteTable(d, table)} (${cols})${returning.inline} VALUES (${placeholderList})`;
543
+ let text: string;
544
+
545
+ if (!conflict) {
546
+ text = insert;
547
+ } else {
548
+ const upsert = dialectTraits(d).upsert;
549
+ if (upsert === 'none') throw new UnsupportedFeatureError('upsert', dialectName(d));
550
+ if (upsert === 'merge') {
551
+ if (typeof d === 'string' || d.compiler === undefined) {
552
+ throw new UnsupportedFeatureError(
553
+ `${upsert} upsert strategy`,
554
+ dialectName(d),
555
+ `dialect "${dialectName(d)}" must provide its package-owned compiler strategy`,
556
+ );
557
+ }
558
+ text = d.compiler.upsert({
559
+ dialect: d,
560
+ table,
561
+ columns: keys,
562
+ placeholders,
563
+ conflict,
564
+ returning,
565
+ renderUpdateValue: (column, value, references) =>
566
+ setValueSql(d, table, column, value, params, 'upsert', references),
567
+ isProposedValue: value => isColumnExpr(value) && value.op === 'proposed',
568
+ });
569
+ } else if (conflict.action === 'ignore') {
570
+ text =
571
+ upsert === 'onDuplicateKey'
572
+ ? `INSERT IGNORE INTO ${quoteTable(d, table)} (${cols}) VALUES (${placeholderList})`
573
+ : `${insert} ON CONFLICT${conflictTarget(d, conflict.target)} DO NOTHING`;
574
+ } else {
575
+ let setSql: string;
576
+
577
+ if (Array.isArray(conflict.updateFields)) {
578
+ setSql = upsertSetSql(d, conflict.updateFields, upsert);
579
+ } else if (conflict.updateFields) {
580
+ setSql = Object.entries(conflict.updateFields)
581
+ .map(([k, val]) => `${quoteIdentifier(d, k)} = ${setValueSql(d, table, k, val, params, 'upsert')}`)
582
+ .join(', ');
583
+ } else {
584
+ const targetSet = new Set(conflict.target ?? []);
585
+ const nonTarget = keys.filter(k => !targetSet.has(k));
586
+ // If every inserted column is a conflict target, nonTarget is empty and
587
+ // the SET list would be empty SQL. Setting them all back to what the
588
+ // INSERT carried is a valid no-op.
589
+ setSql = upsertSetSql(d, nonTarget.length > 0 ? nonTarget : keys, upsert);
590
+ }
591
+
592
+ text =
593
+ upsert === 'onDuplicateKey'
594
+ ? `${insert} ON DUPLICATE KEY UPDATE ${setSql}`
595
+ : `${insert} ON CONFLICT${conflictTarget(d, conflict.target)} DO UPDATE SET ${setSql}`;
596
+ }
597
+ }
598
+
599
+ text += returning.suffix;
600
+ return frozenQuery(text, params, queryTelemetry(d, 'INSERT', table, telemetry));
601
+ },
602
+ };
603
+ }
604
+
605
+ function makeUpdate(
606
+ d: DialectTarget,
607
+ table: string,
608
+ row?: Record<string, unknown>,
609
+ wheres: readonly Predicate[] = [],
610
+ ret?: readonly ReturningColumn[],
611
+ telemetry = false,
612
+ ): UpdateBuilder {
613
+ return {
614
+ set: r => makeUpdate(d, table, r, wheres, ret, telemetry),
615
+ where: (col, op, value) =>
616
+ makeUpdate(d, table, row, [...wheres, { col, op, value, connector: 'AND' }], ret, telemetry),
617
+ orWhere: (col, op, value) =>
618
+ makeUpdate(d, table, row, [...wheres, { col, op, value, connector: 'OR' }], ret, telemetry),
619
+ whereGroup: predicates =>
620
+ makeUpdate(d, table, row, [...wheres, { kind: 'group', predicates, connector: 'AND' }], ret, telemetry),
621
+ whereIn: (col, values) =>
622
+ makeUpdate(d, table, row, [...wheres, { col, op: 'in', value: values, connector: 'AND' }], ret, telemetry),
623
+ whereNotIn: (col, values) =>
624
+ makeUpdate(d, table, row, [...wheres, { col, op: 'not in', value: values, connector: 'AND' }], ret, telemetry),
625
+ returning: cols => makeUpdate(d, table, row, wheres, cols ?? [], telemetry),
626
+ compile: () => {
627
+ if (!row) throw new Error('updateTable requires set()');
628
+ const params: unknown[] = [];
629
+ const sets = Object.keys(row)
630
+ .map(k => `${quoteIdentifier(d, k)} = ${setValueSql(d, table, k, row[k], params, 'update')}`)
631
+ .join(', ');
632
+ const returning = returningSql(d, 'update', 'new', ret);
633
+ const text =
634
+ `UPDATE ${quoteTable(d, table)} SET ${sets}` +
635
+ returning.inline +
636
+ whereClause(d, wheres, params) +
637
+ returning.suffix;
638
+ return frozenQuery(text, params, queryTelemetry(d, 'UPDATE', table, telemetry));
639
+ },
640
+ };
641
+ }
642
+
643
+ function makeDelete(
644
+ d: DialectTarget,
645
+ table: string,
646
+ wheres: readonly Predicate[] = [],
647
+ ret?: readonly ReturningColumn[],
648
+ telemetry = false,
649
+ ): DeleteBuilder {
650
+ return {
651
+ where: (col, op, value) => makeDelete(d, table, [...wheres, { col, op, value, connector: 'AND' }], ret, telemetry),
652
+ orWhere: (col, op, value) => makeDelete(d, table, [...wheres, { col, op, value, connector: 'OR' }], ret, telemetry),
653
+ whereGroup: predicates =>
654
+ makeDelete(d, table, [...wheres, { kind: 'group', predicates, connector: 'AND' }], ret, telemetry),
655
+ whereIn: (col, values) =>
656
+ makeDelete(d, table, [...wheres, { col, op: 'in', value: values, connector: 'AND' }], ret, telemetry),
657
+ whereNotIn: (col, values) =>
658
+ makeDelete(d, table, [...wheres, { col, op: 'not in', value: values, connector: 'AND' }], ret, telemetry),
659
+ returning: cols => makeDelete(d, table, wheres, cols ?? [], telemetry),
660
+ compile: () => {
661
+ const params: unknown[] = [];
662
+ const returning = returningSql(d, 'delete', 'old', ret);
663
+ const text =
664
+ `DELETE FROM ${quoteTable(d, table)}` + returning.inline + whereClause(d, wheres, params) + returning.suffix;
665
+ return frozenQuery(text, params, queryTelemetry(d, 'DELETE', table, telemetry));
666
+ },
667
+ };
668
+ }
669
+
670
+ export function createQueryCompiler<Name extends string>(
671
+ dialect: SqlDialect<Name>,
672
+ options?: QueryCompilerOptions,
673
+ ): QueryCompiler;
674
+ export function createQueryCompiler(dialect: DialectTarget, options?: QueryCompilerOptions): QueryCompiler {
675
+ const telemetry = options?.telemetry === true;
676
+ return {
677
+ selectFrom: table => makeSelect(dialect, { table, wheres: [], orderBys: [] }, telemetry),
678
+ insertInto: table => makeInsert(dialect, table, undefined, undefined, undefined, telemetry),
679
+ updateTable: table => makeUpdate(dialect, table, undefined, [], undefined, telemetry),
680
+ deleteFrom: table => makeDelete(dialect, table, [], undefined, telemetry),
681
+ callFunction: (name, args) => routineCall(dialect, name, args, 'function'),
682
+ callTableFunction: (name, args) => routineCall(dialect, name, args, 'table-function'),
683
+ callProcedure: (name, args) => routineCall(dialect, name, args, 'procedure'),
684
+ };
685
+ }