@zerotal/arch 1.7.0

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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,465 @@
1
+ ---
2
+ title: Query Builder
3
+ description: Build parameterized SQL with a fluent, model-free chain that runs only when you call a terminal method.
4
+ ---
5
+
6
+ # Query Builder
7
+
8
+ The query builder is Zerotal's fluent, model-free interface for building SQL. You
9
+ get one by calling `DB.table(name)` — every clause method returns the builder, so
10
+ calls chain, and nothing runs until you call a terminal like `get()`, `first()`,
11
+ or `count()`. Values are always parameterized, never interpolated.
12
+
13
+ ```typescript
14
+ // in a controller or service
15
+ import { DB } from "@zerotal/orm";
16
+
17
+ const builder = DB.table("posts"); // a QueryBuilder bound to the "posts" table
18
+ ```
19
+
20
+ > **Note** — The same builder powers the [ORM](/docs/orm/queries) — `Model.query()`
21
+ > returns a `ModelQueryBuilder` that adds relations, casts, and model hydration on top
22
+ > of everything here. For transactions, raw SQL, replicas, and N+1 detection, see
23
+ > [Database](/docs/database).
24
+
25
+ ## Getting Started
26
+
27
+ The query builder ships with `@zerotal/orm`. If you have the
28
+ [database](/docs/database) set up there is nothing further to install:
29
+
30
+ ```typescript
31
+ import { DB } from "@zerotal/orm";
32
+ ```
33
+
34
+ ## When to reach for it
35
+
36
+ The query builder sits between two neighbours, and picking the right one keeps your
37
+ code both safe and readable:
38
+
39
+ - **Use a model (`Post.query()`)** when you're working with a table that has a model
40
+ and you want relations, casts, accessors, or hydrated instances back. This is the
41
+ default for domain logic — you get typed records and lifecycle hooks.
42
+ - **Use the query builder (`DB.table()`)** for tables without a model (pivot tables,
43
+ reporting views, ad-hoc joins), for bulk writes where you don't need model events,
44
+ and for read-heavy aggregate or analytics queries where hydrating models is wasted
45
+ work. It returns plain rows, so it's lighter.
46
+ - **Drop to raw SQL** (`whereRaw`, `selectRaw`, or `DB.raw` — see
47
+ [Database](/docs/database)) only for the slice a clause method can't express. Keep
48
+ the rest of the query fluent so you don't lose parameterization.
49
+
50
+ Because nothing executes until a terminal method, you can build a query up across
51
+ several lines, branches, or helper functions and pass the builder around freely — it's
52
+ just a description of a query until you `await` it.
53
+
54
+ ## Selecting columns
55
+
56
+ ```typescript
57
+ // in a controller or service
58
+ DB.table("posts").select("id", "title", "created_at");
59
+ DB.table("posts").distinct().select("status");
60
+ ```
61
+
62
+ With no `select()`, all columns (`*`) are returned. For computed columns use
63
+ `selectRaw("price * quantity AS revenue")` — the expression is injected verbatim,
64
+ so build it only from trusted constants.
65
+
66
+ ## Where clauses
67
+
68
+ ```typescript
69
+ // in a controller or service
70
+ DB.table("posts").where("status", "published"); // column = value
71
+ DB.table("posts").where("views", ">", 1000); // explicit operator
72
+ DB.table("posts").where("title", "like", "%bun%");
73
+
74
+ DB.table("posts").where("status", "published").orWhere("featured", true); // OR
75
+
76
+ DB.table("users").whereIn("id", [1, 2, 3]);
77
+ DB.table("users").whereNotIn("role", ["banned", "guest"]);
78
+ DB.table("posts").whereNull("deleted_at");
79
+ DB.table("posts").whereNotNull("published_at");
80
+ DB.table("posts").whereBetween("views", [100, 1000]);
81
+ DB.table("posts").whereLike("title", "%release%");
82
+ DB.table("users").whereJson("preferences->theme", "dark"); // JSON column path
83
+ ```
84
+
85
+ | Method | SQL |
86
+ | -------------------------------------------------- | ------------------------------------------------------------------------ |
87
+ | `where(col, value)` | `col = ?` |
88
+ | `where(col, op, value)` | `col <op> ?` |
89
+ | `orWhere(...)` | `OR …` |
90
+ | `whereIn(col, values)` / `whereNotIn(col, values)` | `col IN (…)` / `NOT IN` |
91
+ | `whereNull(col)` / `whereNotNull(col)` | `col IS NULL` / `IS NOT NULL` |
92
+ | `whereBetween(col, [a, b])` | `col BETWEEN ? AND ?` |
93
+ | `whereLike(col, pattern)` | `col LIKE ?` |
94
+ | `whereColumn(a, op, b)` | `a <op> b` — compare two columns |
95
+ | `whereJson(col, value)` | match a JSON column (see [Database](/docs/database#json-column-queries)) |
96
+
97
+ Most filters have an `orX` partner (`orWhereIn`, `orWhereNull`, `orWhereLike`, …)
98
+ that joins with `OR` instead of `AND`, plus date helpers (`whereDate`, `whereMonth`,
99
+ `whereYear`) and subquery filters (`whereExists`, `whereNotExists`). For anything the
100
+ fluent methods don't cover, drop to `whereRaw(sql, bindings)` — bindings keep it
101
+ parameterized.
102
+
103
+ > **Warning** — `whereBetween`, `whereLike`, `whereColumn`, and `whereJson` validate
104
+ > the column name as a SQL identifier and throw on anything outside
105
+ > `[a-zA-Z_][a-zA-Z0-9_.]*`. Never pass a user-controlled string as a column name.
106
+
107
+ ## Joins
108
+
109
+ ```typescript
110
+ // in a controller or service
111
+ DB.table("posts")
112
+ .join("users", "posts.user_id", "=", "users.id")
113
+ .leftJoin("comments", "comments.post_id", "=", "posts.id")
114
+ .select("posts.title", "users.name");
115
+
116
+ DB.table("a").crossJoin("b");
117
+ ```
118
+
119
+ `join`, `leftJoin`, and `rightJoin` take `(table, first, operator, second)`;
120
+ `crossJoin` takes just the table. To join a derived table, use `joinSub(builder,
121
+ alias, first, operator, second)`.
122
+
123
+ ## Ordering, grouping, limits
124
+
125
+ ```typescript
126
+ // in a controller or service
127
+ DB.table("posts").orderBy("created_at", "desc");
128
+ DB.table("posts").orderByDesc("created_at"); // shorthand for the line above
129
+ DB.table("posts").latest(); // orderBy("created_at", "desc")
130
+ DB.table("posts").oldest(); // orderBy("created_at", "asc")
131
+ DB.table("posts").oldest("published_at");
132
+ DB.table("posts").inRandomOrder(); // RANDOM() / RAND()
133
+
134
+ DB.table("posts").groupBy("user_id").having("post_count", ">", 5);
135
+
136
+ DB.table("posts").limit(10).offset(20);
137
+ ```
138
+
139
+ `latest()` / `oldest()` default to the `created_at` column; pass a column name to
140
+ order by something else. `reorder(col?, dir?)` clears all existing `ORDER BY`
141
+ clauses and optionally applies a fresh one.
142
+
143
+ ## Conditional clauses — when
144
+
145
+ Apply clauses only when a condition is truthy — handy for optional filters without
146
+ breaking the chain:
147
+
148
+ ```typescript
149
+ // in a controller or service
150
+ DB.table("posts")
151
+ .when(status, (q, value) => q.where("status", value))
152
+ .when(search, (q, value) => q.whereLike("title", `%${value}%`))
153
+ .orderBy("created_at", "desc");
154
+ ```
155
+
156
+ The callback receives the builder and the (truthy) condition value.
157
+
158
+ ## Retrieving results
159
+
160
+ ```typescript
161
+ // in a controller or service
162
+ const rows = await DB.table("posts").where("status", "published").get();
163
+ const row = await DB.table("posts").where("id", 1).first(); // first row or null
164
+
165
+ const title = await DB.table("posts").where("id", 1).value("title"); // single column
166
+ const ids = await DB.table("posts").pluck("id"); // column → array
167
+ const map = await DB.table("posts").pluck("title", "id"); // keyed by a column
168
+
169
+ const has = await DB.table("posts").where("user_id", userId).exists(); // boolean
170
+ ```
171
+
172
+ Pass a row type to `get<T>()` / `first<T>()` for typed results:
173
+
174
+ ```typescript
175
+ // in a controller or service
176
+ const rows = await DB.table("posts").get<{ id: number; title: string }>();
177
+ ```
178
+
179
+ > **Tip** — `sole<T>()` returns the one matching row and throws if zero or more than
180
+ > one row matches — a guard against accidentally acting on the wrong record.
181
+
182
+ ### Aggregates
183
+
184
+ ```typescript
185
+ // in a controller or service
186
+ await DB.table("posts").count();
187
+ await DB.table("posts").where("status", "published").sum("views");
188
+ await DB.table("posts").avg("rating");
189
+ await DB.table("posts").min("created_at");
190
+ await DB.table("posts").max("views");
191
+ ```
192
+
193
+ For paged result sets, see [Pagination](/docs/pagination).
194
+
195
+ ## Streaming large tables
196
+
197
+ For result sets too large to hold in memory, page through them instead of calling
198
+ `get()`:
199
+
200
+ ```typescript
201
+ // in a command or job
202
+ await DB.table("posts").chunk(500, async (rows, page) => {
203
+ for (const row of rows) await archive(row);
204
+ // return false to stop early
205
+ });
206
+
207
+ for await (const row of DB.table("posts").lazy()) {
208
+ await process(row); // one row at a time
209
+ }
210
+ ```
211
+
212
+ > **Tip** — Prefer `chunkById()` / `lazyById()` when rows may be inserted or deleted
213
+ > during iteration: they page by an incrementing key instead of `OFFSET`, so they
214
+ > never skip or repeat a row.
215
+
216
+ ## Writing rows
217
+
218
+ ```typescript
219
+ // in a controller or service
220
+ // INSERT
221
+ await DB.table("post_tags").insert({ post_id: 1, tag_id: 3 });
222
+
223
+ // UPDATE — scope with where() first
224
+ await DB.table("posts").where("id", 1).update({ status: "published" });
225
+
226
+ // DELETE
227
+ await DB.table("sessions").where("user_id", userId).delete();
228
+
229
+ // Atomic counters
230
+ await DB.table("posts").where("id", 1).increment("views"); // +1
231
+ await DB.table("posts").where("id", 1).increment("views", 10); // +10
232
+ await DB.table("accounts").where("id", 1).decrement("balance", 100);
233
+ ```
234
+
235
+ Use `updateOrInsert(attributes, values)` to update a matching row or insert a merged
236
+ one if none exists; it returns `true` when a row was inserted.
237
+
238
+ > **Danger** — `update()` and `delete()` apply to **every** matching row — always set
239
+ > your `where()` constraints first, or you will overwrite or wipe the whole table.
240
+
241
+ ## Locking
242
+
243
+ Inside a [transaction](/docs/database#transactions), lock the selected rows:
244
+
245
+ ```typescript
246
+ // in a controller or service
247
+ await DB.transaction(async (trx) => {
248
+ const row = await trx.table("accounts").where("id", 1).lockForUpdate().first();
249
+ // … exclusive lock held until the transaction commits
250
+ });
251
+ ```
252
+
253
+ | Method | SQL |
254
+ | ----------------- | ------------------------------ |
255
+ | `lockForUpdate()` | `FOR UPDATE` — exclusive |
256
+ | `sharedLock()` | `FOR SHARE` — shared read lock |
257
+
258
+ > **Note** — Row locks are no-ops on SQLite, which has no `FOR UPDATE` / `FOR SHARE`.
259
+ > `sharedLock()` emits `LOCK IN SHARE MODE` on MySQL and `FOR SHARE` elsewhere.
260
+
261
+ ## Debugging
262
+
263
+ ```typescript
264
+ // in a controller or service
265
+ DB.table("posts").where("status", "published").toSql(); // SQL with ? placeholders
266
+ DB.table("posts").where("status", "published").toRawSql(); // values inlined (logging only)
267
+ DB.table("posts").where("id", 1).dump(); // log SQL + bindings, keep chaining
268
+ await DB.table("posts").where("status", "published").explain(); // EXPLAIN / EXPLAIN QUERY PLAN
269
+ ```
270
+
271
+ > **Warning** — `toRawSql()` inlines bindings for readability and is **not** safe to
272
+ > execute. Use it for logging only — `toSql()` plus `toSqlWithBindings()` give you the
273
+ > parameterized form.
274
+
275
+ ## Recipes
276
+
277
+ A few patterns that come up constantly, shown end to end.
278
+
279
+ ### A filtered, sorted, paginated listing
280
+
281
+ The bread and butter of any index page or list endpoint. `when()` lets every filter
282
+ be optional without a tangle of `if` statements, and `paginate()` returns the rows
283
+ plus the page metadata in one call:
284
+
285
+ ```typescript
286
+ // in a controller — req.query holds the optional filters
287
+ const posts = await DB.table("posts")
288
+ .when(req.query.status, (q, status) => q.where("status", status))
289
+ .when(req.query.search, (q, term) => q.whereLike("title", `%${term}%`))
290
+ .when(req.query.author, (q, id) => q.where("user_id", id))
291
+ .latest() // newest first
292
+ .paginate(20, req.query.page ?? 1);
293
+ ```
294
+
295
+ Each `when()` only fires when its value is truthy, so an empty filter is simply
296
+ skipped. See [Pagination](/docs/pagination) for the shape of the returned object.
297
+
298
+ ### Idempotent pivot / settings writes
299
+
300
+ For join tables and key/value rows, `updateOrInsert` avoids the "check then insert"
301
+ race — it updates the row matching the first argument, or inserts the two merged if
302
+ none exists:
303
+
304
+ ```typescript
305
+ await DB.table("user_settings").updateOrInsert(
306
+ { user_id: userId, key: "theme" }, // how to find the row
307
+ { value: "dark" }, // what to set
308
+ );
309
+ ```
310
+
311
+ ### A lightweight report without models
312
+
313
+ When you only need numbers, skip model hydration entirely and let the database do the
314
+ aggregation:
315
+
316
+ ```typescript
317
+ const byAuthor = await DB.table("posts")
318
+ .select("user_id")
319
+ .selectRaw("COUNT(*) AS post_count")
320
+ .where("status", "published")
321
+ .groupBy("user_id")
322
+ .having("post_count", ">", 5)
323
+ .orderByDesc("post_count")
324
+ .get<{ user_id: number; post_count: number }>();
325
+ ```
326
+
327
+ ### Backfilling a large table safely
328
+
329
+ Never load a big table with `get()`. Page through it with `chunkById`, which walks an
330
+ incrementing key so concurrent inserts or deletes can't make it skip or repeat rows:
331
+
332
+ ```typescript
333
+ // in a command or job
334
+ await DB.table("posts")
335
+ .whereNull("slug")
336
+ .chunkById(500, async (rows) => {
337
+ for (const row of rows) {
338
+ await DB.table("posts")
339
+ .where("id", row.id)
340
+ .update({ slug: slugify(row.title) });
341
+ }
342
+ });
343
+ ```
344
+
345
+ ### Guarding against the wrong row
346
+
347
+ When exactly one row should match — looking a user up by email, say — `sole()` turns
348
+ "zero or many matches" into a thrown error instead of a silent bug:
349
+
350
+ ```typescript
351
+ const user = await DB.table("users").where("email", email).sole();
352
+ ```
353
+
354
+ ## Testing
355
+
356
+ Set your suite up once as described in [Testing](/docs/testing). The builder
357
+ gives you two ways to test a query, and they answer different questions.
358
+
359
+ **Assert the SQL when the shape is the point.** `toSql()` returns the statement
360
+ with placeholders and `toSqlWithBindings()` adds the values, so a complex
361
+ condition can be pinned without touching the database:
362
+
363
+ ```typescript
364
+ // tests/queries/ActiveSubscribers.test.ts
365
+ import { test, expect } from "bun:test";
366
+ import { DB } from "@zerotal/orm";
367
+
368
+ test("only active, non-trial subscribers are selected", () => {
369
+ const { sql, bindings } = DB.table("users")
370
+ .where("status", "active")
371
+ .whereNull("trial_ends_at")
372
+ .toSqlWithBindings();
373
+
374
+ expect(sql).toContain('where "status" = ?');
375
+ expect(sql).toContain('"trial_ends_at" is null');
376
+ expect(bindings).toEqual(["active"]);
377
+ });
378
+ ```
379
+
380
+ This catches the mistake that matters most in a query builder — a condition
381
+ silently dropped by a mis-chained `orWhere` — without needing rows to prove it.
382
+
383
+ **Assert the rows when the result is the point.** Arrange with factories and run
384
+ the query for real:
385
+
386
+ ```typescript
387
+ // tests/queries/ActiveSubscribers.test.ts
388
+ test("excludes users still in trial", async () => {
389
+ await UserFactory.create({ status: "active", trialEndsAt: null });
390
+ await UserFactory.create({ status: "active", trialEndsAt: new Date() });
391
+
392
+ const rows = await DB.table("users").where("status", "active").whereNull("trial_ends_at").get();
393
+
394
+ expect(rows).toHaveLength(1);
395
+ });
396
+ ```
397
+
398
+ > **Warning** — `toRawSql()` inlines the bindings for display. It is for reading
399
+ > in a log or a failure message, never for executing: a value with a quote in it
400
+ > produces a statement you do not want to run.
401
+
402
+ ## References
403
+
404
+ ### Filtering
405
+
406
+ | Method | Signature | Description |
407
+ | ---------------------------------------- | ------------------------------------------------- | ---------------------------------------------- |
408
+ | `where` | `where(col, value)` / `where(col, op, value)` | `AND` equality or operator clause. |
409
+ | `orWhere` | `orWhere(col, value)` / `orWhere(col, op, value)` | Same, joined with `OR`. |
410
+ | `whereIn` / `whereNotIn` | `whereIn(col, values[])` | `col IN (…)` / `NOT IN`. |
411
+ | `whereNull` / `whereNotNull` | `whereNull(col)` | `IS NULL` / `IS NOT NULL`. |
412
+ | `whereBetween` / `whereNotBetween` | `whereBetween(col, [a, b])` | Range bounds (inclusive). |
413
+ | `whereColumn` | `whereColumn(a, op?, b)` | Compare two columns. |
414
+ | `whereLike` / `whereNotLike` | `whereLike(col, pattern)` | `LIKE` / `NOT LIKE`. |
415
+ | `whereDate` / `whereMonth` / `whereYear` | `whereDate(col, op?, value)` | Match a date part. |
416
+ | `whereExists` / `whereNotExists` | `whereExists((q) => …)` | Correlated `EXISTS` subquery. |
417
+ | `whereJson` | `whereJson("col->path", value)` | JSON path equality (PostgreSQL/MySQL). |
418
+ | `whereRaw` / `orWhereRaw` | `whereRaw(sql, bindings[])` | Raw clause with parameter bindings. |
419
+ | `when` | `when(condition, (q, value) => …)` | Apply clauses only when `condition` is truthy. |
420
+
421
+ ### Shaping
422
+
423
+ | Method | Signature | Description |
424
+ | --------------------------------- | ---------------------------------------------- | ----------------------------------------------- |
425
+ | `select` | `select(...columns)` | Choose columns (default `*`). |
426
+ | `selectRaw` | `selectRaw(expression)` | Add a raw SELECT expression. |
427
+ | `distinct` | `distinct()` | Emit `SELECT DISTINCT`. |
428
+ | `join` / `leftJoin` / `rightJoin` | `join(table, first, op, second)` | Add a join. |
429
+ | `crossJoin` | `crossJoin(table)` | Cartesian join. |
430
+ | `joinSub` | `joinSub(sub, alias, first, op, second)` | Join a derived table. |
431
+ | `union` / `unionAll` | `union(otherBuilder)` | Combine with another query. |
432
+ | `orderBy` / `orderByDesc` | `orderBy(col, dir?)` | Sort rows. |
433
+ | `latest` / `oldest` | `latest(col = "created_at")` | Sort by a timestamp column. |
434
+ | `inRandomOrder` | `inRandomOrder()` | Random row order. |
435
+ | `reorder` | `reorder(col?, dir?)` | Clear existing `ORDER BY`, optionally re-apply. |
436
+ | `groupBy` / `having` | `groupBy(...cols)` / `having(col, op?, value)` | Group and filter groups. |
437
+ | `limit` / `offset` | `limit(n)` / `offset(n)` | Slice the result set. |
438
+
439
+ ### Terminals
440
+
441
+ | Method | Signature | Description |
442
+ | ------------------------------------------ | ----------------------------------- | --------------------------------------------------- |
443
+ | `get` | `get<T>(): Promise<T[]>` | Run the query, return all rows. |
444
+ | `first` | `first<T>(): Promise<T \| null>` | First matching row or `null`. |
445
+ | `sole` | `sole<T>(): Promise<T>` | The one row; throws on zero or many. |
446
+ | `value` | `value<V>(col): Promise<V \| null>` | Single column of the first row. |
447
+ | `pluck` | `pluck<V>(col, key?)` | Array of one column, or object keyed by `key`. |
448
+ | `exists` / `doesntExist` | `exists(): Promise<boolean>` | Whether any row matches. |
449
+ | `count` / `sum` / `avg` / `min` / `max` | `count(): Promise<number>` | Aggregates. |
450
+ | `insert` | `insert(data): Promise<void>` | Insert a row. |
451
+ | `update` | `update(data): Promise<void>` | Update matching rows. |
452
+ | `updateOrInsert` | `updateOrInsert(attrs, values?)` | Update a match or insert; returns `true` on insert. |
453
+ | `delete` | `delete(): Promise<void>` | Delete matching rows. |
454
+ | `increment` / `decrement` | `increment(col, amount = 1)` | Atomic counter update. |
455
+ | `chunk` / `chunkById` | `chunk(size, (rows, page) => …)` | Page through a large table. |
456
+ | `lazy` / `lazyById` / `cursor` / `each` | `lazy<T>(size = 1000)` | Stream rows one at a time. |
457
+ | `clone` | `clone(): this` | Deep-copy the builder (used by pagination). |
458
+ | `toSql` / `toSqlWithBindings` / `toRawSql` | `toSql(): string` | Inspect the compiled SQL. |
459
+ | `dump` / `dd` / `explain` | `dump(): this` | Log SQL/bindings or run `EXPLAIN`. |
460
+
461
+ ## Next steps
462
+
463
+ - [Database](/docs/database) — transactions, raw SQL, replicas, multiple connections, N+1 detection.
464
+ - [Pagination](/docs/pagination) — `paginate()`, `simplePaginate()`, and cursor pagination.
465
+ - [ORM Queries](/docs/orm/queries) — the model query builder layered on top of this.