sumak 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +694 -416
  2. package/dist/ast/ddl-nodes.d.mts +153 -0
  3. package/dist/ast/ddl-nodes.mjs +1 -0
  4. package/dist/ast/nodes.d.mts +12 -1
  5. package/dist/builder/ddl/alter-table.d.mts +25 -0
  6. package/dist/builder/ddl/alter-table.mjs +146 -0
  7. package/dist/builder/ddl/create-index.d.mts +19 -0
  8. package/dist/builder/ddl/create-index.mjs +67 -0
  9. package/dist/builder/ddl/create-table.d.mts +40 -0
  10. package/dist/builder/ddl/create-table.mjs +186 -0
  11. package/dist/builder/ddl/create-view.d.mts +15 -0
  12. package/dist/builder/ddl/create-view.mjs +57 -0
  13. package/dist/builder/ddl/drop.d.mts +38 -0
  14. package/dist/builder/ddl/drop.mjs +133 -0
  15. package/dist/builder/delete.d.mts +3 -0
  16. package/dist/builder/delete.mjs +36 -0
  17. package/dist/builder/eb.d.mts +122 -6
  18. package/dist/builder/eb.mjs +273 -6
  19. package/dist/builder/select.d.mts +4 -0
  20. package/dist/builder/select.mjs +55 -0
  21. package/dist/builder/typed-delete.d.mts +14 -0
  22. package/dist/builder/typed-delete.mjs +26 -0
  23. package/dist/builder/typed-insert.d.mts +25 -1
  24. package/dist/builder/typed-insert.mjs +51 -0
  25. package/dist/builder/typed-select.d.mts +65 -4
  26. package/dist/builder/typed-select.mjs +132 -2
  27. package/dist/builder/typed-update.d.mts +12 -0
  28. package/dist/builder/typed-update.mjs +22 -0
  29. package/dist/builder/update.d.mts +3 -0
  30. package/dist/builder/update.mjs +36 -0
  31. package/dist/index.d.mts +12 -3
  32. package/dist/index.mjs +12 -2
  33. package/dist/printer/base.d.mts +2 -1
  34. package/dist/printer/base.mjs +25 -3
  35. package/dist/printer/ddl.d.mts +21 -0
  36. package/dist/printer/ddl.mjs +223 -0
  37. package/dist/schema/column.d.mts +10 -1
  38. package/dist/schema/column.mjs +33 -2
  39. package/dist/schema/index.d.mts +1 -1
  40. package/dist/schema/index.mjs +1 -1
  41. package/dist/sumak.d.mts +59 -1
  42. package/dist/sumak.mjs +103 -2
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -14,12 +14,50 @@
14
14
  <a href="https://github.com/productdevbook/sumak/blob/main/LICENSE"><img src="https://img.shields.io/github/license/productdevbook/sumak?style=flat&colorA=18181B&colorB=e11d48" alt="license"></a>
15
15
  </p>
16
16
 
