prisma-ts-select 0.0.34 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1343 -362
  3. package/assets/groupBy.gif +0 -0
  4. package/assets/joinUnsafeIgnoreType.gif +0 -0
  5. package/assets/joinUnsafeTypeEnforced.gif +0 -0
  6. package/assets/typesafe-join.gif +0 -0
  7. package/assets/typesafe-join.png +0 -0
  8. package/assets/{whereisNull.gif → whereIsNull.gif} +0 -0
  9. package/assets/whereNotNull.gif +0 -0
  10. package/dist/bin.cjs +1 -1
  11. package/dist/bin.js +1 -1
  12. package/dist/chunk-47KZVQLD.js +283 -0
  13. package/dist/chunk-54D2J5AR.cjs +291 -0
  14. package/dist/extend/dialects/index.d.ts +13 -0
  15. package/dist/extend/dialects/index.js +194 -0
  16. package/dist/extend/dialects/mysql-v6.d.ts +103 -0
  17. package/dist/extend/dialects/mysql-v6.js +157 -0
  18. package/dist/extend/dialects/mysql-v7.d.ts +6 -0
  19. package/dist/extend/dialects/mysql-v7.js +143 -0
  20. package/dist/extend/dialects/mysql.d.ts +93 -0
  21. package/dist/extend/dialects/mysql.js +161 -0
  22. package/dist/extend/dialects/postgresql-v6.d.ts +101 -0
  23. package/dist/extend/dialects/postgresql-v6.js +147 -0
  24. package/dist/extend/dialects/postgresql-v7.d.ts +101 -0
  25. package/dist/extend/dialects/postgresql-v7.js +147 -0
  26. package/dist/extend/dialects/postgresql.d.ts +92 -0
  27. package/dist/extend/dialects/postgresql.js +158 -0
  28. package/dist/extend/dialects/shared.d.ts +10 -0
  29. package/dist/extend/dialects/shared.js +14 -0
  30. package/dist/extend/dialects/sqlite.d.ts +68 -0
  31. package/dist/extend/dialects/sqlite.js +146 -0
  32. package/dist/extend/dialects/types.d.ts +13 -0
  33. package/dist/extend/dialects/types.js +4 -0
  34. package/dist/extend/extend.d.ts +292 -46
  35. package/dist/extend/extend.js +769 -162
  36. package/dist/extend/sql-expr-BaKWzJ-r.d.ts +10 -0
  37. package/dist/extend/types-B0F8m0ok.d.ts +8 -0
  38. package/dist/generator.cjs +1 -1
  39. package/dist/generator.js +1 -1
  40. package/package.json +44 -42
  41. package/built/extend.cjs +0 -680
  42. package/built/extend.d.cts +0 -520
  43. package/built/extend.d.ts +0 -520
  44. package/built/extend.js +0 -678
  45. package/dist/chunk-TBO3MX7Q.cjs +0 -195
  46. package/dist/chunk-X3N5N5KQ.js +0 -187
  47. package/dist/extend/extend.cjs +0 -357
  48. package/dist/extend/extend.d.cts +0 -264
