@c9up/atlas 0.2.0 → 0.2.2

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 (234) hide show
  1. package/README.md +8 -0
  2. package/db.darwin-arm64.node +0 -0
  3. package/db.darwin-x64.node +0 -0
  4. package/db.linux-arm64-gnu.node +0 -0
  5. package/db.linux-x64-gnu.node +0 -0
  6. package/db.win32-x64-msvc.node +0 -0
  7. package/dist/AtlasProvider.d.ts +60 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +204 -38
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +17 -0
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js.map +1 -1
  14. package/dist/BaseModel.d.ts +63 -27
  15. package/dist/BaseModel.d.ts.map +1 -1
  16. package/dist/BaseModel.js +108 -51
  17. package/dist/BaseModel.js.map +1 -1
  18. package/dist/BaseRepository.d.ts +11 -1
  19. package/dist/BaseRepository.d.ts.map +1 -1
  20. package/dist/BaseRepository.js +95 -20
  21. package/dist/BaseRepository.js.map +1 -1
  22. package/dist/ConnectionManager.d.ts +106 -0
  23. package/dist/ConnectionManager.d.ts.map +1 -0
  24. package/dist/ConnectionManager.js +228 -0
  25. package/dist/ConnectionManager.js.map +1 -0
  26. package/dist/ModelQuery.d.ts +256 -43
  27. package/dist/ModelQuery.d.ts.map +1 -1
  28. package/dist/ModelQuery.js +899 -231
  29. package/dist/ModelQuery.js.map +1 -1
  30. package/dist/Transaction.d.ts +54 -1
  31. package/dist/Transaction.d.ts.map +1 -1
  32. package/dist/Transaction.js +121 -59
  33. package/dist/Transaction.js.map +1 -1
  34. package/dist/adapters/NapiDbAdapter.d.ts +11 -0
  35. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  36. package/dist/adapters/NapiDbAdapter.js +35 -7
  37. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  38. package/dist/console/contract.d.ts +62 -0
  39. package/dist/console/contract.d.ts.map +1 -0
  40. package/dist/console/contract.js +38 -0
  41. package/dist/console/contract.js.map +1 -0
  42. package/dist/console/factoryCommands.d.ts +23 -0
  43. package/dist/console/factoryCommands.d.ts.map +1 -0
  44. package/dist/console/factoryCommands.js +62 -0
  45. package/dist/console/factoryCommands.js.map +1 -0
  46. package/dist/console/migrationCommands.d.ts +38 -8
  47. package/dist/console/migrationCommands.d.ts.map +1 -1
  48. package/dist/console/migrationCommands.js +157 -50
  49. package/dist/console/migrationCommands.js.map +1 -1
  50. package/dist/console/schemaCheckCommand.d.ts +11 -17
  51. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  52. package/dist/console/schemaCheckCommand.js +21 -15
  53. package/dist/console/schemaCheckCommand.js.map +1 -1
  54. package/dist/console/schemaDumpCommand.d.ts +30 -0
  55. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  56. package/dist/console/schemaDumpCommand.js +69 -0
  57. package/dist/console/schemaDumpCommand.js.map +1 -0
  58. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  59. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  60. package/dist/console/schemaGenerateCommand.js +246 -0
  61. package/dist/console/schemaGenerateCommand.js.map +1 -0
  62. package/dist/console/seederCommands.d.ts +46 -0
  63. package/dist/console/seederCommands.d.ts.map +1 -0
  64. package/dist/console/seederCommands.js +136 -0
  65. package/dist/console/seederCommands.js.map +1 -0
  66. package/dist/decorators/entity.d.ts +13 -2
  67. package/dist/decorators/entity.d.ts.map +1 -1
  68. package/dist/decorators/entity.js +10 -0
  69. package/dist/decorators/entity.js.map +1 -1
  70. package/dist/decorators/hooks.d.ts +3 -3
  71. package/dist/decorators/hooks.d.ts.map +1 -1
  72. package/dist/decorators/hooks.js.map +1 -1
  73. package/dist/events.d.ts +5 -0
  74. package/dist/events.d.ts.map +1 -1
  75. package/dist/events.js.map +1 -1
  76. package/dist/factories.d.ts +8 -0
  77. package/dist/factories.d.ts.map +1 -0
  78. package/dist/factories.js +8 -0
  79. package/dist/factories.js.map +1 -0
  80. package/dist/index.d.ts +11 -5
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +12 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/lucid-schema.d.ts +15 -0
  85. package/dist/lucid-schema.d.ts.map +1 -0
  86. package/dist/lucid-schema.js +16 -0
  87. package/dist/lucid-schema.js.map +1 -0
  88. package/dist/orm.d.ts +14 -0
  89. package/dist/orm.d.ts.map +1 -0
  90. package/dist/orm.js +14 -0
  91. package/dist/orm.js.map +1 -0
  92. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  93. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  94. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  95. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  96. package/dist/query/DmlBuilder.d.ts +62 -0
  97. package/dist/query/DmlBuilder.d.ts.map +1 -0
  98. package/dist/query/DmlBuilder.js +84 -0
  99. package/dist/query/DmlBuilder.js.map +1 -0
  100. package/dist/query/QueryBuilder.d.ts +5 -0
  101. package/dist/query/QueryBuilder.d.ts.map +1 -1
  102. package/dist/query/QueryBuilder.js +7 -0
  103. package/dist/query/QueryBuilder.js.map +1 -1
  104. package/dist/query/RawQueryBuilder.d.ts +47 -0
  105. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  106. package/dist/query/RawQueryBuilder.js +138 -0
  107. package/dist/query/RawQueryBuilder.js.map +1 -0
  108. package/dist/query/interpolate.d.ts +45 -0
  109. package/dist/query/interpolate.d.ts.map +1 -0
  110. package/dist/query/interpolate.js +51 -0
  111. package/dist/query/interpolate.js.map +1 -0
  112. package/dist/query/native.d.ts +8 -0
  113. package/dist/query/native.d.ts.map +1 -1
  114. package/dist/query/native.js +14 -0
  115. package/dist/query/native.js.map +1 -1
  116. package/dist/query/operators.d.ts +4 -0
  117. package/dist/query/operators.d.ts.map +1 -0
  118. package/dist/query/operators.js +27 -0
  119. package/dist/query/operators.js.map +1 -0
  120. package/dist/schema/Migration.d.ts +31 -7
  121. package/dist/schema/Migration.d.ts.map +1 -1
  122. package/dist/schema/Migration.js +34 -7
  123. package/dist/schema/Migration.js.map +1 -1
  124. package/dist/schema/MigrationRunner.d.ts +56 -6
  125. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  126. package/dist/schema/MigrationRunner.js +382 -112
  127. package/dist/schema/MigrationRunner.js.map +1 -1
  128. package/dist/schema/Schema.d.ts +59 -2
  129. package/dist/schema/Schema.d.ts.map +1 -1
  130. package/dist/schema/Schema.js +143 -9
  131. package/dist/schema/Schema.js.map +1 -1
  132. package/dist/schema/SchemaDumper.d.ts +121 -0
  133. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  134. package/dist/schema/SchemaDumper.js +365 -0
  135. package/dist/schema/SchemaDumper.js.map +1 -0
  136. package/dist/schema/Seeder.d.ts +13 -0
  137. package/dist/schema/Seeder.d.ts.map +1 -1
  138. package/dist/schema/Seeder.js +28 -5
  139. package/dist/schema/Seeder.js.map +1 -1
  140. package/dist/schema/TableBuilder.d.ts +27 -8
  141. package/dist/schema/TableBuilder.d.ts.map +1 -1
  142. package/dist/schema/TableBuilder.js +50 -12
  143. package/dist/schema/TableBuilder.js.map +1 -1
  144. package/dist/schema/catalog.d.ts +25 -1
  145. package/dist/schema/catalog.d.ts.map +1 -1
  146. package/dist/schema/catalog.js +69 -22
  147. package/dist/schema/catalog.js.map +1 -1
  148. package/dist/schema/introspect.d.ts +1 -1
  149. package/dist/schema/introspect.d.ts.map +1 -1
  150. package/dist/schema/introspect.js +26 -11
  151. package/dist/schema/introspect.js.map +1 -1
  152. package/dist/seeders.d.ts +7 -0
  153. package/dist/seeders.d.ts.map +1 -0
  154. package/dist/seeders.js +7 -0
  155. package/dist/seeders.js.map +1 -0
  156. package/dist/services/db.d.ts +120 -9
  157. package/dist/services/db.d.ts.map +1 -1
  158. package/dist/services/db.js +182 -26
  159. package/dist/services/db.js.map +1 -1
  160. package/dist/testing/DatabaseCleanup.d.ts +29 -6
  161. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  162. package/dist/testing/DatabaseCleanup.js +44 -17
  163. package/dist/testing/DatabaseCleanup.js.map +1 -1
  164. package/dist/testing/DbAssertions.d.ts +54 -0
  165. package/dist/testing/DbAssertions.d.ts.map +1 -0
  166. package/dist/testing/DbAssertions.js +156 -0
  167. package/dist/testing/DbAssertions.js.map +1 -0
  168. package/dist/testing/Factory.d.ts +111 -25
  169. package/dist/testing/Factory.d.ts.map +1 -1
  170. package/dist/testing/Factory.js +331 -56
  171. package/dist/testing/Factory.js.map +1 -1
  172. package/dist/testing/TestUtils.d.ts +49 -0
  173. package/dist/testing/TestUtils.d.ts.map +1 -0
  174. package/dist/testing/TestUtils.js +79 -0
  175. package/dist/testing/TestUtils.js.map +1 -0
  176. package/dist/testing/index.d.ts +3 -1
  177. package/dist/testing/index.d.ts.map +1 -1
  178. package/dist/testing/index.js +3 -1
  179. package/dist/testing/index.js.map +1 -1
  180. package/dist/types/relations.d.ts +31 -0
  181. package/dist/types/relations.d.ts.map +1 -0
  182. package/dist/types/relations.js +20 -0
  183. package/dist/types/relations.js.map +1 -0
  184. package/index.darwin-arm64.node +0 -0
  185. package/index.darwin-x64.node +0 -0
  186. package/index.linux-arm64-gnu.node +0 -0
  187. package/index.linux-x64-gnu.node +0 -0
  188. package/index.win32-x64-msvc.node +0 -0
  189. package/package.json +30 -2
  190. package/src/AtlasProvider.ts +297 -51
  191. package/src/BaseEntity.ts +29 -0
  192. package/src/BaseModel.ts +174 -37
  193. package/src/BaseRepository.ts +136 -17
  194. package/src/ConnectionManager.ts +298 -0
  195. package/src/ModelQuery.ts +1309 -260
  196. package/src/Transaction.ts +194 -60
  197. package/src/adapters/NapiDbAdapter.ts +72 -14
  198. package/src/console/contract.ts +96 -0
  199. package/src/console/factoryCommands.ts +73 -0
  200. package/src/console/migrationCommands.ts +253 -63
  201. package/src/console/schemaCheckCommand.ts +26 -22
  202. package/src/console/schemaDumpCommand.ts +96 -0
  203. package/src/console/schemaGenerateCommand.ts +380 -0
  204. package/src/console/seederCommands.ts +174 -0
  205. package/src/decorators/entity.ts +18 -2
  206. package/src/decorators/hooks.ts +5 -3
  207. package/src/events.ts +5 -0
  208. package/src/factories.ts +10 -0
  209. package/src/index.ts +50 -3
  210. package/src/lucid-schema.ts +22 -0
  211. package/src/orm.ts +13 -0
  212. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  213. package/src/query/DmlBuilder.ts +131 -0
  214. package/src/query/QueryBuilder.ts +8 -0
  215. package/src/query/RawQueryBuilder.ts +173 -0
  216. package/src/query/interpolate.ts +73 -0
  217. package/src/query/native.ts +29 -0
  218. package/src/query/operators.ts +27 -0
  219. package/src/schema/Migration.ts +36 -7
  220. package/src/schema/MigrationRunner.ts +483 -119
  221. package/src/schema/Schema.ts +168 -19
  222. package/src/schema/SchemaDumper.ts +518 -0
  223. package/src/schema/Seeder.ts +47 -10
  224. package/src/schema/TableBuilder.ts +48 -11
  225. package/src/schema/catalog.ts +94 -24
  226. package/src/schema/introspect.ts +26 -9
  227. package/src/seeders.ts +16 -0
  228. package/src/services/db.ts +343 -31
  229. package/src/testing/DatabaseCleanup.ts +73 -21
  230. package/src/testing/DbAssertions.ts +220 -0
  231. package/src/testing/Factory.ts +559 -79
  232. package/src/testing/TestUtils.ts +123 -0
  233. package/src/testing/index.ts +12 -1
  234. package/src/types/relations.ts +43 -0
