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 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").compile(db.printer())
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().compile(db.printer())
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
- .compile(db.printer())
105
+ .toSQL()
106
106
 
107
107
  // DISTINCT
108
- db.selectFrom("users").select("name").distinct().compile(db.printer())
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
- .compile(db.printer())
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" }).compile(db.printer())
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
- .compile(db.printer())
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).compile(db.printer())
140
+ db.insertInto("archive").fromSelect(source).toSQL()
144
141
 
145
142
  // DEFAULT VALUES
146
- db.insertInto("users").defaultValues().compile(db.printer())
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().compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
171
+ .toSQL()
175
172
 
176
173
  // UPDATE with JOIN (MySQL)
177
- db.update("orders").set({ total: 0 }).innerJoin("users", onExpr).compile(db.printer())
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
- .compile(db.printer())
181
+ .toSQL()
185
182
 
186
183
  // ORDER BY + LIMIT (MySQL)
187
- db.update("users").set({ active: false }).orderBy("id").limit(lit(10)).compile(db.printer())
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
- .compile(db.printer())
194
+ .toSQL()
198
195
 
199
196
  // RETURNING
200
197
  db.deleteFrom("users")
201
198
  .where(({ id }) => id.eq(1))
202
199
  .returning("id")
203
- .compile(db.printer())
200
+ .toSQL()
204
201
 
205
202
  // DELETE ... USING (PostgreSQL)
206
- db.deleteFrom("orders").using("users").where(onExpr).compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
343
+ .toSQL()
347
344
 
348
345
  // CROSS JOIN
349
- db.selectFrom("users").crossJoin("posts").compile(db.printer())
346
+ db.selectFrom("users").crossJoin("posts").toSQL()
350
347
 
351
348
  // LATERAL JOINs (correlated subqueries)
352
- db.selectFrom("users").innerJoinLateral(subquery, "recent_posts", onExpr).compile(db.printer())
349
+ db.selectFrom("users").innerJoinLateral(subquery, "recent_posts", onExpr).toSQL()
353
350
 
354
- db.selectFrom("users").leftJoinLateral(subquery, "recent_posts", onExpr).compile(db.printer())
351
+ db.selectFrom("users").leftJoinLateral(subquery, "recent_posts", onExpr).toSQL()
355
352
 
356
- db.selectFrom("users").crossJoinLateral(subquery, "latest").compile(db.printer())
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").compile(db.printer())
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
- .compile(db.printer())
374
+ .toSQL()
378
375
 
379
376
  // CAST
380
377
  db.selectFrom("users")
381
378
  .selectExpr(cast(val(42), "text"), "idAsText")
382
- .compile(db.printer())
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").compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
420
+ .toSQL()
424
421
 
425
422
  // JSON_AGG / TO_JSON
426
- db.selectFrom("users").selectExpr(jsonAgg(col.name), "namesJson").compile(db.printer())
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
- .compile(db.printer())
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").compile(db.printer())
452
- db.selectFrom("users").selectExpr(countDistinct(col.dept), "uniqueDepts").compile(db.printer())
453
- db.selectFrom("orders").selectExpr(sumDistinct(col.amount), "uniqueSum").compile(db.printer())
454
- db.selectFrom("orders").selectExpr(avg(col.amount), "avgAmount").compile(db.printer())
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
- .compile(db.printer())
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").compile(db.printer())
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
- .compile(db.printer())
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").compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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().compile(db.printer())
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
- .compile(db.printer())
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).compile(db.printer()) // UNION
630
- active.unionAll(premium).compile(db.printer()) // UNION ALL
631
- active.intersect(premium).compile(db.printer()) // INTERSECT
632
- active.intersectAll(premium).compile(db.printer()) // INTERSECT ALL
633
- active.except(premium).compile(db.printer()) // EXCEPT
634
- active.exceptAll(premium).compile(db.printer()) // EXCEPT ALL
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).compile(db.printer())
644
+ db.selectFrom("users").with("active_users", activeCte).toSQL()
648
645
 
649
646
  // Recursive CTE
650
- db.selectFrom("categories").with("tree", recursiveQuery, true).compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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().compile(db.printer()) // FOR UPDATE
798
- db.selectFrom("users").select("id").forShare().compile(db.printer()) // FOR SHARE
799
- db.selectFrom("users").select("id").forNoKeyUpdate().compile(db.printer()) // FOR NO KEY UPDATE (PG)
800
- db.selectFrom("users").select("id").forKeyShare().compile(db.printer()) // FOR KEY SHARE (PG)
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().compile(db.printer()) // SKIP LOCKED
804
- db.selectFrom("users").select("id").forUpdate().noWait().compile(db.printer()) // 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().compile(db.printer())
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 }).compile(db.printer())
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" }).compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
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
- .compile(db.printer())
973
+ .toSQL()
983
974
 
984
975
  // Full history
985
- db.selectFrom("users").forSystemTime({ kind: "all" }).compile(db.printer())
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
- User Code
1068
-
1069
- ├── sumak({ dialect, tables }) DB type inferred
1070
-
1071
- ├── db.selectFrom("users") ← TypedSelectBuilder<DB, "users", O>
1072
- .select("id", "name") ← O narrows to Pick<O, "id"|"name">
1073
- │ .where(({ age }) => age.gt(18))
1074
- .build() ← SelectNode (frozen AST)
1075
-
1076
- ├── db.compile(node) ← Plugin → Hooks → Printer
1077
-
1078
- └── { sql, params } ← Parameterized output
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
- **5 layers:**
1082
-
1083
- - **Schema** `defineTable()`, `ColumnType<S,I,U>`, auto type inference
1084
- - **Builder** — `TypedSelectBuilder<DB,TB,O>`, proxy-based expressions
1085
- - **AST** — Frozen node types, discriminated unions, visitor pattern
1086
- - **Plugin/Hook** — `SumakPlugin`, `Hookable` lifecycle hooks
1087
- - **Printer** — `BasePrinter` + 4 dialect subclasses, Wadler document algebra
1088
-
1089
- | | sumak | Drizzle | Kysely |
1090
- | ------------------ | --------------------- | ----------- | -------------- |
1091
- | **Architecture** | AST-first | Template | AST (98 nodes) |
1092
- | **Type inference** | Auto (no codegen) | Auto | Manual DB type |
1093
- | **Plugin system** | Hooks + plugins | None | Plugins only |
1094
- | **DDL support** | Full (schema builder) | drizzle-kit | Full |
1095
- | **Dependencies** | 0 | 0 | 0 |
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
 
@@ -93,7 +93,6 @@ export declare class Col<T> {
93
93
  direction: "DESC";
94
94
  };
95
95
  }
96
- export declare function resetParams(): void;
97
96
  /**
98
97
  * Create typed column proxies for a table's columns.
99
98
  *
@@ -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(_paramIdx++, value);
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;