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.
- package/README.md +657 -533
- package/dist/ast/ddl-nodes.d.mts +153 -0
- package/dist/ast/ddl-nodes.mjs +1 -0
- 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 +1 -0
- package/dist/builder/delete.mjs +17 -0
- package/dist/builder/eb.d.mts +14 -0
- package/dist/builder/eb.mjs +25 -0
- package/dist/builder/select.d.mts +1 -0
- package/dist/builder/select.mjs +17 -0
- package/dist/builder/typed-delete.d.mts +8 -0
- package/dist/builder/typed-delete.mjs +14 -0
- package/dist/builder/typed-insert.d.mts +12 -0
- package/dist/builder/typed-insert.mjs +28 -0
- package/dist/builder/typed-select.d.mts +22 -0
- package/dist/builder/typed-select.mjs +38 -0
- package/dist/builder/typed-update.d.mts +8 -0
- package/dist/builder/typed-update.mjs +14 -0
- package/dist/builder/update.d.mts +1 -0
- package/dist/builder/update.mjs +17 -0
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +10 -2
- 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 +47 -1
- package/dist/sumak.mjs +94 -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,251 +81,304 @@ 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
|
-
|
|
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
|
-
|
|
135
|
-
age.lt(65),
|
|
136
|
-
)
|
|
219
|
+
Every `.where()` takes a callback with typed column proxies.
|
|
137
220
|
|
|
138
|
-
|
|
139
|
-
age.lte(65),
|
|
140
|
-
)
|
|
221
|
+
### Comparisons
|
|
141
222
|
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
###
|
|
232
|
+
### Pattern Matching
|
|
148
233
|
|
|
149
234
|
```ts
|
|
150
|
-
.where(({ name }) =>
|
|
151
|
-
|
|
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 &
|
|
241
|
+
### Range & Lists
|
|
169
242
|
|
|
170
243
|
```ts
|
|
171
|
-
.where(({ age }) =>
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
age.notBetween(18, 65),
|
|
177
|
-
)
|
|
251
|
+
### Null Checks
|
|
178
252
|
|
|
179
|
-
|
|
180
|
-
.where(({
|
|
181
|
-
|
|
182
|
-
|
|
253
|
+
```ts
|
|
254
|
+
.where(({ bio }) => bio.isNull()) // IS NULL
|
|
255
|
+
.where(({ email }) => email.isNotNull()) // IS NOT NULL
|
|
256
|
+
```
|
|
183
257
|
|
|
184
|
-
|
|
185
|
-
id.in([1, 2, 3]),
|
|
186
|
-
)
|
|
258
|
+
### Null-Safe Comparisons
|
|
187
259
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
###
|
|
265
|
+
### IN Subquery
|
|
194
266
|
|
|
195
267
|
```ts
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
268
|
+
const deptIds = db
|
|
269
|
+
.selectFrom("departments")
|
|
270
|
+
.select("id")
|
|
271
|
+
.build()
|
|
199
272
|
|
|
200
|
-
.where(({
|
|
201
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
###
|
|
299
|
+
### Multiple WHERE (implicit AND)
|
|
231
300
|
|
|
232
301
|
```ts
|
|
233
|
-
//
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
###
|
|
311
|
+
### Column-to-Column Comparisons
|
|
245
312
|
|
|
246
313
|
```ts
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
// SELECT COUNT(DISTINCT "dept") AS "uniqueDepts" FROM "users"
|
|
322
|
+
---
|
|
253
323
|
|
|
254
|
-
|
|
324
|
+
## Joins
|
|
255
325
|
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
//
|
|
263
|
-
db.selectFrom("
|
|
264
|
-
|
|
338
|
+
// RIGHT JOIN
|
|
339
|
+
db.selectFrom("users")
|
|
340
|
+
.rightJoin("posts", ({ users, posts }) => users.id.eqCol(posts.userId))
|
|
341
|
+
.compile(db.printer())
|
|
265
342
|
|
|
266
|
-
//
|
|
343
|
+
// FULL JOIN — both sides nullable
|
|
267
344
|
db.selectFrom("users")
|
|
268
|
-
.
|
|
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
|
-
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Expressions
|
|
362
|
+
|
|
363
|
+
### Computed Columns
|
|
273
364
|
|
|
274
365
|
```ts
|
|
275
|
-
import {
|
|
366
|
+
import { val, cast, rawExpr } from "sumak"
|
|
276
367
|
|
|
277
|
-
//
|
|
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
|
-
.
|
|
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(
|
|
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
|
-
###
|
|
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
|
-
###
|
|
415
|
+
### JSON Operations
|
|
353
416
|
|
|
354
417
|
```ts
|
|
355
|
-
import {
|
|
418
|
+
import { jsonRef, jsonAgg, toJson, jsonBuildObject } from "sumak"
|
|
356
419
|
|
|
420
|
+
// Access: -> (JSON object), ->> (text value)
|
|
357
421
|
db.selectFrom("users")
|
|
358
|
-
.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")
|
|
359
431
|
.compile(db.printer())
|
|
360
432
|
```
|
|
361
433
|
|
|
362
|
-
###
|
|
434
|
+
### PostgreSQL Array Operators
|
|
363
435
|
|
|
364
436
|
```ts
|
|
365
|
-
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
|
|
366
447
|
|
|
367
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
//
|
|
413
|
-
|
|
414
|
-
.
|
|
415
|
-
|
|
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
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
528
|
+
### String
|
|
432
529
|
|
|
433
530
|
```ts
|
|
434
531
|
import { upper, lower, concat, substring, trim, length } from "sumak"
|
|
435
532
|
|
|
436
|
-
|
|
437
|
-
//
|
|
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
|
-
|
|
541
|
+
### Numeric
|
|
440
542
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
446
|
-
.selectExpr(substring(col.name, 1, 3), "prefix")
|
|
447
|
-
.compile(db.printer())
|
|
554
|
+
### Conditional
|
|
448
555
|
|
|
449
|
-
|
|
556
|
+
```ts
|
|
557
|
+
import { nullif, coalesce } from "sumak"
|
|
450
558
|
|
|
451
|
-
|
|
559
|
+
nullif(col.age, val(0)) // NULLIF("age", 0)
|
|
560
|
+
coalesce(col.nick, col.name, val("Anonymous")) // COALESCE(...)
|
|
452
561
|
```
|
|
453
562
|
|
|
454
|
-
###
|
|
563
|
+
### Date/Time
|
|
455
564
|
|
|
456
565
|
```ts
|
|
457
|
-
import {
|
|
458
|
-
|
|
459
|
-
db.selectFrom("orders").selectExpr(abs(col.balance), "absBalance").compile(db.printer())
|
|
566
|
+
import { now, currentTimestamp } from "sumak"
|
|
460
567
|
|
|
461
|
-
|
|
568
|
+
now() // NOW()
|
|
569
|
+
currentTimestamp() // CURRENT_TIMESTAMP()
|
|
570
|
+
```
|
|
462
571
|
|
|
463
|
-
|
|
572
|
+
---
|
|
464
573
|
|
|
465
|
-
|
|
466
|
-
```
|
|
574
|
+
## Subqueries
|
|
467
575
|
|
|
468
|
-
###
|
|
576
|
+
### EXISTS / NOT EXISTS
|
|
469
577
|
|
|
470
578
|
```ts
|
|
471
|
-
import {
|
|
579
|
+
import { exists, notExists } from "sumak"
|
|
472
580
|
|
|
473
581
|
db.selectFrom("users")
|
|
474
|
-
.
|
|
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
|
-
|
|
478
|
-
.selectExpr(greatest(col.price, col.minPrice), "effectivePrice")
|
|
479
|
-
.compile(db.printer())
|
|
593
|
+
### Derived Tables (Subquery in FROM)
|
|
480
594
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
.
|
|
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
|
-
###
|
|
605
|
+
### IN Subquery
|
|
487
606
|
|
|
488
607
|
```ts
|
|
489
|
-
|
|
608
|
+
const deptIds = db.selectFrom("departments").select("id").build()
|
|
490
609
|
|
|
491
|
-
db.selectFrom("users")
|
|
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(({
|
|
506
|
-
|
|
507
|
-
// UNION
|
|
508
|
-
active.
|
|
509
|
-
|
|
510
|
-
//
|
|
511
|
-
active.
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
.
|
|
528
|
-
|
|
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
|
-
|
|
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("
|
|
650
|
+
db.selectFrom("categories").with("tree", recursiveQuery, true).compile(db.printer())
|
|
553
651
|
```
|
|
554
652
|
|
|
555
|
-
|
|
653
|
+
---
|
|
556
654
|
|
|
557
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
675
|
+
const onlyActive = (qb) => qb.where(({ active }) => active.eq(true))
|
|
594
676
|
|
|
595
677
|
db.selectFrom("users")
|
|
596
678
|
.select("id", "name")
|
|
597
|
-
.$call(
|
|
679
|
+
.$call(onlyActive)
|
|
598
680
|
.$call(withPagination)
|
|
599
681
|
.compile(db.printer())
|
|
682
|
+
```
|
|
600
683
|
|
|
601
|
-
|
|
684
|
+
### `clear*()` — reset clauses
|
|
685
|
+
|
|
686
|
+
```ts
|
|
602
687
|
db.selectFrom("users")
|
|
603
|
-
.
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
618
|
-
|
|
706
|
+
// Primitives are parameterized
|
|
707
|
+
sql`SELECT * FROM users WHERE name = ${"Alice"}`
|
|
708
|
+
// params: ["Alice"]
|
|
619
709
|
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
//
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## ON CONFLICT / Upsert
|
|
629
743
|
|
|
630
744
|
```ts
|
|
631
|
-
// DO NOTHING
|
|
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 (
|
|
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
|
-
.
|
|
760
|
+
.onConflictDoUpdateSet(["email"], { name: "Alice Updated" })
|
|
641
761
|
.compile(db.printer())
|
|
642
762
|
|
|
643
|
-
//
|
|
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
|
-
//
|
|
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").
|
|
679
|
-
|
|
680
|
-
// FOR SHARE
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
//
|
|
684
|
-
db.selectFrom("users").select("id").
|
|
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
|
-
|
|
807
|
+
---
|
|
693
808
|
|
|
694
|
-
|
|
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
|
-
|
|
708
|
-
|
|
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
|
-
|
|
714
|
-
|
|
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
|
-
|
|
720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
737
|
-
// SELECT * FROM "users" INNER JOIN LATERAL (SELECT ...) AS "rp" ON ...
|
|
824
|
+
## Schema Builder (DDL)
|
|
738
825
|
|
|
739
|
-
|
|
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
|
-
|
|
828
|
+
### CREATE TABLE
|
|
744
829
|
|
|
745
830
|
```ts
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
.
|
|
751
|
-
.
|
|
752
|
-
|
|
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
|
-
|
|
856
|
+
### ALTER TABLE
|
|
756
857
|
|
|
757
858
|
```ts
|
|
758
|
-
|
|
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
|
-
|
|
761
|
-
sql`SELECT * FROM users WHERE name = ${"Alice"}`
|
|
762
|
-
// params: ["Alice"]
|
|
875
|
+
### CREATE INDEX
|
|
763
876
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
769
|
-
sql`SELECT ${sql.ref("id")} FROM ${sql.table("users", "public")}`
|
|
770
|
-
// → SELECT "id" FROM "public"."users"
|
|
901
|
+
### CREATE VIEW
|
|
771
902
|
|
|
772
|
-
|
|
773
|
-
db.
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
909
|
+
### DROP
|
|
779
910
|
|
|
780
911
|
```ts
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
795
|
-
|
|
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
|
-
//
|
|
798
|
-
db.
|
|
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
|
|
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
|
-
|
|
969
|
+
---
|
|
825
970
|
|
|
826
|
-
|
|
971
|
+
## Temporal Tables (SQL:2011)
|
|
827
972
|
|
|
828
973
|
```ts
|
|
829
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
984
|
+
// Full history
|
|
847
985
|
db.selectFrom("users").forSystemTime({ kind: "all" }).compile(db.printer())
|
|
848
986
|
```
|
|
849
987
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
## Tree Shaking
|
|
988
|
+
Modes: `as_of`, `from_to`, `between`, `contained_in`, `all`.
|
|
853
989
|
|
|
854
|
-
|
|
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
|
-
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
## Dialects
|
|
1035
|
+
|
|
1036
|
+
4 dialects supported. Same query, different SQL:
|
|
952
1037
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
- **
|
|
972
|
-
- **
|
|
973
|
-
- **
|
|
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
|
|