@@ -0,0 +1,2724 @@
1
+ /**
2
+ * Connection-level query builder — Adonis Lucid's `db.query()` / `db.from()` /
3
+ * `db.table()` / `db.insertQuery()`. Unlike {@link ModelQuery} it is NOT bound to
4
+ * a model: it reads and writes plain rows against a table, executing through a
5
+ * connection (or a transaction client passed as `{ client }`).
6
+ *
7
+ * Reads AND writes go through the native compiler directly (`compileStatementNative`),
8
+ * the same path the repository uses — so quoting, casts, the full WHERE grammar
9
+ * (between/like/raw/exists), joins, locks and parameter binding are identical.
10
+ *
11
+ * const rows = await db.from('users').where('is_active', true).orderBy('id')
12
+ * const user = await db.query().from('users').where('id', 1).first()
13
+ * await db.table('audit_logs').insert({ user_id: 1, action: 'login' })
14
+ * await db.from('users').where('id', 1).update({ is_active: false })
15
+ */
16
+
17
+ import type { QueryMeta } from "../adapters/NapiDbAdapter.js";
18
+ import { Paginator } from "../ModelQuery.js";
19
+ import { DmlBuilder, type DmlChainHooks } from "./DmlBuilder.js";
20
+ import {
21
+ type CompiledStatement,
22
+ compiledStatement,
23
+ interpolateQuery,
24
+ } from "./interpolate.js";
25
+ import {
26
+ type AtlasDialect,
27
+ compileStatementNative,
28
+ type DialectName,
29
+ normalizeDialect,
30
+ } from "./native.js";
31
+ import { negateOperator } from "./operators.js";
32
+ import { RawSql, type WhereOperator } from "./QueryBuilder.js";
33
+ import { RawQueryBuilder } from "./RawQueryBuilder.js";
34
+
35
+ /** The minimal execute/query surface a connection or transaction client offers. */
36
+ export interface QueryExecutor {
37
+ query<T = Record<string, unknown>>(
38
+ sql: string,
39
+ params?: unknown[],
40
+ meta?: QueryMeta,
41
+ ): Promise<T[]>;
42
+ execute(sql: string, params?: unknown[], meta?: QueryMeta): Promise<unknown>;
43
+ }
44
+
45
+ /** The Lucid query-builder entry points a transaction client exposes. */
46
+ export interface TransactionQueryBuilders {
47
+ /** Query builder pre-selected on `table` (Lucid `trx.from`). */
48
+ from(table: string): DatabaseQueryBuilder;
49
+ /** Write builder pre-selected on `table` (Lucid `trx.table`). */
50
+ table(table: string): DatabaseQueryBuilder;
51
+ /** An insert builder (Lucid `trx.insertQuery()`). */
52
+ insertQuery(): DatabaseQueryBuilder;
53
+ /**
54
+ * No argument → a query builder (Lucid `trx.query()`). With SQL → run it
55
+ * low-level, preserving the connection-level `query(sql, params)` executor.
56
+ */
57
+ query(): DatabaseQueryBuilder;
58
+ query<T = Record<string, unknown>>(
59
+ sql: string,
60
+ params?: unknown[],
61
+ meta?: QueryMeta,
62
+ ): Promise<T[]>;
63
+ /** A chainable raw query bound to the transaction (Lucid `trx.rawQuery`). */
64
+ rawQuery<T = Record<string, unknown>>(
65
+ sql: string,
66
+ bindings?: unknown[] | Record<string, unknown>,
67
+ ): RawQueryBuilder<T>;
68
+ /** A raw SQL fragment (Lucid `trx.raw(sql, bindings)`). */
69
+ raw(sql: string, params?: unknown[]): RawSql;
70
+ }
71
+
72
+ /** Build the query-builder entry points a transaction client exposes. */
73
+ export function makeTransactionQueryBuilders(
74
+ exec: QueryExecutor,
75
+ dialect: AtlasDialect,
76
+ ): TransactionQueryBuilders {
77
+ // Capture the ORIGINAL low-level executor now — trx assembly does
78
+ // `Object.assign(conn, makeTransactionQueryBuilders(conn, …))`, which
79
+ // overwrites `conn.query` with the dispatcher below. Without this bind, the
80
+ // with-SQL branch would call itself and recurse forever.
81
+ const rawExecQuery = exec.query.bind(exec);
82
+ function query(): DatabaseQueryBuilder;
83
+ function query<T = Record<string, unknown>>(
84
+ sql: string,
85
+ params?: unknown[],
86
+ meta?: QueryMeta,
87
+ ): Promise<T[]>;
88
+ function query(
89
+ sql?: string,
90
+ params?: unknown[],
91
+ meta?: QueryMeta,
92
+ ): DatabaseQueryBuilder | Promise<unknown[]> {
93
+ return sql === undefined
94
+ ? new DatabaseQueryBuilder(exec, dialect)
95
+ : rawExecQuery(sql, params, meta);
96
+ }
97
+ return {
98
+ from: (table) => new DatabaseQueryBuilder(exec, dialect, table),
99
+ table: (table) => new DatabaseQueryBuilder(exec, dialect, table),
100
+ insertQuery: () => new DatabaseQueryBuilder(exec, dialect),
101
+ query,
102
+ rawQuery: (sql, bindings = []) =>
103
+ new RawQueryBuilder(exec, dialect, sql, bindings),
104
+ raw: (sql, params = []) => new RawSql(sql, params),
105
+ };
106
+ }
107
+
108
+ /** One accumulated WHERE — comparison, raw fragment, EXISTS subquery, or JSON. */
109
+ type WhereEntry =
110
+ | {
111
+ kind: "cmp";
112
+ column: string;
113
+ operator: string;
114
+ value: unknown;
115
+ boolean: "and" | "or";
116
+ }
117
+ | { kind: "raw"; sql: string; bindings: unknown[]; boolean: "and" | "or" }
118
+ | {
119
+ kind: "exists";
120
+ negated: boolean;
121
+ subquery: Record<string, unknown>;
122
+ boolean: "and" | "or";
123
+ }
124
+ | {
125
+ kind: "inSub";
126
+ column: string;
127
+ negated: boolean;
128
+ subquery: Record<string, unknown>;
129
+ boolean: "and" | "or";
130
+ }
131
+ | {
132
+ kind: "inTuple";
133
+ columns: string[];
134
+ rows: unknown[][];
135
+ negated: boolean;
136
+ boolean: "and" | "or";
137
+ }
138
+ | {
139
+ kind: "json";
140
+ jsonOp: "path" | "superset" | "subset" | "equals";
141
+ column: string;
142
+ negated: boolean;
143
+ path?: string;
144
+ operator?: string;
145
+ value: unknown;
146
+ boolean: "and" | "or";
147
+ }
148
+ | {
149
+ kind: "group";
150
+ conditions: CompiledWhere[];
151
+ boolean: "and" | "or";
152
+ negated?: boolean;
153
+ };
154
+
155
+ /** The native compiler's WHERE entry JSON (camelCase). */
156
+ type CompiledWhere = Record<string, unknown>;
157
+
158
+ /** A sub-query argument — an explicit builder OR a callback that builds one. */
159
+ type SubqueryArg =
160
+ | DatabaseQueryBuilder
161
+ | ((query: DatabaseQueryBuilder) => void);
162
+
163
+ /** A raw JOIN fragment (Knex/Lucid `joinRaw` / `innerJoin` / `leftJoin`). */
164
+ interface JoinEntry {
165
+ sql: string;
166
+ params: unknown[];
167
+ }
168
+
169
+ /**
170
+ * The `ON` builder passed to the callback form of `join`/`innerJoin`/`leftJoin`/…
171
+ * (Lucid/Knex). `on*` join two columns; `onVal*` bind a column to a value. The
172
+ * `and`/`or` prefix chains conditions. Mirrors {@link ModelQuery}'s JoinBuilder
173
+ * but with no model value-preparation (the db builder is model-agnostic).
174
+ */
175
+ export interface DbJoinBuilder {
176
+ on(left: string, right: string): DbJoinBuilder;
177
+ on(left: string, operator: string, right: string): DbJoinBuilder;
178
+ andOn(left: string, right: string): DbJoinBuilder;
179
+ andOn(left: string, operator: string, right: string): DbJoinBuilder;
180
+ orOn(left: string, right: string): DbJoinBuilder;
181
+ orOn(left: string, operator: string, right: string): DbJoinBuilder;
182
+ onVal(left: string, value: unknown): DbJoinBuilder;
183
+ andOnVal(left: string, value: unknown): DbJoinBuilder;
184
+ orOnVal(left: string, value: unknown): DbJoinBuilder;
185
+ /** `ON col IN (?, ?)` — bound values (Lucid/Knex `onIn`). */
186
+ onIn(left: string, values: unknown[]): DbJoinBuilder;
187
+ /** `ON col NOT IN (?, ?)` — bound values (Lucid/Knex `onNotIn`). */
188
+ onNotIn(left: string, values: unknown[]): DbJoinBuilder;
189
+ /** `ON col IS NULL` (Lucid/Knex `onNull`). */
190
+ onNull(left: string): DbJoinBuilder;
191
+ /** `ON col IS NOT NULL` (Lucid/Knex `onNotNull`). */
192
+ onNotNull(left: string): DbJoinBuilder;
193
+ /** `ON col BETWEEN ? AND ?` — inclusive (Lucid/Knex `onBetween`). */
194
+ onBetween(left: string, range: readonly [unknown, unknown]): DbJoinBuilder;
195
+ /** `ON col NOT BETWEEN ? AND ?` (Lucid/Knex `onNotBetween`). */
196
+ onNotBetween(left: string, range: readonly [unknown, unknown]): DbJoinBuilder;
197
+ /** `ON EXISTS (subquery)` — a builder or a callback (Lucid/Knex `onExists`). */
198
+ onExists(subquery: SubqueryArg): DbJoinBuilder;
199
+ /** `ON NOT EXISTS (subquery)` (Lucid/Knex `onNotExists`). */
200
+ onNotExists(subquery: SubqueryArg): DbJoinBuilder;
201
+ }
202
+
203
+ /**
204
+ * One accumulated `ON` part. `right` is a column ref (with `operator`, default
205
+ * `=`); `value` binds a scalar; `values` binds an `IN`/`NOT IN` list; `between`
206
+ * binds a range; `nullOp` is `IS NULL` / `IS NOT NULL`; `exists` embeds a
207
+ * compiled subquery.
208
+ */
209
+ interface JoinPart {
210
+ kind: "and" | "or";
211
+ left?: string;
212
+ operator?: string;
213
+ right?: string;
214
+ value?: { v: unknown };
215
+ values?: unknown[];
216
+ notIn?: boolean;
217
+ between?: [unknown, unknown];
218
+ notBetween?: boolean;
219
+ nullOp?: "IS NULL" | "IS NOT NULL";
220
+ exists?: { sql: string; params: unknown[]; not: boolean };
221
+ }
222
+
223
+ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
224
+ readonly #exec: QueryExecutor;
225
+ readonly #dialect: AtlasDialect;
226
+ #table: string;
227
+ #selects: string[] = [];
228
+ #wheres: WhereEntry[] = [];
229
+ #orderBys: Array<
230
+ { column: string; direction: "asc" | "desc" } | { raw: string }
231
+ > = [];
232
+ #groupBys: string[] = [];
233
+ #havings: Array<
234
+ | { column: string; operator: string; value: unknown; type: "and" | "or" }
235
+ | { kind: "raw"; sql: string; bindings: unknown[]; type: "and" | "or" }
236
+ > = [];
237
+ #joins: JoinEntry[] = [];
238
+ #unions: Array<{
239
+ sql: string;
240
+ params: unknown[];
241
+ all: boolean;
242
+ op: "union" | "intersect" | "except" | null;
243
+ }> = [];
244
+ #ctes: Array<{
245
+ name: string;
246
+ sql: string;
247
+ params: unknown[];
248
+ recursive: boolean;
249
+ materialized: boolean | null;
250
+ columns?: string[];
251
+ }> = [];
252
+ #schema?: string;
253
+ #lockMode?: string;
254
+ #lockModifier?: string;
255
+ #distinctOn: string[] = [];
256
+ #returningCols: string[] = [];
257
+ #onConflictCols?: string[];
258
+ #mergeMode?: "merge" | "ignore";
259
+ #mergeCols: string[] = [];
260
+ /** Custom merge assignments from `merge({ col: value | db.raw(...) })`. */
261
+ #mergeSet?: Array<{
262
+ column: string;
263
+ value?: unknown;
264
+ raw?: string;
265
+ rawParams?: unknown[];
266
+ }>;
267
+ #distinctFlag = false;
268
+ #limit?: number;
269
+ #offset?: number;
270
+ #debug = false;
271
+ #comments: string[] = [];
272
+ #reporterData?: Record<string, unknown>;
273
+ #fromSubquery?: { sql: string; params: unknown[]; alias: string };
274
+ /**
275
+ * Raw / subquery SELECT fragments that carry their own bound params — Lucid
276
+ * `select(db.raw(sql, bindings))` and `select(subquery.as('x'))`. Rendered into
277
+ * the SELECT list by the native compiler with their placeholders remapped.
278
+ */
279
+ #selectRaw: Array<{ sql: string; params: unknown[] }> = [];
280
+ /**
281
+ * This builder's own alias, set by `.as(alias)`. Consumed when the builder is
282
+ * used as a derived `FROM (…) AS <alias>` or as a `SELECT (…) AS <alias>`
283
+ * subquery — the Lucid/Knex `.as()` convention.
284
+ */
285
+ #alias?: string;
286
+ /** Caller-facing statement timeout in ms (Lucid `timeout(ms)`), applied via a race in the read paths. */
287
+ #timeoutMs?: number;
288
+ /** `timeout(ms, { cancel: true })` — also apply a SERVER-side statement timeout. */
289
+ #cancelTimeout = false;
290
+
291
+ /**
292
+ * When true this builder came from a `db.connection(name, { mode: 'read' })`
293
+ * scope — its write methods throw (Lucid read/write replica guard).
294
+ */
295
+ readonly #readOnly: boolean;
296
+
297
+ constructor(
298
+ exec: QueryExecutor,
299
+ dialect: AtlasDialect,
300
+ table = "",
301
+ options?: { readOnly?: boolean },
302
+ ) {
303
+ this.#exec = exec;
304
+ this.#dialect = dialect;
305
+ this.#table = table;
306
+ this.#readOnly = options?.readOnly ?? false;
307
+ }
308
+
309
+ /** Guard the write methods when the builder is scoped to a read connection. */
310
+ #assertWritable(): void {
311
+ if (this.#readOnly) {
312
+ throw new Error(
313
+ "[atlas] write blocked: this query builder is scoped to a connection opened with { mode: 'read' }. Use { mode: 'write' } for mutations.",
314
+ );
315
+ }
316
+ }
317
+
318
+ /** Select the table (Lucid `db.from`). */
319
+ from(table: string): this;
320
+ /**
321
+ * Select a derived-table source — `FROM (<subquery>) AS <alias>` (Lucid
322
+ * `from(subquery)`). The subquery is a builder OR a callback that builds one.
323
+ */
324
+ from(subquery: SubqueryArg, alias?: string): this;
325
+ from(source: string | SubqueryArg, alias?: string): this {
326
+ if (typeof source === "string") {
327
+ this.#table = source;
328
+ this.#fromSubquery = undefined;
329
+ return this;
330
+ }
331
+ // A callback builds the subquery on a fresh sub-builder. The alias may come
332
+ // from the explicit 2nd arg (atlas DX) OR from `.as()` inside the callback
333
+ // (`from((sub) => sub.from('x').as('totals'))` — the Lucid convention).
334
+ const sub = typeof source === "function" ? this.#buildSub(source) : source;
335
+ const { sql, params } = sub.#compiledNative();
336
+ this.#fromSubquery = {
337
+ sql,
338
+ params,
339
+ alias: alias ?? sub.#alias ?? "derived",
340
+ };
341
+ return this;
342
+ }
343
+
344
+ /**
345
+ * Name this builder as a derived table / SELECT subquery — Lucid/Knex `.as()`.
346
+ * `db.from((s) => s.from('exams').sum('marks as total').as('totals'))` or
347
+ * `parent.select(db.from('logins').select('ip').limit(1).as('last_ip'))`.
348
+ */
349
+ as(alias: string): this {
350
+ this.#alias = alias;
351
+ return this;
352
+ }
353
+
354
+ /** Run `cb` against a fresh sub-builder (same executor/dialect) and return it. */
355
+ #buildSub(cb: (query: DatabaseQueryBuilder) => void): DatabaseQueryBuilder {
356
+ const sub = new DatabaseQueryBuilder(this.#exec, this.#dialect);
357
+ cb(sub);
358
+ return sub;
359
+ }
360
+
361
+ /** Select the table for a write (Lucid `db.table`). Alias of {@link from}. */
362
+ table(table: string): this {
363
+ this.#table = table;
364
+ return this;
365
+ }
366
+
367
+ /**
368
+ * Add columns to the SELECT list (Lucid/Knex `select`). Accepts bare names,
369
+ * arrays, and `{ alias: 'column' }` objects for aliasing —
370
+ * `select('id', ['name', 'email'], { total: 'COUNT(*)' })`.
371
+ */
372
+ select(
373
+ ...columns: Array<
374
+ string | string[] | Record<string, string> | RawSql | DatabaseQueryBuilder
375
+ >
376
+ ): this {
377
+ for (const col of columns) {
378
+ if (typeof col === "string") {
379
+ this.#selects.push(col);
380
+ } else if (Array.isArray(col)) {
381
+ this.#selects.push(...col);
382
+ } else if (col instanceof RawSql) {
383
+ // Lucid `select(db.raw(sql, bindings))` — verbatim fragment + params.
384
+ this.#selectRaw.push({ sql: col.sql, params: [...col.params] });
385
+ } else if (col instanceof DatabaseQueryBuilder) {
386
+ // Lucid `select(subquery.as('alias'))` — a correlated subquery column.
387
+ const alias = col.#alias;
388
+ if (!alias) {
389
+ throw new Error(
390
+ "select(subquery) requires the subquery to be named with .as('alias')",
391
+ );
392
+ }
393
+ const { sql, params } = col.#compiledNative();
394
+ this.#selectRaw.push({
395
+ sql: `(${sql}) AS ${this.#quoteAlias(alias)}`,
396
+ params,
397
+ });
398
+ } else {
399
+ for (const [alias, expr] of Object.entries(col)) {
400
+ this.#selects.push(`${expr} AS ${alias}`);
401
+ }
402
+ }
403
+ }
404
+ return this;
405
+ }
406
+
407
+ /** Validate + dialect-quote a bare alias identifier. */
408
+ #quoteAlias(alias: string): string {
409
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
410
+ throw new Error(`Invalid alias '${alias}' — expected a bare identifier.`);
411
+ }
412
+ const q = this.#dialect === "mysql" ? "`" : '"';
413
+ return `${q}${alias}${q}`;
414
+ }
415
+
416
+ /** A parenthesised group of conditions, built on a sub-builder (Lucid `where(cb)`). */
417
+ where(callback: (query: DatabaseQueryBuilder) => void): this;
418
+ /** Every key as an AND equality (Lucid/Knex `where({ a: 1, b: 2 })`). */
419
+ where(conditions: Record<string, unknown>): this;
420
+ where(column: string, value: unknown): this;
421
+ where(column: string, operator: WhereOperator, value: unknown): this;
422
+ where(
423
+ columnOrCbOrObj:
424
+ | string
425
+ | ((query: DatabaseQueryBuilder) => void)
426
+ | Record<string, unknown>,
427
+ operatorOrValue?: WhereOperator | unknown,
428
+ value?: unknown,
429
+ ): this {
430
+ return this.#where("and", columnOrCbOrObj, operatorOrValue, value);
431
+ }
432
+
433
+ /** OR WHERE — joins the previous condition with OR (Lucid/Knex `orWhere`). */
434
+ orWhere(callback: (query: DatabaseQueryBuilder) => void): this;
435
+ orWhere(conditions: Record<string, unknown>): this;
436
+ orWhere(column: string, value: unknown): this;
437
+ orWhere(column: string, operator: WhereOperator, value: unknown): this;
438
+ orWhere(
439
+ columnOrCbOrObj:
440
+ | string
441
+ | ((query: DatabaseQueryBuilder) => void)
442
+ | Record<string, unknown>,
443
+ operatorOrValue?: WhereOperator | unknown,
444
+ value?: unknown,
445
+ ): this {
446
+ return this.#where("or", columnOrCbOrObj, operatorOrValue, value);
447
+ }
448
+
449
+ #where(
450
+ boolean: "and" | "or",
451
+ columnOrCbOrObj:
452
+ | string
453
+ | ((query: DatabaseQueryBuilder) => void)
454
+ | Record<string, unknown>,
455
+ operatorOrValue?: WhereOperator | unknown,
456
+ value?: unknown,
457
+ ): this {
458
+ if (typeof columnOrCbOrObj === "function") {
459
+ // Parenthesised group: collect the callback's wheres on a sub-builder.
460
+ const sub = new DatabaseQueryBuilder(this.#exec, this.#dialect);
461
+ columnOrCbOrObj(sub);
462
+ this.#wheres.push({
463
+ kind: "group",
464
+ conditions: sub.#compiledWheres(),
465
+ boolean,
466
+ });
467
+ return this;
468
+ }
469
+ if (typeof columnOrCbOrObj === "object") {
470
+ for (const [col, val] of Object.entries(columnOrCbOrObj)) {
471
+ this.#cmp(boolean, col, "=", val);
472
+ }
473
+ return this;
474
+ }
475
+ return this.#pushBasic(boolean, columnOrCbOrObj, operatorOrValue, value);
476
+ }
477
+
478
+ #pushBasic(
479
+ boolean: "and" | "or",
480
+ column: string,
481
+ operatorOrValue: WhereOperator | unknown,
482
+ value?: unknown,
483
+ ): this {
484
+ if (value === undefined) {
485
+ this.#cmp(boolean, column, "=", operatorOrValue);
486
+ } else {
487
+ this.#cmp(boolean, column, operatorOrValue as WhereOperator, value);
488
+ }
489
+ return this;
490
+ }
491
+
492
+ /** Push a `col <op> value` comparison. */
493
+ #cmp(
494
+ boolean: "and" | "or",
495
+ column: string,
496
+ operator: string,
497
+ value: unknown,
498
+ ): void {
499
+ this.#wheres.push({ kind: "cmp", column, operator, value, boolean });
500
+ }
501
+
502
+ /** WHERE col NOT IN (…) — a value list, a subquery, OR a tuple (Lucid `whereNotIn`). */
503
+ whereNotIn(column: string, values: unknown[]): this;
504
+ whereNotIn(column: string, subquery: SubqueryArg): this;
505
+ whereNotIn(columns: string[], rows: unknown[][]): this;
506
+ whereNotIn(
507
+ column: string | string[],
508
+ arg: unknown[] | unknown[][] | SubqueryArg,
509
+ ): this {
510
+ return this.#applyIn("and", true, column, arg);
511
+ }
512
+
513
+ /** Shared IN/NOT IN dispatch — values, tuple (`[cols],[rows]`) or subquery. */
514
+ #applyIn(
515
+ boolean: "and" | "or",
516
+ negated: boolean,
517
+ column: string | string[],
518
+ arg: unknown[] | unknown[][] | SubqueryArg,
519
+ ): this {
520
+ if (Array.isArray(column)) {
521
+ const rows: unknown[][] = (Array.isArray(arg) ? arg : []).map((r) =>
522
+ Array.isArray(r) ? r : [r],
523
+ );
524
+ return this.#pushInTuple(boolean, negated, column, rows);
525
+ }
526
+ if (arg instanceof DatabaseQueryBuilder || typeof arg === "function") {
527
+ return this.#pushInSub(boolean, negated, column, arg);
528
+ }
529
+ this.#cmp(boolean, column, negated ? "NOT IN" : "IN", arg);
530
+ return this;
531
+ }
532
+
533
+ /** WHERE col BETWEEN ? AND ? — inclusive (Lucid/Knex `whereBetween`). */
534
+ whereBetween(column: string, range: readonly [unknown, unknown]): this {
535
+ this.#cmp("and", column, "BETWEEN", [...range]);
536
+ return this;
537
+ }
538
+
539
+ /** WHERE col NOT BETWEEN ? AND ? (Lucid/Knex `whereNotBetween`). */
540
+ whereNotBetween(column: string, range: readonly [unknown, unknown]): this {
541
+ this.#cmp("and", column, "NOT BETWEEN", [...range]);
542
+ return this;
543
+ }
544
+
545
+ /** WHERE col LIKE ? — case-sensitive (Lucid/Knex `whereLike`). */
546
+ whereLike(column: string, pattern: string): this {
547
+ this.#cmp("and", column, "LIKE", pattern);
548
+ return this;
549
+ }
550
+
551
+ /** WHERE col ILIKE ? — case-insensitive; compiled to LOWER(..) LIKE on sqlite/mysql. */
552
+ whereILike(column: string, pattern: string): this {
553
+ this.#cmp("and", column, "ILIKE", pattern);
554
+ return this;
555
+ }
556
+
557
+ /** A raw WHERE fragment with `?` bindings (Lucid/Knex `whereRaw`). */
558
+ whereRaw(sql: string, bindings: unknown[] = []): this {
559
+ return this.#pushRaw("and", false, sql, bindings);
560
+ }
561
+
562
+ /** Alias of {@link whereRaw} — AND is the default (Lucid `andWhereRaw`). */
563
+ andWhereRaw(sql: string, bindings: unknown[] = []): this {
564
+ return this.#pushRaw("and", false, sql, bindings);
565
+ }
566
+
567
+ /** OR-combined raw WHERE fragment (Lucid `orWhereRaw`). */
568
+ orWhereRaw(sql: string, bindings: unknown[] = []): this {
569
+ return this.#pushRaw("or", false, sql, bindings);
570
+ }
571
+
572
+ /** WHERE NOT (raw fragment) (Lucid `whereNotRaw`). */
573
+ whereNotRaw(sql: string, bindings: unknown[] = []): this {
574
+ return this.#pushRaw("and", true, sql, bindings);
575
+ }
576
+
577
+ /** Alias of {@link whereNotRaw} (Lucid `andWhereNotRaw`). */
578
+ andWhereNotRaw(sql: string, bindings: unknown[] = []): this {
579
+ return this.#pushRaw("and", true, sql, bindings);
580
+ }
581
+
582
+ /** OR NOT (raw fragment) (Lucid `orWhereNotRaw`). */
583
+ orWhereNotRaw(sql: string, bindings: unknown[] = []): this {
584
+ return this.#pushRaw("or", true, sql, bindings);
585
+ }
586
+
587
+ #pushRaw(
588
+ boolean: "and" | "or",
589
+ negated: boolean,
590
+ sql: string,
591
+ bindings: unknown[],
592
+ ): this {
593
+ this.#wheres.push({
594
+ kind: "raw",
595
+ sql: negated ? `NOT (${sql})` : sql,
596
+ bindings,
597
+ boolean,
598
+ });
599
+ return this;
600
+ }
601
+
602
+ /** WHERE left <op> right — both COLUMNS (Lucid/Knex `whereColumn`). */
603
+ whereColumn(left: string, operator: string, right: string): this {
604
+ return this.#pushColumn("and", false, left, operator, right);
605
+ }
606
+
607
+ /** Alias of {@link whereColumn} — AND is the default (Lucid `andWhereColumn`). */
608
+ andWhereColumn(left: string, operator: string, right: string): this {
609
+ return this.#pushColumn("and", false, left, operator, right);
610
+ }
611
+
612
+ /** OR left <op> right — both COLUMNS (Lucid `orWhereColumn`). */
613
+ orWhereColumn(left: string, operator: string, right: string): this {
614
+ return this.#pushColumn("or", false, left, operator, right);
615
+ }
616
+
617
+ /** WHERE NOT (left <op> right) — both COLUMNS (Lucid `whereNotColumn`). */
618
+ whereNotColumn(left: string, operator: string, right: string): this {
619
+ return this.#pushColumn("and", true, left, operator, right);
620
+ }
621
+
622
+ /** Alias of {@link whereNotColumn} (Lucid `andWhereNotColumn`). */
623
+ andWhereNotColumn(left: string, operator: string, right: string): this {
624
+ return this.#pushColumn("and", true, left, operator, right);
625
+ }
626
+
627
+ /** OR NOT (left <op> right) — both COLUMNS (Lucid `orWhereNotColumn`). */
628
+ orWhereNotColumn(left: string, operator: string, right: string): this {
629
+ return this.#pushColumn("or", true, left, operator, right);
630
+ }
631
+
632
+ #pushColumn(
633
+ boolean: "and" | "or",
634
+ negated: boolean,
635
+ left: string,
636
+ operator: string,
637
+ right: string,
638
+ ): this {
639
+ const ops = new Set(["=", "!=", "<>", "<", ">", "<=", ">="]);
640
+ if (!ops.has(operator)) {
641
+ throw new Error(`whereColumn: unsupported operator '${operator}'`);
642
+ }
643
+ const base = `${this.#quoteIdent(left)} ${operator} ${this.#quoteIdent(right)}`;
644
+ const sql = negated ? `NOT (${base})` : base;
645
+ this.#wheres.push({ kind: "raw", sql, bindings: [], boolean });
646
+ return this;
647
+ }
648
+
649
+ /** WHERE EXISTS (subquery) — a builder or a callback (Lucid/Knex `whereExists`). */
650
+ whereExists(sub: SubqueryArg): this {
651
+ return this.#pushExists("and", false, sub);
652
+ }
653
+
654
+ /** Alias of {@link whereExists} — AND is the default (Lucid `andWhereExists`). */
655
+ andWhereExists(sub: SubqueryArg): this {
656
+ return this.#pushExists("and", false, sub);
657
+ }
658
+
659
+ /** WHERE NOT EXISTS (subquery). */
660
+ whereNotExists(sub: SubqueryArg): this {
661
+ return this.#pushExists("and", true, sub);
662
+ }
663
+
664
+ /** Alias of {@link whereNotExists} (Lucid `andWhereNotExists`). */
665
+ andWhereNotExists(sub: SubqueryArg): this {
666
+ return this.#pushExists("and", true, sub);
667
+ }
668
+
669
+ /** OR EXISTS (subquery). */
670
+ orWhereExists(sub: SubqueryArg): this {
671
+ return this.#pushExists("or", false, sub);
672
+ }
673
+
674
+ /** OR NOT EXISTS (subquery) (Lucid `orWhereNotExists`). */
675
+ orWhereNotExists(sub: SubqueryArg): this {
676
+ return this.#pushExists("or", true, sub);
677
+ }
678
+
679
+ #pushExists(boolean: "and" | "or", negated: boolean, sub: SubqueryArg): this {
680
+ this.#wheres.push({
681
+ kind: "exists",
682
+ negated,
683
+ subquery: this.#resolveSub(sub).#selectSpec(),
684
+ boolean,
685
+ });
686
+ return this;
687
+ }
688
+
689
+ /**
690
+ * WHERE json `$.path` <op> value (Lucid/Knex `whereJsonPath`). The operator is
691
+ * optional and defaults to `=` — `whereJsonPath('data', '$.theme', 'dark')` or
692
+ * `whereJsonPath('data', '$.total', '>', 1000)`.
693
+ */
694
+ whereJsonPath(column: string, path: string, value: unknown): this;
695
+ whereJsonPath(
696
+ column: string,
697
+ path: string,
698
+ operator: string,
699
+ value: unknown,
700
+ ): this;
701
+ whereJsonPath(
702
+ column: string,
703
+ path: string,
704
+ operatorOrValue: unknown,
705
+ value?: unknown,
706
+ ): this {
707
+ return this.#pushJsonPath("and", column, path, operatorOrValue, value);
708
+ }
709
+
710
+ /** Alias of {@link whereJsonPath} (Lucid `andWhereJsonPath`). */
711
+ andWhereJsonPath(column: string, path: string, value: unknown): this;
712
+ andWhereJsonPath(
713
+ column: string,
714
+ path: string,
715
+ operator: string,
716
+ value: unknown,
717
+ ): this;
718
+ andWhereJsonPath(
719
+ column: string,
720
+ path: string,
721
+ operatorOrValue: unknown,
722
+ value?: unknown,
723
+ ): this {
724
+ return this.#pushJsonPath("and", column, path, operatorOrValue, value);
725
+ }
726
+
727
+ /** OR form of {@link whereJsonPath} (Lucid `orWhereJsonPath`). */
728
+ orWhereJsonPath(column: string, path: string, value: unknown): this;
729
+ orWhereJsonPath(
730
+ column: string,
731
+ path: string,
732
+ operator: string,
733
+ value: unknown,
734
+ ): this;
735
+ orWhereJsonPath(
736
+ column: string,
737
+ path: string,
738
+ operatorOrValue: unknown,
739
+ value?: unknown,
740
+ ): this {
741
+ return this.#pushJsonPath("or", column, path, operatorOrValue, value);
742
+ }
743
+
744
+ #pushJsonPath(
745
+ boolean: "and" | "or",
746
+ column: string,
747
+ path: string,
748
+ operatorOrValue: unknown,
749
+ value?: unknown,
750
+ ): this {
751
+ // 3-arg form defaults the operator to `=`; 4-arg passes it explicitly.
752
+ const [operator, val] =
753
+ value === undefined
754
+ ? ["=", operatorOrValue]
755
+ : [String(operatorOrValue), value];
756
+ this.#wheres.push({
757
+ kind: "json",
758
+ jsonOp: "path",
759
+ column,
760
+ negated: false,
761
+ path,
762
+ operator: String(operator),
763
+ value: val,
764
+ boolean,
765
+ });
766
+ return this;
767
+ }
768
+
769
+ /** WHERE json column `@>` value — contains (Postgres/MySQL; `whereJsonSupersetOf`). */
770
+ whereJsonSupersetOf(column: string, value: unknown): this {
771
+ return this.#pushJsonContainment("and", false, "superset", column, value);
772
+ }
773
+ /** Lucid alias of {@link whereJsonSupersetOf} (`whereJsonSuperset`). */
774
+ whereJsonSuperset(column: string, value: unknown): this {
775
+ return this.#pushJsonContainment("and", false, "superset", column, value);
776
+ }
777
+ /** OR json `@>` (Lucid `orWhereJsonSuperset`). */
778
+ orWhereJsonSupersetOf(column: string, value: unknown): this {
779
+ return this.#pushJsonContainment("or", false, "superset", column, value);
780
+ }
781
+ orWhereJsonSuperset(column: string, value: unknown): this {
782
+ return this.#pushJsonContainment("or", false, "superset", column, value);
783
+ }
784
+ /** WHERE NOT json `@>` (Lucid `whereNotJsonSuperset`). */
785
+ whereNotJsonSupersetOf(column: string, value: unknown): this {
786
+ return this.#pushJsonContainment("and", true, "superset", column, value);
787
+ }
788
+ whereNotJsonSuperset(column: string, value: unknown): this {
789
+ return this.#pushJsonContainment("and", true, "superset", column, value);
790
+ }
791
+ /** OR NOT json `@>` (Lucid `orWhereNotJsonSuperset`). */
792
+ orWhereNotJsonSupersetOf(column: string, value: unknown): this {
793
+ return this.#pushJsonContainment("or", true, "superset", column, value);
794
+ }
795
+ orWhereNotJsonSuperset(column: string, value: unknown): this {
796
+ return this.#pushJsonContainment("or", true, "superset", column, value);
797
+ }
798
+
799
+ /** WHERE json column `<@` value — contained by (`whereJsonSubsetOf`). */
800
+ whereJsonSubsetOf(column: string, value: unknown): this {
801
+ return this.#pushJsonContainment("and", false, "subset", column, value);
802
+ }
803
+ /** Lucid alias of {@link whereJsonSubsetOf} (`whereJsonSubset`). */
804
+ whereJsonSubset(column: string, value: unknown): this {
805
+ return this.#pushJsonContainment("and", false, "subset", column, value);
806
+ }
807
+ /** OR json `<@` (Lucid `orWhereJsonSubset`). */
808
+ orWhereJsonSubsetOf(column: string, value: unknown): this {
809
+ return this.#pushJsonContainment("or", false, "subset", column, value);
810
+ }
811
+ orWhereJsonSubset(column: string, value: unknown): this {
812
+ return this.#pushJsonContainment("or", false, "subset", column, value);
813
+ }
814
+ /** WHERE NOT json `<@` (Lucid `whereNotJsonSubset`). */
815
+ whereNotJsonSubsetOf(column: string, value: unknown): this {
816
+ return this.#pushJsonContainment("and", true, "subset", column, value);
817
+ }
818
+ whereNotJsonSubset(column: string, value: unknown): this {
819
+ return this.#pushJsonContainment("and", true, "subset", column, value);
820
+ }
821
+ /** OR NOT json `<@` (Lucid `orWhereNotJsonSubset`). */
822
+ orWhereNotJsonSubsetOf(column: string, value: unknown): this {
823
+ return this.#pushJsonContainment("or", true, "subset", column, value);
824
+ }
825
+ orWhereNotJsonSubset(column: string, value: unknown): this {
826
+ return this.#pushJsonContainment("or", true, "subset", column, value);
827
+ }
828
+
829
+ /**
830
+ * Structural JSON match (Lucid `whereJson`) — the column's JSON must equal
831
+ * `value` (canonical comparison on Postgres/MySQL). AND is the default.
832
+ */
833
+ whereJson(column: string, value: unknown): this {
834
+ return this.#pushJsonContainment("and", false, "equals", column, value);
835
+ }
836
+ /** Alias of {@link whereJson} (Lucid `andWhereJson`). */
837
+ andWhereJson(column: string, value: unknown): this {
838
+ return this.#pushJsonContainment("and", false, "equals", column, value);
839
+ }
840
+ /** OR form of {@link whereJson} (Lucid `orWhereJson`). */
841
+ orWhereJson(column: string, value: unknown): this {
842
+ return this.#pushJsonContainment("or", false, "equals", column, value);
843
+ }
844
+ /** WHERE NOT structural JSON match (Lucid `whereNotJson`). */
845
+ whereNotJson(column: string, value: unknown): this {
846
+ return this.#pushJsonContainment("and", true, "equals", column, value);
847
+ }
848
+ /** Alias of {@link whereNotJson} (Lucid `andWhereNotJson`). */
849
+ andWhereNotJson(column: string, value: unknown): this {
850
+ return this.#pushJsonContainment("and", true, "equals", column, value);
851
+ }
852
+ /** OR NOT structural JSON match (Lucid `orWhereNotJson`). */
853
+ orWhereNotJson(column: string, value: unknown): this {
854
+ return this.#pushJsonContainment("or", true, "equals", column, value);
855
+ }
856
+
857
+ #pushJsonContainment(
858
+ boolean: "and" | "or",
859
+ negated: boolean,
860
+ jsonOp: "superset" | "subset" | "equals",
861
+ column: string,
862
+ value: unknown,
863
+ ): this {
864
+ this.#wheres.push({
865
+ kind: "json",
866
+ jsonOp,
867
+ column,
868
+ negated,
869
+ value: typeof value === "string" ? value : JSON.stringify(value),
870
+ boolean,
871
+ });
872
+ return this;
873
+ }
874
+
875
+ /** ORDER BY <raw> — keeps its position among orderBy terms (Lucid `orderByRaw`). */
876
+ orderByRaw(sql: string): this {
877
+ this.#orderBys.push({ raw: sql });
878
+ return this;
879
+ }
880
+
881
+ /** GROUP BY <raw expression> (Lucid/Knex `groupByRaw`). */
882
+ groupByRaw(sql: string): this {
883
+ this.#groupBys.push(sql);
884
+ return this;
885
+ }
886
+
887
+ /** HAVING <raw> with `?` bindings (Lucid/Knex `havingRaw`). */
888
+ havingRaw(sql: string, bindings: unknown[] = []): this {
889
+ this.#havings.push({ kind: "raw", sql, bindings, type: "and" });
890
+ return this;
891
+ }
892
+
893
+ /** `INNER JOIN` — alias of {@link innerJoin} (Lucid/Knex `join`). */
894
+ join(table: string, left: string, right: string): this;
895
+ join(table: string, left: string, operator: string, right: string): this;
896
+ join(table: string, build: (j: DbJoinBuilder) => void): this;
897
+ join(
898
+ table: string,
899
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
900
+ operatorOrRight?: string,
901
+ right?: string,
902
+ ): this {
903
+ return this.#pushJoin("INNER", table, leftOrBuild, operatorOrRight, right);
904
+ }
905
+
906
+ /** `INNER JOIN table ON <left> [op] <right>` or a callback `ON` builder (Lucid/Knex). */
907
+ innerJoin(table: string, left: string, right: string): this;
908
+ innerJoin(table: string, left: string, operator: string, right: string): this;
909
+ innerJoin(table: string, build: (j: DbJoinBuilder) => void): this;
910
+ innerJoin(
911
+ table: string,
912
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
913
+ operatorOrRight?: string,
914
+ right?: string,
915
+ ): this {
916
+ return this.#pushJoin("INNER", table, leftOrBuild, operatorOrRight, right);
917
+ }
918
+
919
+ /** `LEFT JOIN table ON <left> [op] <right>` or a callback `ON` builder (Lucid/Knex). */
920
+ leftJoin(table: string, left: string, right: string): this;
921
+ leftJoin(table: string, left: string, operator: string, right: string): this;
922
+ leftJoin(table: string, build: (j: DbJoinBuilder) => void): this;
923
+ leftJoin(
924
+ table: string,
925
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
926
+ operatorOrRight?: string,
927
+ right?: string,
928
+ ): this {
929
+ return this.#pushJoin("LEFT", table, leftOrBuild, operatorOrRight, right);
930
+ }
931
+
932
+ /** `LEFT OUTER JOIN` — alias of {@link leftJoin} (Lucid/Knex `leftOuterJoin`). */
933
+ leftOuterJoin(table: string, left: string, right: string): this;
934
+ leftOuterJoin(
935
+ table: string,
936
+ left: string,
937
+ operator: string,
938
+ right: string,
939
+ ): this;
940
+ leftOuterJoin(table: string, build: (j: DbJoinBuilder) => void): this;
941
+ leftOuterJoin(
942
+ table: string,
943
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
944
+ operatorOrRight?: string,
945
+ right?: string,
946
+ ): this {
947
+ return this.#pushJoin("LEFT", table, leftOrBuild, operatorOrRight, right);
948
+ }
949
+
950
+ /** `RIGHT JOIN table ON <left> [op] <right>` or a callback `ON` builder (Lucid/Knex). */
951
+ rightJoin(table: string, left: string, right: string): this;
952
+ rightJoin(table: string, left: string, operator: string, right: string): this;
953
+ rightJoin(table: string, build: (j: DbJoinBuilder) => void): this;
954
+ rightJoin(
955
+ table: string,
956
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
957
+ operatorOrRight?: string,
958
+ right?: string,
959
+ ): this {
960
+ return this.#pushJoin("RIGHT", table, leftOrBuild, operatorOrRight, right);
961
+ }
962
+
963
+ /** `RIGHT OUTER JOIN` — alias of {@link rightJoin} (Lucid/Knex `rightOuterJoin`). */
964
+ rightOuterJoin(table: string, left: string, right: string): this;
965
+ rightOuterJoin(
966
+ table: string,
967
+ left: string,
968
+ operator: string,
969
+ right: string,
970
+ ): this;
971
+ rightOuterJoin(table: string, build: (j: DbJoinBuilder) => void): this;
972
+ rightOuterJoin(
973
+ table: string,
974
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
975
+ operatorOrRight?: string,
976
+ right?: string,
977
+ ): this {
978
+ return this.#pushJoin("RIGHT", table, leftOrBuild, operatorOrRight, right);
979
+ }
980
+
981
+ /** `FULL OUTER JOIN table ON …` (Lucid/Knex `fullOuterJoin`; Postgres — MySQL/SQLite lack it). */
982
+ fullOuterJoin(table: string, left: string, right: string): this;
983
+ fullOuterJoin(
984
+ table: string,
985
+ left: string,
986
+ operator: string,
987
+ right: string,
988
+ ): this;
989
+ fullOuterJoin(table: string, build: (j: DbJoinBuilder) => void): this;
990
+ fullOuterJoin(
991
+ table: string,
992
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
993
+ operatorOrRight?: string,
994
+ right?: string,
995
+ ): this {
996
+ return this.#pushJoin(
997
+ "FULL OUTER",
998
+ table,
999
+ leftOrBuild,
1000
+ operatorOrRight,
1001
+ right,
1002
+ );
1003
+ }
1004
+
1005
+ /** `INNER JOIN table ON <left> = <right>` — quoted equi-join sugar (Lucid `joinOn`). */
1006
+ joinOn(table: string, left: string, right: string): this {
1007
+ return this.#pushJoin("INNER", table, left, right);
1008
+ }
1009
+
1010
+ /** `CROSS JOIN table` (Lucid/Knex `crossJoin`). */
1011
+ crossJoin(table: string): this {
1012
+ this.#joins.push({
1013
+ sql: `CROSS JOIN ${this.#quoteJoinRef(table)}`,
1014
+ params: [],
1015
+ });
1016
+ return this;
1017
+ }
1018
+
1019
+ /** Validate + quote a `[[schema.]table.]column` join reference (up to 3 segments). */
1020
+ #quoteJoinRef(ref: string): string {
1021
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*){0,2}$/.test(ref)) {
1022
+ throw new Error(
1023
+ `Invalid join/column identifier '${ref}' — expected [[schema.]table.]column. Use joinRaw() for anything else.`,
1024
+ );
1025
+ }
1026
+ const q = this.#dialect === "mysql" ? "`" : '"';
1027
+ return ref
1028
+ .split(".")
1029
+ .map((seg) => `${q}${seg}${q}`)
1030
+ .join(".");
1031
+ }
1032
+
1033
+ /**
1034
+ * Build a JOIN from the string forms — 3-arg `(left, right)` or 4-arg
1035
+ * `(left, operator, right)` — or the callback `ON` builder (Lucid/Knex).
1036
+ */
1037
+ #pushJoin(
1038
+ kind: "INNER" | "LEFT" | "RIGHT" | "FULL OUTER",
1039
+ table: string,
1040
+ leftOrBuild: string | ((j: DbJoinBuilder) => void),
1041
+ operatorOrRight?: string,
1042
+ right?: string,
1043
+ ): this {
1044
+ const tq = this.#quoteJoinRef(table);
1045
+ if (typeof leftOrBuild === "function") {
1046
+ const parts: JoinPart[] = [];
1047
+ const jb: DbJoinBuilder = {
1048
+ on(l: string, opOrR: string, r?: string) {
1049
+ parts.push(
1050
+ r === undefined
1051
+ ? { kind: "and", left: l, right: opOrR }
1052
+ : { kind: "and", left: l, operator: opOrR, right: r },
1053
+ );
1054
+ return jb;
1055
+ },
1056
+ andOn(l: string, opOrR: string, r?: string) {
1057
+ parts.push(
1058
+ r === undefined
1059
+ ? { kind: "and", left: l, right: opOrR }
1060
+ : { kind: "and", left: l, operator: opOrR, right: r },
1061
+ );
1062
+ return jb;
1063
+ },
1064
+ orOn(l: string, opOrR: string, r?: string) {
1065
+ parts.push(
1066
+ r === undefined
1067
+ ? { kind: "or", left: l, right: opOrR }
1068
+ : { kind: "or", left: l, operator: opOrR, right: r },
1069
+ );
1070
+ return jb;
1071
+ },
1072
+ onVal(l, v) {
1073
+ parts.push({ kind: "and", left: l, value: { v } });
1074
+ return jb;
1075
+ },
1076
+ andOnVal(l, v) {
1077
+ parts.push({ kind: "and", left: l, value: { v } });
1078
+ return jb;
1079
+ },
1080
+ orOnVal(l, v) {
1081
+ parts.push({ kind: "or", left: l, value: { v } });
1082
+ return jb;
1083
+ },
1084
+ onIn(l, values) {
1085
+ parts.push({ kind: "and", left: l, values: [...values] });
1086
+ return jb;
1087
+ },
1088
+ onNotIn(l, values) {
1089
+ parts.push({
1090
+ kind: "and",
1091
+ left: l,
1092
+ values: [...values],
1093
+ notIn: true,
1094
+ });
1095
+ return jb;
1096
+ },
1097
+ onNull(l) {
1098
+ parts.push({ kind: "and", left: l, nullOp: "IS NULL" });
1099
+ return jb;
1100
+ },
1101
+ onNotNull(l) {
1102
+ parts.push({ kind: "and", left: l, nullOp: "IS NOT NULL" });
1103
+ return jb;
1104
+ },
1105
+ onBetween(l, range) {
1106
+ parts.push({ kind: "and", left: l, between: [range[0], range[1]] });
1107
+ return jb;
1108
+ },
1109
+ onNotBetween(l, range) {
1110
+ parts.push({
1111
+ kind: "and",
1112
+ left: l,
1113
+ between: [range[0], range[1]],
1114
+ notBetween: true,
1115
+ });
1116
+ return jb;
1117
+ },
1118
+ onExists: (sub) => {
1119
+ const { sql, params } = this.#resolveSub(sub).#compiledNative();
1120
+ parts.push({ kind: "and", exists: { sql, params, not: false } });
1121
+ return jb;
1122
+ },
1123
+ onNotExists: (sub) => {
1124
+ const { sql, params } = this.#resolveSub(sub).#compiledNative();
1125
+ parts.push({ kind: "and", exists: { sql, params, not: true } });
1126
+ return jb;
1127
+ },
1128
+ };
1129
+ leftOrBuild(jb);
1130
+ const { sql: on, params } = this.#compileJoinParts(parts);
1131
+ this.#joins.push({ sql: `${kind} JOIN ${tq} ${on}`, params });
1132
+ return this;
1133
+ }
1134
+ // String form: 3-arg `(left, right)` or 4-arg `(left, operator, right)`.
1135
+ const left = leftOrBuild;
1136
+ const operator = right === undefined ? "=" : (operatorOrRight ?? "=");
1137
+ const rightCol = right === undefined ? operatorOrRight : right;
1138
+ if (rightCol === undefined) {
1139
+ throw new Error(
1140
+ "join() string form requires both left and right operands",
1141
+ );
1142
+ }
1143
+ this.#joins.push({
1144
+ sql: `${kind} JOIN ${tq} ON ${this.#quoteJoinRef(left)} ${this.#validateJoinOp(operator)} ${this.#quoteJoinRef(rightCol)}`,
1145
+ params: [],
1146
+ });
1147
+ return this;
1148
+ }
1149
+
1150
+ /** Render accumulated `ON` parts to SQL + ordered bound params. */
1151
+ #compileJoinParts(parts: JoinPart[]): { sql: string; params: unknown[] } {
1152
+ const params: unknown[] = [];
1153
+ const sql = parts
1154
+ .map((p, i) => {
1155
+ const prefix = i === 0 ? "ON" : p.kind === "or" ? "OR" : "AND";
1156
+ if (p.exists) {
1157
+ params.push(...p.exists.params);
1158
+ return `${prefix} ${p.exists.not ? "NOT EXISTS" : "EXISTS"} (${p.exists.sql})`;
1159
+ }
1160
+ const col = this.#quoteJoinRef(p.left ?? "");
1161
+ if (p.nullOp) {
1162
+ return `${prefix} ${col} ${p.nullOp}`;
1163
+ }
1164
+ if (p.between) {
1165
+ params.push(p.between[0], p.between[1]);
1166
+ return `${prefix} ${col} ${p.notBetween ? "NOT BETWEEN" : "BETWEEN"} ? AND ?`;
1167
+ }
1168
+ if (p.values) {
1169
+ const placeholders = p.values.map(() => "?").join(", ");
1170
+ params.push(...p.values);
1171
+ return `${prefix} ${col} ${p.notIn ? "NOT IN" : "IN"} (${placeholders})`;
1172
+ }
1173
+ if (p.value) {
1174
+ params.push(p.value.v);
1175
+ return `${prefix} ${col} ${this.#validateJoinOp(p.operator ?? "=")} ?`;
1176
+ }
1177
+ return `${prefix} ${col} ${this.#validateJoinOp(p.operator ?? "=")} ${this.#quoteJoinRef(p.right ?? "")}`;
1178
+ })
1179
+ .join(" ");
1180
+ return { sql, params };
1181
+ }
1182
+
1183
+ /** Allowlist the comparison operator embedded verbatim into a JOIN's ON SQL. */
1184
+ #validateJoinOp(op: string): string {
1185
+ const t = op.trim();
1186
+ const up = t.toUpperCase();
1187
+ const allowed = new Set([
1188
+ "=",
1189
+ "<>",
1190
+ "!=",
1191
+ "<",
1192
+ "<=",
1193
+ ">",
1194
+ ">=",
1195
+ "LIKE",
1196
+ "NOT LIKE",
1197
+ "ILIKE",
1198
+ ]);
1199
+ if (allowed.has(t)) return t;
1200
+ if (allowed.has(up)) return up;
1201
+ throw new Error(`Unsupported join operator '${op}'.`);
1202
+ }
1203
+
1204
+ /** A sub-query passed as a builder, or built in a callback (Lucid accepts both). */
1205
+ #resolveSub(sub: SubqueryArg): DatabaseQueryBuilder {
1206
+ return typeof sub === "function" ? this.#buildSub(sub) : sub;
1207
+ }
1208
+
1209
+ /** `UNION` with another query — a builder or a callback (Lucid/Knex `union`). */
1210
+ union(sub: SubqueryArg): this {
1211
+ return this.#pushUnion(this.#resolveSub(sub), false);
1212
+ }
1213
+
1214
+ /** `UNION ALL` (Lucid/Knex `unionAll`). */
1215
+ unionAll(sub: SubqueryArg): this {
1216
+ return this.#pushUnion(this.#resolveSub(sub), true, "union");
1217
+ }
1218
+
1219
+ /** `INTERSECT` — rows present in both queries (Lucid/Knex `intersect`). */
1220
+ intersect(sub: SubqueryArg): this {
1221
+ return this.#pushUnion(this.#resolveSub(sub), false, "intersect");
1222
+ }
1223
+
1224
+ /** `INTERSECT ALL` — duplicate-preserving {@link intersect} (Postgres/MySQL). */
1225
+ intersectAll(sub: SubqueryArg): this {
1226
+ return this.#pushUnion(this.#resolveSub(sub), true, "intersect");
1227
+ }
1228
+
1229
+ /** `EXCEPT` — rows in this query but not the other (Lucid/Knex `except`). */
1230
+ except(sub: SubqueryArg): this {
1231
+ return this.#pushUnion(this.#resolveSub(sub), false, "except");
1232
+ }
1233
+
1234
+ /** `EXCEPT ALL` — duplicate-preserving {@link except} (Postgres/MySQL). */
1235
+ exceptAll(sub: SubqueryArg): this {
1236
+ return this.#pushUnion(this.#resolveSub(sub), true, "except");
1237
+ }
1238
+
1239
+ #pushUnion(
1240
+ sub: DatabaseQueryBuilder,
1241
+ all: boolean,
1242
+ op: "union" | "intersect" | "except" = "union",
1243
+ ): this {
1244
+ const { sql, params } = sub.#compiledNative();
1245
+ this.#unions.push({ sql, params, all, op });
1246
+ return this;
1247
+ }
1248
+
1249
+ /**
1250
+ * `WITH name AS (subquery)` common table expression (Lucid/Knex `with`). The
1251
+ * body is a pre-built builder OR a callback that builds one.
1252
+ */
1253
+ with(
1254
+ name: string,
1255
+ sub: SubqueryArg,
1256
+ options: {
1257
+ recursive?: boolean;
1258
+ materialized?: boolean;
1259
+ columns?: string[];
1260
+ } = {},
1261
+ ): this {
1262
+ const { sql, params } = this.#resolveSub(sub).#compiledNative();
1263
+ this.#ctes.push({
1264
+ name,
1265
+ sql,
1266
+ params,
1267
+ recursive: options.recursive ?? false,
1268
+ materialized: options.materialized ?? null,
1269
+ columns: options.columns,
1270
+ });
1271
+ return this;
1272
+ }
1273
+
1274
+ /**
1275
+ * `WITH RECURSIVE name[(cols)] AS (subquery)` (Lucid/Knex `withRecursive`).
1276
+ * The optional `columns` list restricts/names the CTE's output columns.
1277
+ */
1278
+ withRecursive(name: string, sub: SubqueryArg, columns?: string[]): this {
1279
+ return this.with(name, sub, { recursive: true, columns });
1280
+ }
1281
+
1282
+ /** `WITH name AS MATERIALIZED (subquery)` — Postgres (Lucid/Knex `withMaterialized`). */
1283
+ withMaterialized(name: string, sub: SubqueryArg): this {
1284
+ return this.with(name, sub, { materialized: true });
1285
+ }
1286
+
1287
+ /** `WITH name AS NOT MATERIALIZED (subquery)` — Postgres (Lucid/Knex `withNotMaterialized`). */
1288
+ withNotMaterialized(name: string, sub: SubqueryArg): this {
1289
+ return this.with(name, sub, { materialized: false });
1290
+ }
1291
+
1292
+ /**
1293
+ * A deep copy of this builder — every accumulated clause is duplicated so
1294
+ * mutating the clone never touches the original (Lucid/Knex `clone`). Shares
1295
+ * only the executor + dialect.
1296
+ */
1297
+ clone(): DatabaseQueryBuilder<T> {
1298
+ const c = new DatabaseQueryBuilder<T>(
1299
+ this.#exec,
1300
+ this.#dialect,
1301
+ this.#table,
1302
+ {
1303
+ readOnly: this.#readOnly,
1304
+ },
1305
+ );
1306
+ c.#selects = [...this.#selects];
1307
+ c.#selectRaw = this.#selectRaw.map((s) => ({
1308
+ ...s,
1309
+ params: [...s.params],
1310
+ }));
1311
+ c.#alias = this.#alias;
1312
+ c.#wheres = this.#wheres.map((w) => ({ ...w }));
1313
+ c.#orderBys = this.#orderBys.map((o) => ({ ...o }));
1314
+ c.#groupBys = [...this.#groupBys];
1315
+ c.#havings = this.#havings.map((h) => ({ ...h }));
1316
+ c.#joins = this.#joins.map((j) => ({ sql: j.sql, params: [...j.params] }));
1317
+ c.#unions = this.#unions.map((u) => ({ ...u, params: [...u.params] }));
1318
+ c.#ctes = this.#ctes.map((cte) => ({ ...cte, params: [...cte.params] }));
1319
+ c.#schema = this.#schema;
1320
+ c.#lockMode = this.#lockMode;
1321
+ c.#lockModifier = this.#lockModifier;
1322
+ c.#distinctOn = [...this.#distinctOn];
1323
+ c.#returningCols = [...this.#returningCols];
1324
+ c.#onConflictCols = this.#onConflictCols
1325
+ ? [...this.#onConflictCols]
1326
+ : undefined;
1327
+ c.#mergeMode = this.#mergeMode;
1328
+ c.#mergeCols = [...this.#mergeCols];
1329
+ c.#mergeSet = this.#mergeSet
1330
+ ? this.#mergeSet.map((s) => ({ ...s }))
1331
+ : undefined;
1332
+ c.#distinctFlag = this.#distinctFlag;
1333
+ c.#limit = this.#limit;
1334
+ c.#offset = this.#offset;
1335
+ c.#debug = this.#debug;
1336
+ c.#comments = [...this.#comments];
1337
+ c.#reporterData = this.#reporterData
1338
+ ? { ...this.#reporterData }
1339
+ : undefined;
1340
+ c.#fromSubquery = this.#fromSubquery
1341
+ ? { ...this.#fromSubquery, params: [...this.#fromSubquery.params] }
1342
+ : undefined;
1343
+ return c;
1344
+ }
1345
+
1346
+ /** Qualify the table with a schema (Lucid/Knex `withSchema`). */
1347
+ withSchema(schema: string): this {
1348
+ this.#schema = schema;
1349
+ return this;
1350
+ }
1351
+
1352
+ /**
1353
+ * Wrap every WHERE clause added so far into its own parenthesised group, so
1354
+ * subsequent clauses combine with the group rather than its inner conditions
1355
+ * (Lucid `wrapExisting`): `q.where(a).orWhere(b).wrapExisting().where(c)` →
1356
+ * `WHERE (a OR b) AND c`.
1357
+ */
1358
+ wrapExisting(): this {
1359
+ if (this.#wheres.length > 0) {
1360
+ this.#wheres = [
1361
+ {
1362
+ kind: "group",
1363
+ conditions: this.#compiledWheres(),
1364
+ boolean: "and",
1365
+ },
1366
+ ];
1367
+ }
1368
+ return this;
1369
+ }
1370
+
1371
+ /** Log the compiled SQL + bindings to the console on the next run (Lucid/Knex `debug`). */
1372
+ debug(enabled = true): this {
1373
+ this.#debug = enabled;
1374
+ return this;
1375
+ }
1376
+
1377
+ /**
1378
+ * Set a caller-facing statement timeout in ms (Lucid `timeout(ms)`). The
1379
+ * awaiting promise rejects after `ms` on the read paths (exec/first/pluck/
1380
+ * aggregate). With `{ cancel: true }` a SERVER-side statement timeout is also
1381
+ * applied — Postgres `statement_timeout`, MySQL `MAX_EXECUTION_TIME` (SELECT) —
1382
+ * so the server aborts the query, not just the client. (SQLite has no server
1383
+ * timeout; the client race applies.) No argument clears the timeout.
1384
+ */
1385
+ timeout(ms?: number, options?: { cancel?: boolean }): this {
1386
+ this.#timeoutMs = ms;
1387
+ this.#cancelTimeout = options?.cancel === true;
1388
+ return this;
1389
+ }
1390
+
1391
+ /**
1392
+ * Race `work` against the configured `.timeout(ms)`. Rejects the awaiter after
1393
+ * `ms`; the losing DB promise is swallowed so a post-timeout driver error never
1394
+ * surfaces as an unhandled rejection. No timeout set → returns `work` as-is.
1395
+ * Matches Lucid's DEFAULT (non-cancelling) timeout — the driver still completes
1396
+ * the query server-side.
1397
+ */
1398
+ #raceTimeout<R>(work: Promise<R>): Promise<R> {
1399
+ const ms = this.#timeoutMs;
1400
+ if (!ms || ms <= 0) return work;
1401
+ let timer: ReturnType<typeof setTimeout> | undefined;
1402
+ const guard = new Promise<never>((_, reject) => {
1403
+ timer = setTimeout(
1404
+ () => reject(new Error(`Query timed out after ${ms}ms`)),
1405
+ ms,
1406
+ );
1407
+ });
1408
+ work.catch(() => {});
1409
+ return Promise.race([work, guard]).finally(() => clearTimeout(timer));
1410
+ }
1411
+
1412
+ /** Prepend a `/* … *​/` SQL comment to the compiled query (Lucid/Knex `comment`). */
1413
+ comment(text: string): this {
1414
+ // Reject the comment terminator so a comment can never break out of `/* */`.
1415
+ if (text.includes("*/")) {
1416
+ throw new Error("comment() text may not contain '*/'");
1417
+ }
1418
+ this.#comments.push(text);
1419
+ return this;
1420
+ }
1421
+
1422
+ /**
1423
+ * Attach arbitrary metadata to the `db:query` event this query emits (Adonis
1424
+ * Lucid `reporterData`) — a listener reads it off `event.reporterData`.
1425
+ * Repeated calls merge; setting it forces emission so the data reaches a
1426
+ * listener even when the connection has `debug: false`.
1427
+ */
1428
+ reporterData(data: Record<string, unknown>): this {
1429
+ this.#reporterData = { ...this.#reporterData, ...data };
1430
+ this.#debug = true;
1431
+ return this;
1432
+ }
1433
+
1434
+ /** QueryMeta carrying the debug/reporterData channel to the connection. */
1435
+ #queryMeta(method: string): QueryMeta {
1436
+ return {
1437
+ method,
1438
+ debug: this.#debug,
1439
+ reporterData: this.#reporterData,
1440
+ serverTimeoutMs:
1441
+ this.#cancelTimeout && this.#timeoutMs != null
1442
+ ? this.#timeoutMs
1443
+ : undefined,
1444
+ };
1445
+ }
1446
+
1447
+ /** Validate + quote an identifier (optionally `table.column`) for the dialect. */
1448
+ #quoteIdent(name: string): string {
1449
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(name)) {
1450
+ throw new Error(`unsafe identifier ${JSON.stringify(name)}`);
1451
+ }
1452
+ const q = this.#dialect === "mysql" ? "`" : '"';
1453
+ return name
1454
+ .split(".")
1455
+ .map((p) => `${q}${p}${q}`)
1456
+ .join(".");
1457
+ }
1458
+
1459
+ /**
1460
+ * SELECT DISTINCT (Lucid/Knex `distinct`). With columns, those are added to
1461
+ * the SELECT list too — `distinct('a', 'b')` ≈ `SELECT DISTINCT a, b`.
1462
+ */
1463
+ distinct(...columns: string[]): this {
1464
+ this.#distinctFlag = true;
1465
+ if (columns.length > 0) this.#selects.push(...columns);
1466
+ return this;
1467
+ }
1468
+
1469
+ /** Postgres `SELECT DISTINCT ON (cols)` (Lucid/Knex `distinctOn`). */
1470
+ distinctOn(...columns: string[]): this {
1471
+ this.#distinctOn.push(...columns);
1472
+ return this;
1473
+ }
1474
+
1475
+ /** GROUP BY columns (Lucid/Knex `groupBy`). */
1476
+ groupBy(...columns: string[]): this {
1477
+ this.#groupBys.push(...columns);
1478
+ return this;
1479
+ }
1480
+
1481
+ /** HAVING condition after GROUP BY (Lucid/Knex `having`). */
1482
+ having(
1483
+ column: string,
1484
+ operatorOrValue: WhereOperator | unknown,
1485
+ value?: unknown,
1486
+ ): this {
1487
+ this.#havings.push(
1488
+ value === undefined
1489
+ ? { column, operator: "=", value: operatorOrValue, type: "and" }
1490
+ : {
1491
+ column,
1492
+ operator: operatorOrValue as WhereOperator,
1493
+ value,
1494
+ type: "and",
1495
+ },
1496
+ );
1497
+ return this;
1498
+ }
1499
+
1500
+ /** OR-combined HAVING condition (Lucid/Knex `orHaving`). */
1501
+ orHaving(
1502
+ column: string,
1503
+ operatorOrValue: WhereOperator | unknown,
1504
+ value?: unknown,
1505
+ ): this {
1506
+ this.#havings.push(
1507
+ value === undefined
1508
+ ? { column, operator: "=", value: operatorOrValue, type: "or" }
1509
+ : {
1510
+ column,
1511
+ operator: operatorOrValue as WhereOperator,
1512
+ value,
1513
+ type: "or",
1514
+ },
1515
+ );
1516
+ return this;
1517
+ }
1518
+
1519
+ /** HAVING col IS NULL (Lucid/Knex `havingNull`). */
1520
+ havingNull(column: string): this {
1521
+ this.#havings.push({
1522
+ column,
1523
+ operator: "IS NULL",
1524
+ value: null,
1525
+ type: "and",
1526
+ });
1527
+ return this;
1528
+ }
1529
+
1530
+ /** HAVING col IS NOT NULL (Lucid/Knex `havingNotNull`). */
1531
+ havingNotNull(column: string): this {
1532
+ this.#havings.push({
1533
+ column,
1534
+ operator: "IS NOT NULL",
1535
+ value: null,
1536
+ type: "and",
1537
+ });
1538
+ return this;
1539
+ }
1540
+
1541
+ /** HAVING col IN (...) (Lucid/Knex `havingIn`). */
1542
+ havingIn(column: string, values: unknown[]): this {
1543
+ this.#havings.push({ column, operator: "IN", value: values, type: "and" });
1544
+ return this;
1545
+ }
1546
+
1547
+ /** HAVING col NOT IN (...) (Lucid/Knex `havingNotIn`). */
1548
+ havingNotIn(column: string, values: unknown[]): this {
1549
+ this.#havings.push({
1550
+ column,
1551
+ operator: "NOT IN",
1552
+ value: values,
1553
+ type: "and",
1554
+ });
1555
+ return this;
1556
+ }
1557
+
1558
+ /** HAVING col BETWEEN ? AND ? (Lucid/Knex `havingBetween`). */
1559
+ havingBetween(column: string, range: readonly [unknown, unknown]): this {
1560
+ this.#havings.push({
1561
+ column,
1562
+ operator: "BETWEEN",
1563
+ value: [...range],
1564
+ type: "and",
1565
+ });
1566
+ return this;
1567
+ }
1568
+
1569
+ /** HAVING col NOT BETWEEN ? AND ? (Lucid/Knex `havingNotBetween`). */
1570
+ havingNotBetween(column: string, range: readonly [unknown, unknown]): this {
1571
+ this.#havings.push({
1572
+ column,
1573
+ operator: "NOT BETWEEN",
1574
+ value: [...range],
1575
+ type: "and",
1576
+ });
1577
+ return this;
1578
+ }
1579
+
1580
+ /**
1581
+ * WHERE col IN (…) — a value list, a subquery, OR a multi-column tuple
1582
+ * (Lucid/Knex `whereIn`): `whereIn('id', [1,2])`, `whereIn('id', subquery)`,
1583
+ * `whereIn(['a','b'], [[1,2],[3,4]])`.
1584
+ */
1585
+ whereIn(column: string, values: unknown[]): this;
1586
+ whereIn(column: string, subquery: SubqueryArg): this;
1587
+ whereIn(columns: string[], rows: unknown[][]): this;
1588
+ whereIn(
1589
+ column: string | string[],
1590
+ arg: unknown[] | unknown[][] | SubqueryArg,
1591
+ ): this {
1592
+ return this.#applyIn("and", false, column, arg);
1593
+ }
1594
+
1595
+ #pushInSub(
1596
+ boolean: "and" | "or",
1597
+ negated: boolean,
1598
+ column: string,
1599
+ sub: SubqueryArg,
1600
+ ): this {
1601
+ this.#wheres.push({
1602
+ kind: "inSub",
1603
+ column,
1604
+ negated,
1605
+ subquery: this.#resolveSub(sub).#selectSpec(),
1606
+ boolean,
1607
+ });
1608
+ return this;
1609
+ }
1610
+
1611
+ #pushInTuple(
1612
+ boolean: "and" | "or",
1613
+ negated: boolean,
1614
+ columns: string[],
1615
+ rows: unknown[][],
1616
+ ): this {
1617
+ this.#wheres.push({ kind: "inTuple", columns, rows, negated, boolean });
1618
+ return this;
1619
+ }
1620
+
1621
+ whereNull(column: string): this {
1622
+ this.#cmp("and", column, "IS NULL", null);
1623
+ return this;
1624
+ }
1625
+
1626
+ whereNotNull(column: string): this {
1627
+ this.#cmp("and", column, "IS NOT NULL", null);
1628
+ return this;
1629
+ }
1630
+
1631
+ // AND-default aliases (Lucid `andWhereIn`/`andWhereNull`/… — same as the
1632
+ // `where*` forms, which already default to AND; provided for source parity).
1633
+
1634
+ /** Alias of {@link whereIn} (Lucid `andWhereIn`). */
1635
+ andWhereIn(column: string, values: unknown[]): this;
1636
+ andWhereIn(column: string, subquery: SubqueryArg): this;
1637
+ andWhereIn(columns: string[], rows: unknown[][]): this;
1638
+ andWhereIn(
1639
+ column: string | string[],
1640
+ arg: unknown[] | unknown[][] | SubqueryArg,
1641
+ ): this {
1642
+ return this.#applyIn("and", false, column, arg);
1643
+ }
1644
+
1645
+ /** Alias of {@link whereNotIn} (Lucid `andWhereNotIn`). */
1646
+ andWhereNotIn(column: string, values: unknown[]): this;
1647
+ andWhereNotIn(column: string, subquery: SubqueryArg): this;
1648
+ andWhereNotIn(columns: string[], rows: unknown[][]): this;
1649
+ andWhereNotIn(
1650
+ column: string | string[],
1651
+ arg: unknown[] | unknown[][] | SubqueryArg,
1652
+ ): this {
1653
+ return this.#applyIn("and", true, column, arg);
1654
+ }
1655
+
1656
+ /** Alias of {@link whereNull} (Lucid `andWhereNull`). */
1657
+ andWhereNull(column: string): this {
1658
+ return this.whereNull(column);
1659
+ }
1660
+
1661
+ /** Alias of {@link whereNotNull} (Lucid `andWhereNotNull`). */
1662
+ andWhereNotNull(column: string): this {
1663
+ return this.whereNotNull(column);
1664
+ }
1665
+
1666
+ /** Alias of {@link whereBetween} (Lucid `andWhereBetween`). */
1667
+ andWhereBetween(column: string, range: readonly [unknown, unknown]): this {
1668
+ return this.whereBetween(column, range);
1669
+ }
1670
+
1671
+ /** Alias of {@link whereNotBetween} (Lucid `andWhereNotBetween`). */
1672
+ andWhereNotBetween(column: string, range: readonly [unknown, unknown]): this {
1673
+ return this.whereNotBetween(column, range);
1674
+ }
1675
+
1676
+ /**
1677
+ * Negated WHERE (Lucid/Knex `whereNot`) — the same forms as {@link where}: a
1678
+ * `(column, [operator,] value)` comparison, an object (`whereNot({ a: 1 })` →
1679
+ * `a <> 1`), or a callback group (`whereNot((q) => …)` → `NOT (…)`).
1680
+ */
1681
+ whereNot(callback: (query: DatabaseQueryBuilder) => void): this;
1682
+ whereNot(conditions: Record<string, unknown>): this;
1683
+ whereNot(column: string, value: unknown): this;
1684
+ whereNot(column: string, operator: WhereOperator, value: unknown): this;
1685
+ whereNot(
1686
+ columnOrCbOrObj:
1687
+ | string
1688
+ | ((query: DatabaseQueryBuilder) => void)
1689
+ | Record<string, unknown>,
1690
+ operatorOrValue?: WhereOperator | unknown,
1691
+ value?: unknown,
1692
+ ): this {
1693
+ return this.#applyWhereNot("and", columnOrCbOrObj, operatorOrValue, value);
1694
+ }
1695
+
1696
+ /** Alias of {@link whereNot} — AND is the default (Lucid `andWhereNot`). */
1697
+ andWhereNot(callback: (query: DatabaseQueryBuilder) => void): this;
1698
+ andWhereNot(conditions: Record<string, unknown>): this;
1699
+ andWhereNot(column: string, value: unknown): this;
1700
+ andWhereNot(column: string, operator: WhereOperator, value: unknown): this;
1701
+ andWhereNot(
1702
+ columnOrCbOrObj:
1703
+ | string
1704
+ | ((query: DatabaseQueryBuilder) => void)
1705
+ | Record<string, unknown>,
1706
+ operatorOrValue?: WhereOperator | unknown,
1707
+ value?: unknown,
1708
+ ): this {
1709
+ return this.#applyWhereNot("and", columnOrCbOrObj, operatorOrValue, value);
1710
+ }
1711
+
1712
+ /** OR form of {@link whereNot} (Lucid/Knex `orWhereNot`). Same argument forms. */
1713
+ orWhereNot(callback: (query: DatabaseQueryBuilder) => void): this;
1714
+ orWhereNot(conditions: Record<string, unknown>): this;
1715
+ orWhereNot(column: string, value: unknown): this;
1716
+ orWhereNot(column: string, operator: WhereOperator, value: unknown): this;
1717
+ orWhereNot(
1718
+ columnOrCbOrObj:
1719
+ | string
1720
+ | ((query: DatabaseQueryBuilder) => void)
1721
+ | Record<string, unknown>,
1722
+ operatorOrValue?: WhereOperator | unknown,
1723
+ value?: unknown,
1724
+ ): this {
1725
+ return this.#applyWhereNot("or", columnOrCbOrObj, operatorOrValue, value);
1726
+ }
1727
+
1728
+ #applyWhereNot(
1729
+ boolean: "and" | "or",
1730
+ columnOrCbOrObj:
1731
+ | string
1732
+ | ((query: DatabaseQueryBuilder) => void)
1733
+ | Record<string, unknown>,
1734
+ operatorOrValue?: WhereOperator | unknown,
1735
+ value?: unknown,
1736
+ ): this {
1737
+ if (typeof columnOrCbOrObj === "function") {
1738
+ const sub = new DatabaseQueryBuilder(this.#exec, this.#dialect);
1739
+ columnOrCbOrObj(sub);
1740
+ this.#wheres.push({
1741
+ kind: "group",
1742
+ conditions: sub.#compiledWheres(),
1743
+ boolean,
1744
+ negated: true,
1745
+ });
1746
+ return this;
1747
+ }
1748
+ if (typeof columnOrCbOrObj === "object") {
1749
+ for (const [col, val] of Object.entries(columnOrCbOrObj)) {
1750
+ this.#cmp(boolean, col, "<>", val);
1751
+ }
1752
+ return this;
1753
+ }
1754
+ if (value === undefined) {
1755
+ this.#cmp(boolean, columnOrCbOrObj, negateOperator("="), operatorOrValue);
1756
+ } else {
1757
+ this.#cmp(
1758
+ boolean,
1759
+ columnOrCbOrObj,
1760
+ negateOperator(String(operatorOrValue)),
1761
+ value,
1762
+ );
1763
+ }
1764
+ return this;
1765
+ }
1766
+
1767
+ /** OR col IN (...) (Lucid/Knex `orWhereIn`). */
1768
+ orWhereIn(column: string, values: unknown[]): this;
1769
+ orWhereIn(column: string, subquery: SubqueryArg): this;
1770
+ orWhereIn(column: string, valuesOrSub: unknown[] | SubqueryArg): this {
1771
+ if (
1772
+ valuesOrSub instanceof DatabaseQueryBuilder ||
1773
+ typeof valuesOrSub === "function"
1774
+ ) {
1775
+ return this.#pushInSub("or", false, column, valuesOrSub);
1776
+ }
1777
+ this.#cmp("or", column, "IN", valuesOrSub);
1778
+ return this;
1779
+ }
1780
+
1781
+ /** OR col NOT IN (...) (Lucid/Knex `orWhereNotIn`). */
1782
+ orWhereNotIn(column: string, values: unknown[]): this {
1783
+ this.#cmp("or", column, "NOT IN", values);
1784
+ return this;
1785
+ }
1786
+
1787
+ /** OR col IS NULL (Lucid/Knex `orWhereNull`). */
1788
+ orWhereNull(column: string): this {
1789
+ this.#cmp("or", column, "IS NULL", null);
1790
+ return this;
1791
+ }
1792
+
1793
+ /** OR col IS NOT NULL (Lucid/Knex `orWhereNotNull`). */
1794
+ orWhereNotNull(column: string): this {
1795
+ this.#cmp("or", column, "IS NOT NULL", null);
1796
+ return this;
1797
+ }
1798
+
1799
+ /** OR col BETWEEN ? AND ? (Lucid/Knex `orWhereBetween`). */
1800
+ orWhereBetween(column: string, range: readonly [unknown, unknown]): this {
1801
+ this.#cmp("or", column, "BETWEEN", [...range]);
1802
+ return this;
1803
+ }
1804
+
1805
+ /** OR col NOT BETWEEN ? AND ? (Lucid/Knex `orWhereNotBetween`). */
1806
+ orWhereNotBetween(column: string, range: readonly [unknown, unknown]): this {
1807
+ this.#cmp("or", column, "NOT BETWEEN", [...range]);
1808
+ return this;
1809
+ }
1810
+
1811
+ /** OR col LIKE ? (Lucid/Knex `orWhereLike`). */
1812
+ orWhereLike(column: string, pattern: string): this {
1813
+ this.#cmp("or", column, "LIKE", pattern);
1814
+ return this;
1815
+ }
1816
+
1817
+ /** OR col ILIKE ? (Lucid/Knex `orWhereILike`). */
1818
+ orWhereILike(column: string, pattern: string): this {
1819
+ this.#cmp("or", column, "ILIKE", pattern);
1820
+ return this;
1821
+ }
1822
+
1823
+ /** A raw JOIN fragment with `?` bindings (Lucid/Knex `joinRaw`). */
1824
+ joinRaw(sql: string, bindings: unknown[] = []): this {
1825
+ this.#joins.push({ sql, params: bindings });
1826
+ return this;
1827
+ }
1828
+
1829
+ /** `FOR UPDATE` row lock (Lucid/Knex `forUpdate`). Dropped on SQLite. */
1830
+ forUpdate(): this {
1831
+ if (this.#dialect === "sqlite") {
1832
+ console.warn(
1833
+ "[atlas] forUpdate ignored on sqlite (no row-level lock support)",
1834
+ );
1835
+ } else {
1836
+ this.#lockMode = "FOR UPDATE";
1837
+ }
1838
+ return this;
1839
+ }
1840
+
1841
+ /** `FOR SHARE` row lock (Lucid/Knex `forShare`). Dropped on SQLite. */
1842
+ forShare(): this {
1843
+ if (this.#dialect === "sqlite") {
1844
+ console.warn(
1845
+ "[atlas] forShare ignored on sqlite (no row-level lock support)",
1846
+ );
1847
+ } else {
1848
+ this.#lockMode = "FOR SHARE";
1849
+ }
1850
+ return this;
1851
+ }
1852
+
1853
+ /** Postgres `FOR NO KEY UPDATE` — weaker lock that doesn't block FK checks (Lucid/Knex). */
1854
+ forNoKeyUpdate(): this {
1855
+ if (this.#dialect === "postgres") {
1856
+ this.#lockMode = "FOR NO KEY UPDATE";
1857
+ } else {
1858
+ console.warn(
1859
+ `[atlas] forNoKeyUpdate ignored on ${this.#dialect} (Postgres-only lock)`,
1860
+ );
1861
+ }
1862
+ return this;
1863
+ }
1864
+
1865
+ /** Postgres `FOR KEY SHARE` — the weakest share lock (Lucid/Knex). */
1866
+ forKeyShare(): this {
1867
+ if (this.#dialect === "postgres") {
1868
+ this.#lockMode = "FOR KEY SHARE";
1869
+ } else {
1870
+ console.warn(
1871
+ `[atlas] forKeyShare ignored on ${this.#dialect} (Postgres-only lock)`,
1872
+ );
1873
+ }
1874
+ return this;
1875
+ }
1876
+
1877
+ /** Append `SKIP LOCKED` — skip locked rows instead of waiting (Lucid/Knex). */
1878
+ skipLocked(): this {
1879
+ if (this.#dialect === "sqlite") {
1880
+ console.warn("[atlas] skipLocked ignored on sqlite (no row-level lock)");
1881
+ } else {
1882
+ this.#lockModifier = "SKIP LOCKED";
1883
+ }
1884
+ return this;
1885
+ }
1886
+
1887
+ /** Append `NOWAIT` — error immediately on a locked row (Lucid/Knex). */
1888
+ noWait(): this {
1889
+ if (this.#dialect === "sqlite") {
1890
+ console.warn("[atlas] noWait ignored on sqlite (no row-level lock)");
1891
+ } else {
1892
+ this.#lockModifier = "NOWAIT";
1893
+ }
1894
+ return this;
1895
+ }
1896
+
1897
+ /** Apply `cb` only when `condition` is truthy, else `elseCb` (Lucid `if`). */
1898
+ if(
1899
+ condition: unknown,
1900
+ cb: (query: this) => void,
1901
+ elseCb?: (query: this) => void,
1902
+ ): this {
1903
+ if (condition) cb(this);
1904
+ else elseCb?.(this);
1905
+ return this;
1906
+ }
1907
+
1908
+ /** Inverse of {@link if} — apply `cb` only when `condition` is falsy (Lucid `unless`). */
1909
+ unless(
1910
+ condition: unknown,
1911
+ cb: (query: this) => void,
1912
+ elseCb?: (query: this) => void,
1913
+ ): this {
1914
+ if (!condition) cb(this);
1915
+ else elseCb?.(this);
1916
+ return this;
1917
+ }
1918
+
1919
+ /** Apply the first `[guard, cb]` whose guard is truthy; a trailing bare cb is
1920
+ * the default (Adonis Lucid `match`). */
1921
+ match(
1922
+ ...blocks: Array<[unknown, (query: this) => void] | ((query: this) => void)>
1923
+ ): this {
1924
+ for (const block of blocks) {
1925
+ if (typeof block === "function") {
1926
+ block(this);
1927
+ return this;
1928
+ }
1929
+ const [guard, cb] = block;
1930
+ if (guard) {
1931
+ cb(this);
1932
+ return this;
1933
+ }
1934
+ }
1935
+ return this;
1936
+ }
1937
+
1938
+ /** ORDER BY a column, a `db.ref()` fragment, or an array of terms (Lucid/Knex `orderBy`). */
1939
+ orderBy(column: string | RawSql, direction?: "asc" | "desc"): this;
1940
+ orderBy(
1941
+ terms: Array<string | { column: string; order?: "asc" | "desc" }>,
1942
+ ): this;
1943
+ orderBy(
1944
+ columnOrTerms:
1945
+ | string
1946
+ | RawSql
1947
+ | Array<string | { column: string; order?: "asc" | "desc" }>,
1948
+ direction: "asc" | "desc" = "asc",
1949
+ ): this {
1950
+ // A raw fragment (e.g. `db.ref('posts.created_at')`) orders verbatim.
1951
+ if (columnOrTerms instanceof RawSql) {
1952
+ const dir = direction === "desc" ? "DESC" : "ASC";
1953
+ this.#orderBys.push({ raw: `${columnOrTerms.sql} ${dir}` });
1954
+ return this;
1955
+ }
1956
+ if (Array.isArray(columnOrTerms)) {
1957
+ for (const t of columnOrTerms) {
1958
+ if (typeof t === "string") {
1959
+ this.#orderBys.push({ column: t, direction: "asc" });
1960
+ } else {
1961
+ this.#orderBys.push({
1962
+ column: t.column,
1963
+ direction: t.order ?? "asc",
1964
+ });
1965
+ }
1966
+ }
1967
+ return this;
1968
+ }
1969
+ this.#orderBys.push({ column: columnOrTerms, direction });
1970
+ return this;
1971
+ }
1972
+
1973
+ limit(n: number): this {
1974
+ this.#limit = n;
1975
+ return this;
1976
+ }
1977
+
1978
+ offset(n: number): this {
1979
+ this.#offset = n;
1980
+ return this;
1981
+ }
1982
+
1983
+ /** The table, qualified with a schema when {@link withSchema} was used. */
1984
+ #qualifiedTable(): string {
1985
+ return this.#schema ? `${this.#schema}.${this.#table}` : this.#table;
1986
+ }
1987
+
1988
+ /** `FOR UPDATE`(+`SKIP LOCKED`/`NOWAIT`) or null — the modifier needs a base lock. */
1989
+ #composedLockMode(): string | null {
1990
+ if (!this.#lockMode) return null;
1991
+ return this.#lockModifier
1992
+ ? `${this.#lockMode} ${this.#lockModifier}`
1993
+ : this.#lockMode;
1994
+ }
1995
+
1996
+ /** LIMIT/OFFSET for a 1-based page (Lucid/Knex `forPage`). */
1997
+ forPage(page: number, perPage = 20): this {
1998
+ this.#limit = perPage;
1999
+ this.#offset = (Math.max(1, page) - 1) * perPage;
2000
+ return this;
2001
+ }
2002
+
2003
+ /** Build the SELECT spec JSON directly (full grammar: joins, locks, raw, etc.). */
2004
+ #selectSpec(select?: string[]): Record<string, unknown> {
2005
+ return {
2006
+ kind: "select",
2007
+ table: this.#qualifiedTable(),
2008
+ fromSubquery: this.#fromSubquery ?? null,
2009
+ select: select ?? (this.#selects.length > 0 ? this.#selects : ["*"]),
2010
+ wheres: this.#compiledWheres(),
2011
+ orderBy: this.#orderBys,
2012
+ groupBy: this.#groupBys,
2013
+ having: this.#havings,
2014
+ limit: this.#limit ?? null,
2015
+ offset: this.#offset ?? null,
2016
+ distinct: this.#distinctFlag,
2017
+ distinctOn: this.#distinctOn,
2018
+ ctes: this.#ctes,
2019
+ unions: this.#unions,
2020
+ selectSubqueries: [],
2021
+ selectRaw: this.#selectRaw,
2022
+ joins: this.#joins,
2023
+ lockMode: this.#composedLockMode(),
2024
+ };
2025
+ }
2026
+
2027
+ /**
2028
+ * Native compiled SELECT `{ sql, params }` (Postgres `$N` placeholders) — the
2029
+ * form atlas executes against the driver and embeds inside parent queries.
2030
+ * Public `toSQL()` normalizes this to Knex `?` for Lucid parity.
2031
+ */
2032
+ #compiledNative(): { sql: string; params: unknown[] } {
2033
+ const compiled = compileStatementNative(this.#selectSpec(), this.#dialect);
2034
+ const sql = this.#commentPrefix() + compiled.statements[0];
2035
+ if (this.#debug) {
2036
+ console.debug("[atlas:sql]", sql, compiled.params);
2037
+ }
2038
+ return { sql, params: compiled.params };
2039
+ }
2040
+
2041
+ /**
2042
+ * The compiled SELECT WITHOUT executing (Lucid `toSQL`). `.sql` uses `?`
2043
+ * placeholders (Knex-normalized, like Lucid); `.toNative()` yields the native
2044
+ * form. Returns both `bindings` (Lucid's name) and `params` (atlas's) — same
2045
+ * array, so either name ports.
2046
+ */
2047
+ toSQL(): CompiledStatement {
2048
+ const { sql, params } = this.#compiledNative();
2049
+ return compiledStatement(sql, params);
2050
+ }
2051
+
2052
+ /** Apply `cb` only on the given dialect(s) (Lucid `ifDialect`; Lucid names accepted). */
2053
+ ifDialect(
2054
+ dialect: DialectName | DialectName[],
2055
+ cb: (query: this) => void,
2056
+ ): this {
2057
+ const set = (Array.isArray(dialect) ? dialect : [dialect]).map(
2058
+ normalizeDialect,
2059
+ );
2060
+ if (set.includes(this.#dialect)) cb(this);
2061
+ return this;
2062
+ }
2063
+
2064
+ /** Apply `cb` on every dialect EXCEPT the given one(s) (Lucid `unlessDialect`). */
2065
+ unlessDialect(
2066
+ dialect: DialectName | DialectName[],
2067
+ cb: (query: this) => void,
2068
+ ): this {
2069
+ const set = (Array.isArray(dialect) ? dialect : [dialect]).map(
2070
+ normalizeDialect,
2071
+ );
2072
+ if (!set.includes(this.#dialect)) cb(this);
2073
+ return this;
2074
+ }
2075
+
2076
+ /** Run the SELECT and return every row. */
2077
+ async exec(): Promise<T[]> {
2078
+ const { sql, params } = this.#compiledNative();
2079
+ return this.#raceTimeout(
2080
+ this.#exec.query<T>(sql, params, this.#queryMeta("exec")),
2081
+ );
2082
+ }
2083
+
2084
+ /** Run the SELECT and return the first row (Lucid `first`), or `null`. */
2085
+ async first(): Promise<T | null> {
2086
+ this.#limit = 1;
2087
+ const rows = await this.exec();
2088
+ return rows[0] ?? null;
2089
+ }
2090
+
2091
+ /** {@link first} but throws when no row matches (Lucid `firstOrFail`). */
2092
+ async firstOrFail(): Promise<T> {
2093
+ const row = await this.first();
2094
+ if (row === null) throw new Error("firstOrFail: no matching row");
2095
+ return row;
2096
+ }
2097
+
2098
+ /** Return a single column's values across the result set (Lucid/Knex `pluck`). */
2099
+ async pluck(column: string): Promise<unknown[]> {
2100
+ const { sql, params } = this.#compiledNative();
2101
+ const rows = await this.#raceTimeout(
2102
+ this.#exec.query<Record<string, unknown>>(
2103
+ sql,
2104
+ params,
2105
+ this.#queryMeta("pluck"),
2106
+ ),
2107
+ );
2108
+ return rows.map((r) => r[column]);
2109
+ }
2110
+
2111
+ /** SQL with bindings substituted as literals, for inspection (Lucid `toQuery`). */
2112
+ toQuery(): string {
2113
+ const { sql, params } = this.toSQL();
2114
+ return interpolateQuery(sql, params);
2115
+ }
2116
+
2117
+ /** `{ sql, bindings }` — the compiled native query (Lucid `toNative`). */
2118
+ toNative(): { sql: string; bindings: unknown[] } {
2119
+ const { sql, params } = this.#compiledNative();
2120
+ return { sql, bindings: params };
2121
+ }
2122
+
2123
+ /** Run a scalar aggregate (COUNT/SUM/AVG/MIN/MAX) over the current WHERE. */
2124
+ async #aggregate(expr: string): Promise<number> {
2125
+ const compiled = compileStatementNative(
2126
+ this.#selectSpec([`${expr} AS aggregate`]),
2127
+ this.#dialect,
2128
+ );
2129
+ const rows = await this.#raceTimeout(
2130
+ this.#exec.query<{ aggregate: number | string | null }>(
2131
+ compiled.statements[0],
2132
+ compiled.params,
2133
+ this.#queryMeta("aggregate"),
2134
+ ),
2135
+ );
2136
+ return Number(rows[0]?.aggregate ?? 0);
2137
+ }
2138
+
2139
+ /**
2140
+ * Build a `FN(expr) AS alias` projection string from an `expr [as alias]` form.
2141
+ * `'* as total'` → `COUNT(*) AS total`; `'amount'` → `SUM(amount)`.
2142
+ */
2143
+ #aggProjection(fn: string, expr: string): string {
2144
+ const m = expr.match(/^(.*?)\s+as\s+(.+)$/i);
2145
+ return m ? `${fn}(${m[1].trim()}) AS ${m[2].trim()}` : `${fn}(${expr})`;
2146
+ }
2147
+
2148
+ /**
2149
+ * COUNT — terminal scalar with no argument (atlas DX: `await q.count()` → n),
2150
+ * or a chainable projection with an aliased expression (Lucid/Knex:
2151
+ * `q.count('* as total').groupBy(...)`).
2152
+ */
2153
+ count(): Promise<number>;
2154
+ count(aliasExpr: `${string} as ${string}`): this;
2155
+ count(aliasExpr?: string): Promise<number> | this {
2156
+ if (aliasExpr === undefined) return this.#aggregate("COUNT(*)");
2157
+ this.#selects.push(this.#aggProjection("COUNT", aliasExpr));
2158
+ return this;
2159
+ }
2160
+
2161
+ /** SUM — terminal scalar (`sum('amount')`) or chainable projection (`sum('amount as total')`). */
2162
+ sum(aliasExpr: `${string} as ${string}`): this;
2163
+ sum(column: string): Promise<number>;
2164
+ sum(expr: string): Promise<number> | this {
2165
+ return this.#aggMethod("SUM", expr);
2166
+ }
2167
+
2168
+ /** AVG — terminal scalar or chainable projection (Lucid/Knex `avg`). */
2169
+ avg(aliasExpr: `${string} as ${string}`): this;
2170
+ avg(column: string): Promise<number>;
2171
+ avg(expr: string): Promise<number> | this {
2172
+ return this.#aggMethod("AVG", expr);
2173
+ }
2174
+
2175
+ /** MIN — terminal scalar or chainable projection (Lucid/Knex `min`). */
2176
+ min(aliasExpr: `${string} as ${string}`): this;
2177
+ min(column: string): Promise<number>;
2178
+ min(expr: string): Promise<number> | this {
2179
+ return this.#aggMethod("MIN", expr);
2180
+ }
2181
+
2182
+ /** MAX — terminal scalar or chainable projection (Lucid/Knex `max`). */
2183
+ max(aliasExpr: `${string} as ${string}`): this;
2184
+ max(column: string): Promise<number>;
2185
+ max(expr: string): Promise<number> | this {
2186
+ return this.#aggMethod("MAX", expr);
2187
+ }
2188
+
2189
+ /** An aliased `expr` (`col as alias`) is a chainable projection; a bare column is a terminal scalar. */
2190
+ #aggMethod(fn: string, expr: string): Promise<number> | this {
2191
+ if (/\s+as\s+/i.test(expr)) {
2192
+ this.#selects.push(this.#aggProjection(fn, expr));
2193
+ return this;
2194
+ }
2195
+ return this.#aggregate(`${fn}(${expr})`);
2196
+ }
2197
+
2198
+ /** COUNT(DISTINCT column) (Lucid/Knex `countDistinct`). */
2199
+ countDistinct(column: string): Promise<number> {
2200
+ return this.#aggregate(`COUNT(DISTINCT ${column})`);
2201
+ }
2202
+
2203
+ /** SUM(DISTINCT column) (Lucid/Knex `sumDistinct`). */
2204
+ sumDistinct(column: string): Promise<number> {
2205
+ return this.#aggregate(`SUM(DISTINCT ${column})`);
2206
+ }
2207
+
2208
+ /** AVG(DISTINCT column) (Lucid/Knex `avgDistinct`). */
2209
+ avgDistinct(column: string): Promise<number> {
2210
+ return this.#aggregate(`AVG(DISTINCT ${column})`);
2211
+ }
2212
+
2213
+ /** WHERE clauses translated to the native compiler's JSON shapes. */
2214
+ #compiledWheres(): CompiledWhere[] {
2215
+ return this.#wheres.map((w): CompiledWhere => {
2216
+ switch (w.kind) {
2217
+ case "raw":
2218
+ return {
2219
+ kind: "raw",
2220
+ sql: w.sql,
2221
+ bindings: w.bindings,
2222
+ type: w.boolean,
2223
+ };
2224
+ case "exists":
2225
+ return {
2226
+ kind: "exists",
2227
+ negated: w.negated,
2228
+ subquery: w.subquery,
2229
+ type: w.boolean,
2230
+ };
2231
+ case "inSub":
2232
+ return {
2233
+ kind: "inSub",
2234
+ column: w.column,
2235
+ negated: w.negated,
2236
+ subquery: w.subquery,
2237
+ type: w.boolean,
2238
+ };
2239
+ case "inTuple":
2240
+ return {
2241
+ kind: "inTuple",
2242
+ columns: w.columns,
2243
+ rows: w.rows,
2244
+ negated: w.negated,
2245
+ type: w.boolean,
2246
+ };
2247
+ case "json":
2248
+ return {
2249
+ kind: "json",
2250
+ jsonOp: w.jsonOp,
2251
+ column: w.column,
2252
+ negated: w.negated,
2253
+ path: w.path,
2254
+ operator: w.operator,
2255
+ value: w.value,
2256
+ type: w.boolean,
2257
+ };
2258
+ case "group":
2259
+ return {
2260
+ kind: "group",
2261
+ conditions: w.conditions,
2262
+ type: w.boolean,
2263
+ negated: w.negated ?? false,
2264
+ };
2265
+ default:
2266
+ return {
2267
+ column: w.column,
2268
+ operator: w.operator,
2269
+ value: w.value,
2270
+ type: w.boolean,
2271
+ };
2272
+ }
2273
+ });
2274
+ }
2275
+
2276
+ /**
2277
+ * Columns to return from a subsequent insert/update/delete (Lucid `returning`).
2278
+ * Accepts spread names, an array, or `'*'` — `returning('id')`,
2279
+ * `returning(['id', 'created_at'])`, `returning('*')`.
2280
+ */
2281
+ returning(...columns: Array<string | string[]>): this {
2282
+ for (const c of columns) {
2283
+ if (Array.isArray(c)) this.#returningCols.push(...c);
2284
+ else this.#returningCols.push(c);
2285
+ }
2286
+ return this;
2287
+ }
2288
+
2289
+ /** The `/* … *​/` prefix for the compiled SQL (Lucid `comment`), or empty. */
2290
+ #commentPrefix(): string {
2291
+ return this.#comments.length > 0
2292
+ ? `${this.#comments.map((c) => `/* ${c} */`).join(" ")} `
2293
+ : "";
2294
+ }
2295
+
2296
+ /**
2297
+ * Compile a DML spec (adding RETURNING when set) with the comment prefix —
2298
+ * shared by the lazy builders' `.toSQL()` and their execution.
2299
+ */
2300
+ #compileDmlSpec(spec: Record<string, unknown>): {
2301
+ sql: string;
2302
+ bindings: unknown[];
2303
+ params: unknown[];
2304
+ } {
2305
+ const withReturning =
2306
+ this.#returningCols.length > 0
2307
+ ? { ...spec, returning: this.#returningCols }
2308
+ : spec;
2309
+ const compiled = compileStatementNative(withReturning, this.#dialect);
2310
+ const sql = this.#commentPrefix() + compiled.statements[0];
2311
+ return { sql, bindings: compiled.params, params: compiled.params };
2312
+ }
2313
+
2314
+ /** Run a DML spec: RETURNING rows when set, else execute; `interpret` shapes the result. */
2315
+ async #runDml<R>(
2316
+ spec: Record<string, unknown>,
2317
+ interpret: (result: unknown, rows: Record<string, unknown>[] | null) => R,
2318
+ ): Promise<R> {
2319
+ const { sql, params } = this.#compileDmlSpec(spec);
2320
+ const method = String(spec.kind ?? "dml");
2321
+ if (this.#returningCols.length > 0) {
2322
+ const rows = await this.#raceTimeout(
2323
+ this.#exec.query<Record<string, unknown>>(
2324
+ sql,
2325
+ params,
2326
+ this.#queryMeta(method),
2327
+ ),
2328
+ );
2329
+ return interpret(null, rows);
2330
+ }
2331
+ const result = await this.#raceTimeout(
2332
+ this.#exec.execute(sql, params, this.#queryMeta(method)),
2333
+ );
2334
+ return interpret(result, null);
2335
+ }
2336
+
2337
+ /** An insert/upsert result: RETURNING rows, else `[insertId]` (MySQL/SQLite) or `[]`. */
2338
+ readonly #interpretInsert = (
2339
+ result: unknown,
2340
+ rows: Record<string, unknown>[] | null,
2341
+ ): Array<Record<string, unknown> | number> => {
2342
+ if (rows) return rows;
2343
+ if (this.#dialect === "mysql" || this.#dialect === "sqlite") {
2344
+ const id = lastInsertIdOf(result);
2345
+ if (id !== undefined) return [id];
2346
+ }
2347
+ return [];
2348
+ };
2349
+
2350
+ /** An update/delete result: RETURNING rows, else the affected-row count. */
2351
+ readonly #interpretWrite = (
2352
+ result: unknown,
2353
+ rows: Record<string, unknown>[] | null,
2354
+ ): number | Record<string, unknown>[] => {
2355
+ return rows ?? rowsAffected(result);
2356
+ };
2357
+
2358
+ /** The chainable-clause hooks the lazy {@link DmlBuilder} delegates back to. */
2359
+ #dmlHooks(): DmlChainHooks {
2360
+ return {
2361
+ onConflict: (...c) => {
2362
+ this.onConflict(...c);
2363
+ },
2364
+ merge: (...a) => {
2365
+ this.merge(...a);
2366
+ },
2367
+ ignore: () => {
2368
+ this.ignore();
2369
+ },
2370
+ returning: (...c) => {
2371
+ this.returning(...c);
2372
+ },
2373
+ timeout: (ms, options) => {
2374
+ this.timeout(ms, options);
2375
+ },
2376
+ comment: (t) => {
2377
+ this.comment(t);
2378
+ },
2379
+ debug: (enabled) => {
2380
+ this.debug(enabled);
2381
+ },
2382
+ reporterData: (data) => {
2383
+ this.reporterData(data);
2384
+ },
2385
+ };
2386
+ }
2387
+
2388
+ /**
2389
+ * Conflict target for an upsert (Lucid/Knex `onConflict`). Accepts spread
2390
+ * names, an array, or no argument (any unique constraint) —
2391
+ * `onConflict('email')`, `onConflict(['email', 'tenant_id'])`, `onConflict()`.
2392
+ */
2393
+ onConflict(...columns: Array<string | string[]>): this {
2394
+ this.#onConflictCols = columns.flat();
2395
+ return this;
2396
+ }
2397
+
2398
+ /**
2399
+ * On conflict, UPDATE columns (Lucid/Knex `merge`). No argument updates every
2400
+ * insert column; spread names or an array update only those; an object sets
2401
+ * custom values (scalars or `db.raw(...)` expressions) —
2402
+ * `merge()`, `merge(['a', 'b'])`, `merge({ login_count: db.raw('users.login_count + 1') })`.
2403
+ */
2404
+ merge(...args: Array<string | string[] | Record<string, unknown>>): this {
2405
+ this.#mergeMode = "merge";
2406
+ const cols: string[] = [];
2407
+ const set: Array<{
2408
+ column: string;
2409
+ value?: unknown;
2410
+ raw?: string;
2411
+ rawParams?: unknown[];
2412
+ }> = [];
2413
+ for (const a of args) {
2414
+ if (typeof a === "string") {
2415
+ cols.push(a);
2416
+ } else if (Array.isArray(a)) {
2417
+ cols.push(...a);
2418
+ } else {
2419
+ for (const [col, v] of Object.entries(a)) {
2420
+ if (v instanceof RawSql) {
2421
+ set.push({ column: col, raw: v.sql, rawParams: [...v.params] });
2422
+ } else {
2423
+ set.push({ column: col, value: v });
2424
+ }
2425
+ }
2426
+ }
2427
+ }
2428
+ this.#mergeCols = cols;
2429
+ this.#mergeSet = set.length > 0 ? set : undefined;
2430
+ return this;
2431
+ }
2432
+
2433
+ /** On conflict, do nothing (Lucid/Knex `onConflict(...).ignore()`). */
2434
+ ignore(): this {
2435
+ this.#mergeMode = "ignore";
2436
+ return this;
2437
+ }
2438
+
2439
+ /** Build the insert or upsert spec from the current onConflict/merge/returning state. */
2440
+ #buildInsertOrUpsertSpec(
2441
+ rows: Array<Array<[string, unknown]>>,
2442
+ ): Record<string, unknown> {
2443
+ if (this.#onConflictCols) {
2444
+ const conflictColumns = this.#onConflictCols;
2445
+ const allCols = rows[0]?.map(([c]) => c) ?? [];
2446
+ const updateColumns =
2447
+ this.#mergeMode === "ignore"
2448
+ ? []
2449
+ : this.#mergeCols.length > 0
2450
+ ? this.#mergeCols
2451
+ : allCols.filter((c) => !conflictColumns.includes(c));
2452
+ return {
2453
+ kind: "upsert",
2454
+ table: this.#qualifiedTable(),
2455
+ rows,
2456
+ conflictColumns,
2457
+ updateColumns,
2458
+ updateSet: this.#mergeSet ?? [],
2459
+ ctes: this.#ctes,
2460
+ };
2461
+ }
2462
+ return {
2463
+ kind: "insert",
2464
+ table: this.#qualifiedTable(),
2465
+ rows,
2466
+ ctes: this.#ctes,
2467
+ };
2468
+ }
2469
+
2470
+ #buildUpdateSpec(set: Array<[string, unknown]>): Record<string, unknown> {
2471
+ return {
2472
+ kind: "update",
2473
+ table: this.#qualifiedTable(),
2474
+ set,
2475
+ wheres: this.#compiledWheres(),
2476
+ ctes: this.#ctes,
2477
+ };
2478
+ }
2479
+
2480
+ #buildDeleteSpec(): Record<string, unknown> {
2481
+ return {
2482
+ kind: "delete",
2483
+ table: this.#qualifiedTable(),
2484
+ wheres: this.#compiledWheres(),
2485
+ ctes: this.#ctes,
2486
+ };
2487
+ }
2488
+
2489
+ /**
2490
+ * Insert one row (Lucid `db.table(t).insert(data)`). Lazy + chainable: the
2491
+ * statement runs on `await`/`.exec()`, so `insert(data).onConflict(...).merge()`,
2492
+ * `insert(data).returning(...)` and `insert(data).toSQL()` all work. Resolves to
2493
+ * the RETURNING rows, or `[insertId]` (MySQL/SQLite) / `[]` otherwise.
2494
+ */
2495
+ insert(
2496
+ data: Record<string, unknown>,
2497
+ ): DmlBuilder<Array<Record<string, unknown> | number>> {
2498
+ this.#assertWritable();
2499
+ const rows = [Object.entries(data)];
2500
+ return new DmlBuilder(
2501
+ () =>
2502
+ rows[0].length === 0
2503
+ ? Promise.resolve<Array<Record<string, unknown> | number>>([])
2504
+ : this.#runDml(
2505
+ this.#buildInsertOrUpsertSpec(rows),
2506
+ this.#interpretInsert,
2507
+ ),
2508
+ () => this.#compileDmlSpec(this.#buildInsertOrUpsertSpec(rows)),
2509
+ this.#dmlHooks(),
2510
+ );
2511
+ }
2512
+
2513
+ /** Insert many rows in one statement (Lucid/Knex `multiInsert`). Lazy + chainable. */
2514
+ multiInsert(
2515
+ rows: Array<Record<string, unknown>>,
2516
+ ): DmlBuilder<Array<Record<string, unknown> | number>> {
2517
+ this.#assertWritable();
2518
+ // Lucid fills missing keys with NULL — take the union of every row's
2519
+ // columns, then project each row onto it so all rows share one column set.
2520
+ const cols = Array.from(new Set(rows.flatMap((r) => Object.keys(r))));
2521
+ const rowEntries = rows.map((r) =>
2522
+ cols.map((c): [string, unknown] => [c, c in r ? r[c] : null]),
2523
+ );
2524
+ return new DmlBuilder(
2525
+ () =>
2526
+ rows.length === 0
2527
+ ? Promise.resolve<Array<Record<string, unknown> | number>>([])
2528
+ : this.#runDml(
2529
+ this.#buildInsertOrUpsertSpec(rowEntries),
2530
+ this.#interpretInsert,
2531
+ ),
2532
+ () => this.#compileDmlSpec(this.#buildInsertOrUpsertSpec(rowEntries)),
2533
+ this.#dmlHooks(),
2534
+ );
2535
+ }
2536
+
2537
+ /**
2538
+ * Update rows matching the current WHERE (Lucid/Knex `update`). Lazy + chainable
2539
+ * (`update(data).returning(...)`, `.toSQL()`). Accepts a `{ col: value }` map OR
2540
+ * a `(column, value)` pair; a value may be a `db.raw(...)` expression. Resolves to
2541
+ * the affected count, or the RETURNING rows when {@link returning} is set.
2542
+ */
2543
+ update(
2544
+ column: string,
2545
+ value: unknown,
2546
+ ): DmlBuilder<number | Record<string, unknown>[]>;
2547
+ update(
2548
+ data: Record<string, unknown>,
2549
+ ): DmlBuilder<number | Record<string, unknown>[]>;
2550
+ update(
2551
+ dataOrColumn: Record<string, unknown> | string,
2552
+ value?: unknown,
2553
+ ): DmlBuilder<number | Record<string, unknown>[]> {
2554
+ this.#assertWritable();
2555
+ const data =
2556
+ typeof dataOrColumn === "string"
2557
+ ? { [dataOrColumn]: value }
2558
+ : dataOrColumn;
2559
+ const set: Array<[string, unknown]> = Object.entries(data).map(
2560
+ ([col, v]): [string, unknown] =>
2561
+ v instanceof RawSql
2562
+ ? [col, { raw: v.sql, rawParams: [...v.params] }]
2563
+ : [col, v],
2564
+ );
2565
+ return new DmlBuilder(
2566
+ () =>
2567
+ set.length === 0
2568
+ ? Promise.resolve<number | Record<string, unknown>[]>(0)
2569
+ : this.#runDml(this.#buildUpdateSpec(set), this.#interpretWrite),
2570
+ () => this.#compileDmlSpec(this.#buildUpdateSpec(set)),
2571
+ this.#dmlHooks(),
2572
+ );
2573
+ }
2574
+
2575
+ /** Delete rows matching the current WHERE (Lucid `delete`). Lazy + chainable. */
2576
+ delete(): DmlBuilder<number | Record<string, unknown>[]> {
2577
+ this.#assertWritable();
2578
+ return new DmlBuilder(
2579
+ () => this.#runDml(this.#buildDeleteSpec(), this.#interpretWrite),
2580
+ () => this.#compileDmlSpec(this.#buildDeleteSpec()),
2581
+ this.#dmlHooks(),
2582
+ );
2583
+ }
2584
+
2585
+ /** Alias of {@link delete} (Lucid/Knex `del`). */
2586
+ del(): DmlBuilder<number | Record<string, unknown>[]> {
2587
+ return this.delete();
2588
+ }
2589
+
2590
+ /**
2591
+ * Atomically add to a column (or a `{col: amount}` map) — `SET col = col + ?`,
2592
+ * never read-modify-write (Lucid/Knex `increment`). Returns affected count.
2593
+ */
2594
+ increment(column: string, amount?: number): Promise<number>;
2595
+ increment(patch: Record<string, number>): Promise<number>;
2596
+ increment(
2597
+ colOrPatch: string | Record<string, number>,
2598
+ amount = 1,
2599
+ ): Promise<number> {
2600
+ return this.#runIncDec("increment", colOrPatch, amount);
2601
+ }
2602
+
2603
+ /** Atomically subtract from a column (Lucid/Knex `decrement`). */
2604
+ decrement(column: string, amount?: number): Promise<number>;
2605
+ decrement(patch: Record<string, number>): Promise<number>;
2606
+ decrement(
2607
+ colOrPatch: string | Record<string, number>,
2608
+ amount = 1,
2609
+ ): Promise<number> {
2610
+ return this.#runIncDec("decrement", colOrPatch, amount);
2611
+ }
2612
+
2613
+ async #runIncDec(
2614
+ op: "increment" | "decrement",
2615
+ colOrPatch: string | Record<string, number>,
2616
+ amount: number,
2617
+ ): Promise<number> {
2618
+ this.#assertWritable();
2619
+ const patch =
2620
+ typeof colOrPatch === "string" ? { [colOrPatch]: amount } : colOrPatch;
2621
+ const set = Object.entries(patch).map(
2622
+ ([col, value]) => [col, { op, value }] as [string, unknown],
2623
+ );
2624
+ if (set.length === 0) return 0;
2625
+ const compiled = compileStatementNative(
2626
+ {
2627
+ kind: "update",
2628
+ table: this.#qualifiedTable(),
2629
+ set,
2630
+ wheres: this.#compiledWheres(),
2631
+ ctes: this.#ctes,
2632
+ },
2633
+ this.#dialect,
2634
+ );
2635
+ const result = await this.#raceTimeout(
2636
+ this.#exec.execute(
2637
+ this.#commentPrefix() + compiled.statements[0],
2638
+ compiled.params,
2639
+ this.#queryMeta(op),
2640
+ ),
2641
+ );
2642
+ return rowsAffected(result);
2643
+ }
2644
+
2645
+ /**
2646
+ * Offset paginate the current query (Lucid/Knex `paginate`). Runs a COUNT over
2647
+ * the WHERE, then the page slice, and returns a {@link Paginator}.
2648
+ */
2649
+ async paginate(page: number, perPage = 20): Promise<Paginator<T>> {
2650
+ const p = Math.max(1, Math.floor(page));
2651
+ const pp = Math.max(1, Math.floor(perPage));
2652
+ // COUNT over the WHERE only — ignore limit/offset/orderBy (they don't apply
2653
+ // to a total). Built directly so paginate() doesn't disturb builder state.
2654
+ const countCompiled = compileStatementNative(
2655
+ {
2656
+ kind: "select",
2657
+ table: this.#qualifiedTable(),
2658
+ select: ["COUNT(*) AS aggregate"],
2659
+ wheres: this.#compiledWheres(),
2660
+ orderBy: [],
2661
+ groupBy: this.#groupBys,
2662
+ having: this.#havings,
2663
+ limit: null,
2664
+ offset: null,
2665
+ distinct: this.#distinctFlag,
2666
+ distinctOn: this.#distinctOn,
2667
+ ctes: this.#ctes,
2668
+ unions: this.#unions,
2669
+ selectSubqueries: [],
2670
+ joins: this.#joins,
2671
+ lockMode: null,
2672
+ },
2673
+ this.#dialect,
2674
+ );
2675
+ const countRows = await this.#exec.query<{
2676
+ aggregate: number | string | null;
2677
+ }>(
2678
+ countCompiled.statements[0],
2679
+ countCompiled.params,
2680
+ this.#queryMeta("paginate"),
2681
+ );
2682
+ const total = Number(countRows[0]?.aggregate ?? 0);
2683
+
2684
+ this.#limit = pp;
2685
+ this.#offset = (p - 1) * pp;
2686
+ const items = await this.exec();
2687
+ return new Paginator<T>(items, { total, perPage: pp, currentPage: p });
2688
+ }
2689
+
2690
+ /** Thenable, so `await db.from('users').where(...)` resolves to the rows. */
2691
+ // biome-ignore lint/suspicious/noThenProperty: Adonis Lucid query builders are awaitable by design — `await db.from(t).where(...)` must resolve to the rows; the thenable is the intended public API.
2692
+ then<R1 = T[], R2 = never>(
2693
+ onfulfilled?: ((value: T[]) => R1 | PromiseLike<R1>) | null,
2694
+ onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null,
2695
+ ): Promise<R1 | R2> {
2696
+ return this.exec().then(onfulfilled, onrejected);
2697
+ }
2698
+ }
2699
+
2700
+ /** Read an affected-row count from an execute() result, whatever its shape. */
2701
+ function rowsAffected(result: unknown): number {
2702
+ if (
2703
+ result !== null &&
2704
+ typeof result === "object" &&
2705
+ "rowsAffected" in result &&
2706
+ typeof result.rowsAffected === "number"
2707
+ ) {
2708
+ return result.rowsAffected;
2709
+ }
2710
+ return 0;
2711
+ }
2712
+
2713
+ /** Read the auto-increment id off an execute outcome (MySQL/SQLite). */
2714
+ function lastInsertIdOf(result: unknown): number | undefined {
2715
+ if (
2716
+ result !== null &&
2717
+ typeof result === "object" &&
2718
+ "lastInsertId" in result &&
2719
+ typeof result.lastInsertId === "number"
2720
+ ) {
2721
+ return result.lastInsertId;
2722
+ }
2723
+ return undefined;
2724
+ }