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