@vibeorm/sql 2.0.0-alpha.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.
package/dist/index.js ADDED
@@ -0,0 +1,1030 @@
1
+ // @bun
2
+ // packages/sql/src/ast.ts
3
+ var SET_OPERATORS = ["increment", "decrement", "multiply", "divide"];
4
+ function isSetOperation(value) {
5
+ if (typeof value !== "object" || value === null || Array.isArray(value))
6
+ return false;
7
+ const keys = Object.keys(value);
8
+ if (keys.length !== 2 || !keys.includes("__op") || !keys.includes("value"))
9
+ return false;
10
+ const candidate = value;
11
+ return typeof candidate.__op === "string" && SET_OPERATORS.includes(candidate.__op) && typeof candidate.value === "number" && Number.isFinite(candidate.value);
12
+ }
13
+ var LIST_SET_OPERATORS = ["append", "concat"];
14
+ function isListSetOperation(value) {
15
+ if (typeof value !== "object" || value === null || Array.isArray(value))
16
+ return false;
17
+ const keys = Object.keys(value);
18
+ if (keys.length !== 2 || !keys.includes("__listOp") || !keys.includes("value"))
19
+ return false;
20
+ const candidate = value;
21
+ return typeof candidate.__listOp === "string" && LIST_SET_OPERATORS.includes(candidate.__listOp);
22
+ }
23
+ var SQL_DEFAULT_CELL = Object.freeze({ __sqlDefault: true });
24
+ function isSqlDefaultCell(value) {
25
+ return value === SQL_DEFAULT_CELL;
26
+ }
27
+ function jsonParamCell(params) {
28
+ return { __jsonParam: true, text: params.text };
29
+ }
30
+ function isJsonParamCell(value) {
31
+ return typeof value === "object" && value !== null && value.__jsonParam === true;
32
+ }
33
+ // packages/sql/src/capabilities.ts
34
+ var POSTGRES_CAPABILITIES = {
35
+ returning: true,
36
+ scalarArrays: true,
37
+ nativeEnums: true,
38
+ ilike: true,
39
+ lateralJoin: true,
40
+ distinctOn: true,
41
+ partialIndexes: true,
42
+ transactionalDdl: true,
43
+ createManyAndReturn: true,
44
+ jsonColumnType: true,
45
+ orderByNulls: true,
46
+ fullTextEngines: ["bm25", "tsvector"]
47
+ };
48
+ var SQLITE_CAPABILITIES = {
49
+ returning: true,
50
+ scalarArrays: false,
51
+ nativeEnums: false,
52
+ ilike: false,
53
+ lateralJoin: false,
54
+ distinctOn: false,
55
+ partialIndexes: true,
56
+ transactionalDdl: true,
57
+ createManyAndReturn: true,
58
+ jsonColumnType: false,
59
+ orderByNulls: true,
60
+ fullTextEngines: ["fts5"]
61
+ };
62
+ var MYSQL_CAPABILITIES = {
63
+ returning: false,
64
+ scalarArrays: false,
65
+ nativeEnums: true,
66
+ ilike: false,
67
+ lateralJoin: false,
68
+ distinctOn: false,
69
+ partialIndexes: false,
70
+ transactionalDdl: false,
71
+ createManyAndReturn: false,
72
+ jsonColumnType: true,
73
+ orderByNulls: false,
74
+ fullTextEngines: ["fulltext"]
75
+ };
76
+ var DIALECT_CAPABILITIES = {
77
+ postgres: POSTGRES_CAPABILITIES,
78
+ sqlite: SQLITE_CAPABILITIES,
79
+ mysql: MYSQL_CAPABILITIES
80
+ };
81
+
82
+ // packages/sql/src/dialects/postgres.ts
83
+ var postgresDialect = {
84
+ name: "postgres",
85
+ capabilities: POSTGRES_CAPABILITIES,
86
+ inArrayStrategy: "any",
87
+ upsertForm: "onConflict",
88
+ doNothingForm: "onConflictDoNothing",
89
+ insensitiveLike: "ilike",
90
+ strictMatchStrategy: "native",
91
+ orderByNullsMode: "native",
92
+ offsetRequiresLimit: null,
93
+ likeEscapeClause: null,
94
+ jsonPathMode: "operators",
95
+ supportedJsonOps: [
96
+ "equals",
97
+ "string_contains",
98
+ "string_starts_with",
99
+ "string_ends_with",
100
+ "array_contains",
101
+ "array_starts_with",
102
+ "array_ends_with"
103
+ ],
104
+ jsonParamCast: "::text::jsonb",
105
+ lastInsertIdSql: null,
106
+ selectForUpdate: " FOR UPDATE",
107
+ rowDefaultKeyword: true,
108
+ paramBudget: 30000,
109
+ quoteIdent({ name }) {
110
+ return `"${name.replaceAll('"', '""')}"`;
111
+ },
112
+ placeholder({ index }) {
113
+ return `$${index}`;
114
+ }
115
+ };
116
+ // packages/sql/src/dialects/sqlite.ts
117
+ var sqliteDialect = {
118
+ name: "sqlite",
119
+ capabilities: SQLITE_CAPABILITIES,
120
+ inArrayStrategy: "expand",
121
+ upsertForm: "onConflict",
122
+ doNothingForm: "onConflictDoNothing",
123
+ insensitiveLike: "lower",
124
+ strictMatchStrategy: "glob",
125
+ orderByNullsMode: "native",
126
+ offsetRequiresLimit: { limitText: "-1" },
127
+ likeEscapeClause: " ESCAPE '\\'",
128
+ jsonPathMode: "sqliteExtract",
129
+ supportedJsonOps: [
130
+ "equals",
131
+ "string_contains",
132
+ "string_starts_with",
133
+ "string_ends_with",
134
+ "array_contains",
135
+ "array_starts_with",
136
+ "array_ends_with"
137
+ ],
138
+ jsonParamCast: "",
139
+ lastInsertIdSql: null,
140
+ selectForUpdate: null,
141
+ rowDefaultKeyword: false,
142
+ paramBudget: 900,
143
+ quoteIdent({ name }) {
144
+ return `"${name.replaceAll('"', '""')}"`;
145
+ },
146
+ placeholder() {
147
+ return "?";
148
+ }
149
+ };
150
+ // packages/sql/src/dialects/mysql.ts
151
+ var mysqlDialect = {
152
+ name: "mysql",
153
+ capabilities: MYSQL_CAPABILITIES,
154
+ inArrayStrategy: "expand",
155
+ upsertForm: "onDuplicateKey",
156
+ doNothingForm: "insertIgnore",
157
+ insensitiveLike: "lower",
158
+ strictMatchStrategy: "binary",
159
+ orderByNullsMode: "emulate",
160
+ offsetRequiresLimit: { limitText: "18446744073709551615" },
161
+ likeEscapeClause: null,
162
+ jsonPathMode: "mysqlExtract",
163
+ supportedJsonOps: [
164
+ "equals",
165
+ "string_contains",
166
+ "string_starts_with",
167
+ "string_ends_with",
168
+ "array_contains",
169
+ "array_starts_with",
170
+ "array_ends_with"
171
+ ],
172
+ jsonParamCast: "",
173
+ lastInsertIdSql: "SELECT LAST_INSERT_ID() AS `id`",
174
+ selectForUpdate: " FOR UPDATE",
175
+ rowDefaultKeyword: true,
176
+ paramBudget: 1e4,
177
+ quoteIdent({ name }) {
178
+ return `\`${name.replaceAll("`", "``")}\``;
179
+ },
180
+ placeholder() {
181
+ return "?";
182
+ }
183
+ };
184
+ // packages/sql/src/dialects/registry.ts
185
+ var SQL_DIALECTS = {
186
+ postgres: postgresDialect,
187
+ sqlite: sqliteDialect,
188
+ mysql: mysqlDialect
189
+ };
190
+ function getSqlDialect(params) {
191
+ return SQL_DIALECTS[params.dialect];
192
+ }
193
+ // packages/sql/src/codecs.ts
194
+ import { VibeError } from "@vibeorm/schema";
195
+ function decodeIsNoop(params) {
196
+ const { scalarType, wire } = params;
197
+ if (wire === undefined)
198
+ return false;
199
+ if (scalarType === "DateTime")
200
+ return wire.dateTime === "native";
201
+ if (scalarType === "Json")
202
+ return wire.json === "parsed";
203
+ if (scalarType === "Int" || scalarType === "Float")
204
+ return wire.numbers === "native";
205
+ return false;
206
+ }
207
+ var identity = (value) => value;
208
+ var passthroughCodec = { encode: identity, decode: identity };
209
+ var numberCodec = {
210
+ encode: identity,
211
+ decode: (value) => typeof value === "string" ? Number(value) : value
212
+ };
213
+ var bigIntCodec = {
214
+ encode: (value) => typeof value === "bigint" ? value.toString() : value,
215
+ decode: (value) => {
216
+ if (typeof value === "bigint")
217
+ return value;
218
+ if (typeof value === "string" || typeof value === "number")
219
+ return BigInt(value);
220
+ return value;
221
+ }
222
+ };
223
+ var decimalCodec = {
224
+ encode: (value) => typeof value === "number" || typeof value === "bigint" ? String(value) : value,
225
+ decode: identity
226
+ };
227
+ var mysqlDecimalCodec = {
228
+ encode: decimalCodec.encode,
229
+ decode: (value) => {
230
+ if (typeof value !== "string" || !value.includes("."))
231
+ return value;
232
+ const trimmed = value.replace(/0+$/, "").replace(/\.$/, "");
233
+ return trimmed === "" || trimmed === "-" ? "0" : trimmed;
234
+ }
235
+ };
236
+ var dateNativeCodec = {
237
+ encode: identity,
238
+ decode: (value) => {
239
+ if (value instanceof Date)
240
+ return value;
241
+ if (typeof value === "string" || typeof value === "number")
242
+ return new Date(value);
243
+ return value;
244
+ }
245
+ };
246
+ var dateTextCodec = {
247
+ encode: (value) => value instanceof Date ? value.toISOString() : value,
248
+ decode: dateNativeCodec.decode
249
+ };
250
+ var EXPLICIT_ZONE = /(?:Z|[+-]\d{2}:?\d{2})$/i;
251
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
252
+ var mysqlDateCodec = {
253
+ encode: (value) => {
254
+ if (value instanceof Date)
255
+ return value;
256
+ if (typeof value === "number") {
257
+ const parsed = new Date(value);
258
+ if (!Number.isNaN(parsed.getTime()))
259
+ return parsed;
260
+ return value;
261
+ }
262
+ if (typeof value === "string") {
263
+ const trimmed = value.trim();
264
+ if (!EXPLICIT_ZONE.test(trimmed) && !DATE_ONLY.test(trimmed)) {
265
+ const asUtc = new Date(`${trimmed.replace(" ", "T")}Z`);
266
+ if (!Number.isNaN(asUtc.getTime()))
267
+ return asUtc;
268
+ }
269
+ const parsed = new Date(trimmed);
270
+ if (!Number.isNaN(parsed.getTime()))
271
+ return parsed;
272
+ }
273
+ return value;
274
+ },
275
+ decode: dateNativeCodec.decode
276
+ };
277
+ var booleanIntCodec = {
278
+ encode: (value) => typeof value === "boolean" ? value ? 1 : 0 : value,
279
+ decode: (value) => typeof value === "number" ? value !== 0 : value
280
+ };
281
+ var booleanNativeInCodec = {
282
+ encode: identity,
283
+ decode: booleanIntCodec.decode
284
+ };
285
+ var jsonCodec = {
286
+ encode: (value) => JSON.stringify(value),
287
+ decode: (value) => {
288
+ if (typeof value !== "string")
289
+ return value;
290
+ try {
291
+ return JSON.parse(value);
292
+ } catch (cause) {
293
+ throw new VibeError({
294
+ code: "VIBE_INTERNAL",
295
+ message: "a Json column returned text that is not valid JSON",
296
+ meta: { value },
297
+ cause
298
+ });
299
+ }
300
+ }
301
+ };
302
+ var POSTGRES_CODECS = {
303
+ String: passthroughCodec,
304
+ Boolean: passthroughCodec,
305
+ Int: numberCodec,
306
+ BigInt: bigIntCodec,
307
+ Float: numberCodec,
308
+ Decimal: decimalCodec,
309
+ DateTime: dateNativeCodec,
310
+ Json: jsonCodec,
311
+ Bytes: passthroughCodec
312
+ };
313
+ var SQLITE_CODECS = {
314
+ String: passthroughCodec,
315
+ Boolean: booleanIntCodec,
316
+ Int: numberCodec,
317
+ BigInt: bigIntCodec,
318
+ Float: numberCodec,
319
+ Decimal: decimalCodec,
320
+ DateTime: dateTextCodec,
321
+ Json: jsonCodec,
322
+ Bytes: passthroughCodec
323
+ };
324
+ var MYSQL_CODECS = {
325
+ String: passthroughCodec,
326
+ Boolean: booleanNativeInCodec,
327
+ Int: numberCodec,
328
+ BigInt: bigIntCodec,
329
+ Float: numberCodec,
330
+ Decimal: mysqlDecimalCodec,
331
+ DateTime: mysqlDateCodec,
332
+ Json: jsonCodec,
333
+ Bytes: passthroughCodec
334
+ };
335
+ var DIALECT_CODECS = {
336
+ postgres: POSTGRES_CODECS,
337
+ sqlite: SQLITE_CODECS,
338
+ mysql: MYSQL_CODECS
339
+ };
340
+ // packages/sql/src/render.ts
341
+ import { internalError, unsupportedCapability } from "@vibeorm/schema";
342
+
343
+ // packages/sql/src/like.ts
344
+ function escapeLikePattern(params) {
345
+ return params.raw.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
346
+ }
347
+ function containsPattern(params) {
348
+ return `%${escapeLikePattern({ raw: params.raw })}%`;
349
+ }
350
+ function startsWithPattern(params) {
351
+ return `${escapeLikePattern({ raw: params.raw })}%`;
352
+ }
353
+ function endsWithPattern(params) {
354
+ return `%${escapeLikePattern({ raw: params.raw })}`;
355
+ }
356
+ function escapeGlobPattern(params) {
357
+ return params.raw.replace(/[*?[]/g, (char) => `[${char}]`);
358
+ }
359
+ function strictMatchPattern(params) {
360
+ const { strategy, op, value } = params;
361
+ if (strategy === "glob") {
362
+ const inner = escapeGlobPattern({ raw: value });
363
+ return op === "contains" ? `*${inner}*` : op === "startsWith" ? `${inner}*` : `*${inner}`;
364
+ }
365
+ return op === "contains" ? containsPattern({ raw: value }) : op === "startsWith" ? startsWithPattern({ raw: value }) : endsWithPattern({ raw: value });
366
+ }
367
+
368
+ // packages/sql/src/params.ts
369
+ class ParamCollector {
370
+ dialect;
371
+ values;
372
+ constructor(dialect) {
373
+ this.dialect = dialect;
374
+ this.values = [];
375
+ }
376
+ add(value) {
377
+ this.values.push(value);
378
+ return this.dialect.placeholder({ index: this.values.length });
379
+ }
380
+ addMany(values) {
381
+ return values.map((value) => this.add(value));
382
+ }
383
+ }
384
+
385
+ // packages/sql/src/render.ts
386
+ function renderStatement(params) {
387
+ const { dialect, statement } = params;
388
+ const collector = new ParamCollector(dialect);
389
+ switch (statement.kind) {
390
+ case "select":
391
+ return renderSelect({ dialect, statement, collector });
392
+ case "insert":
393
+ return renderInsert({ dialect, statement, collector });
394
+ case "update":
395
+ return renderUpdate({ dialect, statement, collector });
396
+ case "delete":
397
+ return renderDelete({ dialect, statement, collector });
398
+ }
399
+ }
400
+ function quoteCol(dialect, column) {
401
+ const name = dialect.quoteIdent({ name: column.name });
402
+ const ref = column.table === undefined ? name : `${dialect.quoteIdent({ name: column.table })}.${name}`;
403
+ if (column.castReal === true) {
404
+ if (dialect.name !== "sqlite") {
405
+ throw internalError({ message: "ColRef.castReal is a sqlite Decimal construct" });
406
+ }
407
+ return `CAST(${ref} AS REAL)`;
408
+ }
409
+ return ref;
410
+ }
411
+ function quoteProjection(dialect, column) {
412
+ if (column.transform === undefined) {
413
+ const base = quoteCol(dialect, column);
414
+ if (column.alias === undefined)
415
+ return base;
416
+ return `${base} AS ${dialect.quoteIdent({ name: column.alias })}`;
417
+ }
418
+ if (dialect.name !== "postgres") {
419
+ throw internalError({ message: "ColRef.transform is a postgres lateral-join construct" });
420
+ }
421
+ const ref = quoteCol(dialect, column);
422
+ const alias = dialect.quoteIdent({ name: column.name });
423
+ if (column.transform === "castText")
424
+ return `${ref}::text AS ${alias}`;
425
+ return `encode(${ref}, 'base64') AS ${alias}`;
426
+ }
427
+ function renderTable(dialect, table) {
428
+ const name = dialect.quoteIdent({ name: table.name });
429
+ if (table.alias === undefined)
430
+ return name;
431
+ return `${name} AS ${dialect.quoteIdent({ name: table.alias })}`;
432
+ }
433
+ function assertCount(value, what) {
434
+ if (!Number.isSafeInteger(value) || value < 0) {
435
+ throw internalError({ message: `${what} must be a non-negative integer, got ${value}` });
436
+ }
437
+ }
438
+ var COMPARISON_SQL = {
439
+ eq: "=",
440
+ ne: "<>",
441
+ gt: ">",
442
+ gte: ">=",
443
+ lt: "<",
444
+ lte: "<="
445
+ };
446
+ var SET_OPERATION_SQL = {
447
+ increment: "+",
448
+ decrement: "-",
449
+ multiply: "*",
450
+ divide: "/"
451
+ };
452
+ var AGG_FN_SQL = {
453
+ count: "COUNT",
454
+ avg: "AVG",
455
+ sum: "SUM",
456
+ min: "MIN",
457
+ max: "MAX"
458
+ };
459
+ function renderAggRef(dialect, agg) {
460
+ if (agg.fn === "countAll")
461
+ return "COUNT(*)";
462
+ return `${AGG_FN_SQL[agg.fn]}(${quoteCol(dialect, agg.column)})`;
463
+ }
464
+ function assertScalarArrays(dialect, feature) {
465
+ if (dialect.capabilities.scalarArrays)
466
+ return;
467
+ throw unsupportedCapability({
468
+ dialect: dialect.name,
469
+ feature,
470
+ hint: "only postgres has native array columns \u2014 model the list as a related table instead"
471
+ });
472
+ }
473
+ function renderExpr(params) {
474
+ const { dialect, expr, collector } = params;
475
+ switch (expr.kind) {
476
+ case "cmp": {
477
+ const op = COMPARISON_SQL[expr.op];
478
+ if (op === undefined) {
479
+ throw internalError({ message: `unknown comparison operator "${expr.op}"` });
480
+ }
481
+ return `${quoteCol(dialect, expr.column)} ${op} ${collector.add(expr.value)}`;
482
+ }
483
+ case "cmpCol": {
484
+ const op = COMPARISON_SQL[expr.op];
485
+ if (op === undefined) {
486
+ throw internalError({ message: `unknown comparison operator "${expr.op}"` });
487
+ }
488
+ return `${quoteCol(dialect, expr.left)} ${op} ${quoteCol(dialect, expr.right)}`;
489
+ }
490
+ case "isNull":
491
+ return `${quoteCol(dialect, expr.column)} IS ${expr.negate === true ? "NOT NULL" : "NULL"}`;
492
+ case "inArray": {
493
+ if (expr.values.length === 0) {
494
+ return expr.negate === true ? "1 = 1" : "1 = 0";
495
+ }
496
+ const column = quoteCol(dialect, expr.column);
497
+ if (dialect.inArrayStrategy === "any") {
498
+ const base = `${column} = ANY(${collector.add([...expr.values])})`;
499
+ return expr.negate === true ? `NOT (${base})` : base;
500
+ }
501
+ const placeholders = collector.addMany(expr.values);
502
+ return `${column} ${expr.negate === true ? "NOT IN" : "IN"} (${placeholders.join(", ")})`;
503
+ }
504
+ case "like": {
505
+ const column = quoteCol(dialect, expr.column);
506
+ const placeholder = collector.add(expr.pattern);
507
+ let base;
508
+ if (expr.insensitive === true) {
509
+ base = dialect.insensitiveLike === "ilike" ? `${column} ILIKE ${placeholder}` : `LOWER(${column}) LIKE LOWER(${placeholder})`;
510
+ } else {
511
+ base = `${column} LIKE ${placeholder}`;
512
+ }
513
+ if (dialect.likeEscapeClause !== null) {
514
+ base = `${base}${dialect.likeEscapeClause}`;
515
+ }
516
+ return expr.negate === true ? `NOT (${base})` : base;
517
+ }
518
+ case "strictMatch": {
519
+ const column = quoteCol(dialect, expr.column);
520
+ let base;
521
+ if (expr.op === "equals") {
522
+ base = dialect.strictMatchStrategy === "binary" ? `${column} = CAST(${collector.add(expr.value)} AS BINARY)` : `${column} = ${collector.add(expr.value)}`;
523
+ } else {
524
+ const pattern = strictMatchPattern({
525
+ strategy: dialect.strictMatchStrategy,
526
+ op: expr.op,
527
+ value: expr.value
528
+ });
529
+ if (dialect.strictMatchStrategy === "glob") {
530
+ base = `${column} GLOB ${collector.add(pattern)}`;
531
+ } else {
532
+ const placeholder = collector.add(pattern);
533
+ base = dialect.strictMatchStrategy === "binary" ? `${column} LIKE CAST(${placeholder} AS BINARY)` : `${column} LIKE ${placeholder}`;
534
+ if (dialect.likeEscapeClause !== null)
535
+ base = `${base}${dialect.likeEscapeClause}`;
536
+ }
537
+ }
538
+ return expr.negate === true ? `NOT (${base})` : base;
539
+ }
540
+ case "and":
541
+ case "or": {
542
+ if (expr.operands.length === 0) {
543
+ return expr.kind === "and" ? "1 = 1" : "1 = 0";
544
+ }
545
+ if (expr.operands.length === 1) {
546
+ return renderExpr({ dialect, expr: expr.operands[0], collector });
547
+ }
548
+ const joiner = expr.kind === "and" ? " AND " : " OR ";
549
+ const rendered = expr.operands.map((operand) => renderExpr({ dialect, expr: operand, collector }));
550
+ return `(${rendered.join(joiner)})`;
551
+ }
552
+ case "not":
553
+ return `NOT (${renderExpr({ dialect, expr: expr.operand, collector })})`;
554
+ case "aggCmp": {
555
+ const op = COMPARISON_SQL[expr.op];
556
+ if (op === undefined) {
557
+ throw internalError({ message: `unknown comparison operator "${expr.op}"` });
558
+ }
559
+ return `${renderAggRef(dialect, expr.agg)} ${op} ${collector.add(expr.value)}`;
560
+ }
561
+ case "arrayHas": {
562
+ assertScalarArrays(dialect, 'the scalar-list filter "has"');
563
+ const base = `${collector.add(expr.value)} = ANY(${quoteCol(dialect, expr.column)})`;
564
+ return expr.negate === true ? `NOT (${base})` : base;
565
+ }
566
+ case "arrayContainsAll": {
567
+ assertScalarArrays(dialect, 'the scalar-list filter "hasEvery"');
568
+ return `${quoteCol(dialect, expr.column)} @> ${collector.add([...expr.values])}`;
569
+ }
570
+ case "arrayOverlaps": {
571
+ assertScalarArrays(dialect, 'the scalar-list filter "hasSome"');
572
+ return `${quoteCol(dialect, expr.column)} && ${collector.add([...expr.values])}`;
573
+ }
574
+ case "arrayIsEmpty": {
575
+ assertScalarArrays(dialect, 'the scalar-list filter "isEmpty"');
576
+ const column = quoteCol(dialect, expr.column);
577
+ return `array_length(${column}, 1) IS ${expr.empty ? "NULL" : "NOT NULL"}`;
578
+ }
579
+ case "jsonFilter":
580
+ return renderJsonFilter({ dialect, expr, collector });
581
+ case "exists": {
582
+ const sub = renderSelect({ dialect, statement: expr.select, collector });
583
+ return `${expr.negate === true ? "NOT " : ""}EXISTS (${sub.text})`;
584
+ }
585
+ case "fragment":
586
+ return renderFragment({ dialect, fragment: expr.fragment, collector });
587
+ case "raw":
588
+ return expr.text;
589
+ }
590
+ }
591
+ function renderFragment(params) {
592
+ const { dialect, fragment, collector } = params;
593
+ let text = "";
594
+ for (const part of fragment.parts) {
595
+ if (typeof part === "string") {
596
+ text += part;
597
+ } else if ("param" in part) {
598
+ text += collector.add(part.param);
599
+ } else {
600
+ text += part.ident.map((name) => dialect.quoteIdent({ name })).join(".");
601
+ }
602
+ }
603
+ return text;
604
+ }
605
+ function jsonPathString(path) {
606
+ let out = "$";
607
+ for (const segment of path) {
608
+ if (typeof segment === "number") {
609
+ out += `[${segment}]`;
610
+ } else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)) {
611
+ out += `.${segment}`;
612
+ } else {
613
+ out += `."${segment.replaceAll('"', "\\\"")}"`;
614
+ }
615
+ }
616
+ return out;
617
+ }
618
+ function jsonStringPattern(params) {
619
+ const { expr } = params;
620
+ if (typeof expr.value !== "string") {
621
+ throw internalError({ message: `json ${expr.op} expects a string operand` });
622
+ }
623
+ if (expr.op === "string_contains")
624
+ return containsPattern({ raw: expr.value });
625
+ if (expr.op === "string_starts_with")
626
+ return startsWithPattern({ raw: expr.value });
627
+ return endsWithPattern({ raw: expr.value });
628
+ }
629
+ function renderJsonLike(params) {
630
+ const { dialect, target, expr, collector } = params;
631
+ const placeholder = collector.add(jsonStringPattern({ expr }));
632
+ let base;
633
+ if (expr.insensitive === true) {
634
+ base = dialect.insensitiveLike === "ilike" ? `${target} ILIKE ${placeholder}` : `LOWER(${target}) LIKE LOWER(${placeholder})`;
635
+ } else {
636
+ base = `${target} LIKE ${placeholder}`;
637
+ }
638
+ if (dialect.likeEscapeClause !== null)
639
+ base = `${base}${dialect.likeEscapeClause}`;
640
+ return expr.negate === true ? `NOT (${base})` : base;
641
+ }
642
+ function renderJsonFilter(params) {
643
+ const { dialect, expr, collector } = params;
644
+ if (!dialect.supportedJsonOps.includes(expr.op)) {
645
+ throw unsupportedCapability({
646
+ dialect: dialect.name,
647
+ feature: `the json filter "${expr.op}"`,
648
+ hint: `this dialect can express: ${dialect.supportedJsonOps.join(", ")}`
649
+ });
650
+ }
651
+ const cmp = expr.negate === true ? "<>" : "=";
652
+ if (dialect.jsonPathMode === "operators") {
653
+ const column2 = quoteCol(dialect, expr.column);
654
+ const pathParam = `${collector.add(expr.path.map(String))}::text[]`;
655
+ switch (expr.op) {
656
+ case "equals":
657
+ return `(${column2} #> ${pathParam}) ${cmp} ${collector.add(JSON.stringify(expr.value))}${dialect.jsonParamCast}`;
658
+ case "string_contains":
659
+ case "string_starts_with":
660
+ case "string_ends_with":
661
+ return renderJsonLike({ dialect, target: `(${column2} #>> ${pathParam})`, expr, collector });
662
+ case "array_contains":
663
+ return `(${column2} #> ${pathParam}) @> ${collector.add(JSON.stringify(expr.value))}${dialect.jsonParamCast}`;
664
+ case "array_starts_with":
665
+ return `(${column2} #> ${pathParam}) -> 0 = ${collector.add(JSON.stringify(expr.value))}${dialect.jsonParamCast}`;
666
+ case "array_ends_with":
667
+ return `(${column2} #> ${pathParam}) -> -1 = ${collector.add(JSON.stringify(expr.value))}${dialect.jsonParamCast}`;
668
+ }
669
+ }
670
+ if (dialect.jsonPathMode === "sqliteExtract") {
671
+ const column2 = quoteCol(dialect, expr.column);
672
+ const compareExtracted = (target, value) => {
673
+ if (typeof value === "boolean") {
674
+ return `${target} ${cmp} ${collector.add(value ? 1 : 0)}`;
675
+ }
676
+ if (typeof value === "object" && value !== null) {
677
+ return `${target} ${cmp} json(${collector.add(JSON.stringify(value))})`;
678
+ }
679
+ return `${target} ${cmp} ${collector.add(value)}`;
680
+ };
681
+ switch (expr.op) {
682
+ case "equals":
683
+ return compareExtracted(`json_extract(${column2}, ${collector.add(jsonPathString(expr.path))})`, expr.value);
684
+ case "string_contains":
685
+ case "string_starts_with":
686
+ case "string_ends_with": {
687
+ const target = `json_extract(${column2}, ${collector.add(jsonPathString(expr.path))})`;
688
+ return renderJsonLike({ dialect, target, expr, collector });
689
+ }
690
+ case "array_starts_with":
691
+ case "array_ends_with": {
692
+ const edge = expr.op === "array_starts_with" ? "[0]" : "[#-1]";
693
+ return compareExtracted(`json_extract(${column2}, ${collector.add(`${jsonPathString(expr.path)}${edge}`)})`, expr.value);
694
+ }
695
+ case "array_contains": {
696
+ const guard = `json_type(${column2}, ${collector.add(jsonPathString(expr.path))}) = 'array'`;
697
+ const elements = Array.isArray(expr.value) ? expr.value : [expr.value];
698
+ const tests = elements.map((element) => {
699
+ if (element === null || typeof element === "object" && element !== null) {
700
+ throw unsupportedCapability({
701
+ dialect: dialect.name,
702
+ feature: "json array_contains with object/array/null elements",
703
+ hint: "sqlite expresses scalar-element containment only \u2014 deep structural containment needs postgres or mysql"
704
+ });
705
+ }
706
+ const bound = typeof element === "boolean" ? element ? 1 : 0 : element;
707
+ return `EXISTS (SELECT 1 FROM json_each(${column2}, ${collector.add(jsonPathString(expr.path))}) WHERE ${dialect.quoteIdent({ name: "json_each" })}.${dialect.quoteIdent({ name: "value" })} = ${collector.add(bound)})`;
708
+ });
709
+ return tests.length === 0 ? guard : `(${[guard, ...tests].join(" AND ")})`;
710
+ }
711
+ default:
712
+ throw internalError({ message: `json op "${expr.op}" leaked past supportedJsonOps on sqlite` });
713
+ }
714
+ }
715
+ const column = quoteCol(dialect, expr.column);
716
+ const pathString = jsonPathString(expr.path);
717
+ switch (expr.op) {
718
+ case "equals":
719
+ return `JSON_EXTRACT(${column}, ${collector.add(pathString)}) ${cmp} CAST(${collector.add(JSON.stringify(expr.value))} AS JSON)`;
720
+ case "string_contains":
721
+ case "string_starts_with":
722
+ case "string_ends_with": {
723
+ const target = `JSON_UNQUOTE(JSON_EXTRACT(${column}, ${collector.add(pathString)}))`;
724
+ return renderJsonLike({ dialect, target, expr, collector });
725
+ }
726
+ case "array_contains":
727
+ return `JSON_CONTAINS(JSON_EXTRACT(${column}, ${collector.add(pathString)}), CAST(${collector.add(JSON.stringify(expr.value))} AS JSON))`;
728
+ case "array_starts_with":
729
+ return `JSON_EXTRACT(${column}, ${collector.add(`${pathString}[0]`)}) ${cmp} CAST(${collector.add(JSON.stringify(expr.value))} AS JSON)`;
730
+ case "array_ends_with":
731
+ return `JSON_EXTRACT(${column}, ${collector.add(`${pathString}[last]`)}) ${cmp} CAST(${collector.add(JSON.stringify(expr.value))} AS JSON)`;
732
+ }
733
+ }
734
+ function renderOrderTerm(params) {
735
+ const { dialect, term, collector } = params;
736
+ if ("agg" in term) {
737
+ return `${renderAggRef(dialect, term.agg)} ${term.direction === "asc" ? "ASC" : "DESC"}`;
738
+ }
739
+ if ("select" in term) {
740
+ const sub = renderSelect({ dialect, statement: term.select, collector });
741
+ return `(${sub.text}) ${term.direction === "asc" ? "ASC" : "DESC"}`;
742
+ }
743
+ const column = quoteCol(dialect, term.column);
744
+ const direction = term.direction === "asc" ? "ASC" : "DESC";
745
+ if (term.nulls === undefined) {
746
+ return `${column} ${direction}`;
747
+ }
748
+ if (dialect.orderByNullsMode === "native") {
749
+ return `${column} ${direction} NULLS ${term.nulls === "first" ? "FIRST" : "LAST"}`;
750
+ }
751
+ if (term.nulls === "first" === (term.direction === "asc")) {
752
+ return `${column} ${direction}`;
753
+ }
754
+ const nullsTerm = term.nulls === "first" ? `${column} IS NULL DESC` : `${column} IS NULL`;
755
+ return `${nullsTerm}, ${column} ${direction}`;
756
+ }
757
+ function renderSelectExpr(params) {
758
+ const { dialect, expr } = params;
759
+ if (expr.kind === "one")
760
+ return "1";
761
+ const alias = expr.alias === undefined ? "" : ` AS ${dialect.quoteIdent({ name: expr.alias })}`;
762
+ return `COUNT(*)${alias}`;
763
+ }
764
+ function renderLateralJoin(params) {
765
+ const { dialect, join, collector } = params;
766
+ const inner = renderSelect({ dialect, statement: join.select, collector });
767
+ const columnAlias = dialect.quoteIdent({ name: join.columnAlias });
768
+ const subAlias = dialect.quoteIdent({ name: "__sub" });
769
+ let aggOrder = "";
770
+ if (join.agg === "jsonArray" && join.select.orderBy !== undefined && join.select.orderBy.length > 0) {
771
+ const projected = new Set(join.select.columns.filter((column) => column.transform === undefined && column.castReal !== true).map((column) => column.name));
772
+ const terms = join.select.orderBy;
773
+ const columnTerms = terms.filter((term) => !("agg" in term));
774
+ if (columnTerms.length === terms.length && columnTerms.every((term) => projected.has(term.column.name))) {
775
+ const rendered = columnTerms.map((term) => renderOrderTerm({ dialect, term: { ...term, column: { table: "__sub", name: term.column.name } }, collector }));
776
+ aggOrder = ` ORDER BY ${rendered.join(", ")}`;
777
+ }
778
+ }
779
+ const projection = join.agg === "jsonArray" ? `COALESCE(jsonb_agg(to_jsonb(${subAlias}.*)${aggOrder}), '[]'::jsonb) AS ${columnAlias}` : `to_jsonb(${subAlias}.*) AS ${columnAlias}`;
780
+ const alias = dialect.quoteIdent({ name: join.alias });
781
+ return `LEFT JOIN LATERAL (SELECT ${projection} FROM (${inner.text}) AS ${subAlias}) AS ${alias} ON true`;
782
+ }
783
+ function applyReturning(params) {
784
+ const { dialect, requested } = params;
785
+ if (requested === undefined || requested.length === 0) {
786
+ return { suffix: "", status: "none" };
787
+ }
788
+ if (!dialect.capabilities.returning) {
789
+ return { suffix: "", status: "omitted" };
790
+ }
791
+ const columns = requested.map((name) => dialect.quoteIdent({ name })).join(", ");
792
+ return { suffix: ` RETURNING ${columns}`, status: "native" };
793
+ }
794
+ function renderSelect(params) {
795
+ const { dialect, statement, collector } = params;
796
+ if (statement.selectExpr !== undefined && statement.columns.length > 0 && statement.selectExpr.kind !== "countAll") {
797
+ throw internalError({ message: "only countAll can combine with a column list" });
798
+ }
799
+ if (statement.selectExpr !== undefined && statement.aggregates !== undefined && statement.aggregates.length > 0) {
800
+ throw internalError({ message: "selectExpr and aggregates cannot combine" });
801
+ }
802
+ const columnList = statement.columns.map((column) => quoteProjection(dialect, column));
803
+ if (statement.selectExpr !== undefined) {
804
+ columnList.push(renderSelectExpr({ dialect, expr: statement.selectExpr }));
805
+ }
806
+ if (statement.aggregates !== undefined) {
807
+ for (const selection of statement.aggregates) {
808
+ columnList.push(`${renderAggRef(dialect, selection.agg)} AS ${dialect.quoteIdent({ name: selection.alias })}`);
809
+ }
810
+ }
811
+ if (statement.rowNumber !== undefined) {
812
+ const rowNumber = statement.rowNumber;
813
+ if (rowNumber.partitionBy.length === 0) {
814
+ throw internalError({ message: "rowNumber needs at least one partition column" });
815
+ }
816
+ const partition = rowNumber.partitionBy.map((column) => quoteCol(dialect, column)).join(", ");
817
+ const order = rowNumber.orderBy.length === 0 ? "" : ` ORDER BY ${rowNumber.orderBy.map((term) => renderOrderTerm({ dialect, term, collector })).join(", ")}`;
818
+ columnList.push(`ROW_NUMBER() OVER (PARTITION BY ${partition}${order}) AS ${dialect.quoteIdent({ name: rowNumber.alias })}`);
819
+ }
820
+ let distinctPrefix = "";
821
+ if (statement.distinctOn !== undefined && statement.distinctOn.length > 0) {
822
+ if (!dialect.capabilities.distinctOn) {
823
+ throw unsupportedCapability({
824
+ dialect: dialect.name,
825
+ feature: "SELECT DISTINCT ON",
826
+ hint: "postgres-only \u2014 the runtime emulates distinct client-side on this dialect"
827
+ });
828
+ }
829
+ distinctPrefix = `DISTINCT ON (${statement.distinctOn.map((column) => quoteCol(dialect, column)).join(", ")}) `;
830
+ }
831
+ const columns = columnList.length === 0 ? "*" : columnList.join(", ");
832
+ let from;
833
+ if (statement.fromSelect !== undefined) {
834
+ if (statement.table.alias !== undefined) {
835
+ throw internalError({ message: "fromSelect uses table.name as the subquery alias \u2014 table.alias must be unset" });
836
+ }
837
+ const inner = renderSelect({ dialect, statement: statement.fromSelect, collector });
838
+ from = `(${inner.text}) AS ${dialect.quoteIdent({ name: statement.table.name })}`;
839
+ } else {
840
+ from = renderTable(dialect, statement.table);
841
+ }
842
+ let text = `SELECT ${distinctPrefix}${columns} FROM ${from}`;
843
+ if (statement.innerJoins !== undefined && statement.innerJoins.length > 0) {
844
+ for (const join of statement.innerJoins) {
845
+ if (join.on.length === 0) {
846
+ throw internalError({ message: "inner join needs at least one ON pair" });
847
+ }
848
+ const on = join.on.map((pair) => `${quoteCol(dialect, pair.left)} = ${quoteCol(dialect, pair.right)}`).join(" AND ");
849
+ text += ` INNER JOIN ${renderTable(dialect, join.table)} ON ${on}`;
850
+ }
851
+ }
852
+ if (statement.lateralJoins !== undefined && statement.lateralJoins.length > 0) {
853
+ if (!dialect.capabilities.lateralJoin) {
854
+ throw unsupportedCapability({
855
+ dialect: dialect.name,
856
+ feature: "LATERAL JOIN relation loading",
857
+ hint: 'use the portable "query" relation strategy on this dialect'
858
+ });
859
+ }
860
+ for (const join of statement.lateralJoins) {
861
+ text += ` ${renderLateralJoin({ dialect, join, collector })}`;
862
+ }
863
+ }
864
+ if (statement.where !== undefined) {
865
+ text += ` WHERE ${renderExpr({ dialect, expr: statement.where, collector })}`;
866
+ }
867
+ if (statement.groupBy !== undefined && statement.groupBy.length > 0) {
868
+ text += ` GROUP BY ${statement.groupBy.map((column) => quoteCol(dialect, column)).join(", ")}`;
869
+ }
870
+ if (statement.having !== undefined) {
871
+ text += ` HAVING ${renderExpr({ dialect, expr: statement.having, collector })}`;
872
+ }
873
+ if (statement.orderBy !== undefined && statement.orderBy.length > 0) {
874
+ const terms = statement.orderBy.map((term) => renderOrderTerm({ dialect, term, collector }));
875
+ text += ` ORDER BY ${terms.join(", ")}`;
876
+ }
877
+ if (statement.limit !== undefined) {
878
+ assertCount(statement.limit, "limit");
879
+ text += ` LIMIT ${statement.limit}`;
880
+ }
881
+ if (statement.offset !== undefined) {
882
+ assertCount(statement.offset, "offset");
883
+ if (statement.limit === undefined && dialect.offsetRequiresLimit !== null) {
884
+ text += ` LIMIT ${dialect.offsetRequiresLimit.limitText}`;
885
+ }
886
+ text += ` OFFSET ${statement.offset}`;
887
+ }
888
+ if (statement.lockForUpdate === true && dialect.selectForUpdate !== null) {
889
+ text += dialect.selectForUpdate;
890
+ }
891
+ return { text, values: collector.values, returning: "none" };
892
+ }
893
+ function renderInsert(params) {
894
+ const { dialect, statement, collector } = params;
895
+ if (statement.columns.length === 0) {
896
+ throw internalError({ message: "insert requires at least one column" });
897
+ }
898
+ if (statement.rows.length === 0) {
899
+ throw internalError({ message: "insert requires at least one row" });
900
+ }
901
+ for (const row of statement.rows) {
902
+ if (row.length !== statement.columns.length) {
903
+ throw internalError({
904
+ message: `insert row has ${row.length} values but ${statement.columns.length} columns`
905
+ });
906
+ }
907
+ }
908
+ const onConflict = statement.onConflict;
909
+ const skipConflicts = onConflict !== undefined && "doNothing" in onConflict;
910
+ const insertKeyword = skipConflicts && dialect.doNothingForm === "insertIgnore" ? "INSERT IGNORE INTO" : "INSERT INTO";
911
+ const columnList = statement.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
912
+ const rowsSql = statement.rows.map((row) => `(${row.map((value) => {
913
+ if (isJsonParamCell(value))
914
+ return `${collector.add(value.text)}${dialect.jsonParamCast}`;
915
+ if (!isSqlDefaultCell(value))
916
+ return collector.add(value);
917
+ if (!dialect.rowDefaultKeyword) {
918
+ throw internalError({
919
+ message: `${dialect.name} cannot render a per-row DEFAULT cell \u2014 rows must be grouped by column set`
920
+ });
921
+ }
922
+ return "DEFAULT";
923
+ }).join(", ")})`).join(", ");
924
+ let text = `${insertKeyword} ${renderTable(dialect, statement.table)} (${columnList}) VALUES ${rowsSql}`;
925
+ if (onConflict !== undefined) {
926
+ if ("doNothing" in onConflict) {
927
+ if (dialect.doNothingForm === "onConflictDoNothing") {
928
+ const target = onConflict.columns === undefined || onConflict.columns.length === 0 ? "" : ` (${onConflict.columns.map((name) => dialect.quoteIdent({ name })).join(", ")})`;
929
+ text += ` ON CONFLICT${target} DO NOTHING`;
930
+ }
931
+ } else {
932
+ const updates = Object.entries(onConflict.update);
933
+ if (updates.length === 0) {
934
+ throw internalError({ message: "upsert update must set at least one column" });
935
+ }
936
+ const selfRef = dialect.quoteIdent({ name: statement.table.name });
937
+ const assignments = renderAssignments({ dialect, entries: updates, collector, selfRef });
938
+ if (dialect.upsertForm === "onConflict") {
939
+ if (onConflict.lastInsertIdColumn !== undefined) {
940
+ throw internalError({ message: "lastInsertIdColumn is a mysql (onDuplicateKey) construct" });
941
+ }
942
+ const conflictColumns = onConflict.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
943
+ text += ` ON CONFLICT (${conflictColumns}) DO UPDATE SET ${assignments}`;
944
+ } else {
945
+ const trick = onConflict.lastInsertIdColumn === undefined ? "" : `, ${dialect.quoteIdent({ name: onConflict.lastInsertIdColumn })} = LAST_INSERT_ID(${dialect.quoteIdent({ name: onConflict.lastInsertIdColumn })})`;
946
+ text += ` ON DUPLICATE KEY UPDATE ${assignments}${trick}`;
947
+ }
948
+ }
949
+ }
950
+ const returning = applyReturning({ dialect, requested: statement.returning });
951
+ return { text: text + returning.suffix, values: collector.values, returning: returning.status };
952
+ }
953
+ function renderUpdate(params) {
954
+ const { dialect, statement, collector } = params;
955
+ const assignments = Object.entries(statement.set);
956
+ if (assignments.length === 0) {
957
+ throw internalError({ message: "update must set at least one column" });
958
+ }
959
+ const setSql = renderAssignments({ dialect, entries: assignments, collector });
960
+ let text = `UPDATE ${renderTable(dialect, statement.table)} SET ${setSql}`;
961
+ if (statement.where !== undefined) {
962
+ text += ` WHERE ${renderExpr({ dialect, expr: statement.where, collector })}`;
963
+ }
964
+ const returning = applyReturning({ dialect, requested: statement.returning });
965
+ return { text: text + returning.suffix, values: collector.values, returning: returning.status };
966
+ }
967
+ function renderAssignments(params) {
968
+ const { dialect, entries, collector, selfRef } = params;
969
+ return entries.map(([name, value]) => {
970
+ const column = dialect.quoteIdent({ name });
971
+ const read = selfRef === undefined ? column : `${selfRef}.${column}`;
972
+ if (isJsonParamCell(value)) {
973
+ return `${column} = ${collector.add(value.text)}${dialect.jsonParamCast}`;
974
+ }
975
+ if (isSetOperation(value)) {
976
+ return `${column} = ${read} ${SET_OPERATION_SQL[value.__op]} ${collector.add(value.value)}`;
977
+ }
978
+ if (isListSetOperation(value)) {
979
+ assertScalarArrays(dialect, 'the scalar-list update operation "push"');
980
+ if (value.__listOp === "append") {
981
+ return `${column} = array_append(${read}, ${collector.add(value.value)})`;
982
+ }
983
+ return `${column} = ${read} || ${collector.add(value.value)}`;
984
+ }
985
+ return `${column} = ${collector.add(value)}`;
986
+ }).join(", ");
987
+ }
988
+ function renderDelete(params) {
989
+ const { dialect, statement, collector } = params;
990
+ let text = `DELETE FROM ${renderTable(dialect, statement.table)}`;
991
+ if (statement.where !== undefined) {
992
+ text += ` WHERE ${renderExpr({ dialect, expr: statement.where, collector })}`;
993
+ }
994
+ const returning = applyReturning({ dialect, requested: statement.returning });
995
+ return { text: text + returning.suffix, values: collector.values, returning: returning.status };
996
+ }
997
+ export {
998
+ strictMatchPattern,
999
+ startsWithPattern,
1000
+ sqliteDialect,
1001
+ renderStatement,
1002
+ renderFragment,
1003
+ renderExpr,
1004
+ postgresDialect,
1005
+ mysqlDialect,
1006
+ jsonParamCell,
1007
+ isSqlDefaultCell,
1008
+ isSetOperation,
1009
+ isListSetOperation,
1010
+ isJsonParamCell,
1011
+ getSqlDialect,
1012
+ escapeLikePattern,
1013
+ escapeGlobPattern,
1014
+ endsWithPattern,
1015
+ decodeIsNoop,
1016
+ containsPattern,
1017
+ SQL_DIALECTS,
1018
+ SQL_DEFAULT_CELL,
1019
+ SQLITE_CODECS,
1020
+ SQLITE_CAPABILITIES,
1021
+ ParamCollector,
1022
+ POSTGRES_CODECS,
1023
+ POSTGRES_CAPABILITIES,
1024
+ MYSQL_CODECS,
1025
+ MYSQL_CAPABILITIES,
1026
+ DIALECT_CODECS,
1027
+ DIALECT_CAPABILITIES
1028
+ };
1029
+
1030
+ //# debugId=2B8F0892BD46367D64756E2164756E21