sumak 0.0.7 → 0.0.8
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/README.md +134 -121
- package/dist/builder/eb.d.mts +0 -1
- package/dist/builder/eb.mjs +1 -5
- package/dist/builder/typed-delete.d.mts +2 -0
- package/dist/builder/typed-delete.mjs +11 -2
- package/dist/builder/typed-insert.d.mts +3 -6
- package/dist/builder/typed-insert.mjs +30 -29
- package/dist/builder/typed-merge.d.mts +1 -2
- package/dist/builder/typed-merge.mjs +9 -22
- package/dist/builder/typed-select.d.mts +5 -2
- package/dist/builder/typed-select.mjs +58 -52
- package/dist/builder/typed-update.d.mts +3 -2
- package/dist/builder/typed-update.mjs +22 -18
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/sumak.mjs +10 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,11 +89,11 @@ That's it. `db` now knows every table, column, and type. All queries are fully t
|
|
|
89
89
|
|
|
90
90
|
```ts
|
|
91
91
|
// Basic select
|
|
92
|
-
db.selectFrom("users").select("id", "name").
|
|
92
|
+
db.selectFrom("users").select("id", "name").toSQL()
|
|
93
93
|
// SELECT "id", "name" FROM "users"
|
|
94
94
|
|
|
95
95
|
// Select all columns
|
|
96
|
-
db.selectFrom("users").selectAll().
|
|
96
|
+
db.selectFrom("users").selectAll().toSQL()
|
|
97
97
|
|
|
98
98
|
// With WHERE, ORDER BY, LIMIT, OFFSET
|
|
99
99
|
db.selectFrom("users")
|
|
@@ -102,10 +102,10 @@ db.selectFrom("users")
|
|
|
102
102
|
.orderBy("name")
|
|
103
103
|
.limit(10)
|
|
104
104
|
.offset(20)
|
|
105
|
-
.
|
|
105
|
+
.toSQL()
|
|
106
106
|
|
|
107
107
|
// DISTINCT
|
|
108
|
-
db.selectFrom("users").select("name").distinct().
|
|
108
|
+
db.selectFrom("users").select("name").distinct().toSQL()
|
|
109
109
|
|
|
110
110
|
// DISTINCT ON (PostgreSQL)
|
|
111
111
|
db.selectFrom("users")
|
|
@@ -113,7 +113,7 @@ db.selectFrom("users")
|
|
|
113
113
|
.distinctOn("dept")
|
|
114
114
|
.orderBy("dept")
|
|
115
115
|
.orderBy("salary", "DESC")
|
|
116
|
-
.
|
|
116
|
+
.toSQL()
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
---
|
|
@@ -122,7 +122,7 @@ db.selectFrom("users")
|
|
|
122
122
|
|
|
123
123
|
```ts
|
|
124
124
|
// Single row
|
|
125
|
-
db.insertInto("users").values({ name: "Alice", email: "alice@example.com" }).
|
|
125
|
+
db.insertInto("users").values({ name: "Alice", email: "alice@example.com" }).toSQL()
|
|
126
126
|
|
|
127
127
|
// Multiple rows
|
|
128
128
|
db.insertInto("users")
|
|
@@ -130,23 +130,20 @@ db.insertInto("users")
|
|
|
130
130
|
{ name: "Alice", email: "a@b.com" },
|
|
131
131
|
{ name: "Bob", email: "b@b.com" },
|
|
132
132
|
])
|
|
133
|
-
.
|
|
133
|
+
.toSQL()
|
|
134
134
|
|
|
135
135
|
// RETURNING
|
|
136
|
-
db.insertInto("users")
|
|
137
|
-
.values({ name: "Alice", email: "a@b.com" })
|
|
138
|
-
.returningAll()
|
|
139
|
-
.compile(db.printer())
|
|
136
|
+
db.insertInto("users").values({ name: "Alice", email: "a@b.com" }).returningAll().toSQL()
|
|
140
137
|
|
|
141
138
|
// INSERT ... SELECT
|
|
142
139
|
const source = db.selectFrom("users").select("name", "email").build()
|
|
143
|
-
db.insertInto("archive").fromSelect(source).
|
|
140
|
+
db.insertInto("archive").fromSelect(source).toSQL()
|
|
144
141
|
|
|
145
142
|
// DEFAULT VALUES
|
|
146
|
-
db.insertInto("users").defaultValues().
|
|
143
|
+
db.insertInto("users").defaultValues().toSQL()
|
|
147
144
|
|
|
148
145
|
// SQLite: INSERT OR IGNORE / INSERT OR REPLACE
|
|
149
|
-
db.insertInto("users").values({ name: "Alice" }).orIgnore().
|
|
146
|
+
db.insertInto("users").values({ name: "Alice" }).orIgnore().toSQL()
|
|
150
147
|
```
|
|
151
148
|
|
|
152
149
|
---
|
|
@@ -158,33 +155,33 @@ db.insertInto("users").values({ name: "Alice" }).orIgnore().compile(db.printer()
|
|
|
158
155
|
db.update("users")
|
|
159
156
|
.set({ active: false })
|
|
160
157
|
.where(({ id }) => id.eq(1))
|
|
161
|
-
.
|
|
158
|
+
.toSQL()
|
|
162
159
|
|
|
163
160
|
// SET with expression
|
|
164
161
|
db.update("users")
|
|
165
162
|
.setExpr("name", val("Anonymous"))
|
|
166
163
|
.where(({ active }) => active.eq(false))
|
|
167
|
-
.
|
|
164
|
+
.toSQL()
|
|
168
165
|
|
|
169
166
|
// UPDATE ... FROM (PostgreSQL)
|
|
170
167
|
db.update("users")
|
|
171
168
|
.set({ name: "Bob" })
|
|
172
169
|
.from("posts")
|
|
173
170
|
.where(({ id }) => id.eq(1))
|
|
174
|
-
.
|
|
171
|
+
.toSQL()
|
|
175
172
|
|
|
176
173
|
// UPDATE with JOIN (MySQL)
|
|
177
|
-
db.update("orders").set({ total: 0 }).innerJoin("users", onExpr).
|
|
174
|
+
db.update("orders").set({ total: 0 }).innerJoin("users", onExpr).toSQL()
|
|
178
175
|
|
|
179
176
|
// RETURNING
|
|
180
177
|
db.update("users")
|
|
181
178
|
.set({ active: false })
|
|
182
179
|
.where(({ id }) => id.eq(1))
|
|
183
180
|
.returningAll()
|
|
184
|
-
.
|
|
181
|
+
.toSQL()
|
|
185
182
|
|
|
186
183
|
// ORDER BY + LIMIT (MySQL)
|
|
187
|
-
db.update("users").set({ active: false }).orderBy("id").limit(lit(10)).
|
|
184
|
+
db.update("users").set({ active: false }).orderBy("id").limit(lit(10)).toSQL()
|
|
188
185
|
```
|
|
189
186
|
|
|
190
187
|
---
|
|
@@ -194,22 +191,22 @@ db.update("users").set({ active: false }).orderBy("id").limit(lit(10)).compile(d
|
|
|
194
191
|
```ts
|
|
195
192
|
db.deleteFrom("users")
|
|
196
193
|
.where(({ id }) => id.eq(1))
|
|
197
|
-
.
|
|
194
|
+
.toSQL()
|
|
198
195
|
|
|
199
196
|
// RETURNING
|
|
200
197
|
db.deleteFrom("users")
|
|
201
198
|
.where(({ id }) => id.eq(1))
|
|
202
199
|
.returning("id")
|
|
203
|
-
.
|
|
200
|
+
.toSQL()
|
|
204
201
|
|
|
205
202
|
// DELETE ... USING (PostgreSQL)
|
|
206
|
-
db.deleteFrom("orders").using("users").where(onExpr).
|
|
203
|
+
db.deleteFrom("orders").using("users").where(onExpr).toSQL()
|
|
207
204
|
|
|
208
205
|
// DELETE with JOIN (MySQL)
|
|
209
206
|
db.deleteFrom("orders")
|
|
210
207
|
.innerJoin("users", onExpr)
|
|
211
208
|
.where(({ id }) => id.eq(1))
|
|
212
|
-
.
|
|
209
|
+
.toSQL()
|
|
213
210
|
```
|
|
214
211
|
|
|
215
212
|
---
|
|
@@ -304,7 +301,7 @@ db.selectFrom("users")
|
|
|
304
301
|
.select("id")
|
|
305
302
|
.where(({ age }) => age.gt(18))
|
|
306
303
|
.where(({ active }) => active.eq(true))
|
|
307
|
-
.
|
|
304
|
+
.toSQL()
|
|
308
305
|
// WHERE ("age" > $1) AND ("active" = $2)
|
|
309
306
|
```
|
|
310
307
|
|
|
@@ -328,32 +325,32 @@ db.selectFrom("users")
|
|
|
328
325
|
db.selectFrom("users")
|
|
329
326
|
.innerJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
|
|
330
327
|
.select("id", "title")
|
|
331
|
-
.
|
|
328
|
+
.toSQL()
|
|
332
329
|
|
|
333
330
|
// LEFT JOIN — joined columns become nullable
|
|
334
331
|
db.selectFrom("users")
|
|
335
332
|
.leftJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
|
|
336
|
-
.
|
|
333
|
+
.toSQL()
|
|
337
334
|
|
|
338
335
|
// RIGHT JOIN
|
|
339
336
|
db.selectFrom("users")
|
|
340
337
|
.rightJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
|
|
341
|
-
.
|
|
338
|
+
.toSQL()
|
|
342
339
|
|
|
343
340
|
// FULL JOIN — both sides nullable
|
|
344
341
|
db.selectFrom("users")
|
|
345
342
|
.fullJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
|
|
346
|
-
.
|
|
343
|
+
.toSQL()
|
|
347
344
|
|
|
348
345
|
// CROSS JOIN
|
|
349
|
-
db.selectFrom("users").crossJoin("posts").
|
|
346
|
+
db.selectFrom("users").crossJoin("posts").toSQL()
|
|
350
347
|
|
|
351
348
|
// LATERAL JOINs (correlated subqueries)
|
|
352
|
-
db.selectFrom("users").innerJoinLateral(subquery, "recent_posts", onExpr).
|
|
349
|
+
db.selectFrom("users").innerJoinLateral(subquery, "recent_posts", onExpr).toSQL()
|
|
353
350
|
|
|
354
|
-
db.selectFrom("users").leftJoinLateral(subquery, "recent_posts", onExpr).
|
|
351
|
+
db.selectFrom("users").leftJoinLateral(subquery, "recent_posts", onExpr).toSQL()
|
|
355
352
|
|
|
356
|
-
db.selectFrom("users").crossJoinLateral(subquery, "latest").
|
|
353
|
+
db.selectFrom("users").crossJoinLateral(subquery, "latest").toSQL()
|
|
357
354
|
```
|
|
358
355
|
|
|
359
356
|
---
|
|
@@ -366,7 +363,7 @@ db.selectFrom("users").crossJoinLateral(subquery, "latest").compile(db.printer()
|
|
|
366
363
|
import { val, cast, rawExpr } from "sumak"
|
|
367
364
|
|
|
368
365
|
// Add a computed column with alias
|
|
369
|
-
db.selectFrom("users").selectExpr(val("hello"), "greeting").
|
|
366
|
+
db.selectFrom("users").selectExpr(val("hello"), "greeting").toSQL()
|
|
370
367
|
|
|
371
368
|
// Multiple expressions at once
|
|
372
369
|
db.selectFrom("users")
|
|
@@ -374,12 +371,12 @@ db.selectFrom("users")
|
|
|
374
371
|
total: count(),
|
|
375
372
|
greeting: val("hello"),
|
|
376
373
|
})
|
|
377
|
-
.
|
|
374
|
+
.toSQL()
|
|
378
375
|
|
|
379
376
|
// CAST
|
|
380
377
|
db.selectFrom("users")
|
|
381
378
|
.selectExpr(cast(val(42), "text"), "idAsText")
|
|
382
|
-
.
|
|
379
|
+
.toSQL()
|
|
383
380
|
```
|
|
384
381
|
|
|
385
382
|
### Arithmetic
|
|
@@ -387,12 +384,12 @@ db.selectFrom("users")
|
|
|
387
384
|
```ts
|
|
388
385
|
import { add, sub, mul, div, mod, neg } from "sumak"
|
|
389
386
|
|
|
390
|
-
db.selectFrom("orders").selectExpr(mul(col.price, col.qty), "total").
|
|
387
|
+
db.selectFrom("orders").selectExpr(mul(col.price, col.qty), "total").toSQL()
|
|
391
388
|
// ("price" * "qty") AS "total"
|
|
392
389
|
|
|
393
390
|
db.selectFrom("orders")
|
|
394
391
|
.selectExpr(add(col.price, val(10)), "adjusted")
|
|
395
|
-
.
|
|
392
|
+
.toSQL()
|
|
396
393
|
```
|
|
397
394
|
|
|
398
395
|
### CASE / WHEN
|
|
@@ -409,7 +406,7 @@ db.selectFrom("users")
|
|
|
409
406
|
.end(),
|
|
410
407
|
"status",
|
|
411
408
|
)
|
|
412
|
-
.
|
|
409
|
+
.toSQL()
|
|
413
410
|
```
|
|
414
411
|
|
|
415
412
|
### JSON Operations
|
|
@@ -420,15 +417,15 @@ import { jsonRef, jsonAgg, toJson, jsonBuildObject } from "sumak"
|
|
|
420
417
|
// Access: -> (JSON object), ->> (text value)
|
|
421
418
|
db.selectFrom("users")
|
|
422
419
|
.selectExpr(jsonRef(col.meta, "name", "->>"), "metaName")
|
|
423
|
-
.
|
|
420
|
+
.toSQL()
|
|
424
421
|
|
|
425
422
|
// JSON_AGG / TO_JSON
|
|
426
|
-
db.selectFrom("users").selectExpr(jsonAgg(col.name), "namesJson").
|
|
423
|
+
db.selectFrom("users").selectExpr(jsonAgg(col.name), "namesJson").toSQL()
|
|
427
424
|
|
|
428
425
|
// JSON_BUILD_OBJECT
|
|
429
426
|
db.selectFrom("users")
|
|
430
427
|
.selectExpr(jsonBuildObject(["name", col.name], ["age", col.age]), "obj")
|
|
431
|
-
.
|
|
428
|
+
.toSQL()
|
|
432
429
|
```
|
|
433
430
|
|
|
434
431
|
### PostgreSQL Array Operators
|
|
@@ -448,15 +445,15 @@ import { arrayContains, arrayContainedBy, arrayOverlaps, rawExpr } from "sumak"
|
|
|
448
445
|
```ts
|
|
449
446
|
import { count, countDistinct, sum, sumDistinct, avg, avgDistinct, min, max, coalesce } from "sumak"
|
|
450
447
|
|
|
451
|
-
db.selectFrom("users").selectExpr(count(), "total").
|
|
452
|
-
db.selectFrom("users").selectExpr(countDistinct(col.dept), "uniqueDepts").
|
|
453
|
-
db.selectFrom("orders").selectExpr(sumDistinct(col.amount), "uniqueSum").
|
|
454
|
-
db.selectFrom("orders").selectExpr(avg(col.amount), "avgAmount").
|
|
448
|
+
db.selectFrom("users").selectExpr(count(), "total").toSQL()
|
|
449
|
+
db.selectFrom("users").selectExpr(countDistinct(col.dept), "uniqueDepts").toSQL()
|
|
450
|
+
db.selectFrom("orders").selectExpr(sumDistinct(col.amount), "uniqueSum").toSQL()
|
|
451
|
+
db.selectFrom("orders").selectExpr(avg(col.amount), "avgAmount").toSQL()
|
|
455
452
|
|
|
456
453
|
// COALESCE (variadic)
|
|
457
454
|
db.selectFrom("users")
|
|
458
455
|
.selectExpr(coalesce(col.nick, col.name, val("Anonymous")), "displayName")
|
|
459
|
-
.
|
|
456
|
+
.toSQL()
|
|
460
457
|
```
|
|
461
458
|
|
|
462
459
|
### Aggregate with FILTER (PostgreSQL)
|
|
@@ -464,7 +461,7 @@ db.selectFrom("users")
|
|
|
464
461
|
```ts
|
|
465
462
|
import { filter, count } from "sumak"
|
|
466
463
|
|
|
467
|
-
db.selectFrom("users").selectExpr(filter(count(), activeExpr), "activeCount").
|
|
464
|
+
db.selectFrom("users").selectExpr(filter(count(), activeExpr), "activeCount").toSQL()
|
|
468
465
|
// COUNT(*) FILTER (WHERE ...)
|
|
469
466
|
```
|
|
470
467
|
|
|
@@ -476,11 +473,11 @@ import { stringAgg, arrayAgg } from "sumak"
|
|
|
476
473
|
// STRING_AGG with ORDER BY
|
|
477
474
|
db.selectFrom("users")
|
|
478
475
|
.selectExpr(stringAgg(col.name, ", ", [{ expr: col.name, direction: "ASC" }]), "names")
|
|
479
|
-
.
|
|
476
|
+
.toSQL()
|
|
480
477
|
// STRING_AGG("name", ', ' ORDER BY "name" ASC)
|
|
481
478
|
|
|
482
479
|
// ARRAY_AGG
|
|
483
|
-
db.selectFrom("users").selectExpr(arrayAgg(col.id), "ids").
|
|
480
|
+
db.selectFrom("users").selectExpr(arrayAgg(col.id), "ids").toSQL()
|
|
484
481
|
```
|
|
485
482
|
|
|
486
483
|
---
|
|
@@ -496,7 +493,7 @@ db.selectFrom("employees")
|
|
|
496
493
|
over(rowNumber(), (w) => w.partitionBy("dept").orderBy("salary", "DESC")),
|
|
497
494
|
"rn",
|
|
498
495
|
)
|
|
499
|
-
.
|
|
496
|
+
.toSQL()
|
|
500
497
|
|
|
501
498
|
// RANK / DENSE_RANK
|
|
502
499
|
over(rank(), (w) => w.orderBy("score", "DESC"))
|
|
@@ -587,7 +584,7 @@ db.selectFrom("users")
|
|
|
587
584
|
.build(),
|
|
588
585
|
),
|
|
589
586
|
)
|
|
590
|
-
.
|
|
587
|
+
.toSQL()
|
|
591
588
|
```
|
|
592
589
|
|
|
593
590
|
### Derived Tables (Subquery in FROM)
|
|
@@ -598,7 +595,7 @@ const sub = db
|
|
|
598
595
|
.select("id", "name")
|
|
599
596
|
.where(({ age }) => age.gt(18))
|
|
600
597
|
|
|
601
|
-
db.selectFromSubquery(sub, "adults").selectAll().
|
|
598
|
+
db.selectFromSubquery(sub, "adults").selectAll().toSQL()
|
|
602
599
|
// SELECT * FROM (SELECT ...) AS "adults"
|
|
603
600
|
```
|
|
604
601
|
|
|
@@ -609,7 +606,7 @@ const deptIds = db.selectFrom("departments").select("id").build()
|
|
|
609
606
|
|
|
610
607
|
db.selectFrom("users")
|
|
611
608
|
.where(({ dept_id }) => dept_id.inSubquery(deptIds))
|
|
612
|
-
.
|
|
609
|
+
.toSQL()
|
|
613
610
|
```
|
|
614
611
|
|
|
615
612
|
---
|
|
@@ -626,12 +623,12 @@ const premium = db
|
|
|
626
623
|
.select("id")
|
|
627
624
|
.where(({ tier }) => tier.eq("premium"))
|
|
628
625
|
|
|
629
|
-
active.union(premium).
|
|
630
|
-
active.unionAll(premium).
|
|
631
|
-
active.intersect(premium).
|
|
632
|
-
active.intersectAll(premium).
|
|
633
|
-
active.except(premium).
|
|
634
|
-
active.exceptAll(premium).
|
|
626
|
+
active.union(premium).toSQL() // UNION
|
|
627
|
+
active.unionAll(premium).toSQL() // UNION ALL
|
|
628
|
+
active.intersect(premium).toSQL() // INTERSECT
|
|
629
|
+
active.intersectAll(premium).toSQL() // INTERSECT ALL
|
|
630
|
+
active.except(premium).toSQL() // EXCEPT
|
|
631
|
+
active.exceptAll(premium).toSQL() // EXCEPT ALL
|
|
635
632
|
```
|
|
636
633
|
|
|
637
634
|
---
|
|
@@ -644,10 +641,10 @@ const activeCte = db
|
|
|
644
641
|
.where(({ active }) => active.eq(true))
|
|
645
642
|
.build()
|
|
646
643
|
|
|
647
|
-
db.selectFrom("users").with("active_users", activeCte).
|
|
644
|
+
db.selectFrom("users").with("active_users", activeCte).toSQL()
|
|
648
645
|
|
|
649
646
|
// Recursive CTE
|
|
650
|
-
db.selectFrom("categories").with("tree", recursiveQuery, true).
|
|
647
|
+
db.selectFrom("categories").with("tree", recursiveQuery, true).toSQL()
|
|
651
648
|
```
|
|
652
649
|
|
|
653
650
|
---
|
|
@@ -664,7 +661,7 @@ db.selectFrom("users")
|
|
|
664
661
|
.select("id", "name")
|
|
665
662
|
.$if(withFilter, (qb) => qb.where(({ age }) => age.gt(18)))
|
|
666
663
|
.$if(withOrder, (qb) => qb.orderBy("name"))
|
|
667
|
-
.
|
|
664
|
+
.toSQL()
|
|
668
665
|
// WHERE applied, ORDER BY skipped
|
|
669
666
|
```
|
|
670
667
|
|
|
@@ -674,11 +671,7 @@ db.selectFrom("users")
|
|
|
674
671
|
const withPagination = (qb) => qb.limit(10).offset(20)
|
|
675
672
|
const onlyActive = (qb) => qb.where(({ active }) => active.eq(true))
|
|
676
673
|
|
|
677
|
-
db.selectFrom("users")
|
|
678
|
-
.select("id", "name")
|
|
679
|
-
.$call(onlyActive)
|
|
680
|
-
.$call(withPagination)
|
|
681
|
-
.compile(db.printer())
|
|
674
|
+
db.selectFrom("users").select("id", "name").$call(onlyActive).$call(withPagination).toSQL()
|
|
682
675
|
```
|
|
683
676
|
|
|
684
677
|
### `clear*()` — reset clauses
|
|
@@ -689,7 +682,7 @@ db.selectFrom("users")
|
|
|
689
682
|
.orderBy("name")
|
|
690
683
|
.clearOrderBy() // removes ORDER BY
|
|
691
684
|
.orderBy("id", "DESC") // re-add different order
|
|
692
|
-
.
|
|
685
|
+
.toSQL()
|
|
693
686
|
```
|
|
694
687
|
|
|
695
688
|
Available: `clearWhere()`, `clearOrderBy()`, `clearLimit()`, `clearOffset()`, `clearGroupBy()`, `clearHaving()`, `clearSelect()`.
|
|
@@ -718,7 +711,7 @@ sql`SELECT ${sql.ref("id")} FROM ${sql.table("users", "public")}`
|
|
|
718
711
|
// In queries
|
|
719
712
|
db.selectFrom("users")
|
|
720
713
|
.selectExpr(sql`CURRENT_DATE`, "today")
|
|
721
|
-
.
|
|
714
|
+
.toSQL()
|
|
722
715
|
```
|
|
723
716
|
|
|
724
717
|
### `rawExpr()` escape hatch
|
|
@@ -729,12 +722,10 @@ import { rawExpr } from "sumak"
|
|
|
729
722
|
// In WHERE
|
|
730
723
|
db.selectFrom("users")
|
|
731
724
|
.where(() => rawExpr<boolean>("age > 18"))
|
|
732
|
-
.
|
|
725
|
+
.toSQL()
|
|
733
726
|
|
|
734
727
|
// In SELECT
|
|
735
|
-
db.selectFrom("users")
|
|
736
|
-
.selectExpr(rawExpr<number>("EXTRACT(YEAR FROM created_at)"), "year")
|
|
737
|
-
.compile(db.printer())
|
|
728
|
+
db.selectFrom("users").selectExpr(rawExpr<number>("EXTRACT(YEAR FROM created_at)"), "year").toSQL()
|
|
738
729
|
```
|
|
739
730
|
|
|
740
731
|
---
|
|
@@ -746,31 +737,31 @@ db.selectFrom("users")
|
|
|
746
737
|
db.insertInto("users")
|
|
747
738
|
.values({ name: "Alice", email: "a@b.com" })
|
|
748
739
|
.onConflictDoNothing("email")
|
|
749
|
-
.
|
|
740
|
+
.toSQL()
|
|
750
741
|
|
|
751
742
|
// ON CONFLICT DO UPDATE (with Expression)
|
|
752
743
|
db.insertInto("users")
|
|
753
744
|
.values({ name: "Alice", email: "a@b.com" })
|
|
754
745
|
.onConflictDoUpdate(["email"], [{ column: "name", value: val("Updated") }])
|
|
755
|
-
.
|
|
746
|
+
.toSQL()
|
|
756
747
|
|
|
757
748
|
// ON CONFLICT DO UPDATE (with plain object — auto-parameterized)
|
|
758
749
|
db.insertInto("users")
|
|
759
750
|
.values({ name: "Alice", email: "a@b.com" })
|
|
760
751
|
.onConflictDoUpdateSet(["email"], { name: "Alice Updated" })
|
|
761
|
-
.
|
|
752
|
+
.toSQL()
|
|
762
753
|
|
|
763
754
|
// ON CONFLICT ON CONSTRAINT
|
|
764
755
|
db.insertInto("users")
|
|
765
756
|
.values({ name: "Alice", email: "a@b.com" })
|
|
766
757
|
.onConflictConstraintDoNothing("users_email_key")
|
|
767
|
-
.
|
|
758
|
+
.toSQL()
|
|
768
759
|
|
|
769
760
|
// MySQL: ON DUPLICATE KEY UPDATE
|
|
770
761
|
db.insertInto("users")
|
|
771
762
|
.values({ name: "Alice" })
|
|
772
763
|
.onDuplicateKeyUpdate([{ column: "name", value: val("Alice") }])
|
|
773
|
-
.
|
|
764
|
+
.toSQL()
|
|
774
765
|
```
|
|
775
766
|
|
|
776
767
|
---
|
|
@@ -781,12 +772,12 @@ db.insertInto("users")
|
|
|
781
772
|
db.mergeInto("users", "staging", "s", ({ target, source }) => target.id.eqCol(source.id))
|
|
782
773
|
.whenMatchedThenUpdate({ name: "updated" })
|
|
783
774
|
.whenNotMatchedThenInsert({ name: "Alice", email: "a@b.com" })
|
|
784
|
-
.
|
|
775
|
+
.toSQL()
|
|
785
776
|
|
|
786
777
|
// Conditional delete
|
|
787
778
|
db.mergeInto("users", "staging", "s", ({ target, source }) => target.id.eqCol(source.id))
|
|
788
779
|
.whenMatchedThenDelete()
|
|
789
|
-
.
|
|
780
|
+
.toSQL()
|
|
790
781
|
```
|
|
791
782
|
|
|
792
783
|
---
|
|
@@ -794,14 +785,14 @@ db.mergeInto("users", "staging", "s", ({ target, source }) => target.id.eqCol(so
|
|
|
794
785
|
## Row Locking
|
|
795
786
|
|
|
796
787
|
```ts
|
|
797
|
-
db.selectFrom("users").select("id").forUpdate().
|
|
798
|
-
db.selectFrom("users").select("id").forShare().
|
|
799
|
-
db.selectFrom("users").select("id").forNoKeyUpdate().
|
|
800
|
-
db.selectFrom("users").select("id").forKeyShare().
|
|
788
|
+
db.selectFrom("users").select("id").forUpdate().toSQL() // FOR UPDATE
|
|
789
|
+
db.selectFrom("users").select("id").forShare().toSQL() // FOR SHARE
|
|
790
|
+
db.selectFrom("users").select("id").forNoKeyUpdate().toSQL() // FOR NO KEY UPDATE (PG)
|
|
791
|
+
db.selectFrom("users").select("id").forKeyShare().toSQL() // FOR KEY SHARE (PG)
|
|
801
792
|
|
|
802
793
|
// Modifiers
|
|
803
|
-
db.selectFrom("users").select("id").forUpdate().skipLocked().
|
|
804
|
-
db.selectFrom("users").select("id").forUpdate().noWait().
|
|
794
|
+
db.selectFrom("users").select("id").forUpdate().skipLocked().toSQL() // SKIP LOCKED
|
|
795
|
+
db.selectFrom("users").select("id").forUpdate().noWait().toSQL() // NOWAIT
|
|
805
796
|
```
|
|
806
797
|
|
|
807
798
|
---
|
|
@@ -809,13 +800,13 @@ db.selectFrom("users").select("id").forUpdate().noWait().compile(db.printer()) /
|
|
|
809
800
|
## EXPLAIN
|
|
810
801
|
|
|
811
802
|
```ts
|
|
812
|
-
db.selectFrom("users").select("id").explain().
|
|
803
|
+
db.selectFrom("users").select("id").explain().toSQL()
|
|
813
804
|
// EXPLAIN SELECT "id" FROM "users"
|
|
814
805
|
|
|
815
|
-
db.selectFrom("users").select("id").explain({ analyze: true }).
|
|
806
|
+
db.selectFrom("users").select("id").explain({ analyze: true }).toSQL()
|
|
816
807
|
// EXPLAIN ANALYZE SELECT ...
|
|
817
808
|
|
|
818
|
-
db.selectFrom("users").select("id").explain({ format: "JSON" }).
|
|
809
|
+
db.selectFrom("users").select("id").explain({ format: "JSON" }).toSQL()
|
|
819
810
|
// EXPLAIN (FORMAT JSON) SELECT ...
|
|
820
811
|
```
|
|
821
812
|
|
|
@@ -959,7 +950,7 @@ import { textSearch, val } from "sumak"
|
|
|
959
950
|
// PostgreSQL: to_tsvector("name") @@ to_tsquery('alice')
|
|
960
951
|
db.selectFrom("users")
|
|
961
952
|
.where(({ name }) => textSearch([name.toExpr()], val("alice")))
|
|
962
|
-
.
|
|
953
|
+
.toSQL()
|
|
963
954
|
|
|
964
955
|
// MySQL: MATCH(`name`) AGAINST(? IN BOOLEAN MODE)
|
|
965
956
|
// SQLite: ("name" MATCH ?)
|
|
@@ -974,15 +965,15 @@ db.selectFrom("users")
|
|
|
974
965
|
// Point-in-time query
|
|
975
966
|
db.selectFrom("users")
|
|
976
967
|
.forSystemTime({ kind: "as_of", timestamp: lit("2024-01-01") })
|
|
977
|
-
.
|
|
968
|
+
.toSQL()
|
|
978
969
|
|
|
979
970
|
// Time range
|
|
980
971
|
db.selectFrom("users")
|
|
981
972
|
.forSystemTime({ kind: "between", start: lit("2024-01-01"), end: lit("2024-12-31") })
|
|
982
|
-
.
|
|
973
|
+
.toSQL()
|
|
983
974
|
|
|
984
975
|
// Full history
|
|
985
|
-
db.selectFrom("users").forSystemTime({ kind: "all" }).
|
|
976
|
+
db.selectFrom("users").forSystemTime({ kind: "all" }).toSQL()
|
|
986
977
|
```
|
|
987
978
|
|
|
988
979
|
Modes: `as_of`, `from_to`, `between`, `contained_in`, `all`.
|
|
@@ -1063,36 +1054,58 @@ import { serial, text } from "sumak/schema"
|
|
|
1063
1054
|
|
|
1064
1055
|
## Architecture
|
|
1065
1056
|
|
|
1057
|
+
sumak uses a 5-layer pipeline. Your code never touches SQL strings — everything flows through an AST.
|
|
1058
|
+
|
|
1066
1059
|
```
|
|
1067
|
-
|
|
1068
|
-
│
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1060
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
1061
|
+
│ 1. SCHEMA │
|
|
1062
|
+
│ sumak({ dialect, tables: { users: { id: serial(), ... } } })│
|
|
1063
|
+
│ → DB type auto-inferred, zero codegen │
|
|
1064
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
1065
|
+
│ 2. BUILDER │
|
|
1066
|
+
│ db.selectFrom("users").select("id").where(...) │
|
|
1067
|
+
│ → Immutable, chainable, fully type-checked │
|
|
1068
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
1069
|
+
│ 3. AST │
|
|
1070
|
+
│ .build() → SelectNode (frozen, discriminated union) │
|
|
1071
|
+
│ → ~40 node types, Object.freeze on all outputs │
|
|
1072
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
1073
|
+
│ 4. PLUGIN / HOOK │
|
|
1074
|
+
│ Plugin.transformNode() → Hook "query:before" │
|
|
1075
|
+
│ → AST rewriting, tenant isolation, soft delete, logging │
|
|
1076
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
1077
|
+
│ 5. PRINTER │
|
|
1078
|
+
│ .toSQL() → { sql: "SELECT ...", params: [...] } │
|
|
1079
|
+
│ → Dialect-specific: PG ($1), MySQL (?), MSSQL (@p0) │
|
|
1080
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
1079
1081
|
```
|
|
1080
1082
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
- **
|
|
1086
|
-
- **
|
|
1087
|
-
- **
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1083
|
+
### Why AST-first?
|
|
1084
|
+
|
|
1085
|
+
The query is never a string until the very last step. This means:
|
|
1086
|
+
|
|
1087
|
+
- **Plugins can rewrite queries** — add WHERE clauses, prefix schemas, transform joins
|
|
1088
|
+
- **Hooks can inspect/modify** — logging, tracing, tenant isolation
|
|
1089
|
+
- **Printers are swappable** — same AST, different SQL per dialect
|
|
1090
|
+
- **No SQL injection** — values are always parameterized
|
|
1091
|
+
|
|
1092
|
+
### Key design decisions
|
|
1093
|
+
|
|
1094
|
+
- **Params at print time** — no global state, no index tracking during build
|
|
1095
|
+
- **Immutable builders** — every method returns a new instance
|
|
1096
|
+
- **Proxy-based column access** — `({ age }) => age.gt(18)` with full type safety
|
|
1097
|
+
- **Phantom types** — `Expression<T>` carries type info with zero runtime cost
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
## Acknowledgments
|
|
1102
|
+
|
|
1103
|
+
sumak wouldn't exist without the incredible work of these projects:
|
|
1104
|
+
|
|
1105
|
+
- **[Kysely](https://github.com/kysely-org/kysely)** — Pioneered the AST-first approach for TypeScript query builders. The `DB/TB/O` generic threading pattern, immutable builder design, and visitor-based printer architecture are directly inspired by Kysely.
|
|
1106
|
+
- **[Drizzle ORM](https://github.com/drizzle-team/drizzle-orm)** — Proved that schema-as-code (no codegen) is the right developer experience. The `defineTable()` + column builder pattern in sumak follows Drizzle's lead.
|
|
1107
|
+
- **[JOOQ](https://github.com/jOOQ/jOOQ)** — The original AST-first SQL builder (Java). Showed that a clean AST layer makes multi-dialect support elegant.
|
|
1108
|
+
- **[SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)** — Demonstrated that separating the expression layer from the ORM layer gives maximum flexibility.
|
|
1096
1109
|
|
|
1097
1110
|
---
|
|
1098
1111
|
|
package/dist/builder/eb.d.mts
CHANGED
package/dist/builder/eb.mjs
CHANGED
|
@@ -209,12 +209,8 @@ export class Col {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
let _paramIdx = 0;
|
|
213
|
-
export function resetParams() {
|
|
214
|
-
_paramIdx = 0;
|
|
215
|
-
}
|
|
216
212
|
function autoParam(value) {
|
|
217
|
-
return rawParam(
|
|
213
|
+
return rawParam(0, value);
|
|
218
214
|
}
|
|
219
215
|
function binOp(op, left, right) {
|
|
220
216
|
return {
|
|
@@ -37,6 +37,8 @@ export declare class TypedDeleteBuilder<
|
|
|
37
37
|
$if(condition: boolean, fn: (qb: TypedDeleteBuilder<DB, TB>) => TypedDeleteBuilder<DB, TB>): TypedDeleteBuilder<DB, TB>;
|
|
38
38
|
build(): DeleteNode;
|
|
39
39
|
compile(printer: Printer): CompiledQuery;
|
|
40
|
+
/** Compile to SQL using the dialect's printer. */
|
|
41
|
+
toSQL(): CompiledQuery;
|
|
40
42
|
/** EXPLAIN this query. */
|
|
41
43
|
explain(options?: {
|
|
42
44
|
analyze?: boolean;
|