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.
- package/README.md +694 -416
- package/dist/ast/ddl-nodes.d.mts +153 -0
- package/dist/ast/ddl-nodes.mjs +1 -0
- package/dist/ast/nodes.d.mts +12 -1
- package/dist/builder/ddl/alter-table.d.mts +25 -0
- package/dist/builder/ddl/alter-table.mjs +146 -0
- package/dist/builder/ddl/create-index.d.mts +19 -0
- package/dist/builder/ddl/create-index.mjs +67 -0
- package/dist/builder/ddl/create-table.d.mts +40 -0
- package/dist/builder/ddl/create-table.mjs +186 -0
- package/dist/builder/ddl/create-view.d.mts +15 -0
- package/dist/builder/ddl/create-view.mjs +57 -0
- package/dist/builder/ddl/drop.d.mts +38 -0
- package/dist/builder/ddl/drop.mjs +133 -0
- package/dist/builder/delete.d.mts +3 -0
- package/dist/builder/delete.mjs +36 -0
- package/dist/builder/eb.d.mts +122 -6
- package/dist/builder/eb.mjs +273 -6
- package/dist/builder/select.d.mts +4 -0
- package/dist/builder/select.mjs +55 -0
- package/dist/builder/typed-delete.d.mts +14 -0
- package/dist/builder/typed-delete.mjs +26 -0
- package/dist/builder/typed-insert.d.mts +25 -1
- package/dist/builder/typed-insert.mjs +51 -0
- package/dist/builder/typed-select.d.mts +65 -4
- package/dist/builder/typed-select.mjs +132 -2
- package/dist/builder/typed-update.d.mts +12 -0
- package/dist/builder/typed-update.mjs +22 -0
- package/dist/builder/update.d.mts +3 -0
- package/dist/builder/update.mjs +36 -0
- package/dist/index.d.mts +12 -3
- package/dist/index.mjs +12 -2
- package/dist/printer/base.d.mts +2 -1
- package/dist/printer/base.mjs +25 -3
- package/dist/printer/ddl.d.mts +21 -0
- package/dist/printer/ddl.mjs +223 -0
- package/dist/schema/column.d.mts +10 -1
- package/dist/schema/column.mjs +33 -2
- package/dist/schema/index.d.mts +1 -1
- package/dist/schema/index.mjs +1 -1
- package/dist/sumak.d.mts +59 -1
- package/dist/sumak.mjs +103 -2
- 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
|
-
|
|
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
|
-
|
|
84
|
+
That's it. `db` now knows every table, column, and type. All queries are fully type-checked.
|
|
47
85
|
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
.
|
|
64
|
-
name: "Alice",
|
|
65
|
-
email: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
db.
|
|
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
|
-
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## DELETE
|
|
90
193
|
|
|
91
194
|
```ts
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
99
|
-
db.
|
|
100
|
-
.
|
|
199
|
+
// RETURNING
|
|
200
|
+
db.deleteFrom("users")
|
|
201
|
+
.where(({ id }) => id.eq(1))
|
|
202
|
+
.returning("id")
|
|
101
203
|
.compile(db.printer())
|
|
102
204
|
|
|
103
|
-
//
|
|
104
|
-
db.
|
|
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
|
-
//
|
|
109
|
-
db.
|
|
110
|
-
.
|
|
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
|
-
|
|
215
|
+
---
|
|
118
216
|
|
|
119
|
-
|
|
217
|
+
## WHERE Conditions
|
|
120
218
|
|
|
121
|
-
|
|
122
|
-
.where(({ id }) =>
|
|
123
|
-
id.eq(42),
|
|
124
|
-
)
|
|
219
|
+
Every `.where()` takes a callback with typed column proxies.
|
|
125
220
|
|
|
126
|
-
|
|
127
|
-
age.gt(18),
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
.where(({ age }) =>
|
|
131
|
-
age.gte(18),
|
|
132
|
-
)
|
|
221
|
+
### Comparisons
|
|
133
222
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
age.lte(65),
|
|
140
|
-
)
|
|
232
|
+
### Pattern Matching
|
|
141
233
|
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
###
|
|
241
|
+
### Range & Lists
|
|
148
242
|
|
|
149
243
|
```ts
|
|
150
|
-
.where(({
|
|
151
|
-
|
|
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
|
-
###
|
|
251
|
+
### Null Checks
|
|
156
252
|
|
|
157
253
|
```ts
|
|
158
|
-
.where(({
|
|
159
|
-
|
|
160
|
-
|
|
254
|
+
.where(({ bio }) => bio.isNull()) // IS NULL
|
|
255
|
+
.where(({ email }) => email.isNotNull()) // IS NOT NULL
|
|
256
|
+
```
|
|
161
257
|
|
|
162
|
-
|
|
163
|
-
id.in([1, 2, 3]),
|
|
164
|
-
)
|
|
258
|
+
### Null-Safe Comparisons
|
|
165
259
|
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
###
|
|
265
|
+
### IN Subquery
|
|
172
266
|
|
|
173
267
|
```ts
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
268
|
+
const deptIds = db
|
|
269
|
+
.selectFrom("departments")
|
|
270
|
+
.select("id")
|
|
271
|
+
.build()
|
|
177
272
|
|
|
178
|
-
.where(({
|
|
179
|
-
|
|
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
|
-
//
|
|
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
|
-
###
|
|
299
|
+
### Multiple WHERE (implicit AND)
|
|
209
300
|
|
|
210
301
|
```ts
|
|
211
|
-
|
|
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
|
-
|
|
311
|
+
### Column-to-Column Comparisons
|
|
214
312
|
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
322
|
+
---
|
|
219
323
|
|
|
220
|
-
|
|
324
|
+
## Joins
|
|
221
325
|
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Expressions
|
|
362
|
+
|
|
363
|
+
### Computed Columns
|
|
228
364
|
|
|
229
365
|
```ts
|
|
230
|
-
import {
|
|
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
|
-
.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
-
###
|
|
415
|
+
### JSON Operations
|
|
273
416
|
|
|
274
417
|
```ts
|
|
275
|
-
import {
|
|
418
|
+
import { jsonRef, jsonAgg, toJson, jsonBuildObject } from "sumak"
|
|
276
419
|
|
|
420
|
+
// Access: -> (JSON object), ->> (text value)
|
|
277
421
|
db.selectFrom("users")
|
|
278
|
-
.selectExpr(
|
|
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
|
-
###
|
|
434
|
+
### PostgreSQL Array Operators
|
|
283
435
|
|
|
284
436
|
```ts
|
|
285
|
-
import {
|
|
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
|
-
//
|
|
456
|
+
// COALESCE (variadic)
|
|
288
457
|
db.selectFrom("users")
|
|
289
|
-
.selectExpr(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
//
|
|
333
|
-
|
|
334
|
-
.
|
|
335
|
-
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
528
|
+
### String
|
|
352
529
|
|
|
353
530
|
```ts
|
|
354
531
|
import { upper, lower, concat, substring, trim, length } from "sumak"
|
|
355
532
|
|
|
356
|
-
|
|
357
|
-
//
|
|
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
|
-
|
|
541
|
+
### Numeric
|
|
360
542
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
.selectExpr(substring(col.name, 1, 3), "prefix")
|
|
367
|
-
.compile(db.printer())
|
|
554
|
+
### Conditional
|
|
368
555
|
|
|
369
|
-
|
|
556
|
+
```ts
|
|
557
|
+
import { nullif, coalesce } from "sumak"
|
|
370
558
|
|
|
371
|
-
|
|
559
|
+
nullif(col.age, val(0)) // NULLIF("age", 0)
|
|
560
|
+
coalesce(col.nick, col.name, val("Anonymous")) // COALESCE(...)
|
|
372
561
|
```
|
|
373
562
|
|
|
374
|
-
###
|
|
563
|
+
### Date/Time
|
|
375
564
|
|
|
376
565
|
```ts
|
|
377
|
-
import {
|
|
566
|
+
import { now, currentTimestamp } from "sumak"
|
|
378
567
|
|
|
379
|
-
|
|
568
|
+
now() // NOW()
|
|
569
|
+
currentTimestamp() // CURRENT_TIMESTAMP()
|
|
570
|
+
```
|
|
380
571
|
|
|
381
|
-
|
|
572
|
+
---
|
|
382
573
|
|
|
383
|
-
|
|
574
|
+
## Subqueries
|
|
384
575
|
|
|
385
|
-
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
### Conditional Functions
|
|
576
|
+
### EXISTS / NOT EXISTS
|
|
389
577
|
|
|
390
578
|
```ts
|
|
391
|
-
import {
|
|
579
|
+
import { exists, notExists } from "sumak"
|
|
392
580
|
|
|
393
581
|
db.selectFrom("users")
|
|
394
|
-
.
|
|
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
|
-
|
|
398
|
-
.selectExpr(greatest(col.price, col.minPrice), "effectivePrice")
|
|
399
|
-
.compile(db.printer())
|
|
593
|
+
### Derived Tables (Subquery in FROM)
|
|
400
594
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
.
|
|
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
|
-
###
|
|
605
|
+
### IN Subquery
|
|
407
606
|
|
|
408
607
|
```ts
|
|
409
|
-
|
|
608
|
+
const deptIds = db.selectFrom("departments").select("id").build()
|
|
410
609
|
|
|
411
|
-
db.selectFrom("users")
|
|
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(({
|
|
426
|
-
|
|
427
|
-
// UNION
|
|
428
|
-
active.
|
|
429
|
-
|
|
430
|
-
//
|
|
431
|
-
active.
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
.
|
|
448
|
-
|
|
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
|
-
|
|
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("
|
|
650
|
+
db.selectFrom("categories").with("tree", recursiveQuery, true).compile(db.printer())
|
|
473
651
|
```
|
|
474
652
|
|
|
475
|
-
|
|
653
|
+
---
|
|
476
654
|
|
|
477
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
503
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
516
|
-
|
|
706
|
+
// Primitives are parameterized
|
|
707
|
+
sql`SELECT * FROM users WHERE name = ${"Alice"}`
|
|
708
|
+
// params: ["Alice"]
|
|
517
709
|
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
//
|
|
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
|
-
|
|
523
|
-
|
|
718
|
+
// In queries
|
|
719
|
+
db.selectFrom("users")
|
|
720
|
+
.selectExpr(sql`CURRENT_DATE`, "today")
|
|
721
|
+
.compile(db.printer())
|
|
524
722
|
```
|
|
525
723
|
|
|
526
|
-
|
|
724
|
+
### `rawExpr()` escape hatch
|
|
527
725
|
|
|
528
726
|
```ts
|
|
529
|
-
|
|
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 (
|
|
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
|
-
.
|
|
760
|
+
.onConflictDoUpdateSet(["email"], { name: "Alice Updated" })
|
|
539
761
|
.compile(db.printer())
|
|
540
762
|
|
|
541
|
-
//
|
|
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
|
-
//
|
|
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").
|
|
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
|
-
|
|
579
|
-
db.selectFrom("users").select("id").forShare().compile(db.printer())
|
|
807
|
+
---
|
|
580
808
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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.
|
|
594
|
-
.
|
|
595
|
-
.
|
|
596
|
-
.
|
|
597
|
-
.
|
|
598
|
-
.
|
|
599
|
-
|
|
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
|
-
|
|
856
|
+
### ALTER TABLE
|
|
603
857
|
|
|
604
858
|
```ts
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
.
|
|
608
|
-
.
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
618
|
-
db.
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
901
|
+
### CREATE VIEW
|
|
625
902
|
|
|
626
903
|
```ts
|
|
627
|
-
|
|
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
|
-
|
|
630
|
-
|
|
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
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
641
|
-
|
|
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
|
-
//
|
|
644
|
-
db.
|
|
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
|
|
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
|
-
|
|
969
|
+
---
|
|
671
970
|
|
|
672
|
-
|
|
971
|
+
## Temporal Tables (SQL:2011)
|
|
673
972
|
|
|
674
973
|
```ts
|
|
675
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
984
|
+
// Full history
|
|
693
985
|
db.selectFrom("users").forSystemTime({ kind: "all" }).compile(db.printer())
|
|
694
986
|
```
|
|
695
987
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
## Tree Shaking
|
|
988
|
+
Modes: `as_of`, `from_to`, `between`, `contained_in`, `all`.
|
|
699
989
|
|
|
700
|
-
|
|
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
|
-
|
|
1032
|
+
---
|
|
798
1033
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
- **
|
|
818
|
-
- **
|
|
819
|
-
- **
|
|
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
|
|