sumak 0.0.3 → 0.0.5

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 (119) hide show
  1. package/README.md +650 -56
  2. package/dist/ast/expression.d.mts +26 -0
  3. package/dist/ast/expression.mjs +140 -0
  4. package/dist/ast/nodes.d.mts +298 -0
  5. package/dist/ast/nodes.mjs +59 -0
  6. package/dist/ast/transformer.d.mts +10 -0
  7. package/dist/ast/transformer.mjs +140 -0
  8. package/dist/ast/typed-expression.d.mts +37 -0
  9. package/dist/ast/typed-expression.mjs +77 -0
  10. package/dist/ast/visitor.d.mts +13 -0
  11. package/dist/ast/visitor.mjs +11 -0
  12. package/dist/builder/delete.d.mts +18 -0
  13. package/dist/builder/delete.mjs +94 -0
  14. package/dist/builder/eb.d.mts +210 -0
  15. package/dist/builder/eb.mjs +399 -0
  16. package/dist/builder/expression.d.mts +5 -0
  17. package/dist/builder/expression.mjs +10 -0
  18. package/dist/builder/insert.d.mts +40 -0
  19. package/dist/builder/insert.mjs +146 -0
  20. package/dist/builder/merge.d.mts +20 -0
  21. package/dist/builder/merge.mjs +100 -0
  22. package/dist/builder/raw.d.mts +2 -0
  23. package/dist/builder/raw.mjs +4 -0
  24. package/dist/builder/select.d.mts +38 -0
  25. package/dist/builder/select.mjs +242 -0
  26. package/dist/builder/typed-delete.d.mts +43 -0
  27. package/dist/builder/typed-delete.mjs +77 -0
  28. package/dist/builder/typed-insert.d.mts +74 -0
  29. package/dist/builder/typed-insert.mjs +136 -0
  30. package/dist/builder/typed-merge.d.mts +31 -0
  31. package/dist/builder/typed-merge.mjs +93 -0
  32. package/dist/builder/typed-select.d.mts +125 -0
  33. package/dist/builder/typed-select.mjs +217 -0
  34. package/dist/builder/typed-update.d.mts +55 -0
  35. package/dist/builder/typed-update.mjs +102 -0
  36. package/dist/builder/update.d.mts +18 -0
  37. package/dist/builder/update.mjs +102 -0
  38. package/dist/dialect/mssql.d.mts +2 -0
  39. package/dist/dialect/mssql.mjs +9 -0
  40. package/dist/dialect/mysql.d.mts +2 -0
  41. package/dist/dialect/mysql.mjs +9 -0
  42. package/dist/dialect/pg.d.mts +2 -0
  43. package/dist/dialect/pg.mjs +9 -0
  44. package/dist/dialect/sqlite.d.mts +2 -0
  45. package/dist/dialect/sqlite.mjs +9 -0
  46. package/dist/dialect/types.d.mts +6 -0
  47. package/dist/dialect/types.mjs +1 -0
  48. package/dist/errors.d.mts +12 -0
  49. package/dist/errors.mjs +24 -0
  50. package/dist/index.d.mts +49 -660
  51. package/dist/index.mjs +46 -3
  52. package/dist/mssql.d.mts +2 -0
  53. package/dist/mssql.mjs +2 -0
  54. package/dist/mysql.d.mts +2 -2
  55. package/dist/mysql.mjs +2 -1
  56. package/dist/pg.d.mts +2 -2
  57. package/dist/pg.mjs +2 -1
  58. package/dist/plugin/camel-case.d.mts +11 -0
  59. package/dist/plugin/camel-case.mjs +16 -0
  60. package/dist/plugin/hooks.d.mts +72 -0
  61. package/dist/plugin/hooks.mjs +49 -0
  62. package/dist/plugin/plugin-manager.d.mts +17 -0
  63. package/dist/plugin/plugin-manager.mjs +37 -0
  64. package/dist/plugin/soft-delete.d.mts +27 -0
  65. package/dist/plugin/soft-delete.mjs +52 -0
  66. package/dist/plugin/types.d.mts +19 -0
  67. package/dist/plugin/types.mjs +1 -0
  68. package/dist/plugin/with-schema.d.mts +21 -0
  69. package/dist/plugin/with-schema.mjs +53 -0
  70. package/dist/printer/base.d.mts +48 -0
  71. package/dist/printer/base.mjs +450 -0
  72. package/dist/printer/document.d.mts +45 -0
  73. package/dist/printer/document.mjs +153 -0
  74. package/dist/printer/formatter.d.mts +5 -0
  75. package/dist/printer/formatter.mjs +134 -0
  76. package/dist/printer/mssql.d.mts +10 -0
  77. package/dist/printer/mssql.mjs +161 -0
  78. package/dist/printer/mysql.d.mts +8 -0
  79. package/dist/printer/mysql.mjs +41 -0
  80. package/dist/printer/pg.d.mts +6 -0
  81. package/dist/printer/pg.mjs +9 -0
  82. package/dist/printer/sqlite.d.mts +8 -0
  83. package/dist/printer/sqlite.mjs +29 -0
  84. package/dist/printer/types.d.mts +11 -0
  85. package/dist/printer/types.mjs +1 -0
  86. package/dist/schema/column.d.mts +52 -0
  87. package/dist/schema/column.mjs +120 -0
  88. package/dist/schema/index.d.mts +6 -0
  89. package/dist/schema/index.mjs +4 -0
  90. package/dist/schema/table.d.mts +37 -0
  91. package/dist/schema/table.mjs +7 -0
  92. package/dist/schema/type-utils.d.mts +46 -0
  93. package/dist/schema/type-utils.mjs +1 -0
  94. package/dist/schema/types.d.mts +64 -0
  95. package/dist/schema/types.mjs +1 -0
  96. package/dist/schema.d.mts +2 -2
  97. package/dist/schema.mjs +1 -1
  98. package/dist/sqlite.d.mts +2 -2
  99. package/dist/sqlite.mjs +2 -1
  100. package/dist/sumak.d.mts +98 -0
  101. package/dist/sumak.mjs +132 -0
  102. package/dist/types.d.mts +14 -0
  103. package/dist/types.mjs +1 -0
  104. package/dist/utils/identifier.d.mts +3 -0
  105. package/dist/utils/identifier.mjs +14 -0
  106. package/dist/utils/param.d.mts +2 -0
  107. package/dist/utils/param.mjs +8 -0
  108. package/package.json +7 -1
  109. package/dist/_chunks/base.mjs +0 -1
  110. package/dist/_chunks/errors.mjs +0 -1
  111. package/dist/_chunks/index.d.mts +0 -136
  112. package/dist/_chunks/mysql.d.mts +0 -8
  113. package/dist/_chunks/mysql.mjs +0 -1
  114. package/dist/_chunks/pg.d.mts +0 -7
  115. package/dist/_chunks/pg.mjs +0 -1
  116. package/dist/_chunks/schema.mjs +0 -1
  117. package/dist/_chunks/sqlite.d.mts +0 -8
  118. package/dist/_chunks/sqlite.mjs +0 -1
  119. package/dist/_chunks/types.d.mts +0 -274