package/README.md CHANGED
@@ -4,10 +4,21 @@
4
4
  ![build](https://github.com/adrianbrowning/prisma-ts-select/actions/workflows/CI.yml/badge.svg)
5
5
  ![license](https://img.shields.io/github/license/adrianbrowning/prisma-ts-select)
6
6
 
7
+ ## Test Matrix
8
+
9
+ | | SQLite | MySQL | PostgreSQL |
10
+ |----------------|--------|-------|------------|
11
+ | **Prisma v6** | ![v6-sqlite](https://github.com/adrianbrowning/prisma-ts-select/actions/workflows/test-v6-sqlite.yml/badge.svg) | ![v6-mysql](https://github.com/adrianbrowning/prisma-ts-select/actions/workflows/test-v6-mysql.yml/badge.svg) | ![v6-postgresql](https://github.com/adrianbrowning/prisma-ts-select/actions/workflows/test-v6-pg.yml/badge.svg) |
12
+ | **Prisma v7** | ![v7-sqlite](https://github.com/adrianbrowning/prisma-ts-select/actions/workflows/test-v7-sqlite.yml/badge.svg) | ![v7-mysql](https://github.com/adrianbrowning/prisma-ts-select/actions/workflows/test-v7-mysql.yml/badge.svg) | ![v7-postgresql](https://github.com/adrianbrowning/prisma-ts-select/actions/workflows/test-v7-pg.yml/badge.svg) |
13
+
7
14
  <!-- toc -->
8
15
 
9
16
  * [Summary](#summary)
10
17
  * [Installation](#installation)
18
+ * [Setup](#setup)
19
+ + [Schema](#schema)
20
+ + [Client — Prisma v6](#client--prisma-v6)
21
+ + [Client — Prisma v7](#client--prisma-v7)
11
22
  * [Supported DBs](#supported-dbs)
12
23
  * [Usage](#usage)
13
24
  + [Generator](#generator)
@@ -18,6 +29,13 @@
18
29
  * [SQL](#sql)
19
30
  - [Example - Inline Alias Syntax](#example---inline-alias-syntax)
20
31
  * [SQL](#sql-1)
32
+ + [`.$with`](#with)
33
+ - [Example — CTE as joined table](#example--cte-as-joined-table)
34
+ * [SQL](#sql-2)
35
+ - [Example — CTE as base table](#example--cte-as-base-table)
36
+ * [SQL](#sql-3)
37
+ - [Example — Multiple CTEs](#example--multiple-ctes)
38
+ * [SQL](#sql-4)
21
39
  + [Table Aliases](#table-aliases)
22
40
  - [Table Alias Syntax Options](#table-alias-syntax-options)
23
41
  - [Basic Table Alias](#basic-table-alias)
@@ -32,6 +50,8 @@
32
50
  * [SQL](#sql-5)
33
51
  * [SQL](#sql-6)
34
52
  + [Joins](#joins)
53
+ - [Dialect Support](#dialect-support)
54
+ - [Nullability Semantics](#nullability-semantics)
35
55
  - [`.join`](#join)
36
56
  * [Example](#example-1)
37
57
  * [SQL](#sql-7)
@@ -44,6 +64,15 @@
44
64
  * [Example](#example-3)
45
65
  * [SQL](#sql-9)
46
66
  * [Parameters](#parameters-2)
67
+ - [`.innerJoin`](#innerjoin)
68
+ - [`.leftJoin`](#leftjoin)
69
+ - [`.crossJoin`](#crossjoin)
70
+ - [`.rightJoin`](#rightjoin) *(MySQL / PostgreSQL)*
71
+ - [`.fullJoin`](#fulljoin) *(PostgreSQL only)*
72
+ - [`.manyToManyJoin`](#manytomanyjoin)
73
+ * [Example](#example-4)
74
+ * [SQL](#sql-10)
75
+ * [Parameters](#parameters-3)
47
76
  + [Where](#where)
48
77
  - [`.where`](#where)
49
78
  * [TypeSyntax](#typesyntax)
@@ -75,6 +104,7 @@
75
104
  * [SQL](#sql-15)
76
105
  - [Example - Join table](#example---join-table)
77
106
  * [SQL](#sql-16)
107
+ - [`.selectAllOmit`](#selectallomit)
78
108
  - [`.select`](#select)
79
109
  - [Example - `*`](#example---)
80
110
  * [SQL](#sql-17)
@@ -102,15 +132,14 @@
102
132
  + [Offset](#offset)
103
133
  - [Example](#example-12)
104
134
  * [SQL](#sql-27)
135
+ * [Select Functions](#select-functions)
136
+ + [Shared (all dialects)](#shared-all-dialects)
137
+ + [MySQL-specific](#mysql-specific)
138
+ + [PostgreSQL-specific](#postgresql-specific)
139
+ + [SQLite-specific](#sqlite-specific)
105
140
  * [Future updates](#future-updates)
106
141
  * [Changelog / Versioning](#changelog--versioning)
107
142
  * [License](#license)
108
- - [prisma-ts-select](#prisma-ts-select)
109
- * [Install](#install)
110
- * [Setup](#setup)
111
- + [Extract](#extract)
112
- * [Usage](#usage-1)
113
-
114
143
  <!-- tocstop -->
115
144
 
116
145
  ## Summary
@@ -119,13 +148,9 @@
119
148
  It simplifies the selection of fields in Prisma queries, ensuring type safety and reducing boilerplate when working with nested fields.
120
149
  Ideal for developers seeking an efficient, type-safe way to select data with Prisma in TypeScript.
121
150
 
122
- [!NOTE]
123
- > This has been built mostly around MySQL. Most methods should work across the board.<br/>
124
- > Known exceptions include:
125
- > - HAVING
126
- > - SQLite
127
- > - Requires you to have either an aggregate function in the `SELECT` or make use of `GROUP BY`
128
- > - Can only use columns that are specified in `SELECT` or `GROUP BY`
151
+ > [!NOTE]
152
+ > Fully tested on SQLite, MySQL, and PostgreSQL. Known exceptions:
153
+ > - HAVING on SQLite requires either an aggregate function in `SELECT` or a `GROUP BY` clause, and can only reference columns from `SELECT` or `GROUP BY`.
129
154
 
130
155
 
131
156
  ## Installation
@@ -137,15 +162,149 @@ npm install prisma-ts-select
137
162
  pnpm add prisma-ts-select
138
163
  ```
139
164
 
165
+ ## Setup
166
+
167
+ ### Schema
168
+
169
+ Add both generators to `prisma/schema.prisma`. The `output` path is relative to the schema file.
170
+
171
+ ```prisma
172
+ generator prisma-ts-select {
173
+ provider = "prisma-ts-select"
174
+ output = "../generated/prisma-ts-select"
175
+ }
176
+ ```
177
+
178
+ #### Options
179
+
180
+ | Option | Default | Description |
181
+ |--------|---------|-------------|
182
+ | `output` | *(required)* | Output directory, relative to schema file |
183
+ | `packageName` | `prisma-ts-select-<hash>` | Package name in the generated `package.json`. Defaults to a stable hash of the output path — set this when referencing the generated output as a workspace package. |
184
+
185
+ Example with `packageName`:
186
+
187
+ ```prisma
188
+ generator prisma-ts-select {
189
+ provider = "prisma-ts-select"
190
+ output = "../generated/prisma-ts-select"
191
+ packageName = "my-app-prisma-types"
192
+ }
193
+ ```
194
+
195
+ Then register as a workspace package and import by name:
196
+
197
+ **npm / yarn** — `package.json`:
198
+ ```json
199
+ {
200
+ "workspaces": ["generated/prisma-ts-select"]
201
+ }
202
+ ```
203
+
204
+ **pnpm** — `pnpm-workspace.yaml`:
205
+ ```yaml
206
+ packages:
207
+ - generated/prisma-ts-select
208
+ ```
209
+
210
+ Then add it as a dependency:
211
+ ```shell
212
+ pnpm add my-app-prisma-types
213
+ ```
214
+
215
+ ```typescript
216
+ import tsSelectExtend from 'my-app-prisma-types/extend-v7.js'
217
+ ```
218
+
219
+ The client generator differs between Prisma versions:
220
+
221
+ **Prisma v6**
222
+ ```prisma
223
+ generator client {
224
+ provider = "prisma-client-js"
225
+ output = "../generated/prisma"
226
+ }
227
+ ```
228
+
229
+ **Prisma v7**
230
+ ```prisma
231
+ generator client {
232
+ provider = "prisma-client"
233
+ output = "../generated/prisma"
234
+ }
235
+ ```
236
+
237
+ Then generate:
238
+
239
+ ```shell
240
+ pnpm exec prisma generate
241
+ ```
242
+
243
+ ### Client — Prisma v6
244
+
245
+ No driver adapter needed. Import from the generated `extend-v6.js`:
246
+
247
+ ```typescript
248
+ import { PrismaClient } from './generated/prisma/index.js'
249
+ import tsSelectExtend from './generated/prisma-ts-select/extend-v6.js'
250
+
251
+ export const prisma = new PrismaClient().$extends(tsSelectExtend)
252
+ ```
253
+
254
+ ### Client — Prisma v7
255
+
256
+ Prisma v7 requires a driver adapter. Install the adapter for your database and import from the generated `extend-v7.js`:
257
+
258
+ **SQLite**
259
+ ```shell
260
+ pnpm add @prisma/adapter-better-sqlite3
261
+ ```
262
+ ```typescript
263
+ import { PrismaClient } from './generated/prisma/client.ts'
264
+ import tsSelectExtend from './generated/prisma-ts-select/extend-v7.js'
265
+ import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
266
+
267
+ const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL })
268
+ export const prisma = new PrismaClient({ adapter }).$extends(tsSelectExtend)
269
+ ```
270
+
271
+ **MySQL**
272
+ ```shell
273
+ pnpm add @prisma/adapter-mariadb
274
+ ```
275
+ ```typescript
276
+ import { PrismaClient } from './generated/prisma/client.ts'
277
+ import tsSelectExtend from './generated/prisma-ts-select/extend-v7.js'
278
+ import { PrismaMariaDb } from '@prisma/adapter-mariadb'
279
+
280
+ const url = new URL(process.env.DATABASE_URL!)
281
+ const adapter = new PrismaMariaDb({
282
+ host: url.hostname, port: +url.port,
283
+ user: url.username, password: url.password,
284
+ database: url.pathname.slice(1),
285
+ })
286
+ export const prisma = new PrismaClient({ adapter }).$extends(tsSelectExtend)
287
+ ```
288
+
289
+ **PostgreSQL**
290
+ ```shell
291
+ pnpm add @prisma/adapter-pg
292
+ ```
293
+ ```typescript
294
+ import { PrismaClient } from './generated/prisma/client.ts'
295
+ import tsSelectExtend from './generated/prisma-ts-select/extend-v7.js'
296
+ import { PrismaPg } from '@prisma/adapter-pg'
297
+
298
+ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
299
+ export const prisma = new PrismaClient({ adapter }).$extends(tsSelectExtend)
300
+ ```
301
+
140
302
  ## Supported DBs
141
303
 
142
- I have tested this currently on the following databases.
304
+ Fully tested on:
143
305
 
144
306
  - SQLite
145
307
  - MySQL
146
-
147
- Most items should also work for
148
-
149
308
  - PostgreSQL
150
309
 
151
310
  Other DBs will be added when I have chance.
@@ -207,31 +366,101 @@ The way the methods are chained, are heavily inspired by [Dr Milan Milanović](h
207
366
  This takes the `base` table to work from.
208
367
 
209
368
  #### Example
210
- ```typescript
211
- prisma.$from("User")
369
+ ```typescript file=../usage-sqlite-v7/tests/readme/from-basic.ts region=example-$from
370
+ prisma.$from("User");
212
371
  ```
213
372
 
214
373
  #### Example - With Table Alias
215
- ```typescript
216
- prisma.$from("User", "u")
374
+ ```typescript file=../usage-sqlite-v7/tests/readme/from-inline-alias.ts region=example-$from
375
+ prisma.$from("User u");
376
+ ```
377
+ ##### SQL
378
+ ```sql file=../usage-sqlite-v7/tests/readme/from-inline-alias.ts region=inline-alias-sql
379
+ FROM User AS `u`;
380
+ ```
381
+ **Note:** Alias can be inline (space-separated) or as second parameter.
382
+ **Note:** Table aliases are particularly useful for self-joins where you need to join a table to itself with different aliases.
383
+
384
+
385
+ ### `.$with`
386
+
387
+ Defines one or more Common Table Expressions (CTEs) that can be referenced in `.join()` calls or used directly as the base table via `.from('cteName')`.
388
+
389
+ | Param | Description |
390
+ |-------|-------------|
391
+ | `name` | CTE name — used to reference the CTE in `join()` or `from()` |
392
+ | `query` | Any query built with `.$from()` |
393
+
394
+ Chain `.with(name, query)` before `.from()` to define additional CTEs.
395
+
396
+ #### Example — CTE as joined table
397
+
398
+ ```typescript file=../../shared-tests/readme/with-cte.ts region=join
399
+ const posts = prisma.$from("Post").select("id").select("authorId").select("title");
400
+
401
+ prisma.$with("pp", posts)
402
+ .from("User")
403
+ .join("pp", "authorId", "User.id")
217
404
  ```
218
405
 
219
406
  ##### SQL
220
- ```sql
221
- FROM User AS u
407
+
408
+ ```sql file=../../shared-tests/readme/with-cte.ts region=join-sql
409
+ WITH pp AS (
410
+ SELECT id, authorId, title
411
+ FROM Post)
412
+ FROM User
413
+ JOIN pp ON pp.authorId = User.id;
222
414
  ```
223
415
 
224
- **Note:** Table aliases are particularly useful for self-joins where you need to join a table to itself with different aliases.
416
+ #### Example CTE as base table
225
417
 
226
- #### Example - Inline Alias Syntax
227
- ```typescript
228
- prisma.$from("User u")
418
+ Use `.from('cteName')` to query a CTE directly, without a real table as the base.
419
+
420
+ ```typescript file=../../shared-tests/readme/with-cte.ts region=cte-base
421
+ const posts = prisma.$from("Post").select("id").select("title");
422
+
423
+ prisma.$with("pp", posts)
424
+ .from("pp")
425
+ .select("pp.id")
426
+ .select("pp.title")
229
427
  ```
428
+
230
429
  ##### SQL
231
- ```sql
232
- FROM User AS u
430
+
431
+ ```sql file=../../shared-tests/readme/with-cte.ts region=cte-base-sql
432
+ WITH pp AS (
433
+ SELECT id, title
434
+ FROM Post)
435
+ SELECT pp.id AS `pp.id`, pp.title AS `pp.title`
436
+ FROM pp;
437
+ ```
438
+
439
+ **Type safety:** only CTEs declared in `.$with()` / `.with()` are accepted by `.from()`. Unknown CTE names are rejected at compile time.
440
+
441
+ #### Example — Multiple CTEs
442
+
443
+ ```typescript file=../../shared-tests/readme/with-cte.ts region=multi-cte
444
+ const posts = prisma.$from("Post").select("id").select("authorId").select("title");
445
+ const users = prisma.$from("User").select("id").select("name");
446
+
447
+ prisma.$with("pp", posts)
448
+ .with("uu", users)
449
+ .from("User")
450
+ .join("pp", "authorId", "User.id")
451
+ ```
452
+
453
+ ##### SQL
454
+
455
+ ```sql file=../../shared-tests/readme/with-cte.ts region=multi-cte-sql
456
+ WITH pp AS (
457
+ SELECT id, authorId, title
458
+ FROM Post), uu AS (
459
+ SELECT id, name
460
+ FROM User)
461
+ FROM User
462
+ JOIN pp ON pp.authorId = User.id;
233
463
  ```
234
- **Note:** Alias can be inline (space-separated) or as second parameter.
235
464
 
236
465
  ### Table Aliases
237
466
 
@@ -243,49 +472,33 @@ Table aliases allow you to give tables shorter or more meaningful names in your
243
472
  #### Table Alias Syntax Options
244
473
 
245
474
  Multiple syntaxes supported:
246
- - **Inline in .$from()**: `prisma.$from("User u")``
475
+ - **Inline in .$from()**: `prisma.$from("User u")` - Note: Second parameter syntax `.$from("User", "u")` is NOT supported
247
476
  - **Inline in .join()**: `.join("Post p", "authorId", "User.id")`
248
477
  - **Object syntax**: `.join({table: "Post", src: "authorId", on: "User.id", alias: "p"})`
249
478
 
250
- #### Basic Table Alias
251
-
252
- ```typescript
253
- prisma.$from("User", "u")
254
- .select("u.name")
255
- .select("u.email")
256
- .run();
257
- ```
258
-
259
- ##### SQL
260
- ```sql
261
- SELECT name, email FROM User AS u;
262
- ```
263
-
264
479
  #### Table Aliases with Joins
265
480
 
266
481
  ##### Inline Alias Syntax
267
- ```typescript
482
+ ```typescript file=../usage-sqlite-v7/tests/readme/table-alias.ts region=inline-join
268
483
  prisma.$from("User u")
269
- .join("Post p", "authorId", "u.id")
270
- .select("u.name")
271
- .select("p.title")
272
- .run();
484
+ .join("Post p", "authorId", "u.id")
485
+ .select("u.name")
486
+ .select("p.title");
273
487
  ```
274
488
 
275
489
  ##### Object Syntax
276
- ```typescript
277
- prisma.$from("User", "u")
278
- .join({table: "Post", src: "authorId", on: "u.id", alias: "p"})
279
- .select("u.name")
280
- .select("p.title")
281
- .run();
490
+ ```typescript file=../usage-sqlite-v7/tests/readme/table-alias.ts region=object-join-1
491
+ prisma.$from("User u")
492
+ .join({table: "Post", src: "authorId", on: "u.id", alias: "p"})
493
+ .select("u.name")
494
+ .select("p.title");
282
495
  ```
283
496
 
284
497
  ##### SQL
285
- ```sql
286
- SELECT name, title
287
- FROM User AS u
288
- JOIN Post AS p ON authorId = u.id;
498
+ ```sql file=../usage-sqlite-v7/tests/readme/table-alias.ts region=inline-join-sql
499
+ SELECT name, title
500
+ FROM User AS `u`
501
+ JOIN Post AS `p` ON p.authorId = u.id;
289
502
  ```
290
503
 
291
504
  **Note:** The object syntax provides a foundation for future enhancements like multiple join conditions and complex WHERE-style conditions in joins.
@@ -294,60 +507,77 @@ JOIN Post AS p ON authorId = u.id;
294
507
 
295
508
  Self-joins require aliases to distinguish between the different "instances" of the same table:
296
509
 
297
- ```typescript
298
- prisma.$from("User", "u1")
299
- .joinUnsafeTypeEnforced("User", "id", "u1.id", "u2")
300
- .select("u1.name", "user1Name")
301
- .select("u2.name", "user2Name")
302
- .run();
510
+ ```typescript file=../usage-sqlite-v7/tests/readme/table-alias.ts region=self-join
511
+ prisma.$from("User u1")
512
+ .joinUnsafeTypeEnforced("User u2", "id", "u1.id")
513
+ .select("u1.name", "user1Name")
514
+ .select("u2.name", "user2Name");
303
515
  ```
304
516
 
305
517
  ##### SQL
306
- ```sql
307
- SELECT u1.name AS `user1Name`, u2.name AS `user2Name`
308
- FROM User AS u1
309
- JOIN User AS u2 ON User.id = u1.id;
518
+ ```sql file=../usage-sqlite-v7/tests/readme/table-alias.ts region=self-join-sql
519
+ SELECT u1.name AS `user1Name`, u2.name AS `user2Name`
520
+ FROM User AS `u1`
521
+ JOIN User AS `u2` ON u2.id = u1.id;
310
522
  ```
311
523
 
312
524
  #### Table.* with Aliases
313
525
 
314
526
  You can use the `alias.*` syntax to select all columns from an aliased table:
315
527
 
316
- ```typescript
317
- prisma.$from("User", "u")
318
- .select("u.*")
319
- .run();
528
+ ```typescript file=../usage-sqlite-v7/tests/readme/table-alias.ts region=star-single
529
+ prisma.$from("User u")
530
+ .select("u.*");
320
531
  ```
321
532
 
322
533
  ##### SQL
323
- ```sql
324
- SELECT u.id, u.email, u.name FROM User AS u;
534
+ ```sql file=../usage-sqlite-v7/tests/readme/table-alias.ts region=star-single-sql
535
+ SELECT id, email, name, age
536
+ FROM User AS `u`;
325
537
  ```
326
538
 
327
539
  With joins:
328
- ```typescript
329
- prisma.$from("User", "u")
330
- .join("Post", "authorId", "u.id", "p")
331
- .select("u.*")
332
- .select("p.*")
333
- .run();
540
+ ```typescript file=../usage-sqlite-v7/tests/readme/table-alias.ts region=star-join
541
+ prisma.$from("User u")
542
+ .join("Post p", "authorId", "u.id")
543
+ .select("u.*")
544
+ .select("p.*");
334
545
  ```
335
546
 
336
547
  ##### SQL
337
- ```sql
338
- SELECT u.id AS `u.id`, u.email AS `u.email`, u.name AS `u.name`,
339
- p.id AS `p.id`, p.title AS `p.title`, p.content AS `p.content`
340
- FROM User AS u
341
- JOIN Post AS p ON authorId = u.id;
548
+ ```sql file=../usage-sqlite-v7/tests/readme/table-alias.ts region=star-join-sql
549
+ SELECT u.id AS `u.id`, u.email AS `u.email`, u.name AS `u.name`, u.age AS `u.age`, p.id AS `p.id`, p.title AS `p.title`, p.content AS `p.content`, p.published AS `p.published`, p.createdAt AS `p.createdAt`, p.authorId AS `p.authorId`, p.lastModifiedById AS `p.lastModifiedById`, p.metadata AS `p.metadata`
550
+ FROM User AS `u`
551
+ JOIN Post AS `p` ON p.authorId = u.id;
342
552
  ```
343
553
 
344
554
  ### Joins
555
+
556
+ #### Dialect Support
557
+
558
+ | Method | SQLite | MySQL | PostgreSQL |
559
+ |--------|--------|-------|------------|
560
+ | `join` / `innerJoin` / `crossJoin` / `leftJoin` | ✓ | ✓ | ✓ |
561
+ | `rightJoin` | ✗ | ✓ | ✓ |
562
+ | `fullJoin` | ✗ | ✗ | ✓ |
563
+
564
+ Each method has `*UnsafeTypeEnforced` and `*UnsafeIgnoreType` variants with the same dialect restrictions.
565
+
566
+ #### Nullability Semantics
567
+
568
+ | Join type | Effect on result type |
569
+ |-----------|----------------------|
570
+ | `join` / `innerJoin` / `crossJoin` | No nullable change — both sides guaranteed to match |
571
+ | `leftJoin` | Joined table fields become `T \| null` |
572
+ | `rightJoin` | Base table fields become `T \| null` |
573
+ | `fullJoin` | Both sides become `T \| null` |
574
+
345
575
  #### `.join`
346
576
 
347
577
  Using the defined links (foreign keys) defined in the schema, provides a type-safe way of joining on tables.
348
578
 
349
579
  ##### Example
350
- ```typescript
580
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-basic.ts region=example
351
581
  prisma.$from("User")
352
582
  .join("Post", "authorId", "User.id");
353
583
  ```
@@ -358,9 +588,9 @@ prisma.$from("User")
358
588
 
359
589
  The resulting SQL will look like:
360
590
 
361
- ```sql
362
- FROM User
363
- JOIN Post ON authorId = User.id;
591
+ ```sql file=../usage-sqlite-v7/tests/readme/join-basic.ts region=join-basic-sql
592
+ FROM User
593
+ JOIN Post ON Post.authorId = User.id;
364
594
  ```
365
595
 
366
596
  ##### Parameters
@@ -369,27 +599,107 @@ JOIN Post ON authorId = User.id;
369
599
  | `table` | The table to join on (supports inline alias: `"Post p"` or `"Post", "p"`). <br/>TS autocomplete will show tables that can join with previously defined tables on. |
370
600
  | `field` | Column on table. <br/>TS autocomplete will show known columns that this table, can join with previously defined tables on. |
371
601
  | `reference` | `Table.Column` to a previously defined table (either the base, or another join), with a FK that is defined in the schema definition. |
602
+ | `where` | *(optional)* Criteria added to the `ON` clause (`ON a = b AND ...`). Same syntax as `.where()`. Keys scoped to the joined table only. |
603
+ | `joinType` | *(optional)* Join variant. One of `"INNER"`, `"LEFT"`, `"LEFT OUTER"`, `"RIGHT"`, `"RIGHT OUTER"`, `"FULL"`, `"FULL OUTER"`, `"CROSS"`. Default: plain `JOIN`. |
372
604
 
373
605
  **Alternative Syntaxes:**
374
606
  ```typescript
375
607
  // Inline alias
376
608
  .join("Post p", "authorId", "User.id")
377
-
609
+
378
610
  // Object syntax
379
611
  .join({
380
612
  table: "Post",
381
613
  src: "authorId",
382
614
  on: "User.id",
383
- alias: "p" // optional
615
+ alias: "p", // optional
616
+ joinType: "LEFT", // optional
617
+ where: { "Post.published": true } // optional
384
618
  })
385
- ```
619
+ ```
620
+
621
+ ##### Join Type
622
+
623
+ Control the SQL join variant via the `joinType` option:
624
+
625
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-type.ts region=join-type-left
626
+ prisma.$from("User")
627
+ .join("Post", "authorId", "User.id", { joinType: "LEFT" })
628
+ ```
629
+
630
+ ```sql file=../usage-sqlite-v7/tests/readme/join-type.ts region=join-type-left-sql
631
+ FROM User
632
+ LEFT JOIN Post ON Post.authorId = User.id;
633
+ ```
634
+
635
+ `CROSS JOIN` has no `ON` clause — it is suppressed automatically:
636
+
637
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-type.ts region=join-type-cross
638
+ prisma.$from("User")
639
+ .joinUnsafeIgnoreType("Post", "id", "User.id", { joinType: "CROSS" })
640
+ ```
641
+
642
+ ```sql file=../usage-sqlite-v7/tests/readme/join-type.ts region=join-type-cross-sql
643
+ FROM User
644
+ CROSS JOIN Post;
645
+ ```
646
+
647
+ `joinType` and `where` can be combined — `where` is ignored for `CROSS`:
648
+
649
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-type.ts region=join-type-with-where
650
+ prisma.$from("User")
651
+ .join("Post", "authorId", "User.id", {
652
+ joinType: "LEFT",
653
+ where: { "Post.published": true }
654
+ })
655
+ ```
656
+
657
+ ```sql file=../usage-sqlite-v7/tests/readme/join-type.ts region=join-type-with-where-sql
658
+ FROM User
659
+ LEFT JOIN Post ON Post.authorId = User.id AND Post.published = true;
660
+ ```
661
+
662
+ ##### Join-level WHERE
663
+
664
+ Conditions placed on the `ON` clause instead of the top-level `WHERE`:
665
+
666
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-where.ts region=join-where-example
667
+ prisma.$from("User")
668
+ .join("Post", "authorId", "User.id", { where: { "Post.published": true } })
669
+ ```
670
+
671
+ ```sql file=../usage-sqlite-v7/tests/readme/join-where.ts region=join-where-sql
672
+ FROM User
673
+ JOIN Post ON Post.authorId = User.id AND Post.published = true;
674
+ ```
675
+
676
+ Supports the same MongoDB-inspired operators as `.where()` — `$AND`, `$OR`, `$NOT`, `$NOR`:
677
+
678
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-where.ts region=join-where-ops-example
679
+ prisma.$from("User")
680
+ .join("Post", "authorId", "User.id", {
681
+ where: {
682
+ $AND: [
683
+ { "Post.published": true },
684
+ { "Post.id": { op: ">", value: 0 } }
685
+ ]
686
+ }
687
+ })
688
+ ```
689
+
690
+ ```sql file=../usage-sqlite-v7/tests/readme/join-where.ts region=join-where-ops-sql
691
+ FROM User
692
+ JOIN Post ON Post.authorId = User.id AND (Post.published = true AND Post.id > 0);
693
+ ```
694
+
695
+ > **Type safety**: only `"JoinedTable.field"` keys are accepted — other tables' fields are rejected at compile time.
386
696
 
387
697
  #### `.joinUnsafeTypeEnforced`
388
698
 
389
699
  Unlike the `.join` command, this will allow you to join on columns that are not explicitly linked by a FK, but have the same type.
390
700
 
391
701
  ##### Example
392
- ```typescript
702
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-unsafe.ts region=type-enforced
393
703
  prisma.$from("User")
394
704
  .joinUnsafeTypeEnforced("Post", "title", "User.name");
395
705
  ```
@@ -398,7 +708,7 @@ prisma.$from("User")
398
708
  ##### SQL
399
709
  The resulting SQL will look like:
400
710
 
401
- ```sql
711
+ ```sql file=../usage-sqlite-v7/tests/readme/join-unsafe.ts region=type-enforced-sql
402
712
  FROM User
403
713
  JOIN Post ON Post.title = User.name;
404
714
  ```
@@ -409,13 +719,15 @@ JOIN Post ON Post.title = User.name;
409
719
  | `table` | The table to join on (supports inline alias: `"Post p"` or `"Post", "p"`). <br/>TS autocomplete will show tables that can join with previously defined tables on. |
410
720
  | `field` | Column on table. <br/>TS autocomplete will show known columns that this table, can join with previously defined tables on. |
411
721
  | `reference` | `Table.Column` to a previously defined table (either the base, or another join), with a column that is of the same type. |
722
+ | `where` | *(optional)* Criteria added to the `ON` clause. See [Join-level WHERE](#join-level-where). |
723
+ | `joinType` | *(optional)* Join variant. See [Join Type](#join-type). |
412
724
 
413
725
  #### `.joinUnsafeIgnoreType`
414
726
 
415
727
  Unlike the `.joinUnsafeIgnoreType` command, this will allow you to join on columns that are not explicitly linked by a FK, and do not have the same type.
416
728
 
417
729
  ##### Example
418
- ```typescript
730
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-unsafe.ts region=ignore-type
419
731
  prisma.$from("User")
420
732
  .joinUnsafeIgnoreType("Post", "id", "User.name");
421
733
  ```
@@ -424,9 +736,9 @@ prisma.$from("User")
424
736
  ##### SQL
425
737
  The resulting SQL will look like:
426
738
 
427
- ```sql
739
+ ```sql file=../usage-sqlite-v7/tests/readme/join-unsafe.ts region=ignore-type-sql
428
740
  FROM User
429
- JOIN Post ON Post.id = User.name
741
+ JOIN Post ON Post.id = User.name;
430
742
  ```
431
743
 
432
744
  ##### Parameters
@@ -435,198 +747,497 @@ JOIN Post ON Post.id = User.name
435
747
  | `table` | The table to join on (supports inline alias: `"Post p"` or `"Post", "p"`). <br/>TS autocomplete will show tables that can join with previously defined tables on. |
436
748
  | `field` | Column on table. <br/>TS autocomplete will show known columns that this table, can join with previously defined tables on. |
437
749
  | `reference` | `Table.Column` to a previously defined table (either the base, or another join). Referencing any column, of any type. |
750
+ | `where` | *(optional)* Criteria added to the `ON` clause. See [Join-level WHERE](#join-level-where). |
751
+ | `joinType` | *(optional)* Join variant. See [Join Type](#join-type). |
438
752
 
439
- ### Where
753
+ #### `.manyToManyJoin`
440
754
 
441
- #### `.where`
755
+ Joins through Prisma's implicit or explicit many-to-many junction tables. Automatically detects the junction table and join columns from the generated schema.
442
756
 
443
- The `where` syntax takes inspiration from how mongoDB does queries.
757
+ ##### Example
758
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-basic
759
+ prisma.$from("M2M_Post")
760
+ .manyToManyJoin("M2M_Category");
761
+ ```
444
762
 
445
- ##### TypeSyntax
446
- ```TypeScript
447
- type WhereClause = {
448
- "Table.Column": <value> | { "op": "<condition>", "value": <value> }
449
- "$AND": [WhereClause, ...Array<WhereClause>],
450
- "$OR": [WhereClause, ...Array<WhereClause>],
451
- "$NOT": [WhereClause, ...Array<WhereClause>],
452
- "$NOR": [WhereClause, ...Array<WhereClause>]
453
- }
763
+ ##### SQL
764
+ ```sql file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-basic-sql
765
+ FROM M2M_Post
766
+ JOIN _M2M_CategoryToM2M_Post ON _M2M_CategoryToM2M_Post.B = M2M_Post.id
767
+ JOIN M2M_Category ON M2M_Category.id = _M2M_CategoryToM2M_Post.A;
454
768
  ```
455
769
 
456
- ##### Operation types
457
- | Op | Description | Supported Types |
458
- |-------------|-------------|-----------------------|
459
- | IN | | Numbers, String, Date |
460
- | NOT IN | | Numbers, String, Date |
461
- | BETWEEN | | Numbers, Date |
462
- | LIKE | | String |
463
- | NOT LIKE | | String |
464
- | IS NULL | | * |
465
- | IS NOT NULL | | * |
466
- | \> | | Numbers, Date |
467
- | \>= | | Numbers, Date |
468
- | < | | Numbers, Date |
469
- | <= | | Numbers, Date |
470
- | != | | Numbers, String, Date |
770
+ ##### Parameters
771
+ | Param | Type | Description |
772
+ |-------|------|-------------|
773
+ | `targetTable` | `string` | Target table, optionally with alias: `"M2M_Category"` or `"M2M_Category mc"` |
774
+ | `options.refName` | `string?` | Junction ref name — required when multiple M2M relations point to the same target |
775
+ | `options.source` | `string?` | Explicit source as `"alias.column"` — useful when the source table is aliased |
471
776
 
777
+ ##### With Alias
778
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-alias
779
+ prisma.$from("M2M_Post")
780
+ .manyToManyJoin("M2M_Category mc");
781
+ ```
472
782
 
473
- ##### Examples
474
- | Type | Description | Example | SQL |
475
- |--------------|--------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
476
- | Table.Column | A particular Table.Column name | <pre>.where({ <br /> "User.age": 20,<br /> "User.name": {op: "LIKE", value:"Stuart%"},<br />}) | `(User.age = 20 AND User.name LIKE "Stuart%")` |
477
- | $AND | Will join all items with a `AND` | <pre>.where({ <br /> $AND:[<br /> {"User.age": {op: ">", value:20}},<br /> {"User.age": {op: "<", value:60}},<br />]})</pre> | `(User.age > 20 AND User.age < 60)` |
478
- | $OR | Will join all items with a `OR` | <pre>.where({ <br /> $OR:[<br /> {"User.name": {op: "LIKE", value:"a%"}},<br /> {"User.name": {op: "LIKE", value:"d%"}},<br />]})</pre> | `(User.name LIKE "a%" OR User.name LIKE "d%")` |
479
- | $NOT | Will wrap statement in a `NOT (/*...*/)` and join any items with a `AND` | <pre>.where({ <br /> $NOT:[<br /> {"User.age": 20 },<br /> {<br /> "User.age": {op: "=", value:60},<br /> "User.name": "Bob",<br /> },<br />]})</pre> | `(NOT (User.age = 20 AND (User.age = 60 AND User.name = "Bob")))` |
480
- | $NOR | Will wrap statement in a `NOT (/*...*/)` and join any items with a `OR` | <pre>.where({ <br /> $NOR:[<br /> {"User.age": 20 },<br /> {<br /> "User.age": {op: "!=", value:60},<br /> "User.name": "Bob",<br /> },<br />]})</pre> | `(NOT (User.age = 20 OR (User.age != 60 AND User.name = "Bob")))` |
783
+ ```sql file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-alias-sql
784
+ FROM M2M_Post
785
+ JOIN _M2M_CategoryToM2M_Post ON _M2M_CategoryToM2M_Post.B = M2M_Post.id
786
+ JOIN M2M_Category AS `mc` ON mc.id = _M2M_CategoryToM2M_Post.A;
787
+ ```
481
788
 
789
+ ##### Named Junction (`refName`)
482
790
 
483
- ###### Columns
484
- ```typescript
485
- prisma.$from("User")
486
- .join("Post", "id", "User.name")
487
- .where({
488
- "User.age": 20,
489
- "User.name": {op: "LIKE", value: "Stuart%"},
490
- });
791
+ Use `refName` when a model has multiple M2M relations to the same target:
792
+
793
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-refname
794
+ prisma.$from("MMM_Post")
795
+ .manyToManyJoin("MMM_Category", { refName: "M2M_NC_M1" });
491
796
  ```
492
797
 
493
- ###### $AND
494
- ```typescript
495
- prisma.$from("User")
496
- .join("Post", "id", "User.name")
497
- .where({
498
- $AND: [
499
- {"User.age": {op: ">", value: 20}},
500
- {"User.age": {op: "<", value: 60}},
501
- ]
502
- });
798
+ ```sql file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-refname-sql
799
+ FROM MMM_Post
800
+ JOIN _M2M_NC_M1 ON _M2M_NC_M1.B = MMM_Post.id
801
+ JOIN MMM_Category ON MMM_Category.id = _M2M_NC_M1.A;
503
802
  ```
504
803
 
505
- ###### $OR
506
- ```typescript
507
- prisma.$from("User")
508
- .join("Post", "id", "User.name")
509
- .where({
510
- $OR: [
511
- {"User.name": {op: "LIKE", value: "a%"}},
512
- {"User.name": {op: "LIKE", value: "d%"}},
513
- ]
514
- });
804
+ ##### Explicit Source (`source`)
805
+
806
+ Use `source` to pin the source alias and column when the source table is aliased:
807
+
808
+ ```typescript file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-source
809
+ prisma.$from("M2M_Post mp")
810
+ .manyToManyJoin("M2M_Category mc", { source: "mp.id" });
515
811
  ```
516
812
 
517
- ###### $NOT
518
- ```typescript
519
- prisma.$from("User")
520
- .join("Post", "id", "User.name")
521
- .where({
522
- $NOT: [
523
- {"User.age": 20},
524
- {
525
- "User.age": {op: "=", value: 60},
526
- "User.name": "Bob",
527
- },
528
- ]
529
- });
813
+ ```sql file=../usage-sqlite-v7/tests/readme/join-many-to-many.ts region=m2m-source-sql
814
+ FROM M2M_Post AS `mp`
815
+ JOIN _M2M_CategoryToM2M_Post ON _M2M_CategoryToM2M_Post.B = mp.id
816
+ JOIN M2M_Category AS `mc` ON mc.id = _M2M_CategoryToM2M_Post.A;
530
817
  ```
531
818
 
532
- ###### $NOR
533
- ```typescript
534
- prisma.$from("User")
535
- .join("Post", "id", "User.name")
536
- .where({
537
- $NOR: [
538
- {"User.age": 20},
539
- {
540
- "User.age": {op: "!=", value: 60},
541
- "User.name": "Bob",
542
- },
543
- ]
544
- });
819
+ #### `.innerJoin`
820
+
821
+ Alias for `.join` — explicitly emits `INNER JOIN`. Same type-safe FK constraints.
822
+
823
+ ##### Example
824
+ ```typescript file=../../shared-tests/readme/join-inner.ts region=example
825
+ prisma.$from("User")
826
+ .innerJoin("Post", "authorId", "User.id")
545
827
  ```
546
828
 
547
- #### `.whereNotNull`
829
+ ##### SQL
830
+ ```sql file=../../shared-tests/readme/join-inner.ts region=sql
831
+ FROM User
832
+ INNER JOIN Post ON Post.authorId = User.id;
833
+ ```
834
+
835
+ ##### `.innerJoinUnsafeTypeEnforced`
836
+
837
+ Same-type column join, INNER semantics.
838
+
839
+ ```typescript file=../../shared-tests/readme/join-inner.ts region=type-enforced
840
+ prisma.$from("User")
841
+ .innerJoinUnsafeTypeEnforced("Post", "title", "User.name")
842
+ ```
843
+
844
+ ```sql file=../../shared-tests/readme/join-inner.ts region=type-enforced-sql
845
+ FROM User
846
+ INNER JOIN Post ON Post.title = User.name;
847
+ ```
848
+
849
+ ##### `.innerJoinUnsafeIgnoreType`
850
+
851
+ Any-column join, INNER semantics.
852
+
853
+ ```typescript file=../../shared-tests/readme/join-inner.ts region=ignore-type
854
+ prisma.$from("User")
855
+ .innerJoinUnsafeIgnoreType("Post", "id", "User.name")
856
+ ```
857
+
858
+ ```sql file=../../shared-tests/readme/join-inner.ts region=ignore-type-sql
859
+ FROM User
860
+ INNER JOIN Post ON Post.id = User.name;
861
+ ```
548
862
 
549
- This will remove the `null` type from the union of types of the current table column.
550
- To use `.whereNotNull`, you need to add it before a `.where`.
863
+ ---
864
+
865
+ #### `.leftJoin`
866
+
867
+ FK-safe LEFT JOIN. Joined table fields become `T | null` in the result type.
551
868
 
552
869
  ##### Example
870
+ ```typescript file=../../shared-tests/readme/join-left.ts region=example
871
+ prisma.$from("User")
872
+ .leftJoin("Post", "authorId", "User.id")
873
+ ```
874
+
875
+ ##### SQL
876
+ ```sql file=../../shared-tests/readme/join-left.ts region=sql
877
+ FROM User
878
+ LEFT JOIN Post ON Post.authorId = User.id;
879
+ ```
880
+
881
+ ##### `.leftJoinUnsafeTypeEnforced`
882
+
883
+ Same-type column join, LEFT semantics.
884
+
885
+ ```typescript file=../../shared-tests/readme/join-left.ts region=type-enforced
886
+ prisma.$from("User")
887
+ .leftJoinUnsafeTypeEnforced("Post", "title", "User.name")
888
+ ```
889
+
890
+ ```sql file=../../shared-tests/readme/join-left.ts region=type-enforced-sql
891
+ FROM User
892
+ LEFT JOIN Post ON Post.title = User.name;
893
+ ```
894
+
895
+ ##### `.leftJoinUnsafeIgnoreType`
896
+
897
+ Any-column join, LEFT semantics.
898
+
899
+ ```typescript file=../../shared-tests/readme/join-left.ts region=ignore-type
900
+ prisma.$from("User")
901
+ .leftJoinUnsafeIgnoreType("Post", "id", "User.name")
902
+ ```
903
+
904
+ ```sql file=../../shared-tests/readme/join-left.ts region=ignore-type-sql
905
+ FROM User
906
+ LEFT JOIN Post ON Post.id = User.name;
907
+ ```
908
+
909
+ ---
910
+
911
+ #### `.crossJoin`
912
+
913
+ Produces a cartesian product — no `ON` clause. All dialects supported.
914
+
915
+ ##### Example
916
+ ```typescript file=../../shared-tests/readme/join-cross.ts region=example
917
+ prisma.$from("User")
918
+ .crossJoin("Post")
919
+ ```
920
+
921
+ ##### SQL
922
+ ```sql file=../../shared-tests/readme/join-cross.ts region=sql
923
+ FROM User
924
+ CROSS JOIN Post;
925
+ ```
926
+
927
+ ##### `.crossJoinUnsafeTypeEnforced` / `.crossJoinUnsafeIgnoreType`
928
+
929
+ Type-permission variants — still emit `CROSS JOIN` with no `ON` clause (takes only a table argument).
930
+
931
+ ```typescript file=../../shared-tests/readme/join-cross.ts region=type-enforced
932
+ prisma.$from("User")
933
+ .crossJoinUnsafeTypeEnforced("Post")
934
+ ```
935
+
936
+ ```sql file=../../shared-tests/readme/join-cross.ts region=type-enforced-sql
937
+ FROM User
938
+ CROSS JOIN Post;
939
+ ```
940
+
941
+ ---
942
+
943
+ #### `.rightJoin`
944
+
945
+ > **MySQL / PostgreSQL only** — not supported by SQLite.
946
+
947
+ Base table fields become `T | null`. Use when the joined table drives the result set.
948
+
949
+ ##### Example
950
+ ```typescript
951
+ prisma.$from("Post")
952
+ .rightJoin("User", "id", "Post.authorId")
953
+ ```
954
+
955
+ ##### SQL
956
+ ```sql
957
+ FROM Post RIGHT JOIN User ON User.id = Post.authorId;
958
+ ```
959
+
960
+ ##### `.rightJoinUnsafeTypeEnforced`
961
+ ```typescript
962
+ prisma.$from("Post")
963
+ .rightJoinUnsafeTypeEnforced("User", "name", "Post.title")
964
+ ```
965
+
966
+ ##### `.rightJoinUnsafeIgnoreType`
967
+ ```typescript
968
+ prisma.$from("Post")
969
+ .rightJoinUnsafeIgnoreType("User", "id", "Post.title")
970
+ ```
971
+
972
+ ---
973
+
974
+ #### `.fullJoin`
975
+
976
+ > **PostgreSQL only** — not supported by SQLite or MySQL.
977
+
978
+ Both sides become `T | null`. Use for outer joins where either side may have no match.
979
+
980
+ ##### Example
981
+ ```typescript
982
+ prisma.$from("User")
983
+ .fullJoin("Post", "authorId", "User.id")
984
+ ```
985
+
986
+ ##### SQL
987
+ ```sql
988
+ FROM User FULL JOIN Post ON Post.authorId = User.id;
989
+ ```
990
+
991
+ ##### `.fullJoinUnsafeTypeEnforced`
992
+ ```typescript
993
+ prisma.$from("User")
994
+ .fullJoinUnsafeTypeEnforced("Post", "title", "User.name")
995
+ ```
996
+
997
+ ##### `.fullJoinUnsafeIgnoreType`
553
998
  ```typescript
554
999
  prisma.$from("User")
555
- .join("Post", "authorId", "User.id")
556
- .whereNotNull("User.name");
1000
+ .fullJoinUnsafeIgnoreType("Post", "id", "User.name")
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ### Where
1006
+
1007
+ #### `.where`
1008
+
1009
+ The `where` syntax takes inspiration from how mongoDB does queries.
1010
+
1011
+ ##### TypeSyntax
1012
+ ```TypeScript
1013
+ type WhereClause = {
1014
+ "Table.Column": <value>
1015
+ | [<value>, ...<value>[]] // scalar array → IN
1016
+ | { "op": "<condition>", "value": <value> }
1017
+ | [{ "op": "<condition>", "value": <value> }, ...] // op-array → OR
1018
+ "$AND": [WhereClause, ...Array<WhereClause>],
1019
+ "$OR": [WhereClause, ...Array<WhereClause>],
1020
+ "$NOT": [WhereClause, ...Array<WhereClause>],
1021
+ "$NOR": [WhereClause, ...Array<WhereClause>]
1022
+ }
1023
+ ```
1024
+
1025
+ ##### Operation types
1026
+ | Op | Description | Supported Types |
1027
+ |-------------|-------------|-----------------------|
1028
+ | IN | | Numbers, String, Date |
1029
+ | NOT IN | | Numbers, String, Date |
1030
+ | BETWEEN | | Numbers, Date |
1031
+ | LIKE | | String |
1032
+ | NOT LIKE | | String |
1033
+ | IS NULL | | * |
1034
+ | IS NOT NULL | | * |
1035
+ | \> | | Numbers, Date |
1036
+ | \>= | | Numbers, Date |
1037
+ | < | | Numbers, Date |
1038
+ | <= | | Numbers, Date |
1039
+ | != | | Numbers, String, Date |
1040
+ | = | | Numbers, String, Date |
1041
+
1042
+
1043
+ ##### Examples
1044
+ | Type | Description | Example | SQL |
1045
+ |--------------|--------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
1046
+ | Table.Column | A particular Table.Column name | <pre>.where({ <br /> "User.age": 20,<br /> "User.name": {op: "LIKE", value:"Stuart%"},<br />}) | `(User.age = 20 AND User.name LIKE "Stuart%")` |
1047
+ | $AND | Will join all items with a `AND` | <pre>.where({ <br /> $AND:[<br /> {"User.age": {op: ">", value:20}},<br /> {"User.age": {op: "<", value:60}},<br />]})</pre> | `(User.age > 20 AND User.age < 60)` |
1048
+ | $OR | Will join all items with a `OR` | <pre>.where({ <br /> $OR:[<br /> {"User.name": {op: "LIKE", value:"a%"}},<br /> {"User.name": {op: "LIKE", value:"d%"}},<br />]})</pre> | `(User.name LIKE "a%" OR User.name LIKE "d%")` |
1049
+ | $NOT | Will wrap statement in a `NOT (/*...*/)` and join any items with a `AND` | <pre>.where({ <br /> $NOT:[<br /> {"User.age": 20 },<br /> {<br /> "User.age": {op: "=", value:60},<br /> "User.name": "Bob",<br /> },<br />]})</pre> | `(NOT (User.age = 20 AND (User.age = 60 AND User.name = "Bob")))` |
1050
+ | $NOR | Will wrap statement in a `NOT (/*...*/)` and join any items with a `OR` | <pre>.where({ <br /> $NOR:[<br /> {"User.age": 20 },<br /> {<br /> "User.age": {op: "!=", value:60},<br /> "User.name": "Bob",<br /> },<br />]})</pre> | `(NOT (User.age = 20 OR (User.age != 60 AND User.name = "Bob")))` |
1051
+ | `Array (scalar)` | Non-empty array of values → SQL `IN` | `.where({ "User.name": ["Alice", "Bob"] })` | `User.name IN ('Alice', 'Bob')` |
1052
+ | `Array (op-objects)` | Non-empty array of op-objects → `OR` chain | `.where({ "User.name": [{ op: "LIKE", value: "A%" }, { op: "LIKE", value: "B%" }] })` | `(User.name LIKE 'A%' OR User.name LIKE 'B%')` |
1053
+
1054
+
1055
+ ###### Columns
1056
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=columns
1057
+ prisma.$from("User")
1058
+ .joinUnsafeIgnoreType("Post", "id", "User.name")
1059
+ .where({
1060
+ "User.age": 20,
1061
+ "User.name": {op: "LIKE", value: "Stuart%"},
1062
+ });
1063
+ ```
1064
+
1065
+ ###### $AND
1066
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=and
1067
+ prisma.$from("User")
1068
+ .joinUnsafeIgnoreType("Post", "id", "User.name")
1069
+ .where({
1070
+ $AND: [
1071
+ {"User.age": {op: ">", value: 20}},
1072
+ {"User.age": {op: "<", value: 60}},
1073
+ ]
1074
+ });
1075
+ ```
1076
+
1077
+ ###### $OR
1078
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=or
1079
+ prisma.$from("User")
1080
+ .joinUnsafeIgnoreType("Post", "id", "User.name")
1081
+ .where({
1082
+ $OR: [
1083
+ {"User.name": {op: "LIKE", value: "a%"}},
1084
+ {"User.name": {op: "LIKE", value: "d%"}},
1085
+ ]
1086
+ });
1087
+ ```
1088
+
1089
+ ###### $NOT
1090
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=not
1091
+ prisma.$from("User")
1092
+ .joinUnsafeIgnoreType("Post", "id", "User.name")
1093
+ .where({
1094
+ $NOT: [
1095
+ {"User.age": 20},
1096
+ {
1097
+ "User.age": {op: "=", value: 60},
1098
+ "User.name": "Bob",
1099
+ },
1100
+ ]
1101
+ });
1102
+ ```
1103
+
1104
+ ###### $NOR
1105
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=nor
1106
+ prisma.$from("User")
1107
+ .joinUnsafeIgnoreType("Post", "id", "User.name")
1108
+ .where({
1109
+ $NOR: [
1110
+ {"User.age": 20},
1111
+ {
1112
+ "User.age": {op: "!=", value: 60},
1113
+ "User.name": "Bob",
1114
+ },
1115
+ ]
1116
+ });
1117
+ ```
1118
+
1119
+ ###### Array (Scalar → IN)
1120
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=array-scalar
1121
+ prisma.$from("User")
1122
+ .joinUnsafeIgnoreType("Post", "id", "User.name")
1123
+ .where({
1124
+ "User.name": ["Alice", "Bob"],
1125
+ });
1126
+ ```
1127
+
1128
+ ###### Array (Op-Object → OR)
1129
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=array-op
1130
+ prisma.$from("User")
1131
+ .joinUnsafeIgnoreType("Post", "id", "User.name")
1132
+ .where({
1133
+ "User.name": [
1134
+ { op: "LIKE", value: "A%" },
1135
+ { op: "LIKE", value: "B%" },
1136
+ ],
1137
+ });
1138
+ ```
1139
+
1140
+ #### `.whereNotNull`
1141
+
1142
+ Removes `null` from the column's type union and adds an `IS NOT NULL` condition to the WHERE clause.
1143
+ Type narrowing is reflected in all downstream `.select()` calls.
1144
+
1145
+ ##### Example
1146
+ ```typescript file=../usage-sqlite-v7/tests/readme/whereNotNull.ts region=whereNotNull
1147
+ prisma.$from("User")
1148
+ .join("Post", "authorId", "User.id")
1149
+ .whereNotNull("User.name")
557
1150
  ```
558
1151
  ![whereNotNull](./assets/whereNotNull.gif)
559
1152
 
560
1153
  ##### SQL
561
1154
  The resulting SQL will look like:
562
1155
 
563
- ```sql
1156
+ ```sql file=../usage-sqlite-v7/tests/readme/whereNotNull.ts region=whereNotNull-sql
564
1157
  FROM User
565
- JOIN Post ON authorId = User.id
566
- WHERE User.name IS NOT NULL;
1158
+ JOIN Post ON Post.authorId = User.id
1159
+ WHERE (User.name IS NOT NULL);
567
1160
  ```
568
1161
 
569
1162
  #### `.whereIsNull`
570
1163
 
571
- This will remove the NonNull type from the union of types of the current table column.
572
- To use `.whereIsNull`, you need to add it before a `.where`.
1164
+ Narrows the column's type to exactly `null` and adds an `IS NULL` condition to the WHERE clause.
573
1165
 
574
1166
  ##### Example
575
- ```typescript
1167
+ ```typescript file=../usage-sqlite-v7/tests/readme/whereNotNull.ts region=whereIsNull
576
1168
  prisma.$from("User")
577
- .join("Post", "authorId", "User.id")
578
- .whereIsNull("Post.content");
1169
+ .join("Post", "authorId", "User.id")
1170
+ .whereIsNull("Post.content")
579
1171
  ```
580
1172
  ![whereIsNull](./assets/whereIsNull.gif)
581
1173
 
582
1174
  ##### SQL
583
1175
  The resulting SQL will look like:
584
1176
 
585
- ```sql
1177
+ ```sql file=../usage-sqlite-v7/tests/readme/whereNotNull.ts region=whereIsNull-sql
1178
+ FROM User
1179
+ JOIN Post ON Post.authorId = User.id
1180
+ WHERE (Post.content IS NULL);
1181
+ ```
1182
+
1183
+ #### `.where` — fn overload (SQL expressions)
1184
+
1185
+ Pass a callback instead of a criteria object to apply SQL functions as conditions. The callback receives the same select-fn context as `.select()`, giving access to `upper`, `lower`, `length`, `count`, `avg`, etc.
1186
+
1187
+ ```typescript file=../../shared-tests/readme/where.ts region=fn-upper-like
1188
+ prisma.$from("User")
1189
+ .where(({ upper }) => [[upper('name'), { op: 'LIKE', value: 'John%' }]])
1190
+ .select("name")
1191
+ ```
1192
+
1193
+ ```sql file=../../shared-tests/readme/where.ts region=fn-upper-like-sql
1194
+ SELECT name
586
1195
  FROM User
587
- JOIN Post ON authorId = User.id
588
- WHERE Post.content IS NULL;
1196
+ WHERE UPPER(name) LIKE 'John%';
589
1197
  ```
590
1198
 
1199
+ Each array element is an `[SQLExpr<T>, condition]` pair — multiple pairs are AND-ed. The condition type is inferred from `SQLExpr<T>`: string expressions accept LIKE/NOT LIKE, numeric expressions accept `>`, `<`, BETWEEN, etc.
1200
+
591
1201
  #### `.whereRaw`
592
1202
 
593
- When you want to write a complex `where`, or you just don't want the TypeSafety offered by the other methods, you can use `.whereRaw`.
1203
+ When you want to write a complex `where`, or you just don't want the TypeSafety offered by the other methods, you can use `.whereRaw`.
594
1204
 
595
1205
  ##### Example
596
- ```typescript
1206
+ ```typescript file=../usage-sqlite-v7/tests/readme/where.ts region=raw
597
1207
  prisma.$from("User")
598
- .join("Post", "authorId", "User.id")
599
- .whereRaw("this is a raw where statement");
1208
+ .join("Post", "authorId", "User.id")
1209
+ .whereRaw("this is a raw where statement");
600
1210
  ```
601
1211
 
602
1212
  ##### SQL
603
1213
  The resulting SQL will look like:
604
1214
 
605
- ```sql
1215
+ ```sql file=../usage-sqlite-v7/tests/readme/where.ts region=raw-sql
606
1216
  FROM User
607
- JOIN Post ON authorId = User.id
608
- WHERE this is a raw where statement;
1217
+ JOIN Post ON Post.authorId = User.id
1218
+ WHERE this is a raw
1219
+ where statement;
609
1220
  ```
610
1221
 
611
1222
 
612
1223
  ### Group By
613
1224
 
614
- Will allow you to pass a list of columns, that haven been specified from the `.$from` and any `.join` methods.
1225
+ Will allow you to pass a list of columns, that haven been specified from the `.$from` and any `.join` methods.
615
1226
 
616
1227
  #### Example
617
- ```typescript
1228
+ ```typescript file=../usage-sqlite-v7/tests/readme/groupby.ts region=basic
618
1229
  prisma.$from("User")
619
- .join("Post", "authorId", "User.id")
620
- .groupBy(["name", "Post.content"]);
1230
+ .join("Post", "authorId", "User.id")
1231
+ .groupBy(["name", "Post.content"]);
621
1232
  ```
622
1233
  ![groupBy](./assets/groupBy.gif)
623
1234
 
624
1235
  #### SQL
625
1236
  The resulting SQL will look like:
626
1237
 
627
- ```sql
1238
+ ```sql file=../usage-sqlite-v7/tests/readme/groupby.ts region=basic-sql
628
1239
  FROM User
629
- JOIN Post ON authorId = User.id
1240
+ JOIN Post ON Post.authorId = User.id
630
1241
  GROUP BY name, Post.content;
631
1242
  ```
632
1243
 
@@ -637,16 +1248,17 @@ GROUP BY name, Post.content;
637
1248
  Will add the keyword `DISTINCT` after the select.
638
1249
 
639
1250
  #### Example
640
- ```typescript
1251
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=distinct
641
1252
  prisma.$from("User")
642
- .selectDistinct();
1253
+ .selectDistinct()
1254
+ .select("User.name");
643
1255
  ```
644
1256
 
645
1257
  #### SQL
646
1258
  The resulting SQL will look like:
647
1259
 
648
- ```sql
649
- SELECT DISTINCT
1260
+ ```sql file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=distinct-sql
1261
+ SELECT DISTINCT name
650
1262
  FROM User;
651
1263
  ```
652
1264
 
@@ -657,71 +1269,100 @@ This method will explicitly list all the tables from the `$from` and `.join`. So
657
1269
 
658
1270
 
659
1271
  #### Example - Single Table
660
- ```typescript
1272
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=all-single
661
1273
  prisma.$from("User")
662
- .selectAll();
1274
+ .selectAll();
663
1275
  ```
664
1276
 
665
1277
  ##### SQL
666
1278
  The resulting SQL will look like:
667
1279
 
668
- ```sql
669
- SELECT id, email, name
670
- FROM User
1280
+ ```sql file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=all-single-sql
1281
+ SELECT id, email, name, age
1282
+ FROM User;
671
1283
  ```
672
1284
 
673
1285
  #### Example - Join table
674
- ```typescript
1286
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=all-join
675
1287
  prisma.$from("User")
676
- .join("Post", "authorId", "User.id")
677
- .selectAll();
1288
+ .join("Post", "authorId", "User.id")
1289
+ .selectAll();
678
1290
  ```
679
1291
 
680
1292
  ##### SQL
681
1293
  The resulting SQL will look like:
682
1294
 
683
- ```sql
684
- SELECT User.id, User. email, User.name, Post.id, Post.title, Post.content, Post.published, Post.author, Post.authorId, Post.LastModifiedBy, Post.lastModifiedById
1295
+ ```sql file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=all-join-sql
1296
+ SELECT User.id AS `User.id`, User.email AS `User.email`, User.name AS `User.name`, User.age AS `User.age`, Post.id AS `Post.id`, Post.title AS `Post.title`, Post.content AS `Post.content`, Post.published AS `Post.published`, Post.createdAt AS `Post.createdAt`, Post.authorId AS `Post.authorId`, Post.lastModifiedById AS `Post.lastModifiedById`, Post.metadata AS `Post.metadata`
685
1297
  FROM User
686
- JOIN Post ON authorId = User.id
1298
+ JOIN Post ON Post.authorId = User.id;
1299
+ ```
1300
+
1301
+ #### `.selectAllOmit`
1302
+
1303
+ Like `.selectAll`, but excludes specific columns. Accepts `Table.column` or bare `column` references.
1304
+
1305
+ #### Example - Single Table
1306
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-all-omit.ts region=single-omit
1307
+ prisma.$from("User")
1308
+ .selectAllOmit(["User.email"]);
1309
+ ```
1310
+
1311
+ ##### SQL
1312
+ ```sql file=../usage-sqlite-v7/tests/readme/select-all-omit.ts region=single-omit-sql
1313
+ SELECT `id`, `name`, `age`
1314
+ FROM `User`;
687
1315
  ```
688
1316
 
689
- [//]: # (#### `.selectAllOmit`)
1317
+ #### Example - Multiple Columns
1318
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-all-omit.ts region=multi-omit
1319
+ prisma.$from("User")
1320
+ .selectAllOmit(["User.email", "User.age"]);
1321
+ ```
1322
+
1323
+ #### Example - With Join
1324
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-all-omit.ts region=join-omit
1325
+ prisma.$from("User")
1326
+ .join("Post", "authorId", "User.id")
1327
+ .selectAllOmit(["User.email", "Post.content"]);
1328
+ ```
1329
+
1330
+ > **Note:** `*` and `Table.*` are not valid arguments — use `Table.column` or bare `column` references.
690
1331
 
691
1332
  #### `.select`
692
1333
 
693
1334
  You can supply either; `*`, `Table.*` OR `table.field` and then chain them together.
694
1335
 
695
1336
  #### Example - `*`
696
- ```typescript
1337
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-star.ts region=example
697
1338
  prisma.$from("User")
698
- .select("*");
1339
+ .select("*");
699
1340
  ```
700
1341
 
701
1342
  ##### SQL
702
1343
  The resulting SQL will look like:
703
1344
 
704
- ```sql
705
- SELECT *
1345
+ ```sql file=../usage-sqlite-v7/tests/readme/select-star.ts region=example-sql
1346
+ SELECT *
706
1347
  FROM User;
707
1348
  ```
708
1349
 
709
1350
  #### Example - `Table.*` (Single Table)
710
- ```typescript
1351
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=table-star-single
711
1352
  prisma.$from("User")
712
- .select("User.*");
1353
+ .select("User.*");
713
1354
  ```
714
1355
 
715
1356
  ##### SQL
716
1357
  The resulting SQL will look like:
717
1358
 
718
- ```sql
719
- SELECT User.id, User.email, User.name
1359
+ ```sql file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=table-star-single-sql
1360
+ SELECT id, email, name, age
720
1361
  FROM User;
721
1362
  ```
722
1363
 
723
1364
  #### Example - `Table.*` (With Join)
724
- ```typescript
1365
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=table-star-join
725
1366
  prisma.$from("User")
726
1367
  .join("Post", "authorId", "User.id")
727
1368
  .select("User.*")
@@ -731,38 +1372,32 @@ prisma.$from("User")
731
1372
  ##### SQL
732
1373
  The resulting SQL will look like:
733
1374
 
734
- ```sql
735
- SELECT User.id AS `User.id`,
736
- User.email AS `User.email`,
737
- User.name AS `User.name`,
738
- Post.id AS `Post.id`,
739
- Post.title AS `Post.title`,
740
- Post.content AS `Post.content`,
741
- Post.published AS `Post.published`
742
- FROM User
743
- JOIN Post ON authorId = User.id;
1375
+ ```sql file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=table-star-join-sql
1376
+ SELECT User.id AS `User.id`, User.email AS `User.email`, User.name AS `User.name`, User.age AS `User.age`, Post.id AS `Post.id`, Post.title AS `Post.title`, Post.content AS `Post.content`, Post.published AS `Post.published`, Post.createdAt AS `Post.createdAt`, Post.authorId AS `Post.authorId`, Post.lastModifiedById AS `Post.lastModifiedById`, Post.metadata AS `Post.metadata`
1377
+ FROM User
1378
+ JOIN Post ON Post.authorId = User.id;
744
1379
  ```
745
1380
 
746
- [!NOTE]
1381
+ > [!NOTE]
747
1382
  > When using `Table.*` with joins, all columns are automatically aliased with the table name prefix to avoid column name conflicts.
748
1383
 
749
1384
  #### Example - Chained
750
- ```typescript
1385
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-chained.ts region=example
751
1386
  prisma.$from("User")
752
- .select("name")
753
- .select("email");
1387
+ .select("name")
1388
+ .select("email");
754
1389
  ```
755
1390
 
756
1391
  ##### SQL
757
1392
  The resulting SQL will look like:
758
1393
 
759
- ```sql
760
- SELECT name, email
1394
+ ```sql file=../usage-sqlite-v7/tests/readme/select-chained.ts region=example-sql
1395
+ SELECT name, email
761
1396
  FROM User;
762
1397
  ```
763
1398
 
764
1399
  #### Example - Join + Chained
765
- ```typescript
1400
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=join-chained
766
1401
  prisma.$from("User")
767
1402
  .join("Post", "authorId", "User.id")
768
1403
  .select("name")
@@ -772,24 +1407,25 @@ prisma.$from("User")
772
1407
  ##### SQL
773
1408
  The resulting SQL will look like:
774
1409
 
775
- ```sql
776
- SELECT name, Post.title
777
- FROM User
778
- JOIN Post ON authorId = User.id;
1410
+ ```sql file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=join-chained-sql
1411
+ SELECT name, title
1412
+ FROM User
1413
+ JOIN Post ON Post.authorId = User.id;
779
1414
  ```
780
1415
 
781
1416
  #### Example - Column Aliases
782
- ```typescript
783
- // Basic alias
1417
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-column-alias.ts region=basic
784
1418
  prisma.$from("User")
785
1419
  .select("User.name", "username");
1420
+ ```
786
1421
 
787
- // Multiple aliases
1422
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-column-alias.ts region=multiple
788
1423
  prisma.$from("User")
789
1424
  .select("User.id", "userId")
790
1425
  .select("User.email", "emailAddress");
1426
+ ```
791
1427
 
792
- // Mixing aliased and non-aliased columns
1428
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-column-alias.ts region=mixed
793
1429
  prisma.$from("User")
794
1430
  .select("User.id")
795
1431
  .select("User.name", "username")
@@ -799,19 +1435,23 @@ prisma.$from("User")
799
1435
  ##### SQL
800
1436
  The resulting SQL will look like:
801
1437
 
802
- ```sql
803
- -- Basic alias
804
- SELECT User.name AS `username` FROM User;
1438
+ ```sql file=../usage-sqlite-v7/tests/readme/select-column-alias.ts region=basic-sql
1439
+ SELECT User.name AS `username`
1440
+ FROM User;
1441
+ ```
805
1442
 
806
- -- Multiple aliases
807
- SELECT User.id AS `userId`, User.email AS `emailAddress` FROM User;
1443
+ ```sql file=../usage-sqlite-v7/tests/readme/select-column-alias.ts region=multiple-sql
1444
+ SELECT User.id AS `userId`, User.email AS `emailAddress`
1445
+ FROM User;
1446
+ ```
808
1447
 
809
- -- Mixed
810
- SELECT User.id, User.name AS `username`, User.email FROM User;
1448
+ ```sql file=../usage-sqlite-v7/tests/readme/select-column-alias.ts region=mixed-sql
1449
+ SELECT id, User.name AS `username`, email
1450
+ FROM User;
811
1451
  ```
812
1452
 
813
1453
  #### Example - Aliases with Joins
814
- ```typescript
1454
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=aliases-joins
815
1455
  prisma.$from("User")
816
1456
  .join("Post", "authorId", "User.id")
817
1457
  .select("User.name", "authorName")
@@ -821,77 +1461,115 @@ prisma.$from("User")
821
1461
  ##### SQL
822
1462
  The resulting SQL will look like:
823
1463
 
824
- ```sql
825
- SELECT User.name AS `authorName`, Post.title AS `postTitle`
826
- FROM User
827
- JOIN Post ON authorId = User.id;
1464
+ ```sql file=../usage-sqlite-v7/tests/readme/select-advanced.ts region=aliases-joins-sql
1465
+ SELECT User.name AS `authorName`, Post.title AS `postTitle`
1466
+ FROM User
1467
+ JOIN Post ON Post.authorId = User.id;
828
1468
  ```
829
1469
 
830
- [!NOTE]
1470
+ > [!NOTE]
831
1471
  > When using column aliases, you can reference the alias in `ORDER BY` clauses. The returned type will use the alias names instead of the original column names.
832
1472
 
833
1473
  ### Having
834
1474
 
835
- `.having` uses the same syntax as [`.where`](#where). Please see the previous section for details.
1475
+ `.having` accepts two overloads — a criteria object (same syntax as [`.where`](#where)) or a fn callback for SQL expressions and aggregate functions.
836
1476
 
837
- #### Example
1477
+ #### Criteria object
838
1478
 
839
- ```typescript
1479
+ ```typescript file=../../shared-tests/readme/having.ts region=with-groupby
840
1480
  prisma.$from("User")
841
- .join("Post", "authorId", "User.id")
842
- .groupBy(["name", "Post.content"])
843
- .having({
844
- "User.name": {
845
- "op": "LIKE",
846
- "value": "bob%"
847
- }
848
- });
1481
+ .join("Post", "authorId", "User.id")
1482
+ .groupBy(["name", "Post.content"])
1483
+ .having({
1484
+ "User.name": {
1485
+ "op": "LIKE",
1486
+ "value": "bob%"
1487
+ }
1488
+ })
1489
+ .select("email");
1490
+ ```
1491
+
1492
+ ```sql file=../../shared-tests/readme/having.ts region=with-groupby-sql
1493
+ SELECT email
1494
+ FROM User
1495
+ JOIN Post ON Post.authorId = User.id
1496
+ GROUP BY name, Post.content HAVING User.name LIKE 'bob%';
849
1497
  ```
850
1498
 
851
- ```typescript
1499
+ #### fn overload — aggregate functions
1500
+
1501
+ Pass a callback returning `Array<[SQLExpr<T>, condition]>` pairs. The callback receives the full select-fn context, including all aggregate and string functions.
1502
+
1503
+ ##### `countAll()` with comparison op
1504
+
1505
+ ```typescript file=../../shared-tests/readme/having.ts region=agg-fn-tuple-countall
852
1506
  prisma.$from("User")
853
- .join("Post", "authorId", "User.id")
854
- .having({
855
- "User.name": {
856
- "op": "LIKE",
857
- "value": "stuart%"
858
- }
859
- });
1507
+ .join("Post", "authorId", "User.id")
1508
+ .groupBy(["User.name"])
1509
+ .having(({ countAll }) => [[countAll(), { op: '>', value: 1 }]])
1510
+ .select("User.name")
860
1511
  ```
861
1512
 
1513
+ ```sql file=../../shared-tests/readme/having.ts region=agg-fn-tuple-countall-sql
1514
+ SELECT name
1515
+ FROM User
1516
+ JOIN Post ON Post.authorId = User.id
1517
+ GROUP BY User.name HAVING COUNT(*) > 1;
1518
+ ```
862
1519
 
863
- ##### SQL
1520
+ ##### `count(col)` with bigint value
864
1521
 
865
- ```SQL
866
- FROM User
867
- JOIN Post ON authorId = User.id
868
- GROUP BY name, Post.content
869
- HAVING (User.name LIKE 'bob%');
1522
+ ```typescript file=../../shared-tests/readme/having.ts region=agg-fn-tuple-count
1523
+ prisma.$from("User")
1524
+ .join("Post", "authorId", "User.id")
1525
+ .groupBy(["User.name"])
1526
+ .having(({ count }) => [[count('User.id'), { op: '>=', value: 2n }]])
1527
+ .select("User.name")
870
1528
  ```
871
1529
 
872
- ```SQL
873
- FROM User
874
- JOIN Post ON authorId = User.id
875
- HAVING (User.name LIKE 'stuart%');
1530
+ ```sql file=../../shared-tests/readme/having.ts region=agg-fn-tuple-count-sql
1531
+ SELECT name
1532
+ FROM User
1533
+ JOIN Post ON Post.authorId = User.id
1534
+ GROUP BY User.name HAVING COUNT(User.id) >= 2;
1535
+ ```
1536
+
1537
+ ##### String expr — `upper(col)` LIKE
1538
+
1539
+ ```typescript file=../../shared-tests/readme/having.ts region=agg-fn-string-upper
1540
+ prisma.$from("User")
1541
+ .join("Post", "authorId", "User.id")
1542
+ .groupBy(["User.name"])
1543
+ .having(({ upper }) => [[upper('User.name'), { op: 'LIKE', value: 'John%' }]])
1544
+ .select("User.name")
1545
+ ```
1546
+
1547
+ ```sql file=../../shared-tests/readme/having.ts region=agg-fn-string-upper-sql
1548
+ SELECT name
1549
+ FROM User
1550
+ JOIN Post ON Post.authorId = User.id
1551
+ GROUP BY User.name HAVING UPPER(User.name) LIKE 'John%';
876
1552
  ```
877
1553
 
1554
+ Multiple pairs in one `.having()` call are AND-ed together. `.having()` can also be chained — each call appends an AND condition.
1555
+
878
1556
  ### Order By
879
1557
 
880
1558
  `.orderBy`, takes an array of column names, with the optional suffix of `ASC` or `DESC`.
881
1559
 
882
1560
  #### Example
883
1561
 
884
- ```typescript
1562
+ ```typescript file=../usage-sqlite-v7/tests/readme/orderby.ts region=basic
885
1563
  prisma.$from("User")
886
1564
  .join("Post", "authorId", "User.id")
887
- .orderBy(["name", "Post.content DESC"]);
1565
+ .orderBy(["name", "Post.content DESC"]);
888
1566
  ```
889
1567
 
890
1568
  ##### SQL
891
1569
 
892
- ```sql
893
- FROM User
894
- JOIN Post ON authorId = User.id
1570
+ ```sql file=../usage-sqlite-v7/tests/readme/orderby.ts region=basic-sql
1571
+ FROM User
1572
+ JOIN Post ON Post.authorId = User.id
895
1573
  ORDER BY name, Post.content DESC;
896
1574
  ```
897
1575
 
@@ -901,7 +1579,7 @@ ORDER BY name, Post.content DESC;
901
1579
 
902
1580
  #### Example
903
1581
 
904
- ```typescript
1582
+ ```typescript file=../usage-sqlite-v7/tests/readme/pagination.ts region=limit
905
1583
  prisma.$from("User")
906
1584
  .join("Post", "authorId", "User.id")
907
1585
  .limit(1);
@@ -909,9 +1587,9 @@ prisma.$from("User")
909
1587
 
910
1588
  ##### SQL
911
1589
 
912
- ```SQL
913
- FROM User
914
- JOIN Post ON authorId = User.id
1590
+ ```sql file=../usage-sqlite-v7/tests/readme/pagination.ts region=limit-sql
1591
+ FROM User
1592
+ JOIN Post ON Post.authorId = User.id
915
1593
  LIMIT 1;
916
1594
  ```
917
1595
 
@@ -920,71 +1598,374 @@ LIMIT 1;
920
1598
  `.offSet`, the number of rows to skip. Requires `.limit` to have been used first.
921
1599
 
922
1600
  #### Example
923
- ```typescript
1601
+ ```typescript file=../usage-sqlite-v7/tests/readme/pagination.ts region=offset
924
1602
  prisma.$from("User")
925
- .join("Post", "authorId", "User.id")
926
- .limit(1)
927
- .offset(1);
1603
+ .join("Post", "authorId", "User.id")
1604
+ .limit(1)
1605
+ .offset(1);
928
1606
  ```
929
1607
 
930
1608
  ##### SQL
931
1609
 
932
- ```SQL
933
- FROM User
934
- JOIN Post ON authorId = User.id
935
- LIMIT 1
936
- OFFSET 1
1610
+ ```sql file=../usage-sqlite-v7/tests/readme/pagination.ts region=offset-sql
1611
+ FROM User
1612
+ JOIN Post ON Post.authorId = User.id
1613
+ LIMIT 1
1614
+ OFFSET 1;
937
1615
  ```
938
1616
 
939
- ## Future updates
1617
+ ## Select Functions
940
1618
 
941
- - Support specifying `JOIN` type [issue#2](https://github.com/adrianbrowning/prisma-ts-select/issues/2)
942
- - Support Select Functions
943
- - [Aggregation #4](https://github.com/adrianbrowning/prisma-ts-select/issues/4)
944
- - [String #5](https://github.com/adrianbrowning/prisma-ts-select/issues/5)
945
- - [Date & Time #6](https://github.com/adrianbrowning/prisma-ts-select/issues/6)
946
- - [Math #7](https://github.com/adrianbrowning/prisma-ts-select/issues/7)
947
- - [Control Flow #8](https://github.com/adrianbrowning/prisma-ts-select/issues/8)
948
- - [JSON #9](https://github.com/adrianbrowning/prisma-ts-select/issues/9)
949
- - [Support a `Many-To-Many` join #19](https://github.com/adrianbrowning/prisma-ts-select/issues/19)
950
- - [Select column alias #27](https://github.com/adrianbrowning/prisma-ts-select/issues/27)
951
- - [Table name alias #28](https://github.com/adrianbrowning/prisma-ts-select/issues/28)
952
- - [whereRaw supporting Prisma.sql](https://github.com/adrianbrowning/prisma-ts-select/issues/29)
1619
+ Pass a callback to `.select()` to use SQL expressions and aggregate functions. The callback receives a context object with all available functions for the active dialect.
953
1620
 
954
- ## Changelog / Versioning
955
- Changelog is available [here](https://github.com/adrianbrowning/prisma-ts-select/releases). We use [semantic versioning](https://semver.org/) for versioning.
1621
+ ### Shared (all dialects)
956
1622
 
957
- ## License
958
- This project is licensed under the MIT License. See the LICENSE file for details.
1623
+ #### `lit(value)` — SQL literal
959
1624
 
960
- Things of note!!!!
1625
+ Produces a typed SQL literal from a JS value.
961
1626
 
962
- - remove typeof from
963
- - `type _db = DeepWriteable<typeof DB>;`
964
- - `}[keyof typeof DB];`
965
- - Merge Items missing //@ts-expect-error - might not be needed
966
- - groupBy -> having,
967
- - missing @deprecated
968
- - ts-exptect-error - might not be needed
969
- - GetColsFromTableType missing ts-expect-error - might not be needed
970
- - DB needs to be in the same file.
1627
+ ##### Example
1628
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=lit-string
1629
+ prisma.$from("User")
1630
+ .select(({ lit }) => lit("hello"), "greeting");
1631
+ ```
971
1632
 
1633
+ #### `countAll()` — COUNT(*)
972
1634
 
1635
+ The most common aggregate. Always produces `COUNT(*)`.
973
1636
 
974
- # prisma-ts-select
1637
+ ##### Example
1638
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=count-all
1639
+ prisma.$from("User")
1640
+ .select(({ countAll }) => countAll(), "total");
1641
+ ```
975
1642
 
976
- ## Install
1643
+ ##### SQL
1644
+ ```sql file=../usage-sqlite-v7/tests/readme/select-fns.ts region=count-all-sql
1645
+ SELECT COUNT(*) AS `total`
1646
+ FROM User;
1647
+ ```
977
1648
 
978
- ```shell
979
- npm i prisma-ts-select
980
- pnpm add prisma-ts-select
1649
+ #### `count(col)` — COUNT(col)
1650
+
1651
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=count-col
1652
+ prisma.$from("User")
1653
+ .select(({ count }) => count("User.id"), "cnt");
981
1654
  ```
982
1655
 
983
- ## Setup
1656
+ #### `countDistinct(col)` — COUNT(DISTINCT col)
984
1657
 
985
- ### Extract
1658
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=count-distinct
1659
+ prisma.$from("User")
1660
+ .select(({ countDistinct }) => countDistinct("User.id"), "cnt");
1661
+ ```
986
1662
 
1663
+ #### `sum(col)` / `avg(col)` / `min(col)` / `max(col)`
987
1664
 
988
- ## Usage
1665
+ Standard numeric aggregates. **Return types vary by dialect** — `sum` and `avg` return `Decimal` on MySQL (matching Prisma's numeric precision model), `number` on SQLite and PostgreSQL. `min`/`max` always return `T | null` (NULL for empty sets) where `T` is the column's TypeScript type.
1666
+
1667
+ | Function | SQLite | MySQL | PostgreSQL |
1668
+ |---|---|---|---|
1669
+ | `sum(col)` | `number` | `Decimal` | `number` |
1670
+ | `avg(col)` | `number` | `Decimal` | `number` |
1671
+ | `min(col)` | `T \| null` | `T \| null` | `T \| null` |
1672
+ | `max(col)` | `T \| null` | `T \| null` | `T \| null` |
1673
+
1674
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=sum
1675
+ prisma.$from("User")
1676
+ .select(({ sum }) => sum("User.age"), "total");
1677
+ ```
1678
+
1679
+ #### String Functions (all dialects)
1680
+
1681
+ | Function | SQL | Returns |
1682
+ |---|---|---|
1683
+ | `upper(col)` | `UPPER(col)` | `string` |
1684
+ | `lower(col)` | `LOWER(col)` | `string` |
1685
+ | `length(col)` | `LENGTH(col)` | `number` |
1686
+ | `trim(col)` | `TRIM(col)` | `string` |
1687
+ | `ltrim(col)` | `LTRIM(col)` | `string` |
1688
+ | `rtrim(col)` | `RTRIM(col)` | `string` |
1689
+ | `replace(col, from, to)` | `REPLACE(col, 'from', 'to')` | `string` |
1690
+
1691
+ > **Note:** MySQL `LENGTH()` returns byte-length (not char-length). For character-length on multi-byte strings use a dialect-specific fn.
1692
+
1693
+ #### DateTime Functions (all dialects)
1694
+
1695
+ All dialects provide these functions. Return types differ for `year`/`month`/`day`/`hour`/`minute`/`second` — see note below.
1696
+
1697
+ | Function | SQL (MySQL / PG / SQLite) | Returns |
1698
+ |---|---|---|
1699
+ | `now()` | `NOW()` / `NOW()` / `datetime('now')` | `Date` |
1700
+ | `curDate()` | `CURDATE()` / `CURRENT_DATE` / `date('now')` | `Date` |
1701
+ | `year(col)` | `YEAR(col)` / `EXTRACT(YEAR FROM col)::integer` / `strftime('%Y', col)` | `number` (SQLite: `string`) |
1702
+ | `month(col)` | `MONTH(col)` / `EXTRACT(MONTH FROM col)::integer` / `strftime('%m', col)` | `number` (SQLite: `string`) |
1703
+ | `day(col)` | `DAY(col)` / `EXTRACT(DAY FROM col)::integer` / `strftime('%d', col)` | `number` (SQLite: `string`) |
1704
+ | `hour(col)` | `HOUR(col)` / `EXTRACT(HOUR FROM col)::integer` / `strftime('%H', col)` | `number` (SQLite: `string`) |
1705
+ | `minute(col)` | `MINUTE(col)` / `EXTRACT(MINUTE FROM col)::integer` / `strftime('%M', col)` | `number` (SQLite: `string`) |
1706
+ | `second(col)` | `SECOND(col)` / `EXTRACT(SECOND FROM col)::integer` / `strftime('%S', col)` | `number` (SQLite: `string`) |
1707
+
1708
+ > **Note:** `year`, `month`, `day`, `hour`, `minute`, and `second` return `string` on SQLite because `strftime()` always returns text (e.g. `'2024'`, `'03'`). MySQL and PostgreSQL return `number`.
1709
+
1710
+ DateTime column args also accept `SQLExpr<Date>`, enabling composition:
1711
+
1712
+ ```typescript
1713
+ prisma.$from("Post").select(({ year, now }) => year(now()), "y");
1714
+ ```
1715
+
1716
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=upper
1717
+ prisma.$from("User")
1718
+ .select(({ upper }) => upper("User.name"), "uname");
1719
+ ```
1720
+
1721
+ String fns accept a `SQLExpr<string>` as input, enabling composition:
1722
+
1723
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=lower
1724
+ prisma.$from("User")
1725
+ .select(({ lower }) => lower("User.name"), "lname");
1726
+ ```
1727
+
1728
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=replace
1729
+ prisma.$from("User")
1730
+ .select(({ replace }) => replace("User.email", "@example.com", ""), "handle");
1731
+ ```
1732
+
1733
+ #### Math Functions (all dialects)
1734
+
1735
+ | Function | SQL | Returns |
1736
+ |---|---|---|
1737
+ | `abs(col)` | `ABS(col)` | `number` |
1738
+ | `ceil(col)` | `CEIL(col)` | `number` |
1739
+ | `floor(col)` | `FLOOR(col)` | `number` |
1740
+ | `round(col, decimals?)` | `ROUND(col)` / `ROUND(col, n)` | `number` |
1741
+ | `power(base, exp)` | `POWER(base, exp)` | `number` |
1742
+ | `sqrt(col)` | `SQRT(col)` | `number` |
1743
+ | `mod(col, divisor)` | `MOD(col, divisor)` | `number` |
1744
+ | `sign(col)` | `SIGN(col)` | `number` |
1745
+ | `exp(col)` | `EXP(col)` | `number` |
1746
+
1747
+ Math fns accept `SQLExpr<number>` or a column reference, enabling composition:
1748
+
1749
+ ```typescript
1750
+ // Absolute value of a literal
1751
+ prisma.$from("User")
1752
+ .select(({ abs, lit }) => abs(lit(-5)), "absVal");
1753
+
1754
+ // Round to 2 decimal places
1755
+ prisma.$from("User")
1756
+ .select(({ round, lit }) => round(lit(4.567), 2), "val");
1757
+
1758
+ // Compose: sqrt(power(x, 2))
1759
+ prisma.$from("User")
1760
+ .select(({ sqrt, power }) => sqrt(power("User.age", 2)), "val");
1761
+ ```
1762
+
1763
+ #### Control Flow Functions (all dialects)
1764
+
1765
+ | Function | SQL | Returns |
1766
+ |---|---|---|
1767
+ | `cond(criteria)` | *(WhereCriteria → SQL condition string)* | `SQLExpr<boolean>` |
1768
+ | `coalesce(...args)` | `COALESCE(a, b, ...)` | `SQLExpr<T>` |
1769
+ | `nullif(expr1, expr2)` | `NULLIF(a, b)` | `SQLExpr<T \| null>` |
1770
+ | `caseWhen(cases, elseVal?)` | `CASE WHEN ... THEN ... END` | `SQLExpr<T \| null>` |
1771
+
1772
+ `cond()` converts a `WhereCriteria` object into a `SQLExpr<unknown>` — useful when you need a condition expression outside of a dedicated function. Note: `$if()`/`iif()` and `caseWhen()` all accept `WhereCriteria` directly, so `cond()` is rarely needed.
1773
+
1774
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns-control-flow.ts region=coalesce
1775
+ prisma.$from("User")
1776
+ .select(({ coalesce, lit }) => coalesce("User.email", lit("unknown")), "contact")
1777
+ ```
1778
+
1779
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns-control-flow.ts region=nullif
1780
+ prisma.$from("User")
1781
+ .select(({ nullif, lit }) => nullif(lit(0), lit(0)), "val")
1782
+ ```
1783
+
1784
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns-control-flow.ts region=case-when
1785
+ prisma.$from("User")
1786
+ .select(({ caseWhen, lit }) => caseWhen([
1787
+ { when: { age: { op: ">=", value: 18 } }, then: lit("adult") },
1788
+ ], lit("minor")), "status")
1789
+ ```
1790
+
1791
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns-control-flow.ts region=cond
1792
+ prisma.$from("User")
1793
+ .select(({ cond }) => cond({ age: { op: ">", value: 0 } }), "flag")
1794
+ ```
1795
+
1796
+ #### Combining with `.groupBy()`
1797
+
1798
+ ```typescript file=../usage-sqlite-v7/tests/readme/select-fns.ts region=count-groupby
1799
+ prisma.$from("User")
1800
+ .join("Post", "authorId", "User.id")
1801
+ .groupBy(["User.name"])
1802
+ .select("User.name")
1803
+ .select(({ countAll }) => countAll(), "postCount");
1804
+ ```
1805
+
1806
+ ##### SQL
1807
+ ```sql
1808
+ SELECT User.name, COUNT(*) AS `postCount`
1809
+ FROM User
1810
+ JOIN Post ON Post.authorId = User.id
1811
+ GROUP BY User.name;
1812
+ ```
1813
+
1814
+ ---
1815
+
1816
+ ### MySQL-specific
1817
+
1818
+ | Function | SQL | Returns |
1819
+ |---|---|---|
1820
+ | `distinct(col)` | `DISTINCT col` | `ColType` (use inside `avg`, `sum`, `count`, `groupConcat`) |
1821
+ | `groupConcat(col, sep?)` | `GROUP_CONCAT(col SEPARATOR sep)` | `string` |
1822
+ | `bitAnd(col)` | `BIT_AND(col)` | `number` |
1823
+ | `bitOr(col)` | `BIT_OR(col)` | `number` |
1824
+ | `bitXor(col)` | `BIT_XOR(col)` | `number` |
1825
+ | `stddev(col)` | `STDDEV(col)` | `number` |
1826
+ | `stddevSamp(col)` | `STDDEV_SAMP(col)` | `number` |
1827
+ | `variance(col)` | `VARIANCE(col)` | `number` |
1828
+ | `varSamp(col)` | `VAR_SAMP(col)` | `number` |
1829
+ | `jsonArrayAgg(col)` | `JSON_ARRAYAGG(col)` | `JSONValue` |
1830
+ | `jsonObjectAgg(key, val)` | `JSON_OBJECTAGG(key, val)` | `JSONValue` |
1831
+ | `concat(...cols)` | `CONCAT(a, b, ...)` | `string` |
1832
+ | `substring(col, start, len?)` | `SUBSTRING(col, start, len)` | `string` |
1833
+ | `left(col, n)` | `LEFT(col, n)` | `string` |
1834
+ | `right(col, n)` | `RIGHT(col, n)` | `string` |
1835
+ | `repeat(col, n)` | `REPEAT(col, n)` | `string` |
1836
+ | `reverse(col)` | `REVERSE(col)` | `string` |
1837
+ | `lpad(col, len, pad)` | `LPAD(col, len, 'pad')` | `string` |
1838
+ | `rpad(col, len, pad)` | `RPAD(col, len, 'pad')` | `string` |
1839
+ | `locate(substr, col)` | `LOCATE('substr', col)` | `number` |
1840
+ | `space(n)` | `SPACE(n)` | `string` |
1841
+ | `$if(cond, trueVal, falseVal)` | `IF(cond, a, b)` | `T` |
1842
+ | `ifNull(col, fallback)` | `IFNULL(col, fallback)` | `NonNullable<T>` |
1843
+ | `greatest(...args)` | `GREATEST(a, b, ...)` | `T \| null` |
1844
+ | `least(...args)` | `LEAST(a, b, ...)` | `T \| null` |
1845
+ | `dateAdd(col, n, unit)` | `DATE_ADD(col, INTERVAL n unit)` | `Date` |
1846
+ | `dateSub(col, n, unit)` | `DATE_SUB(col, INTERVAL n unit)` | `Date` |
1847
+ | `dateFormat(col, fmt)` | `DATE_FORMAT(col, 'fmt')` | `string` |
1848
+ | `dateDiff(d1, d2)` | `DATEDIFF(d1, d2)` | `number` |
1849
+ | `quarter(col)` | `QUARTER(col)` | `number` |
1850
+ | `weekOfYear(col)` | `WEEKOFYEAR(col)` | `number` |
1851
+ | `dayName(col)` | `DAYNAME(col)` | `string` |
1852
+ | `lastDay(col)` | `LAST_DAY(col)` | `Date` |
1853
+ | `pi()` | `PI()` | `number` |
1854
+ | `ln(x)` | `LN(x)` | `number` |
1855
+ | `log(x)` | `LOG(x)` | `number` |
1856
+ | `log2(x)` | `LOG2(x)` | `number` |
1857
+ | `log10(x)` | `LOG10(x)` | `number` |
1858
+ | `truncate(x, n)` | `TRUNCATE(x, n)` | `number` |
1859
+ | `rand(seed?)` | `RAND()` / `RAND(seed)` | `number` |
1860
+
1861
+ > **Note:** MySQL `LOG(x)` is natural log (ln). `rand()` returns a float in [0, 1).
1862
+
1863
+ `unit` is one of: `'MICROSECOND' | 'SECOND' | 'MINUTE' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'QUARTER' | 'YEAR'`
1864
+
1865
+ > **Note:** `jsonArrayAgg` and `jsonObjectAgg` require MySQL 5.7.22+.
1866
+
1867
+ ---
1868
+
1869
+ ### PostgreSQL-specific
1870
+
1871
+ | Function | SQL | Returns |
1872
+ |---|---|---|
1873
+ | `greatest(...args)` | `GREATEST(a, b, ...)` | `T` |
1874
+ | `least(...args)` | `LEAST(a, b, ...)` | `T` |
1875
+ | `distinct(col)` | `DISTINCT col` | `ColType` (use inside `avg`, `sum`, `count`, `stringAgg`, `arrayAgg`) |
1876
+ | `stringAgg(col, sep)` | `STRING_AGG(col, sep)` | `string` |
1877
+ | `arrayAgg(col)` | `ARRAY_AGG(col)` | `unknown[]` |
1878
+ | `stddevPop(col)` | `STDDEV_POP(col)` | `number` |
1879
+ | `stddevSamp(col)` | `STDDEV_SAMP(col)` | `number` |
1880
+ | `varPop(col)` | `VAR_POP(col)` | `number` |
1881
+ | `varSamp(col)` | `VAR_SAMP(col)` | `number` |
1882
+ | `boolAnd(col)` | `BOOL_AND(col)` | `boolean` |
1883
+ | `boolOr(col)` | `BOOL_OR(col)` | `boolean` |
1884
+ | `jsonAgg(col)` | `JSON_AGG(col)` | `JSONValue[]` |
1885
+ | `bitAnd(col)` | `BIT_AND(col)` | `number` |
1886
+ | `bitOr(col)` | `BIT_OR(col)` | `number` |
1887
+ | `jsonObjectAgg(key, val)` | `JSON_OBJECT_AGG(key, val)` | `JSONValue` |
1888
+ | `concat(...cols)` | `CONCAT(a, b, ...)` | `string` |
1889
+ | `substring(col, start, len?)` | `SUBSTRING(col, start, len)` | `string` |
1890
+ | `left(col, n)` | `LEFT(col, n)` | `string` |
1891
+ | `right(col, n)` | `RIGHT(col, n)` | `string` |
1892
+ | `repeat(col, n)` | `REPEAT(col, n)` | `string` |
1893
+ | `reverse(col)` | `REVERSE(col)` | `string` |
1894
+ | `lpad(col, len, pad)` | `LPAD(col, len, 'pad')` | `string` |
1895
+ | `rpad(col, len, pad)` | `RPAD(col, len, 'pad')` | `string` |
1896
+ | `initcap(col)` | `INITCAP(col)` | `string` |
1897
+ | `strpos(col, substr)` | `STRPOS(col, 'substr')` | `number` |
1898
+ | `splitPart(col, delimiter, field)` | `SPLIT_PART(col, 'delimiter', field)` | `string` |
1899
+ | `btrim(col, chars?)` | `BTRIM(col)` / `BTRIM(col, 'chars')` | `string` |
1900
+ | `md5(col)` | `MD5(col)` | `string` |
1901
+ | `extract(field, col)` | `EXTRACT(field FROM col)` | `number` |
1902
+ | `dateTrunc(unit, col)` | `DATE_TRUNC('unit', col)` | `Date` |
1903
+ | `age(ts1, ts2?)` | `AGE(ts1)` / `AGE(ts1, ts2)` | `string` (PG `interval` mapped to string) |
1904
+ | `toDate(text, fmt)` | `TO_DATE(text, 'fmt')` | `Date` |
1905
+ | `pi()` | `PI()` | `number` |
1906
+ | `ln(x)` | `LN(x)` | `number` |
1907
+ | `log(x)` | `LOG(x)` | `number` |
1908
+ | `logBase(base, x)` | `LOG(base, x)` | `number` |
1909
+ | `trunc(x, n?)` | `TRUNC(x)` / `TRUNC(x, n)` | `number` |
1910
+ | `div(x, y)` | `DIV(x, y)` | `number` |
1911
+ | `random()` | `RANDOM()` | `number` |
1912
+
1913
+ > **Note:** PG `LOG(x)` is log base 10 (unlike MySQL where it is natural log). `random()` returns a float in [0, 1).
1914
+
1915
+ `field` for `extract` is one of: `'YEAR' | 'MONTH' | 'DAY' | 'HOUR' | 'MINUTE' | 'SECOND' | 'DOW' | 'DOY' | 'EPOCH' | 'WEEK' | 'QUARTER'`
1916
+
1917
+ `unit` for `dateTrunc` is one of: `'microseconds' | 'milliseconds' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'decade' | 'century' | 'millennium'`
1918
+
1919
+ ---
1920
+
1921
+ ### SQLite-specific
1922
+
1923
+ | Function | SQL | Returns |
1924
+ |---|---|---|
1925
+ | `iif(cond, trueVal, falseVal)` | `IIF(cond, a, b)` | `T` |
1926
+ | `ifNull(col, fallback)` | `IFNULL(col, fallback)` | `NonNullable<T>` |
1927
+ | `distinct(col)` | `DISTINCT col` | `ColType` (use inside `avg`, `sum`, `count`, `groupConcat`; sep with ≥ 3.44) |
1928
+ | `groupConcat(col, sep?)` | `GROUP_CONCAT(col, sep)` | `string` |
1929
+ | `total(col)` | `TOTAL(col)` | `number` |
1930
+ | `concat(...cols)` | `a \|\| b \|\| ...` | `string` |
1931
+ | `substr(col, start, len?)` | `SUBSTR(col, start, len)` | `string` |
1932
+ | `instr(col, substr)` | `INSTR(col, 'substr')` | `number` |
1933
+ | `char(...codes)` | `CHAR(n1, n2, ...)` | `string` |
1934
+ | `hex(col)` | `HEX(col)` | `string` |
1935
+ | `unicode(col)` | `UNICODE(col)` | `number` |
1936
+ | `strftime(fmt, col)` | `strftime('fmt', col)` | `string` |
1937
+ | `julianday(col)` | `julianday(col)` | `number` |
1938
+ | `date(col)` | `date(col)` | `string` |
1939
+ | `datetime(col)` | `datetime(col)` | `string` |
1940
+ | `random()` | `RANDOM()` | `number` |
1941
+ | `log(x)` | `LOG(x)` | `number` |
1942
+ | `log2(x)` | `LOG2(x)` | `number` |
1943
+ | `log10(x)` | `LOG10(x)` | `number` |
1944
+
1945
+ > **Note:** SQLite `random()` returns a random integer (not float). `log`, `log2`, `log10` require SQLite 3.35+. `total()` behaves like `SUM()` but returns `0.0` instead of `NULL` for empty sets. SQLite uses the `||` operator for string concatenation.
1946
+
1947
+ > **Note:** SQLite stores `DateTime` differently between Prisma v6 (integer milliseconds) and v7 (ISO 8601 text). All SQLite datetime fns automatically normalise both formats via a `CASE WHEN typeof(...) = 'integer' THEN datetime(.../1000, 'unixepoch') ELSE ... END` wrapper, so they work correctly on both versions.
1948
+
1949
+ ---
1950
+
1951
+ ## Security
1952
+
1953
+ All queries execute via Prisma's `$queryRawUnsafe`. The `esc()` helper escapes single quotes in string literals, but **never pass user-supplied values directly** to query builder methods — use Prisma's parameterized queries for user input.
1954
+
1955
+ `whereRaw()` in particular inserts SQL verbatim. Only use it with trusted, static SQL strings.
1956
+
1957
+ ## Future updates
1958
+
1959
+ - Support specifying `JOIN` type [issue#2](https://github.com/adrianbrowning/prisma-ts-select/issues/2)
1960
+ - Support additional Select Functions
1961
+ - [JSON #9](https://github.com/adrianbrowning/prisma-ts-select/issues/9)
1962
+ - [CAST #71](https://github.com/adrianbrowning/prisma-ts-select/issues/71)
1963
+ - [whereRaw supporting Prisma.sql](https://github.com/adrianbrowning/prisma-ts-select/issues/29)
1964
+
1965
+ ## Changelog / Versioning
1966
+ Changelog is available [here](https://github.com/adrianbrowning/prisma-ts-select/releases). We use [semantic versioning](https://semver.org/) for versioning.
1967
+
1968
+ ## License
1969
+ This project is licensed under the MIT License. See the LICENSE file for details.
989
1970
 
990
1971