17
- ## Quick Start
17
+ ---
18
+
19
+ ## Table of Contents
20
+
21
+ - [Install](#install)
22
+ - [Quick Start](#quick-start)
23
+ - [SELECT](#select)
24
+ - [INSERT](#insert)
25
+ - [UPDATE](#update)
26
+ - [DELETE](#delete)
27
+ - [WHERE Conditions](#where-conditions)
28
+ - [Joins](#joins)
29
+ - [Expressions](#expressions)
30
+ - [Aggregates](#aggregates)
31
+ - [Window Functions](#window-functions)
32
+ - [SQL Functions](#sql-functions)
33
+ - [Subqueries](#subqueries)
34
+ - [Set Operations](#set-operations)
35
+ - [CTEs (WITH)](#ctes-with)
36
+ - [Conditional / Dynamic Queries](#conditional--dynamic-queries)
37
+ - [Raw SQL](#raw-sql)
38
+ - [ON CONFLICT / Upsert](#on-conflict--upsert)
39
+ - [MERGE](#merge-sql2003)
40
+ - [Row Locking](#row-locking)
41
+ - [Schema Builder (DDL)](#schema-builder-ddl)
42
+ - [Full-Text Search](#full-text-search)
43
+ - [Temporal Tables](#temporal-tables-sql2011)
44
+ - [Plugins](#plugins)
45
+ - [Hooks](#hooks)
46
+ - [Dialects](#dialects)
47
+ - [Architecture](#architecture)
48
+
49
+ ---
50
+
51
+ ## Install
18
52
 
19
53
  ```sh
20
54
  npm install sumak
21
55
  ```
22
56
 
57
+ ## Quick Start
58
+
59
+ Define your tables and create a typed instance:
60
+
23
61
  ```ts
24
62
  import { sumak, pgDialect, serial, text, boolean, integer, jsonb } from "sumak"
25
63
 
@@ -43,216 +81,321 @@ const db = sumak({
43
81
  })
44
82
  ```
45
83
 
46
- ## Query Building
84
+ That's it. `db` now knows every table, column, and type. All queries are fully type-checked.
47
85
 
48
- ### SELECT
86
+ ---
87
+
88
+ ## SELECT
49
89
 
50
90
  ```ts
91
+ // Basic select
92
+ db.selectFrom("users").select("id", "name").compile(db.printer())
93
+ // SELECT "id", "name" FROM "users"
94
+
95
+ // Select all columns
96
+ db.selectFrom("users").selectAll().compile(db.printer())
97
+
98
+ // With WHERE, ORDER BY, LIMIT, OFFSET
51
99
  db.selectFrom("users")
52
100
  .select("id", "name")
53
- .where(({ age, active }) => and(age.gte(18), active.eq(true)))
101
+ .where(({ age }) => age.gte(18))
54
102
  .orderBy("name")
55
103
  .limit(10)
104
+ .offset(20)
105
+ .compile(db.printer())
106
+
107
+ // DISTINCT
108
+ db.selectFrom("users").select("name").distinct().compile(db.printer())
109
+
110
+ // DISTINCT ON (PostgreSQL)
111
+ db.selectFrom("users")
112
+ .selectAll()
113
+ .distinctOn("dept")
114
+ .orderBy("dept")
115
+ .orderBy("salary", "DESC")
56
116
  .compile(db.printer())
57
117
  ```
58
118
 
59
- ### INSERT
119
+ ---
120
+
121
+ ## INSERT
60
122
 
61
123
  ```ts
124
+ // Single row
125
+ db.insertInto("users").values({ name: "Alice", email: "alice@example.com" }).compile(db.printer())
126
+
127
+ // Multiple rows
62
128
  db.insertInto("users")
63
- .values({
64
- name: "Alice",
65
- email: "alice@example.com",
66
- })
129
+ .valuesMany([
130
+ { name: "Alice", email: "a@b.com" },
131
+ { name: "Bob", email: "b@b.com" },
132
+ ])
133
+ .compile(db.printer())
134
+
135
+ // RETURNING
136
+ db.insertInto("users")
137
+ .values({ name: "Alice", email: "a@b.com" })
67
138
  .returningAll()
68
139
  .compile(db.printer())
140
+
141
+ // INSERT ... SELECT
142
+ const source = db.selectFrom("users").select("name", "email").build()
143
+ db.insertInto("archive").fromSelect(source).compile(db.printer())
144
+
145
+ // DEFAULT VALUES
146
+ db.insertInto("users").defaultValues().compile(db.printer())
147
+
148
+ // SQLite: INSERT OR IGNORE / INSERT OR REPLACE
149
+ db.insertInto("users").values({ name: "Alice" }).orIgnore().compile(db.printer())
69
150
  ```
70
151
 
71
- ### UPDATE
152
+ ---
153
+
154
+ ## UPDATE
72
155
 
73
156
  ```ts
157
+ // Basic update
74
158
  db.update("users")
75
159
  .set({ active: false })
76
160
  .where(({ id }) => id.eq(1))
77
161
  .compile(db.printer())
78
- ```
79
162
 
80
- ### DELETE
163
+ // SET with expression
164
+ db.update("users")
165
+ .setExpr("name", val("Anonymous"))
166
+ .where(({ active }) => active.eq(false))
167
+ .compile(db.printer())
81
168
 
82
- ```ts
83
- db.deleteFrom("users")
169
+ // UPDATE ... FROM (PostgreSQL)
170
+ db.update("users")
171
+ .set({ name: "Bob" })
172
+ .from("posts")
84
173
  .where(({ id }) => id.eq(1))
85
- .returning("id")
86
174
  .compile(db.printer())
175
+
176
+ // UPDATE with JOIN (MySQL)
177
+ db.update("orders").set({ total: 0 }).innerJoin("users", onExpr).compile(db.printer())
178
+
179
+ // RETURNING
180
+ db.update("users")
181
+ .set({ active: false })
182
+ .where(({ id }) => id.eq(1))
183
+ .returningAll()
184
+ .compile(db.printer())
185
+
186
+ // ORDER BY + LIMIT (MySQL)
187
+ db.update("users").set({ active: false }).orderBy("id").limit(lit(10)).compile(db.printer())
87
188
  ```
88
189
 
89
- ## Joins
190
+ ---
191
+
192
+ ## DELETE
90
193
 
91
194
  ```ts
92
- // INNER JOIN
93
- db.selectFrom("users")
94
- .innerJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
95
- .select("id", "title")
195
+ db.deleteFrom("users")
196
+ .where(({ id }) => id.eq(1))
96
197
  .compile(db.printer())
97
198
 
98
- // LEFT JOIN — joined columns become nullable
99
- db.selectFrom("users")
100
- .leftJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
199
+ // RETURNING
200
+ db.deleteFrom("users")
201
+ .where(({ id }) => id.eq(1))
202
+ .returning("id")
101
203
  .compile(db.printer())
102
204
 
103
- // RIGHT JOIN
104
- db.selectFrom("users")
105
- .rightJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
106
- .compile(db.printer())
205
+ // DELETE ... USING (PostgreSQL)
206
+ db.deleteFrom("orders").using("users").where(onExpr).compile(db.printer())
107
207
 
108
- // FULL JOIN — both sides become nullable
109
- db.selectFrom("users")
110
- .fullJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
208
+ // DELETE with JOIN (MySQL)
209
+ db.deleteFrom("orders")
210
+ .innerJoin("users", onExpr)
211
+ .where(({ id }) => id.eq(1))
111
212
  .compile(db.printer())
112
-
113
- // CROSS JOIN — cartesian product
114
- db.selectFrom("users").crossJoin("posts").compile(db.printer())
115
213
  ```
116
214
 
117
- ## Expression API
215
+ ---
118
216
 
119
- ### Comparisons
217
+ ## WHERE Conditions
120
218
 
121
- ```ts
122
- .where(({ id }) =>
123
- id.eq(42),
124
- )
219
+ Every `.where()` takes a callback with typed column proxies.
125
220
 
126
- .where(({ age }) =>
127
- age.gt(18),
128
- )
129
-
130
- .where(({ age }) =>
131
- age.gte(18),
132
- )
221
+ ### Comparisons
133
222
 
134
- .where(({ age }) =>
135
- age.lt(65),
136
- )
223
+ ```ts
224
+ .where(({ age }) => age.eq(25)) // = 25
225
+ .where(({ age }) => age.neq(0)) // != 0
226
+ .where(({ age }) => age.gt(18)) // > 18
227
+ .where(({ age }) => age.gte(18)) // >= 18
228
+ .where(({ age }) => age.lt(65)) // < 65
229
+ .where(({ age }) => age.lte(65)) // <= 65
230
+ ```
137
231
 
138
- .where(({ age }) =>
139
- age.lte(65),
140
- )
232
+ ### Pattern Matching
141
233
 
142
- .where(({ active }) =>
143
- active.neq(false),
144
- )
234
+ ```ts
235
+ .where(({ name }) => name.like("%ali%")) // LIKE
236
+ .where(({ name }) => name.notLike("%bob%")) // NOT LIKE
237
+ .where(({ name }) => name.ilike("%alice%")) // ILIKE (PG)
238
+ .where(({ email }) => email.notIlike("%spam%")) // NOT ILIKE
145
239
  ```
146
240
 
147
- ### String Matching
241
+ ### Range & Lists
148
242
 
149
243
  ```ts
150
- .where(({ name }) =>
151
- name.like("%ali%"),
152
- )
244
+ .where(({ age }) => age.between(18, 65)) // BETWEEN
245
+ .where(({ age }) => age.notBetween(0, 17)) // NOT BETWEEN
246
+ .where(({ age }) => age.betweenSymmetric(65, 18)) // BETWEEN SYMMETRIC (PG)
247
+ .where(({ id }) => id.in([1, 2, 3])) // IN
248
+ .where(({ id }) => id.notIn([99, 100])) // NOT IN
153
249
  ```
154
250
 
155
- ### Range & List
251
+ ### Null Checks
156
252
 
157
253
  ```ts
158
- .where(({ age }) =>
159
- age.between(18, 65),
160
- )
254
+ .where(({ bio }) => bio.isNull()) // IS NULL
255
+ .where(({ email }) => email.isNotNull()) // IS NOT NULL
256
+ ```
161
257
 
162
- .where(({ id }) =>
163
- id.in([1, 2, 3]),
164
- )
258
+ ### Null-Safe Comparisons
165
259
 
166
- .where(({ id }) =>
167
- id.notIn([99, 100]),
168
- )
260
+ ```ts
261
+ .where(({ age }) => age.isDistinctFrom(null)) // IS DISTINCT FROM
262
+ .where(({ age }) => age.isNotDistinctFrom(25)) // IS NOT DISTINCT FROM
169
263
  ```
170
264
 
171
- ### Null Checks
265
+ ### IN Subquery
172
266
 
173
267
  ```ts
174
- .where(({ bio }) =>
175
- bio.isNull(),
176
- )
268
+ const deptIds = db
269
+ .selectFrom("departments")
270
+ .select("id")
271
+ .build()
177
272
 
178
- .where(({ email }) =>
179
- email.isNotNull(),
180
- )
273
+ .where(({ dept_id }) => dept_id.inSubquery(deptIds)) // IN (SELECT ...)
274
+ .where(({ dept_id }) => dept_id.notInSubquery(deptIds)) // NOT IN (SELECT ...)
181
275
  ```
182
276
 
183
277
  ### Logical Combinators
184
278
 
185
279
  ```ts
186
- // AND
280
+ // AND (variadic — 2 or more args)
187
281
  .where(({ age, active }) =>
188
- and(
189
- age.gt(0),
190
- active.eq(true),
191
- ),
282
+ and(age.gt(18), active.eq(true)),
192
283
  )
193
284
 
194
- // OR
285
+ // AND with 3+ conditions
286
+ .where(({ id, age, active }) =>
287
+ and(id.gt(0), age.gt(18), active.eq(true)),
288
+ )
289
+
290
+ // OR (variadic)
195
291
  .where(({ name, email }) =>
196
- or(
197
- name.like("%alice%"),
198
- email.like("%alice%"),
199
- ),
292
+ or(name.like("%alice%"), email.like("%alice%")),
200
293
  )
201
294
 
202
295
  // NOT
203
- .where(({ active }) =>
204
- not(active.eq(true)),
205
- )
296
+ .where(({ active }) => not(active.eq(true)))
206
297
  ```
207
298
 
208
- ### Aggregates
299
+ ### Multiple WHERE (implicit AND)
209
300
 
210
301
  ```ts
211
- import { count, countDistinct, sum, avg, min, max, coalesce } from "sumak"
302
+ // Calling .where() multiple times ANDs conditions together
303
+ db.selectFrom("users")
304
+ .select("id")
305
+ .where(({ age }) => age.gt(18))
306
+ .where(({ active }) => active.eq(true))
307
+ .compile(db.printer())
308
+ // WHERE ("age" > $1) AND ("active" = $2)
309
+ ```
212
310
 
213
- db.selectFrom("users").selectExpr(count(), "total").compile(db.printer())
311
+ ### Column-to-Column Comparisons
214
312
 
215
- db.selectFrom("users").selectExpr(countDistinct(col.dept), "uniqueDepts").compile(db.printer())
216
- // SELECT COUNT(DISTINCT "dept") AS "uniqueDepts" FROM "users"
313
+ ```ts
314
+ .where(({ price, cost }) => price.gtCol(cost)) // "price" > "cost"
315
+ .where(({ a, b }) => a.eqCol(b)) // "a" = "b"
316
+ .where(({ a, b }) => a.neqCol(b)) // "a" != "b"
317
+ .where(({ a, b }) => a.gteCol(b)) // "a" >= "b"
318
+ .where(({ a, b }) => a.ltCol(b)) // "a" < "b"
319
+ .where(({ a, b }) => a.lteCol(b)) // "a" <= "b"
320
+ ```
217
321
 
218
- db.selectFrom("orders").selectExpr(sum(col.amount), "totalAmount").compile(db.printer())
322
+ ---
219
323
 
220
- db.selectFrom("orders").selectExpr(avg(col.amount), "avgAmount").compile(db.printer())
324
+ ## Joins
221
325
 
222
- db.selectFrom("orders")
223
- .selectExpr(coalesce(col.discount, val(0)), "safeDiscount")
326
+ ```ts
327
+ // INNER JOIN
328
+ db.selectFrom("users")
329
+ .innerJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
330
+ .select("id", "title")
331
+ .compile(db.printer())
332
+
333
+ // LEFT JOIN — joined columns become nullable
334
+ db.selectFrom("users")
335
+ .leftJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
336
+ .compile(db.printer())
337
+
338
+ // RIGHT JOIN
339
+ db.selectFrom("users")
340
+ .rightJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
224
341
  .compile(db.printer())
342
+
343
+ // FULL JOIN — both sides nullable
344
+ db.selectFrom("users")
345
+ .fullJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
346
+ .compile(db.printer())
347
+
348
+ // CROSS JOIN
349
+ db.selectFrom("users").crossJoin("posts").compile(db.printer())
350
+
351
+ // LATERAL JOINs (correlated subqueries)
352
+ db.selectFrom("users").innerJoinLateral(subquery, "recent_posts", onExpr).compile(db.printer())
353
+
354
+ db.selectFrom("users").leftJoinLateral(subquery, "recent_posts", onExpr).compile(db.printer())
355
+
356
+ db.selectFrom("users").crossJoinLateral(subquery, "latest").compile(db.printer())
225
357
  ```
226
358
 
227
- ### EXISTS / NOT EXISTS
359
+ ---
360
+
361
+ ## Expressions
362
+
363
+ ### Computed Columns
228
364
 
229
365
  ```ts
230
- import { exists, notExists } from "sumak"
366
+ import { val, cast, rawExpr } from "sumak"
367
+
368
+ // Add a computed column with alias
369
+ db.selectFrom("users").selectExpr(val("hello"), "greeting").compile(db.printer())
231
370
 
371
+ // Multiple expressions at once
232
372
  db.selectFrom("users")
233
- .where(() =>
234
- exists(
235
- db
236
- .selectFrom("posts")
237
- .where(({ userId }) => userId.eq(1))
238
- .build(),
239
- ),
240
- )
373
+ .selectExprs({
374
+ total: count(),
375
+ greeting: val("hello"),
376
+ })
241
377
  .compile(db.printer())
242
378
 
379
+ // CAST
243
380
  db.selectFrom("users")
244
- .where(() =>
245
- notExists(
246
- db
247
- .selectFrom("posts")
248
- .where(({ userId }) => userId.eq(1))
249
- .build(),
250
- ),
251
- )
381
+ .selectExpr(cast(val(42), "text"), "idAsText")
382
+ .compile(db.printer())
383
+ ```
384
+
385
+ ### Arithmetic
386
+
387
+ ```ts
388
+ import { add, sub, mul, div, mod, neg } from "sumak"
389
+
390
+ db.selectFrom("orders").selectExpr(mul(col.price, col.qty), "total").compile(db.printer())
391
+ // ("price" * "qty") AS "total"
392
+
393
+ db.selectFrom("orders")
394
+ .selectExpr(add(col.price, val(10)), "adjusted")
252
395
  .compile(db.printer())
253
396
  ```
254
397
 
255
- ### CASE Expression
398
+ ### CASE / WHEN
256
399
 
257
400
  ```ts
258
401
  import { case_, val } from "sumak"
@@ -269,38 +412,85 @@ db.selectFrom("users")
269
412
  .compile(db.printer())
270
413
  ```
271
414
 
272
- ### CAST
415
+ ### JSON Operations
273
416
 
274
417
  ```ts
275
- import { cast, val } from "sumak"
418
+ import { jsonRef, jsonAgg, toJson, jsonBuildObject } from "sumak"
276
419
 
420
+ // Access: -> (JSON object), ->> (text value)
277
421
  db.selectFrom("users")
278
- .selectExpr(cast(val(42), "text"), "idAsText")
422
+ .selectExpr(jsonRef(col.meta, "name", "->>"), "metaName")
423
+ .compile(db.printer())
424
+
425
+ // JSON_AGG / TO_JSON
426
+ db.selectFrom("users").selectExpr(jsonAgg(col.name), "namesJson").compile(db.printer())
427
+
428
+ // JSON_BUILD_OBJECT
429
+ db.selectFrom("users")
430
+ .selectExpr(jsonBuildObject(["name", col.name], ["age", col.age]), "obj")
279
431
  .compile(db.printer())
280
432
  ```
281
433
 
282
- ### JSON Operations
434
+ ### PostgreSQL Array Operators
283
435
 
284
436
  ```ts
285
- import { jsonRef } from "sumak"
437
+ import { arrayContains, arrayContainedBy, arrayOverlaps, rawExpr } from "sumak"
438
+
439
+ .where(() => arrayContains(rawExpr("tags"), rawExpr("ARRAY['sql']"))) // @>
440
+ .where(() => arrayContainedBy(rawExpr("tags"), rawExpr("ARRAY[...]"))) // <@
441
+ .where(() => arrayOverlaps(rawExpr("tags"), rawExpr("ARRAY['sql']"))) // &&
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Aggregates
447
+
448
+ ```ts
449
+ import { count, countDistinct, sum, sumDistinct, avg, avgDistinct, min, max, coalesce } from "sumak"
450
+
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())
286
455
 
287
- // -> (JSON object)
456
+ // COALESCE (variadic)
288
457
  db.selectFrom("users")
289
- .selectExpr(jsonRef(col.meta, "address", "->"), "address")
458
+ .selectExpr(coalesce(col.nick, col.name, val("Anonymous")), "displayName")
290
459
  .compile(db.printer())
460
+ ```
461
+
462
+ ### Aggregate with FILTER (PostgreSQL)
463
+
464
+ ```ts
465
+ import { filter, count } from "sumak"
291
466
 
292
- // ->> (text value)
467
+ db.selectFrom("users").selectExpr(filter(count(), activeExpr), "activeCount").compile(db.printer())
468
+ // COUNT(*) FILTER (WHERE ...)
469
+ ```
470
+
471
+ ### Aggregate with ORDER BY
472
+
473
+ ```ts
474
+ import { stringAgg, arrayAgg } from "sumak"
475
+
476
+ // STRING_AGG with ORDER BY
293
477
  db.selectFrom("users")
294
- .selectExpr(jsonRef(col.meta, "name", "->>"), "metaName")
478
+ .selectExpr(stringAgg(col.name, ", ", [{ expr: col.name, direction: "ASC" }]), "names")
295
479
  .compile(db.printer())
480
+ // STRING_AGG("name", ', ' ORDER BY "name" ASC)
481
+
482
+ // ARRAY_AGG
483
+ db.selectFrom("users").selectExpr(arrayAgg(col.id), "ids").compile(db.printer())
296
484
  ```
297
485
 
486
+ ---
487
+
298
488
  ## Window Functions
299
489
 
300
490
  ```ts
301
491
  import { over, rowNumber, rank, denseRank, lag, lead, ntile, count, sum } from "sumak"
302
492
 
303
- // ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)
493
+ // ROW_NUMBER
304
494
  db.selectFrom("employees")
305
495
  .selectExpr(
306
496
  over(rowNumber(), (w) => w.partitionBy("dept").orderBy("salary", "DESC")),
@@ -308,109 +498,122 @@ db.selectFrom("employees")
308
498
  )
309
499
  .compile(db.printer())
310
500
 
311
- // RANK() OVER (ORDER BY score DESC)
312
- db.selectFrom("students")
313
- .selectExpr(
314
- over(rank(), (w) => w.orderBy("score", "DESC")),
315
- "rnk",
316
- )
317
- .compile(db.printer())
501
+ // RANK / DENSE_RANK
502
+ over(rank(), (w) => w.orderBy("score", "DESC"))
503
+ over(denseRank(), (w) => w.orderBy("score", "DESC"))
318
504
 
319
505
  // Running total with frame
320
- db.selectFrom("orders")
321
- .selectExpr(
322
- over(sum(col.amount), (w) =>
323
- w
324
- .partitionBy("userId")
325
- .orderBy("createdAt")
326
- .rows({ type: "unbounded_preceding" }, { type: "current_row" }),
327
- ),
328
- "runningTotal",
329
- )
330
- .compile(db.printer())
506
+ over(sum(col.amount), (w) =>
507
+ w
508
+ .partitionBy("userId")
509
+ .orderBy("createdAt")
510
+ .rows({ type: "unbounded_preceding" }, { type: "current_row" }),
511
+ )
331
512
 
332
- // LAG / LEAD
333
- db.selectFrom("prices")
334
- .selectExpr(
335
- over(lag(col.price, 1), (w) => w.orderBy("date")),
336
- "prevPrice",
337
- )
338
- .compile(db.printer())
513
+ // RANGE / GROUPS frames
514
+ over(count(), (w) =>
515
+ w.orderBy("salary").range({ type: "preceding", value: 100 }, { type: "following", value: 100 }),
516
+ )
339
517
 
340
- // NTILE(4)
341
- db.selectFrom("employees")
342
- .selectExpr(
343
- over(ntile(4), (w) => w.orderBy("salary", "DESC")),
344
- "quartile",
345
- )
346
- .compile(db.printer())
518
+ // LAG / LEAD / NTILE
519
+ over(lag(col.price, 1), (w) => w.orderBy("date"))
520
+ over(lead(col.price, 1), (w) => w.orderBy("date"))
521
+ over(ntile(4), (w) => w.orderBy("salary", "DESC"))
347
522
  ```
348
523
 
524
+ ---
525
+
349
526
  ## SQL Functions
350
527
 
351
- ### String Functions
528
+ ### String
352
529
 
353
530
  ```ts
354
531
  import { upper, lower, concat, substring, trim, length } from "sumak"
355
532
 
356
- db.selectFrom("users").selectExpr(upper(col.name), "upperName").compile(db.printer())
357
- // SELECT UPPER("name") AS "upperName" FROM "users"
533
+ upper(col.name) // UPPER("name")
534
+ lower(col.email) // LOWER("email")
535
+ concat(col.first, val(" "), col.last) // CONCAT(...)
536
+ substring(col.name, 1, 3) // SUBSTRING("name", 1, 3)
537
+ trim(col.name) // TRIM("name")
538
+ length(col.name) // LENGTH("name")
539
+ ```
358
540
 
359
- db.selectFrom("users").selectExpr(lower(col.email), "lowerEmail").compile(db.printer())
541
+ ### Numeric
360
542
 
361
- db.selectFrom("users")
362
- .selectExpr(concat(col.firstName, val(" "), col.lastName), "fullName")
363
- .compile(db.printer())
543
+ ```ts
544
+ import { abs, round, ceil, floor, greatest, least } from "sumak"
545
+
546
+ abs(col.balance) // ABS("balance")
547
+ round(col.price, 2) // ROUND("price", 2)
548
+ ceil(col.amount) // CEIL("amount")
549
+ floor(col.amount) // FLOOR("amount")
550
+ greatest(col.a, col.b) // GREATEST("a", "b")
551
+ least(col.a, col.b) // LEAST("a", "b")
552
+ ```
364
553
 
365
- db.selectFrom("users")
366
- .selectExpr(substring(col.name, 1, 3), "prefix")
367
- .compile(db.printer())
554
+ ### Conditional
368
555
 
369
- db.selectFrom("users").selectExpr(trim(col.name), "trimmed").compile(db.printer())
556
+ ```ts
557
+ import { nullif, coalesce } from "sumak"
370
558
 
371
- db.selectFrom("users").selectExpr(length(col.name), "nameLen").compile(db.printer())
559
+ nullif(col.age, val(0)) // NULLIF("age", 0)
560
+ coalesce(col.nick, col.name, val("Anonymous")) // COALESCE(...)
372
561
  ```
373
562
 
374
- ### Numeric Functions
563
+ ### Date/Time
375
564
 
376
565
  ```ts
377
- import { abs, round, ceil, floor } from "sumak"
566
+ import { now, currentTimestamp } from "sumak"
378
567
 
379
- db.selectFrom("orders").selectExpr(abs(col.balance), "absBalance").compile(db.printer())
568
+ now() // NOW()
569
+ currentTimestamp() // CURRENT_TIMESTAMP()
570
+ ```
380
571
 
381
- db.selectFrom("orders").selectExpr(round(col.price, 2), "rounded").compile(db.printer())
572
+ ---
382
573
 
383
- db.selectFrom("orders").selectExpr(ceil(col.amount), "ceiling").compile(db.printer())
574
+ ## Subqueries
384
575
 
385
- db.selectFrom("orders").selectExpr(floor(col.amount), "floored").compile(db.printer())
386
- ```
387
-
388
- ### Conditional Functions
576
+ ### EXISTS / NOT EXISTS
389
577
 
390
578
  ```ts
391
- import { nullif, greatest, least } from "sumak"
579
+ import { exists, notExists } from "sumak"
392
580
 
393
581
  db.selectFrom("users")
394
- .selectExpr(nullif(col.age, val(0)), "ageOrNull")
582
+ .where(() =>
583
+ exists(
584
+ db
585
+ .selectFrom("posts")
586
+ .where(({ userId }) => userId.eq(1))
587
+ .build(),
588
+ ),
589
+ )
395
590
  .compile(db.printer())
591
+ ```
396
592
 
397
- db.selectFrom("products")
398
- .selectExpr(greatest(col.price, col.minPrice), "effectivePrice")
399
- .compile(db.printer())
593
+ ### Derived Tables (Subquery in FROM)
400
594
 
401
- db.selectFrom("products")
402
- .selectExpr(least(col.price, col.maxPrice), "cappedPrice")
403
- .compile(db.printer())
595
+ ```ts
596
+ const sub = db
597
+ .selectFrom("users")
598
+ .select("id", "name")
599
+ .where(({ age }) => age.gt(18))
600
+
601
+ db.selectFromSubquery(sub, "adults").selectAll().compile(db.printer())
602
+ // SELECT * FROM (SELECT ...) AS "adults"
404
603
  ```
405
604
 
406
- ### Date/Time Functions
605
+ ### IN Subquery
407
606
 
408
607
  ```ts
409
- import { now, currentTimestamp } from "sumak"
608
+ const deptIds = db.selectFrom("departments").select("id").build()
410
609
 
411
- db.selectFrom("users").selectExpr(now(), "currentTime").compile(db.printer())
610
+ db.selectFrom("users")
611
+ .where(({ dept_id }) => dept_id.inSubquery(deptIds))
612
+ .compile(db.printer())
412
613
  ```
413
614
 
615
+ ---
616
+
414
617
  ## Set Operations
415
618
 
416
619
  ```ts
@@ -418,72 +621,40 @@ const active = db
418
621
  .selectFrom("users")
419
622
  .select("id")
420
623
  .where(({ active }) => active.eq(true))
421
-
422
624
  const premium = db
423
625
  .selectFrom("users")
424
626
  .select("id")
425
- .where(({ active }) => active.eq(true))
426
-
427
- // UNION
428
- active.union(premium).compile(db.printer())
429
-
430
- // UNION ALL
431
- active.unionAll(premium).compile(db.printer())
432
-
433
- // INTERSECT / INTERSECT ALL
434
- active.intersect(premium).compile(db.printer())
435
- active.intersectAll(premium).compile(db.printer())
436
-
437
- // EXCEPT / EXCEPT ALL
438
- active.except(premium).compile(db.printer())
439
- active.exceptAll(premium).compile(db.printer())
627
+ .where(({ tier }) => tier.eq("premium"))
628
+
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
440
635
  ```
441
636
 
637
+ ---
638
+
442
639
  ## CTEs (WITH)
443
640
 
444
641
  ```ts
445
- // SELECT with CTE
446
- db.selectFrom("users")
447
- .with(
448
- "active_users",
449
- db
450
- .selectFrom("users")
451
- .where(({ active }) => active.eq(true))
452
- .build(),
453
- )
454
- .compile(db.printer())
455
-
456
- // INSERT with CTE
457
- db.insertInto("users")
458
- .with("source", sourceCte)
459
- .values({ name: "Alice", email: "a@b.com" })
460
- .compile(db.printer())
461
-
462
- // UPDATE with CTE
463
- db.update("users").with("target", targetCte).set({ active: false }).compile(db.printer())
642
+ const activeCte = db
643
+ .selectFrom("users")
644
+ .where(({ active }) => active.eq(true))
645
+ .build()
464
646
 
465
- // DELETE with CTE
466
- db.deleteFrom("users")
467
- .with("to_delete", deleteCte)
468
- .where(({ id }) => id.eq(1))
469
- .compile(db.printer())
647
+ db.selectFrom("users").with("active_users", activeCte).compile(db.printer())
470
648
 
471
649
  // Recursive CTE
472
- db.selectFrom("users").with("tree", recursiveQuery, true).compile(db.printer())
650
+ db.selectFrom("categories").with("tree", recursiveQuery, true).compile(db.printer())
473
651
  ```
474
652
 
475
- ## UPDATE FROM
653
+ ---
476
654
 
477
- ```ts
478
- db.update("users")
479
- .set({ name: "Bob" })
480
- .from("posts")
481
- .where(({ id }) => id.eq(1))
482
- .compile(db.printer())
483
- // UPDATE "users" SET "name" = $1 FROM "posts" WHERE ("id" = $2)
484
- ```
655
+ ## Conditional / Dynamic Queries
485
656
 
486
- ## Conditional Query Building
657
+ ### `$if()` conditional clause
487
658
 
488
659
  ```ts
489
660
  const withFilter = true
@@ -495,55 +666,105 @@ db.selectFrom("users")
495
666
  .$if(withOrder, (qb) => qb.orderBy("name"))
496
667
  .compile(db.printer())
497
668
  // WHERE applied, ORDER BY skipped
669
+ ```
670
+
671
+ ### `$call()` — reusable query fragments
498
672
 
499
- // Multiple .where() calls are AND'd together
673
+ ```ts
674
+ const withPagination = (qb) => qb.limit(10).offset(20)
675
+ const onlyActive = (qb) => qb.where(({ active }) => active.eq(true))
676
+
677
+ db.selectFrom("users")
678
+ .select("id", "name")
679
+ .$call(onlyActive)
680
+ .$call(withPagination)
681
+ .compile(db.printer())
682
+ ```
683
+
684
+ ### `clear*()` — reset clauses
685
+
686
+ ```ts
500
687
  db.selectFrom("users")
501
688
  .select("id")
502
- .where(({ age }) => age.gt(18))
503
- .where(({ active }) => active.eq(true))
689
+ .orderBy("name")
690
+ .clearOrderBy() // removes ORDER BY
691
+ .orderBy("id", "DESC") // re-add different order
504
692
  .compile(db.printer())
505
- // WHERE ("age" > $1) AND ("active" = $2)
506
693
  ```
507
694
 
508
- ## INSERT Advanced
695
+ Available: `clearWhere()`, `clearOrderBy()`, `clearLimit()`, `clearOffset()`, `clearGroupBy()`, `clearHaving()`, `clearSelect()`.
696
+
697
+ ---
698
+
699
+ ## Raw SQL
700
+
701
+ ### `sql` tagged template
509
702
 
510
703
  ```ts
511
- // INSERT ... SELECT
512
- const selectQuery = db.selectFrom("users").select("name", "age").build()
513
- db.insertInto("archive").fromSelect(selectQuery).compile(db.printer())
704
+ import { sql } from "sumak"
514
705
 
515
- // INSERT ... DEFAULT VALUES
516
- db.insertInto("users").defaultValues().compile(db.printer())
706
+ // Primitives are parameterized
707
+ sql`SELECT * FROM users WHERE name = ${"Alice"}`
708
+ // params: ["Alice"]
517
709
 
518
- // SQLite: INSERT OR IGNORE / INSERT OR REPLACE
519
- db.insertInto("users").values({ name: "Alice" }).orIgnore().compile(db.printer())
520
- // INSERT OR IGNORE INTO "users" ...
710
+ // Expressions are inlined
711
+ sql`SELECT * FROM users WHERE active = ${val(true)}`
712
+ // ... WHERE active = TRUE
713
+
714
+ // Helpers
715
+ sql`SELECT ${sql.ref("id")} FROM ${sql.table("users", "public")}`
716
+ // → SELECT "id" FROM "public"."users"
521
717
 
522
- db.insertInto("users").values({ name: "Alice" }).orReplace().compile(db.printer())
523
- // INSERT OR REPLACE INTO "users" ...
718
+ // In queries
719
+ db.selectFrom("users")
720
+ .selectExpr(sql`CURRENT_DATE`, "today")
721
+ .compile(db.printer())
524
722
  ```
525
723
 
526
- ## ON CONFLICT
724
+ ### `rawExpr()` escape hatch
527
725
 
528
726
  ```ts
529
- // DO NOTHING (by columns)
727
+ import { rawExpr } from "sumak"
728
+
729
+ // In WHERE
730
+ db.selectFrom("users")
731
+ .where(() => rawExpr<boolean>("age > 18"))
732
+ .compile(db.printer())
733
+
734
+ // In SELECT
735
+ db.selectFrom("users")
736
+ .selectExpr(rawExpr<number>("EXTRACT(YEAR FROM created_at)"), "year")
737
+ .compile(db.printer())
738
+ ```
739
+
740
+ ---
741
+
742
+ ## ON CONFLICT / Upsert
743
+
744
+ ```ts
745
+ // PostgreSQL: ON CONFLICT DO NOTHING
530
746
  db.insertInto("users")
531
747
  .values({ name: "Alice", email: "a@b.com" })
532
748
  .onConflictDoNothing("email")
533
749
  .compile(db.printer())
534
750
 
535
- // DO UPDATE (by columns)
751
+ // ON CONFLICT DO UPDATE (with Expression)
752
+ db.insertInto("users")
753
+ .values({ name: "Alice", email: "a@b.com" })
754
+ .onConflictDoUpdate(["email"], [{ column: "name", value: val("Updated") }])
755
+ .compile(db.printer())
756
+
757
+ // ON CONFLICT DO UPDATE (with plain object — auto-parameterized)
536
758
  db.insertInto("users")
537
759
  .values({ name: "Alice", email: "a@b.com" })
538
- .onConflictDoUpdate(["email"], [{ column: "name", value: val("Alice") }])
760
+ .onConflictDoUpdateSet(["email"], { name: "Alice Updated" })
539
761
  .compile(db.printer())
540
762
 
541
- // DO NOTHING (by constraint name)
763
+ // ON CONFLICT ON CONSTRAINT
542
764
  db.insertInto("users")
543
765
  .values({ name: "Alice", email: "a@b.com" })
544
766
  .onConflictConstraintDoNothing("users_email_key")
545
767
  .compile(db.printer())
546
- // ON CONFLICT ON CONSTRAINT "users_email_key" DO NOTHING
547
768
 
548
769
  // MySQL: ON DUPLICATE KEY UPDATE
549
770
  db.insertInto("users")
@@ -552,217 +773,239 @@ db.insertInto("users")
552
773
  .compile(db.printer())
553
774
  ```
554
775
 
776
+ ---
777
+
555
778
  ## MERGE (SQL:2003)
556
779
 
557
780
  ```ts
558
781
  db.mergeInto("users", "staging", "s", ({ target, source }) => target.id.eqCol(source.id))
559
782
  .whenMatchedThenUpdate({ name: "updated" })
560
- .whenNotMatchedThenInsert({
561
- name: "Alice",
562
- email: "alice@example.com",
563
- })
783
+ .whenNotMatchedThenInsert({ name: "Alice", email: "a@b.com" })
564
784
  .compile(db.printer())
565
785
 
566
- // MERGE with conditional delete
786
+ // Conditional delete
567
787
  db.mergeInto("users", "staging", "s", ({ target, source }) => target.id.eqCol(source.id))
568
788
  .whenMatchedThenDelete()
569
789
  .compile(db.printer())
570
790
  ```
571
791
 
792
+ ---
793
+
572
794
  ## Row Locking
573
795
 
574
796
  ```ts
575
- // FOR UPDATE
576
- db.selectFrom("users").select("id").forUpdate().compile(db.printer())
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)
801
+
802
+ // 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
805
+ ```
577
806
 
578
- // FOR SHARE
579
- db.selectFrom("users").select("id").forShare().compile(db.printer())
807
+ ---
580
808
 
581
- // FOR NO KEY UPDATE / FOR KEY SHARE (PG)
582
- db.selectFrom("users").select("id").forNoKeyUpdate().compile(db.printer())
583
- db.selectFrom("users").select("id").forKeyShare().compile(db.printer())
809
+ ## EXPLAIN
810
+
811
+ ```ts
812
+ db.selectFrom("users").select("id").explain().compile(db.printer())
813
+ // EXPLAIN SELECT "id" FROM "users"
814
+
815
+ db.selectFrom("users").select("id").explain({ analyze: true }).compile(db.printer())
816
+ // EXPLAIN ANALYZE SELECT ...
584
817
 
585
- // SKIP LOCKED / NOWAIT
586
- db.selectFrom("users").select("id").forUpdate().skipLocked().compile(db.printer())
587
- db.selectFrom("users").select("id").forUpdate().noWait().compile(db.printer())
818
+ db.selectFrom("users").select("id").explain({ format: "JSON" }).compile(db.printer())
819
+ // EXPLAIN (FORMAT JSON) SELECT ...
588
820
  ```
589
821
 
590
- ## DISTINCT ON (PG)
822
+ ---
823
+
824
+ ## Schema Builder (DDL)
825
+
826
+ The schema builder generates DDL SQL (CREATE, ALTER, DROP). It is separate from the query builder — you use `db.compileDDL(node)` to compile DDL nodes.
827
+
828
+ ### CREATE TABLE
591
829
 
592
830
  ```ts
593
- db.selectFrom("users")
594
- .selectAll()
595
- .distinctOn("dept")
596
- .orderBy("dept")
597
- .orderBy("salary", "DESC")
598
- .compile(db.printer())
599
- // SELECT DISTINCT ON ("dept") * FROM "users" ORDER BY "dept" ASC, "salary" DESC
831
+ db.schema
832
+ .createTable("users")
833
+ .ifNotExists()
834
+ .addColumn("id", "serial", (c) => c.primaryKey())
835
+ .addColumn("name", "varchar(255)", (c) => c.notNull())
836
+ .addColumn("email", "varchar", (c) => c.unique().notNull())
837
+ .addColumn("active", "boolean", (c) => c.defaultTo(lit(true)))
838
+ .build()
839
+
840
+ // Foreign key with ON DELETE CASCADE
841
+ db.schema
842
+ .createTable("posts")
843
+ .addColumn("id", "serial", (c) => c.primaryKey())
844
+ .addColumn("user_id", "integer", (c) => c.notNull().references("users", "id").onDelete("CASCADE"))
845
+ .build()
846
+
847
+ // Composite primary key
848
+ db.schema
849
+ .createTable("order_items")
850
+ .addColumn("order_id", "integer")
851
+ .addColumn("product_id", "integer")
852
+ .addPrimaryKeyConstraint("pk_order_items", ["order_id", "product_id"])
853
+ .build()
600
854
  ```
601
855
 
602
- ## DELETE USING / JOIN in UPDATE & DELETE
856
+ ### ALTER TABLE
603
857
 
604
858
  ```ts
605
- // PG: DELETE ... USING
606
- db.deleteFrom("orders")
607
- .using("users")
608
- .where(eq(col("orders.user_id"), col("users.id")))
609
- .compile(db.printer())
859
+ db.schema
860
+ .alterTable("users")
861
+ .addColumn("age", "integer", (c) => c.notNull())
862
+ .build()
863
+
864
+ db.schema.alterTable("users").dropColumn("age").build()
865
+ db.schema.alterTable("users").renameColumn("name", "full_name").build()
866
+ db.schema.alterTable("users").renameTo("people").build()
867
+
868
+ db.schema
869
+ .alterTable("users")
870
+ .alterColumn("age", { type: "set_data_type", dataType: "bigint" })
871
+ .build()
872
+ db.schema.alterTable("users").alterColumn("name", { type: "set_not_null" }).build()
873
+ ```
610
874
 
611
- // MySQL: DELETE with JOIN
612
- db.deleteFrom("orders")
613
- .innerJoin("users", eq(col("user_id", "orders"), col("id", "users")))
614
- .where(eq(col("name", "users"), lit("Alice")))
615
- .compile(db.printer())
875
+ ### CREATE INDEX
616
876
 
617
- // MySQL: UPDATE with JOIN
618
- db.update("orders")
619
- .set({ total: 0 })
620
- .innerJoin("users", eq(col("user_id", "orders"), col("id", "users")))
621
- .compile(db.printer())
877
+ ```ts
878
+ db.schema.createIndex("idx_users_name").on("users").column("name").build()
879
+ db.schema.createIndex("uq_email").unique().on("users").column("email").build()
880
+
881
+ // Multi-column with direction
882
+ db.schema
883
+ .createIndex("idx_multi")
884
+ .on("users")
885
+ .column("last_name", "ASC")
886
+ .column("age", "DESC")
887
+ .build()
888
+
889
+ // GIN index (PG)
890
+ db.schema.createIndex("idx_tags").on("posts").column("tags").using("gin").build()
891
+
892
+ // Partial index
893
+ db.schema
894
+ .createIndex("idx_active")
895
+ .on("users")
896
+ .column("email")
897
+ .where(rawExpr("active = true"))
898
+ .build()
622
899
  ```
623
900
 
624
- ## Aggregate FILTER (WHERE)
901
+ ### CREATE VIEW
625
902
 
626
903
  ```ts
627
- import { filter, count, sum } from "sumak"
904
+ db.schema.createView("active_users").asSelect(selectQuery).build()
905
+ db.schema.createView("stats").materialized().asSelect(selectQuery).build()
906
+ db.schema.createView("my_view").orReplace().columns("id", "name").asSelect(selectQuery).build()
907
+ ```
628
908
 
629
- // COUNT(*) FILTER (WHERE active = true)
630
- db.selectFrom("users").selectExpr(filter(count(), activeExpr), "activeCount").compile(db.printer())
909
+ ### DROP
910
+
911
+ ```ts
912
+ db.schema.dropTable("users").ifExists().cascade().build()
913
+ db.schema.dropIndex("idx_name").ifExists().build()
914
+ db.schema.dropView("my_view").materialized().ifExists().build()
631
915
  ```
632
916
 
633
- ## EXPLAIN
917
+ ### Auto-Generate from Schema
918
+
919
+ The schema you pass to `sumak({ tables })` can auto-generate CREATE TABLE SQL:
634
920
 
635
921
  ```ts
636
- // EXPLAIN
637
- db.selectFrom("users").select("id").explain().compile(db.printer())
638
- // EXPLAIN SELECT "id" FROM "users"
922
+ const db = sumak({
923
+ dialect: pgDialect(),
924
+ tables: {
925
+ users: {
926
+ id: serial().primaryKey(),
927
+ name: text().notNull(),
928
+ email: text().notNull(),
929
+ },
930
+ posts: {
931
+ id: serial().primaryKey(),
932
+ title: text().notNull(),
933
+ userId: integer().references("users", "id"),
934
+ },
935
+ },
936
+ })
639
937
 
640
- // EXPLAIN ANALYZE
641
- db.selectFrom("users").select("id").explain({ analyze: true }).compile(db.printer())
938
+ const ddl = db.generateDDL()
939
+ // [
940
+ // { sql: 'CREATE TABLE "users" ("id" serial PRIMARY KEY NOT NULL, "name" text NOT NULL, "email" text NOT NULL)', params: [] },
941
+ // { sql: 'CREATE TABLE "posts" ("id" serial PRIMARY KEY NOT NULL, "title" text NOT NULL, "userId" integer REFERENCES "users"("id"))', params: [] },
942
+ // ]
642
943
 
643
- // EXPLAIN with format
644
- db.selectFrom("users").select("id").explain({ format: "JSON" }).compile(db.printer())
645
- // EXPLAIN (FORMAT JSON) SELECT "id" FROM "users"
944
+ // With IF NOT EXISTS
945
+ const safeDDL = db.generateDDL({ ifNotExists: true })
646
946
  ```
647
947
 
948
+ > Compile any DDL node: `db.compileDDL(node)` returns `{ sql, params }`.
949
+
950
+ ---
951
+
648
952
  ## Full-Text Search
649
953
 
650
- Dialect-aware FTS — same API, different SQL per dialect:
954
+ Dialect-aware — same API, different SQL per dialect:
651
955
 
652
956
  ```ts
653
- import { textSearch } from "sumak"
957
+ import { textSearch, val } from "sumak"
654
958
 
655
959
  // PostgreSQL: to_tsvector("name") @@ to_tsquery('alice')
656
960
  db.selectFrom("users")
657
961
  .where(({ name }) => textSearch([name.toExpr()], val("alice")))
658
962
  .compile(db.printer())
659
963
 
660
- // With language config
661
- db.selectFrom("users")
662
- .where(({ name }) => textSearch([name.toExpr()], val("alice"), { language: "english" }))
663
- .compile(db.printer())
664
-
665
964
  // MySQL: MATCH(`name`) AGAINST(? IN BOOLEAN MODE)
666
965
  // SQLite: ("name" MATCH ?)
667
966
  // MSSQL: CONTAINS(([name]), @p0)
668
967
  ```
669
968
 
670
- ## Temporal Tables (SQL:2011)
969
+ ---
671
970
 
672
- Query historical data with `FOR SYSTEM_TIME`:
971
+ ## Temporal Tables (SQL:2011)
673
972
 
674
973
  ```ts
675
- // AS OF — point-in-time query
974
+ // Point-in-time query
676
975
  db.selectFrom("users")
677
- .forSystemTime({
678
- kind: "as_of",
679
- timestamp: lit("2024-01-01"),
680
- })
976
+ .forSystemTime({ kind: "as_of", timestamp: lit("2024-01-01") })
681
977
  .compile(db.printer())
682
978
 
683
- // BETWEEN — time range
979
+ // Time range
684
980
  db.selectFrom("users")
685
- .forSystemTime({
686
- kind: "between",
687
- start: lit("2024-01-01"),
688
- end: lit("2024-12-31"),
689
- })
981
+ .forSystemTime({ kind: "between", start: lit("2024-01-01"), end: lit("2024-12-31") })
690
982
  .compile(db.printer())
691
983
 
692
- // ALL — full history
984
+ // Full history
693
985
  db.selectFrom("users").forSystemTime({ kind: "all" }).compile(db.printer())
694
986
  ```
695
987
 
696
- Supported modes: `as_of`, `from_to`, `between`, `contained_in`, `all`.
697
-
698
- ## Tree Shaking
988
+ Modes: `as_of`, `from_to`, `between`, `contained_in`, `all`.
699
989
 
700
- Import only the dialect you need:
701
-
702
- ```ts
703
- import { sumak } from "sumak"
704
- import { pgDialect } from "sumak/pg"
705
- import { mssqlDialect } from "sumak/mssql"
706
- import { mysqlDialect } from "sumak/mysql"
707
- import { sqliteDialect } from "sumak/sqlite"
708
- import { serial, text } from "sumak/schema"
709
- ```
710
-
711
- ## Dialects
712
-
713
- Same query, different SQL:
714
-
715
- ```ts
716
- // PostgreSQL → SELECT "id" FROM "users" WHERE ("id" = $1)
717
- // MySQL → SELECT `id` FROM `users` WHERE (`id` = ?)
718
- // SQLite → SELECT "id" FROM "users" WHERE ("id" = ?)
719
- // MSSQL → SELECT [id] FROM [users] WHERE ([id] = @p0)
720
- ```
721
-
722
- ### MSSQL Specifics
723
-
724
- ```ts
725
- import { mssqlDialect } from "sumak/mssql"
726
-
727
- const db = sumak({
728
- dialect: mssqlDialect(),
729
- tables: { ... },
730
- })
731
-
732
- // LIMIT → TOP N
733
- // SELECT TOP 10 * FROM [users]
734
-
735
- // LIMIT + OFFSET → OFFSET/FETCH
736
- // SELECT * FROM [users] ORDER BY [id] ASC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
737
-
738
- // RETURNING → OUTPUT INSERTED.*
739
- // INSERT INTO [users] ([name]) OUTPUT INSERTED.* VALUES (@p0)
740
-
741
- // DELETE RETURNING → OUTPUT DELETED.*
742
- // DELETE FROM [users] OUTPUT DELETED.* WHERE ([id] = @p0)
743
- ```
990
+ ---
744
991
 
745
992
  ## Plugins
746
993
 
747
994
  ```ts
748
- import {
749
- WithSchemaPlugin,
750
- SoftDeletePlugin,
751
- CamelCasePlugin,
752
- } from "sumak"
995
+ import { WithSchemaPlugin, SoftDeletePlugin, CamelCasePlugin } from "sumak"
753
996
 
754
997
  const db = sumak({
755
998
  dialect: pgDialect(),
756
999
  plugins: [
757
- new WithSchemaPlugin("public"),
758
- new SoftDeletePlugin({ tables: ["users"] }),
1000
+ new WithSchemaPlugin("public"), // auto "public"."users"
1001
+ new SoftDeletePlugin({ tables: ["users"] }), // auto WHERE deleted_at IS NULL
759
1002
  ],
760
1003
  tables: { ... },
761
1004
  })
762
-
763
- // SELECT * FROM "public"."users" WHERE ("deleted_at" IS NULL)
764
1005
  ```
765
1006
 
1007
+ ---
1008
+
766
1009
  ## Hooks
767
1010
 
768
1011
  ```ts
@@ -771,14 +1014,6 @@ db.hook("query:after", (ctx) => {
771
1014
  console.log(`[SQL] ${ctx.query.sql}`)
772
1015
  })
773
1016
 
774
- // Add request tracing
775
- db.hook("query:after", (ctx) => {
776
- return {
777
- ...ctx.query,
778
- sql: `${ctx.query.sql} /* request_id=${requestId} */`,
779
- }
780
- })
781
-
782
1017
  // Modify AST before compilation
783
1018
  db.hook("select:before", (ctx) => {
784
1019
  // Add tenant isolation, audit filters, etc.
@@ -794,29 +1029,72 @@ const off = db.hook("query:before", handler)
794
1029
  off()
795
1030
  ```
796
1031
 
797
- ## Why sumak?
1032
+ ---
798
1033
 
799
- | | sumak | Drizzle | Kysely |
800
- | ------------------ | ----------------- | --------------- | -------------- |
801
- | **Architecture** | AST-first | Template | AST (98 nodes) |
802
- | **Type inference** | Auto (no codegen) | Auto | Manual DB type |
803
- | **Plugin system** | Hooks + plugins | None | Plugins only |
804
- | **SQL printer** | Wadler algebra | Template concat | String append |
805
- | **Dependencies** | 0 | 0 | 0 |
806
- | **Node types** | ~35 (focused) | Config objects | 98 (complex) |
807
- | **API style** | Callback proxy | Method chain | Method chain |
1034
+ ## Dialects
1035
+
1036
+ 4 dialects supported. Same query, different SQL:
1037
+
1038
+ ```ts
1039
+ // PostgreSQL → SELECT "id" FROM "users" WHERE ("id" = $1)
1040
+ // MySQL → SELECT `id` FROM `users` WHERE (`id` = ?)
1041
+ // SQLite → SELECT "id" FROM "users" WHERE ("id" = ?)
1042
+ // MSSQL → SELECT [id] FROM [users] WHERE ([id] = @p0)
1043
+ ```
1044
+
1045
+ ```ts
1046
+ import { pgDialect } from "sumak/pg"
1047
+ import { mysqlDialect } from "sumak/mysql"
1048
+ import { sqliteDialect } from "sumak/sqlite"
1049
+ import { mssqlDialect } from "sumak/mssql"
1050
+ ```
1051
+
1052
+ ### Tree Shaking
1053
+
1054
+ Import only the dialect you need — unused dialects are eliminated:
1055
+
1056
+ ```ts
1057
+ import { sumak } from "sumak"
1058
+ import { pgDialect } from "sumak/pg"
1059
+ import { serial, text } from "sumak/schema"
1060
+ ```
1061
+
1062
+ ---
808
1063
 
809
1064
  ## Architecture
810
1065
 
811
1066
  ```
812
- Schema → Builder → AST → Plugin/Hook → Printer → SQL
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
813
1079
  ```
814
1080
 
815
- - **Schema Layer** — `defineTable()`, `ColumnType<S,I,U>`, auto type inference
816
- - **Builder Layer** — `Sumak<DB>`, `TypedSelectBuilder<DB,TB,O>`, proxy-based expressions
817
- - **AST Layer** — ~35 frozen node types, discriminated unions, visitor pattern
818
- - **Plugin Layer** — `SumakPlugin` interface, `Hookable` lifecycle hooks
819
- - **Printer Layer** — `BasePrinter` with 4 dialect subclasses (PG, MySQL, SQLite, MSSQL), Wadler document algebra
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 |
1096
+
1097
+ ---
820
1098
 
821
1099
  ## License
822
1100