package/README.md CHANGED
@@ -21,7 +21,7 @@ npm install sumak
21
21
  ```
22
22
 
23
23
  ```ts
24
- import { sumak, pgDialect, serial, text, boolean, integer } from "sumak"
24
+ import { sumak, pgDialect, serial, text, boolean, integer, jsonb } from "sumak"
25
25
 
26
26
  const db = sumak({
27
27
  dialect: pgDialect(),
@@ -30,7 +30,9 @@ const db = sumak({
30
30
  id: serial().primaryKey(),
31
31
  name: text().notNull(),
32
32
  email: text().notNull(),
33
+ age: integer(),
33
34
  active: boolean().defaultTo(true),
35
+ meta: jsonb(),
34
36
  },
35
37
  posts: {
36
38
  id: serial().primaryKey(),
@@ -43,16 +45,20 @@ const db = sumak({
43
45
 
44
46
  ## Query Building
45
47
 
48
+ ### SELECT
49
+
46
50
  ```ts
47
- // SELECT
48
51
  db.selectFrom("users")
49
52
  .select("id", "name")
50
53
  .where(({ age, active }) => and(age.gte(18), active.eq(true)))
51
54
  .orderBy("name")
52
55
  .limit(10)
53
56
  .compile(db.printer())
57
+ ```
58
+
59
+ ### INSERT
54
60
 
55
- // INSERT
61
+ ```ts
56
62
  db.insertInto("users")
57
63
  .values({
58
64
  name: "Alice",
@@ -60,14 +66,20 @@ db.insertInto("users")
60
66
  })
61
67
  .returningAll()
62
68
  .compile(db.printer())
69
+ ```
70
+
71
+ ### UPDATE
63
72
 
64
- // UPDATE
73
+ ```ts
65
74
  db.update("users")
66
75
  .set({ active: false })
67
76
  .where(({ id }) => id.eq(1))
68
77
  .compile(db.printer())
78
+ ```
79
+
80
+ ### DELETE
69
81
 
70
- // DELETE
82
+ ```ts
71
83
  db.deleteFrom("users")
72
84
  .where(({ id }) => id.eq(1))
73
85
  .returning("id")
@@ -77,11 +89,612 @@ db.deleteFrom("users")
77
89
  ## Joins
78
90
 
79
91
  ```ts
92
+ // INNER JOIN
80
93
  db.selectFrom("users")
81
94
  .innerJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
95
+ .select("id", "title")
96
+ .compile(db.printer())
97
+
98
+ // LEFT JOIN — joined columns become nullable
99
+ db.selectFrom("users")
100
+ .leftJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
101
+ .compile(db.printer())
102
+
103
+ // RIGHT JOIN
104
+ db.selectFrom("users")
105
+ .rightJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
106
+ .compile(db.printer())
107
+
108
+ // FULL JOIN — both sides become nullable
109
+ db.selectFrom("users")
110
+ .fullJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
82
111
  .compile(db.printer())
112
+
113
+ // CROSS JOIN — cartesian product
114
+ db.selectFrom("users").crossJoin("posts").compile(db.printer())
83
115
  ```
84
116
 
117
+ ## Expression API
118
+
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
+ )
133
+
134
+ .where(({ age }) =>
135
+ age.lt(65),
136
+ )
137
+
138
+ .where(({ age }) =>
139
+ age.lte(65),
140
+ )
141
+
142
+ .where(({ active }) =>
143
+ active.neq(false),
144
+ )
145
+ ```
146
+
147
+ ### String Matching
148
+
149
+ ```ts
150
+ .where(({ name }) =>
151
+ name.like("%ali%"),
152
+ )
153
+ ```
154
+
155
+ ### Range & List
156
+
157
+ ```ts
158
+ .where(({ age }) =>
159
+ age.between(18, 65),
160
+ )
161
+
162
+ .where(({ id }) =>
163
+ id.in([1, 2, 3]),
164
+ )
165
+
166
+ .where(({ id }) =>
167
+ id.notIn([99, 100]),
168
+ )
169
+ ```
170
+
171
+ ### Null Checks
172
+
173
+ ```ts
174
+ .where(({ bio }) =>
175
+ bio.isNull(),
176
+ )
177
+
178
+ .where(({ email }) =>
179
+ email.isNotNull(),
180
+ )
181
+ ```
182
+
183
+ ### Logical Combinators
184
+
185
+ ```ts
186
+ // AND
187
+ .where(({ age, active }) =>
188
+ and(
189
+ age.gt(0),
190
+ active.eq(true),
191
+ ),
192
+ )
193
+
194
+ // OR
195
+ .where(({ name, email }) =>
196
+ or(
197
+ name.like("%alice%"),
198
+ email.like("%alice%"),
199
+ ),
200
+ )
201
+
202
+ // NOT
203
+ .where(({ active }) =>
204
+ not(active.eq(true)),
205
+ )
206
+ ```
207
+
208
+ ### Aggregates
209
+
210
+ ```ts
211
+ import { count, countDistinct, sum, avg, min, max, coalesce } from "sumak"
212
+
213
+ db.selectFrom("users").selectExpr(count(), "total").compile(db.printer())
214
+
215
+ db.selectFrom("users").selectExpr(countDistinct(col.dept), "uniqueDepts").compile(db.printer())
216
+ // SELECT COUNT(DISTINCT "dept") AS "uniqueDepts" FROM "users"
217
+
218
+ db.selectFrom("orders").selectExpr(sum(col.amount), "totalAmount").compile(db.printer())
219
+
220
+ db.selectFrom("orders").selectExpr(avg(col.amount), "avgAmount").compile(db.printer())
221
+
222
+ db.selectFrom("orders")
223
+ .selectExpr(coalesce(col.discount, val(0)), "safeDiscount")
224
+ .compile(db.printer())
225
+ ```
226
+
227
+ ### EXISTS / NOT EXISTS
228
+
229
+ ```ts
230
+ import { exists, notExists } from "sumak"
231
+
232
+ db.selectFrom("users")
233
+ .where(() =>
234
+ exists(
235
+ db
236
+ .selectFrom("posts")
237
+ .where(({ userId }) => userId.eq(1))
238
+ .build(),
239
+ ),
240
+ )
241
+ .compile(db.printer())
242
+
243
+ db.selectFrom("users")
244
+ .where(() =>
245
+ notExists(
246
+ db
247
+ .selectFrom("posts")
248
+ .where(({ userId }) => userId.eq(1))
249
+ .build(),
250
+ ),
251
+ )
252
+ .compile(db.printer())
253
+ ```
254
+
255
+ ### CASE Expression
256
+
257
+ ```ts
258
+ import { case_, val } from "sumak"
259
+
260
+ db.selectFrom("users")
261
+ .selectExpr(
262
+ case_()
263
+ .when(col.active.eq(true), val("active"))
264
+ .when(col.active.eq(false), val("inactive"))
265
+ .else_(val("unknown"))
266
+ .end(),
267
+ "status",
268
+ )
269
+ .compile(db.printer())
270
+ ```
271
+
272
+ ### CAST
273
+
274
+ ```ts
275
+ import { cast, val } from "sumak"
276
+
277
+ db.selectFrom("users")
278
+ .selectExpr(cast(val(42), "text"), "idAsText")
279
+ .compile(db.printer())
280
+ ```
281
+
282
+ ### JSON Operations
283
+
284
+ ```ts
285
+ import { jsonRef } from "sumak"
286
+
287
+ // -> (JSON object)
288
+ db.selectFrom("users")
289
+ .selectExpr(jsonRef(col.meta, "address", "->"), "address")
290
+ .compile(db.printer())
291
+
292
+ // ->> (text value)
293
+ db.selectFrom("users")
294
+ .selectExpr(jsonRef(col.meta, "name", "->>"), "metaName")
295
+ .compile(db.printer())
296
+ ```
297
+
298
+ ## Window Functions
299
+
300
+ ```ts
301
+ import { over, rowNumber, rank, denseRank, lag, lead, ntile, count, sum } from "sumak"
302
+
303
+ // ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)
304
+ db.selectFrom("employees")
305
+ .selectExpr(
306
+ over(rowNumber(), (w) => w.partitionBy("dept").orderBy("salary", "DESC")),
307
+ "rn",
308
+ )
309
+ .compile(db.printer())
310
+
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())
318
+
319
+ // 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())
331
+
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())
339
+
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())
347
+ ```
348
+
349
+ ## SQL Functions
350
+
351
+ ### String Functions
352
+
353
+ ```ts
354
+ import { upper, lower, concat, substring, trim, length } from "sumak"
355
+
356
+ db.selectFrom("users").selectExpr(upper(col.name), "upperName").compile(db.printer())
357
+ // SELECT UPPER("name") AS "upperName" FROM "users"
358
+
359
+ db.selectFrom("users").selectExpr(lower(col.email), "lowerEmail").compile(db.printer())
360
+
361
+ db.selectFrom("users")
362
+ .selectExpr(concat(col.firstName, val(" "), col.lastName), "fullName")
363
+ .compile(db.printer())
364
+
365
+ db.selectFrom("users")
366
+ .selectExpr(substring(col.name, 1, 3), "prefix")
367
+ .compile(db.printer())
368
+
369
+ db.selectFrom("users").selectExpr(trim(col.name), "trimmed").compile(db.printer())
370
+
371
+ db.selectFrom("users").selectExpr(length(col.name), "nameLen").compile(db.printer())
372
+ ```
373
+
374
+ ### Numeric Functions
375
+
376
+ ```ts
377
+ import { abs, round, ceil, floor } from "sumak"
378
+
379
+ db.selectFrom("orders").selectExpr(abs(col.balance), "absBalance").compile(db.printer())
380
+
381
+ db.selectFrom("orders").selectExpr(round(col.price, 2), "rounded").compile(db.printer())
382
+
383
+ db.selectFrom("orders").selectExpr(ceil(col.amount), "ceiling").compile(db.printer())
384
+
385
+ db.selectFrom("orders").selectExpr(floor(col.amount), "floored").compile(db.printer())
386
+ ```
387
+
388
+ ### Conditional Functions
389
+
390
+ ```ts
391
+ import { nullif, greatest, least } from "sumak"
392
+
393
+ db.selectFrom("users")
394
+ .selectExpr(nullif(col.age, val(0)), "ageOrNull")
395
+ .compile(db.printer())
396
+
397
+ db.selectFrom("products")
398
+ .selectExpr(greatest(col.price, col.minPrice), "effectivePrice")
399
+ .compile(db.printer())
400
+
401
+ db.selectFrom("products")
402
+ .selectExpr(least(col.price, col.maxPrice), "cappedPrice")
403
+ .compile(db.printer())
404
+ ```
405
+
406
+ ### Date/Time Functions
407
+
408
+ ```ts
409
+ import { now, currentTimestamp } from "sumak"
410
+
411
+ db.selectFrom("users").selectExpr(now(), "currentTime").compile(db.printer())
412
+ ```
413
+
414
+ ## Set Operations
415
+
416
+ ```ts
417
+ const active = db
418
+ .selectFrom("users")
419
+ .select("id")
420
+ .where(({ active }) => active.eq(true))
421
+
422
+ const premium = db
423
+ .selectFrom("users")
424
+ .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())
440
+ ```
441
+
442
+ ## CTEs (WITH)
443
+
444
+ ```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())
464
+
465
+ // DELETE with CTE
466
+ db.deleteFrom("users")
467
+ .with("to_delete", deleteCte)
468
+ .where(({ id }) => id.eq(1))
469
+ .compile(db.printer())
470
+
471
+ // Recursive CTE
472
+ db.selectFrom("users").with("tree", recursiveQuery, true).compile(db.printer())
473
+ ```
474
+
475
+ ## UPDATE FROM
476
+
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
+ ```
485
+
486
+ ## Conditional Query Building
487
+
488
+ ```ts
489
+ const withFilter = true
490
+ const withOrder = false
491
+
492
+ db.selectFrom("users")
493
+ .select("id", "name")
494
+ .$if(withFilter, (qb) => qb.where(({ age }) => age.gt(18)))
495
+ .$if(withOrder, (qb) => qb.orderBy("name"))
496
+ .compile(db.printer())
497
+ // WHERE applied, ORDER BY skipped
498
+
499
+ // Multiple .where() calls are AND'd together
500
+ db.selectFrom("users")
501
+ .select("id")
502
+ .where(({ age }) => age.gt(18))
503
+ .where(({ active }) => active.eq(true))
504
+ .compile(db.printer())
505
+ // WHERE ("age" > $1) AND ("active" = $2)
506
+ ```
507
+
508
+ ## INSERT Advanced
509
+
510
+ ```ts
511
+ // INSERT ... SELECT
512
+ const selectQuery = db.selectFrom("users").select("name", "age").build()
513
+ db.insertInto("archive").fromSelect(selectQuery).compile(db.printer())
514
+
515
+ // INSERT ... DEFAULT VALUES
516
+ db.insertInto("users").defaultValues().compile(db.printer())
517
+
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" ...
521
+
522
+ db.insertInto("users").values({ name: "Alice" }).orReplace().compile(db.printer())
523
+ // INSERT OR REPLACE INTO "users" ...
524
+ ```
525
+
526
+ ## ON CONFLICT
527
+
528
+ ```ts
529
+ // DO NOTHING (by columns)
530
+ db.insertInto("users")
531
+ .values({ name: "Alice", email: "a@b.com" })
532
+ .onConflictDoNothing("email")
533
+ .compile(db.printer())
534
+
535
+ // DO UPDATE (by columns)
536
+ db.insertInto("users")
537
+ .values({ name: "Alice", email: "a@b.com" })
538
+ .onConflictDoUpdate(["email"], [{ column: "name", value: val("Alice") }])
539
+ .compile(db.printer())
540
+
541
+ // DO NOTHING (by constraint name)
542
+ db.insertInto("users")
543
+ .values({ name: "Alice", email: "a@b.com" })
544
+ .onConflictConstraintDoNothing("users_email_key")
545
+ .compile(db.printer())
546
+ // ON CONFLICT ON CONSTRAINT "users_email_key" DO NOTHING
547
+
548
+ // MySQL: ON DUPLICATE KEY UPDATE
549
+ db.insertInto("users")
550
+ .values({ name: "Alice" })
551
+ .onDuplicateKeyUpdate([{ column: "name", value: val("Alice") }])
552
+ .compile(db.printer())
553
+ ```
554
+
555
+ ## MERGE (SQL:2003)
556
+
557
+ ```ts
558
+ db.mergeInto("users", "staging", "s", ({ target, source }) => target.id.eqCol(source.id))
559
+ .whenMatchedThenUpdate({ name: "updated" })
560
+ .whenNotMatchedThenInsert({
561
+ name: "Alice",
562
+ email: "alice@example.com",
563
+ })
564
+ .compile(db.printer())
565
+
566
+ // MERGE with conditional delete
567
+ db.mergeInto("users", "staging", "s", ({ target, source }) => target.id.eqCol(source.id))
568
+ .whenMatchedThenDelete()
569
+ .compile(db.printer())
570
+ ```
571
+
572
+ ## Row Locking
573
+
574
+ ```ts
575
+ // FOR UPDATE
576
+ db.selectFrom("users").select("id").forUpdate().compile(db.printer())
577
+
578
+ // FOR SHARE
579
+ db.selectFrom("users").select("id").forShare().compile(db.printer())
580
+
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())
584
+
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())
588
+ ```
589
+
590
+ ## DISTINCT ON (PG)
591
+
592
+ ```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
600
+ ```
601
+
602
+ ## DELETE USING / JOIN in UPDATE & DELETE
603
+
604
+ ```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())
610
+
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())
616
+
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())
622
+ ```
623
+
624
+ ## Aggregate FILTER (WHERE)
625
+
626
+ ```ts
627
+ import { filter, count, sum } from "sumak"
628
+
629
+ // COUNT(*) FILTER (WHERE active = true)
630
+ db.selectFrom("users").selectExpr(filter(count(), activeExpr), "activeCount").compile(db.printer())
631
+ ```
632
+
633
+ ## EXPLAIN
634
+
635
+ ```ts
636
+ // EXPLAIN
637
+ db.selectFrom("users").select("id").explain().compile(db.printer())
638
+ // EXPLAIN SELECT "id" FROM "users"
639
+
640
+ // EXPLAIN ANALYZE
641
+ db.selectFrom("users").select("id").explain({ analyze: true }).compile(db.printer())
642
+
643
+ // EXPLAIN with format
644
+ db.selectFrom("users").select("id").explain({ format: "JSON" }).compile(db.printer())
645
+ // EXPLAIN (FORMAT JSON) SELECT "id" FROM "users"
646
+ ```
647
+
648
+ ## Full-Text Search
649
+
650
+ Dialect-aware FTS — same API, different SQL per dialect:
651
+
652
+ ```ts
653
+ import { textSearch } from "sumak"
654
+
655
+ // PostgreSQL: to_tsvector("name") @@ to_tsquery('alice')
656
+ db.selectFrom("users")
657
+ .where(({ name }) => textSearch([name.toExpr()], val("alice")))
658
+ .compile(db.printer())
659
+
660
+ // With language config
661
+ db.selectFrom("users")
662
+ .where(({ name }) => textSearch([name.toExpr()], val("alice"), { language: "english" }))
663
+ .compile(db.printer())
664
+
665
+ // MySQL: MATCH(`name`) AGAINST(? IN BOOLEAN MODE)
666
+ // SQLite: ("name" MATCH ?)
667
+ // MSSQL: CONTAINS(([name]), @p0)
668
+ ```
669
+
670
+ ## Temporal Tables (SQL:2011)
671
+
672
+ Query historical data with `FOR SYSTEM_TIME`:
673
+
674
+ ```ts
675
+ // AS OF — point-in-time query
676
+ db.selectFrom("users")
677
+ .forSystemTime({
678
+ kind: "as_of",
679
+ timestamp: lit("2024-01-01"),
680
+ })
681
+ .compile(db.printer())
682
+
683
+ // BETWEEN — time range
684
+ db.selectFrom("users")
685
+ .forSystemTime({
686
+ kind: "between",
687
+ start: lit("2024-01-01"),
688
+ end: lit("2024-12-31"),
689
+ })
690
+ .compile(db.printer())
691
+
692
+ // ALL — full history
693
+ db.selectFrom("users").forSystemTime({ kind: "all" }).compile(db.printer())
694
+ ```
695
+
696
+ Supported modes: `as_of`, `from_to`, `between`, `contained_in`, `all`.
697
+
85
698
  ## Tree Shaking
86
699
 
87
700
  Import only the dialect you need:
@@ -89,6 +702,7 @@ Import only the dialect you need:
89
702
  ```ts
90
703
  import { sumak } from "sumak"
91
704
  import { pgDialect } from "sumak/pg"
705
+ import { mssqlDialect } from "sumak/mssql"
92
706
  import { mysqlDialect } from "sumak/mysql"
93
707
  import { sqliteDialect } from "sumak/sqlite"
94
708
  import { serial, text } from "sumak/schema"
@@ -102,12 +716,40 @@ Same query, different SQL:
102
716
  // PostgreSQL → SELECT "id" FROM "users" WHERE ("id" = $1)
103
717
  // MySQL → SELECT `id` FROM `users` WHERE (`id` = ?)
104
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)
105
743
  ```
106
744
 
107
745
  ## Plugins
108
746
 
109
747
  ```ts
110
- import { WithSchemaPlugin, SoftDeletePlugin, CamelCasePlugin } from "sumak";
748
+ import {
749
+ WithSchemaPlugin,
750
+ SoftDeletePlugin,
751
+ CamelCasePlugin,
752
+ } from "sumak"
111
753
 
112
754
  const db = sumak({
113
755
  dialect: pgDialect(),
@@ -116,7 +758,7 @@ const db = sumak({
116
758
  new SoftDeletePlugin({ tables: ["users"] }),
117
759
  ],
118
760
  tables: { ... },
119
- });
761
+ })
120
762
 
121
763
  // SELECT * FROM "public"."users" WHERE ("deleted_at" IS NULL)
122
764
  ```
@@ -152,54 +794,6 @@ const off = db.hook("query:before", handler)
152
794
  off()
153
795
  ```
154
796
 
155
- ## Expression API
156
-
157
- ```ts
158
- // Equality
159
- .where(({ id }) =>
160
- id.eq(42),
161
- )
162
-
163
- // String matching
164
- .where(({ name }) =>
165
- name.like("%ali%"),
166
- )
167
-
168
- // Range
169
- .where(({ age }) =>
170
- age.between(18, 65),
171
- )
172
-
173
- // List
174
- .where(({ id }) =>
175
- id.in([1, 2, 3]),
176
- )
177
-
178
- // Null checks
179
- .where(({ bio }) =>
180
- bio.isNull(),
181
- )
182
- .where(({ email }) =>
183
- email.isNotNull(),
184
- )
185
-
186
- // AND
187
- .where(({ a, b }) =>
188
- and(
189
- a.gt(0),
190
- b.neq("x"),
191
- ),
192
- )
193
-
194
- // OR
195
- .where(({ a, b }) =>
196
- or(
197
- a.eq(1),
198
- b.eq(2),
199
- ),
200
- )
201
- ```
202
-
203
797
  ## Why sumak?
204
798
 
205
799
  | | sumak | Drizzle | Kysely |
@@ -222,7 +816,7 @@ Schema → Builder → AST → Plugin/Hook → Printer → SQL
222
816
  - **Builder Layer** — `Sumak<DB>`, `TypedSelectBuilder<DB,TB,O>`, proxy-based expressions
223
817
  - **AST Layer** — ~35 frozen node types, discriminated unions, visitor pattern
224
818
  - **Plugin Layer** — `SumakPlugin` interface, `Hookable` lifecycle hooks
225
- - **Printer Layer** — `BasePrinter` with dialect subclasses, Wadler document algebra
819
+ - **Printer Layer** — `BasePrinter` with 4 dialect subclasses (PG, MySQL, SQLite, MSSQL), Wadler document algebra
226
820
 
227
821
  ## License
228
822