@atscript/db-mysql 0.1.146 → 0.1.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
2
  import { TMetadataMap } from "@atscript/typescript/utils";
3
3
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
4
- import { TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
4
+ import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
5
5
 
6
6
  //#region src/types.d.ts
7
7
  /**
@@ -136,6 +136,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
136
136
  * to `NO_ENGINE_SUBSTITUTION`) would turn `'abc'` into `0` silently.
137
137
  */
138
138
  private _withStrictSession;
139
+ /**
140
+ * Relational predicates (`$some` / `$none`) render as correlated
141
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
142
+ * An UPDATE / DELETE whose predicate reads the mutated table itself is
143
+ * rewritten through a materialized derived table (MySQL error 1093).
144
+ *
145
+ * @since 0.1.147
146
+ */
147
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
139
148
  /** MySQL InnoDB enforces FK constraints natively. */
140
149
  supportsNativeForeignKeys(): boolean;
141
150
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -146,8 +155,12 @@ declare class MysqlAdapter extends BaseDbAdapter {
146
155
  * server's time zone tables (probed per zone in `aggregate()`).
147
156
  */
148
157
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
149
- /** Every aggregate function, `countDistinct` included. */
158
+ /** Every aggregate function: `countDistinct`, `first` and `last` included. */
150
159
  aggregateFns(): ReadonlySet<AggregateFn>;
160
+ /** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
161
+ supportsAggregateExpressions(): boolean;
162
+ /** Computed view columns and first-row joins. */
163
+ viewCapabilities(): ReadonlySet<TViewCapability>;
151
164
  onBeforeFlatten(_type: unknown): void;
152
165
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
153
166
  getDesiredTableOptions(): TExistingTableOption[];
@@ -165,15 +178,89 @@ declare class MysqlAdapter extends BaseDbAdapter {
165
178
  *
166
179
  * MySQL uses numeric error codes:
167
180
  * - 1062 = ER_DUP_ENTRY (unique constraint violation)
181
+ * - 1586 = ER_DUP_ENTRY_WITH_KEY_NAME (same, for a multi-row INSERT)
168
182
  * - 1451 = ER_ROW_IS_REFERENCED_2 (FK violation on delete)
169
183
  * - 1452 = ER_NO_REFERENCED_ROW_2 (FK violation on insert/update)
170
184
  */
171
185
  private _wrapConstraintError;
186
+ /** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
187
+ private _mapConstraintError;
172
188
  private _mapFkError;
173
189
  insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
174
190
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
191
+ /** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
192
+ private _autoIncrementPk;
193
+ /**
194
+ * Splits `rows` into runs that each map to ONE statement with a derivable
195
+ * id sequence. MySQL reports only the first GENERATED id of a statement, and
196
+ * an explicit auto-increment value above the counter bumps the counter, so a
197
+ * statement mixing explicit and generated PKs cannot be mapped by
198
+ * `insertId + i * step`: the chunk is cut into CONSECUTIVE runs of one kind,
199
+ * executed in input order (so "an earlier row wins" a unique collision, even
200
+ * under a case-insensitive collation). A table without an AUTO_INCREMENT PK,
201
+ * or a chunk of one kind, stays one group.
202
+ */
203
+ private _idGroups;
204
+ /** The session `sql_mode` text on the current connection (`undefined` when the server returns no row). */
205
+ private _sessionSqlMode;
206
+ /** The {@link TZeroIsExplicit} of one `insertMany` / `insertManyIgnore` call. */
207
+ private _zeroIsExplicit;
208
+ /**
209
+ * Warns ONCE per driver when the session `sql_mode` is not strict: writes
210
+ * (insert-ignore included) assume `STRICT_TRANS_TABLES` / `STRICT_ALL_TABLES`
211
+ * (the MySQL 8 default); a non-strict mode coerces a NOT NULL violation to the
212
+ * column default instead of failing. Only probes when the adapter has a logger.
213
+ */
214
+ private _warnNonStrictMode;
215
+ /** The {@link TIncrementStep} of one `insertMany` / `insertManyIgnore` call. */
216
+ private _incrementStep;
217
+ /**
218
+ * Ids of the rows of ONE successful multi-row INSERT of a homogeneous
219
+ * {@link _idGroups} group: generated rows get ids from `insertId` stepping by
220
+ * the session's `@@auto_increment_increment` (consecutive within one
221
+ * statement under `innodb_autoinc_lock_mode` 0 / 1, and — for a known row
222
+ * count — mode 2, the 8.0 default), explicit rows keep their own value.
223
+ */
224
+ private _groupInsertedIds;
225
+ supportsInsertIgnore(): boolean;
226
+ /**
227
+ * Per chunk: ONE optimistic multi-row INSERT (an all-new batch costs a single
228
+ * statement). Only when it hits a duplicate key (errno 1062 / 1586) does ONE
229
+ * SELECT of the chunk's primary / unique key tuples find the stored rows
230
+ * (skipped as conflicts) and the survivors go in as one more multi-row
231
+ * INSERT — a dense-duplicate chunk is three statements, never O(rows). Only
232
+ * if that INSERT still collides (a concurrent writer raced in, or a
233
+ * collation-equal value the exact-match pre-check missed) are the survivors
234
+ * bisected: each half is retried, recursively, and a single row that still
235
+ * collides is skipped. A failed statement is rolled back by InnoDB alone, so
236
+ * the transaction stays usable. A chunk mixing explicit and generated
237
+ * auto-increment ids is processed as one such sequence per consecutive run of a kind. Deliberately
238
+ * NOT `INSERT IGNORE` (it would downgrade NOT NULL / FK / truncation errors
239
+ * to warnings) and not `ON DUPLICATE KEY UPDATE` (a no-op update is
240
+ * indistinguishable from an insert in the affected-rows count).
241
+ */
242
+ insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
243
+ /**
244
+ * Indices of `rows` whose primary / unique-index key tuple is already stored
245
+ * (a row with a null / missing key component never collides). One SELECT
246
+ * covers every key set; it is split only to stay under the parameter limit.
247
+ * Skipped entirely when no row carries a key value (generated PK, no unique
248
+ * index values).
249
+ */
250
+ private _findStoredKeyConflicts;
251
+ /** Optimistic INSERT of a group, then pre-check + survivor INSERT (+ bisect on a race). */
252
+ private _insertIgnoringGroup;
253
+ /** Retries the halves of a group whose one INSERT is known to collide; a lone colliding row is skipped. */
254
+ private _bisectGroup;
255
+ /** ONE INSERT of a group's rows; `undefined` on a duplicate key (errno 1062 / 1586). */
256
+ private _tryInsertGroup;
175
257
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
176
258
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
259
+ /**
260
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
261
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
262
+ */
263
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
177
264
  count(query: DbQuery): Promise<number>;
178
265
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
179
266
  /**
@@ -186,6 +273,26 @@ declare class MysqlAdapter extends BaseDbAdapter {
186
273
  * expression skips `CONVERT_TZ`. Successes are cached per driver.
187
274
  */
188
275
  private _ensureBucketZone;
276
+ /**
277
+ * The WHERE of an UPDATE / DELETE on this table. MySQL rejects a statement
278
+ * whose WHERE reads the mutated table in a subquery (error 1093,
279
+ * `ER_UPDATE_TABLE_USED`) — what a relational predicate (`$some` / `$none`)
280
+ * does when its target or junction table, at any nesting level, is this
281
+ * table (a self relation such as `parent`). Such a filter is re-keyed on the
282
+ * primary key through a materialized derived table:
283
+ *
284
+ * ```sql
285
+ * WHERE (<pk…>) IN (SELECT * FROM (SELECT DISTINCT <pk…> FROM t WHERE <where>) AS `_rfm`)
286
+ * ```
287
+ *
288
+ * `DISTINCT` keeps the optimizer from merging the derived table back into
289
+ * the statement. Every other filter renders as-is.
290
+ *
291
+ * @since 0.1.147
292
+ */
293
+ private _mutationWhere;
294
+ /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
295
+ private _filterReadsOwnTable;
189
296
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
190
297
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
191
298
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
@@ -284,6 +391,18 @@ declare class MysqlAdapter extends BaseDbAdapter {
284
391
  }
285
392
  //#endregion
286
393
  //#region src/mysql2-driver.d.ts
394
+ /** Options of {@link Mysql2Driver}. */
395
+ interface TMysql2DriverOptions {
396
+ /**
397
+ * Add `STRICT_TRANS_TABLES` to the session `sql_mode` of every new pool
398
+ * connection (default `true`, since 0.1.148). The server's own modes are
399
+ * kept — the mode is appended, never replaced. A non-strict server (Amazon
400
+ * RDS defaults to `NO_ENGINE_SUBSTITUTION`) otherwise coerces a `NOT NULL`,
401
+ * out-of-range or too-long value instead of failing the write. Pass `false`
402
+ * to keep the server's `sql_mode` untouched.
403
+ */
404
+ strictMode?: boolean;
405
+ }
287
406
  /**
288
407
  * {@link TMysqlDriver} implementation backed by `mysql2/promise`.
289
408
  *
@@ -311,6 +430,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
311
430
  * const driver = new Mysql2Driver(pool)
312
431
  * ```
313
432
  *
433
+ * Every new pool connection gets `STRICT_TRANS_TABLES` appended to its session
434
+ * `sql_mode` (since 0.1.148); pass `{ strictMode: false }` as the second
435
+ * argument to opt out. A pre-created `Pool` is covered too: connections it
436
+ * opened earlier get the statement when they are first acquired through it.
437
+ *
314
438
  * Requires `mysql2` to be installed:
315
439
  * ```bash
316
440
  * pnpm add mysql2
@@ -319,13 +443,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
319
443
  declare class Mysql2Driver implements TMysqlDriver {
320
444
  private pool;
321
445
  private poolInit;
322
- constructor(poolOrConfig: string | import("mysql2/promise").Pool | import("mysql2/promise").PoolOptions);
446
+ constructor(poolOrConfig: string | import("mysql2/promise").Pool | import("mysql2/promise").PoolOptions, options?: TMysql2DriverOptions);
323
447
  private getPool;
324
448
  run(sql: string, params?: unknown[]): Promise<TMysqlRunResult>;
325
449
  all<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
326
450
  get<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
327
451
  exec(sql: string): Promise<void>;
328
452
  getConnection(): Promise<TMysqlConnection>;
453
+ private _closing?;
454
+ /** Idempotent: every call returns the first call's promise. */
329
455
  close(): Promise<void>;
330
456
  }
331
457
  //#endregion
@@ -335,17 +461,24 @@ declare class Mysql2Driver implements TMysqlDriver {
335
461
  *
336
462
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
337
463
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
464
+ *
465
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
466
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
467
+ * source table) — a statement with an aliased FROM must pass its alias.
468
+ *
469
+ * @param opts - since 0.1.147
338
470
  */
339
- declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
471
+ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions): TSqlFragment$1;
340
472
  //#endregion
341
473
  //#region src/index.d.ts
342
474
  /**
343
475
  * Creates a {@link DbSpace} backed by a MySQL connection pool.
344
476
  *
345
477
  * @param uri - MySQL connection URI (e.g., `mysql://root@localhost:3306/mydb`)
346
- * @param options - Additional pool options passed to mysql2.
478
+ * @param options - Additional pool options passed to mysql2. `strictMode: false`
479
+ * (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver}).
347
480
  * @returns A `DbSpace` that creates `MysqlAdapter` instances per table.
348
481
  */
349
482
  declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
350
483
  //#endregion
351
- export { Mysql2Driver, MysqlAdapter, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };
484
+ export { Mysql2Driver, MysqlAdapter, type TMysql2DriverOptions, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair } from "@atscript/db";
2
- import { TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
+ import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
3
3
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
4
4
  import { TMetadataMap } from "@atscript/typescript/utils";
5
5
 
@@ -136,6 +136,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
136
136
  * to `NO_ENGINE_SUBSTITUTION`) would turn `'abc'` into `0` silently.
137
137
  */
138
138
  private _withStrictSession;
139
+ /**
140
+ * Relational predicates (`$some` / `$none`) render as correlated
141
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
142
+ * An UPDATE / DELETE whose predicate reads the mutated table itself is
143
+ * rewritten through a materialized derived table (MySQL error 1093).
144
+ *
145
+ * @since 0.1.147
146
+ */
147
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
139
148
  /** MySQL InnoDB enforces FK constraints natively. */
140
149
  supportsNativeForeignKeys(): boolean;
141
150
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -146,8 +155,12 @@ declare class MysqlAdapter extends BaseDbAdapter {
146
155
  * server's time zone tables (probed per zone in `aggregate()`).
147
156
  */
148
157
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
149
- /** Every aggregate function, `countDistinct` included. */
158
+ /** Every aggregate function: `countDistinct`, `first` and `last` included. */
150
159
  aggregateFns(): ReadonlySet<AggregateFn>;
160
+ /** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
161
+ supportsAggregateExpressions(): boolean;
162
+ /** Computed view columns and first-row joins. */
163
+ viewCapabilities(): ReadonlySet<TViewCapability>;
151
164
  onBeforeFlatten(_type: unknown): void;
152
165
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
153
166
  getDesiredTableOptions(): TExistingTableOption[];
@@ -165,15 +178,89 @@ declare class MysqlAdapter extends BaseDbAdapter {
165
178
  *
166
179
  * MySQL uses numeric error codes:
167
180
  * - 1062 = ER_DUP_ENTRY (unique constraint violation)
181
+ * - 1586 = ER_DUP_ENTRY_WITH_KEY_NAME (same, for a multi-row INSERT)
168
182
  * - 1451 = ER_ROW_IS_REFERENCED_2 (FK violation on delete)
169
183
  * - 1452 = ER_NO_REFERENCED_ROW_2 (FK violation on insert/update)
170
184
  */
171
185
  private _wrapConstraintError;
186
+ /** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
187
+ private _mapConstraintError;
172
188
  private _mapFkError;
173
189
  insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
174
190
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
191
+ /** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
192
+ private _autoIncrementPk;
193
+ /**
194
+ * Splits `rows` into runs that each map to ONE statement with a derivable
195
+ * id sequence. MySQL reports only the first GENERATED id of a statement, and
196
+ * an explicit auto-increment value above the counter bumps the counter, so a
197
+ * statement mixing explicit and generated PKs cannot be mapped by
198
+ * `insertId + i * step`: the chunk is cut into CONSECUTIVE runs of one kind,
199
+ * executed in input order (so "an earlier row wins" a unique collision, even
200
+ * under a case-insensitive collation). A table without an AUTO_INCREMENT PK,
201
+ * or a chunk of one kind, stays one group.
202
+ */
203
+ private _idGroups;
204
+ /** The session `sql_mode` text on the current connection (`undefined` when the server returns no row). */
205
+ private _sessionSqlMode;
206
+ /** The {@link TZeroIsExplicit} of one `insertMany` / `insertManyIgnore` call. */
207
+ private _zeroIsExplicit;
208
+ /**
209
+ * Warns ONCE per driver when the session `sql_mode` is not strict: writes
210
+ * (insert-ignore included) assume `STRICT_TRANS_TABLES` / `STRICT_ALL_TABLES`
211
+ * (the MySQL 8 default); a non-strict mode coerces a NOT NULL violation to the
212
+ * column default instead of failing. Only probes when the adapter has a logger.
213
+ */
214
+ private _warnNonStrictMode;
215
+ /** The {@link TIncrementStep} of one `insertMany` / `insertManyIgnore` call. */
216
+ private _incrementStep;
217
+ /**
218
+ * Ids of the rows of ONE successful multi-row INSERT of a homogeneous
219
+ * {@link _idGroups} group: generated rows get ids from `insertId` stepping by
220
+ * the session's `@@auto_increment_increment` (consecutive within one
221
+ * statement under `innodb_autoinc_lock_mode` 0 / 1, and — for a known row
222
+ * count — mode 2, the 8.0 default), explicit rows keep their own value.
223
+ */
224
+ private _groupInsertedIds;
225
+ supportsInsertIgnore(): boolean;
226
+ /**
227
+ * Per chunk: ONE optimistic multi-row INSERT (an all-new batch costs a single
228
+ * statement). Only when it hits a duplicate key (errno 1062 / 1586) does ONE
229
+ * SELECT of the chunk's primary / unique key tuples find the stored rows
230
+ * (skipped as conflicts) and the survivors go in as one more multi-row
231
+ * INSERT — a dense-duplicate chunk is three statements, never O(rows). Only
232
+ * if that INSERT still collides (a concurrent writer raced in, or a
233
+ * collation-equal value the exact-match pre-check missed) are the survivors
234
+ * bisected: each half is retried, recursively, and a single row that still
235
+ * collides is skipped. A failed statement is rolled back by InnoDB alone, so
236
+ * the transaction stays usable. A chunk mixing explicit and generated
237
+ * auto-increment ids is processed as one such sequence per consecutive run of a kind. Deliberately
238
+ * NOT `INSERT IGNORE` (it would downgrade NOT NULL / FK / truncation errors
239
+ * to warnings) and not `ON DUPLICATE KEY UPDATE` (a no-op update is
240
+ * indistinguishable from an insert in the affected-rows count).
241
+ */
242
+ insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
243
+ /**
244
+ * Indices of `rows` whose primary / unique-index key tuple is already stored
245
+ * (a row with a null / missing key component never collides). One SELECT
246
+ * covers every key set; it is split only to stay under the parameter limit.
247
+ * Skipped entirely when no row carries a key value (generated PK, no unique
248
+ * index values).
249
+ */
250
+ private _findStoredKeyConflicts;
251
+ /** Optimistic INSERT of a group, then pre-check + survivor INSERT (+ bisect on a race). */
252
+ private _insertIgnoringGroup;
253
+ /** Retries the halves of a group whose one INSERT is known to collide; a lone colliding row is skipped. */
254
+ private _bisectGroup;
255
+ /** ONE INSERT of a group's rows; `undefined` on a duplicate key (errno 1062 / 1586). */
256
+ private _tryInsertGroup;
175
257
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
176
258
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
259
+ /**
260
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
261
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
262
+ */
263
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
177
264
  count(query: DbQuery): Promise<number>;
178
265
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
179
266
  /**
@@ -186,6 +273,26 @@ declare class MysqlAdapter extends BaseDbAdapter {
186
273
  * expression skips `CONVERT_TZ`. Successes are cached per driver.
187
274
  */
188
275
  private _ensureBucketZone;
276
+ /**
277
+ * The WHERE of an UPDATE / DELETE on this table. MySQL rejects a statement
278
+ * whose WHERE reads the mutated table in a subquery (error 1093,
279
+ * `ER_UPDATE_TABLE_USED`) — what a relational predicate (`$some` / `$none`)
280
+ * does when its target or junction table, at any nesting level, is this
281
+ * table (a self relation such as `parent`). Such a filter is re-keyed on the
282
+ * primary key through a materialized derived table:
283
+ *
284
+ * ```sql
285
+ * WHERE (<pk…>) IN (SELECT * FROM (SELECT DISTINCT <pk…> FROM t WHERE <where>) AS `_rfm`)
286
+ * ```
287
+ *
288
+ * `DISTINCT` keeps the optimizer from merging the derived table back into
289
+ * the statement. Every other filter renders as-is.
290
+ *
291
+ * @since 0.1.147
292
+ */
293
+ private _mutationWhere;
294
+ /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
295
+ private _filterReadsOwnTable;
189
296
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
190
297
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
191
298
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
@@ -284,6 +391,18 @@ declare class MysqlAdapter extends BaseDbAdapter {
284
391
  }
285
392
  //#endregion
286
393
  //#region src/mysql2-driver.d.ts
394
+ /** Options of {@link Mysql2Driver}. */
395
+ interface TMysql2DriverOptions {
396
+ /**
397
+ * Add `STRICT_TRANS_TABLES` to the session `sql_mode` of every new pool
398
+ * connection (default `true`, since 0.1.148). The server's own modes are
399
+ * kept — the mode is appended, never replaced. A non-strict server (Amazon
400
+ * RDS defaults to `NO_ENGINE_SUBSTITUTION`) otherwise coerces a `NOT NULL`,
401
+ * out-of-range or too-long value instead of failing the write. Pass `false`
402
+ * to keep the server's `sql_mode` untouched.
403
+ */
404
+ strictMode?: boolean;
405
+ }
287
406
  /**
288
407
  * {@link TMysqlDriver} implementation backed by `mysql2/promise`.
289
408
  *
@@ -311,6 +430,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
311
430
  * const driver = new Mysql2Driver(pool)
312
431
  * ```
313
432
  *
433
+ * Every new pool connection gets `STRICT_TRANS_TABLES` appended to its session
434
+ * `sql_mode` (since 0.1.148); pass `{ strictMode: false }` as the second
435
+ * argument to opt out. A pre-created `Pool` is covered too: connections it
436
+ * opened earlier get the statement when they are first acquired through it.
437
+ *
314
438
  * Requires `mysql2` to be installed:
315
439
  * ```bash
316
440
  * pnpm add mysql2
@@ -319,13 +443,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
319
443
  declare class Mysql2Driver implements TMysqlDriver {
320
444
  private pool;
321
445
  private poolInit;
322
- constructor(poolOrConfig: string | import("mysql2/promise").Pool | import("mysql2/promise").PoolOptions);
446
+ constructor(poolOrConfig: string | import("mysql2/promise").Pool | import("mysql2/promise").PoolOptions, options?: TMysql2DriverOptions);
323
447
  private getPool;
324
448
  run(sql: string, params?: unknown[]): Promise<TMysqlRunResult>;
325
449
  all<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
326
450
  get<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
327
451
  exec(sql: string): Promise<void>;
328
452
  getConnection(): Promise<TMysqlConnection>;
453
+ private _closing?;
454
+ /** Idempotent: every call returns the first call's promise. */
329
455
  close(): Promise<void>;
330
456
  }
331
457
  //#endregion
@@ -335,17 +461,24 @@ declare class Mysql2Driver implements TMysqlDriver {
335
461
  *
336
462
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
337
463
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
464
+ *
465
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
466
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
467
+ * source table) — a statement with an aliased FROM must pass its alias.
468
+ *
469
+ * @param opts - since 0.1.147
338
470
  */
339
- declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
471
+ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions): TSqlFragment$1;
340
472
  //#endregion
341
473
  //#region src/index.d.ts
342
474
  /**
343
475
  * Creates a {@link DbSpace} backed by a MySQL connection pool.
344
476
  *
345
477
  * @param uri - MySQL connection URI (e.g., `mysql://root@localhost:3306/mydb`)
346
- * @param options - Additional pool options passed to mysql2.
478
+ * @param options - Additional pool options passed to mysql2. `strictMode: false`
479
+ * (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver}).
347
480
  * @returns A `DbSpace` that creates `MysqlAdapter` instances per table.
348
481
  */
349
482
  declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
350
483
  //#endregion
351
- export { Mysql2Driver, MysqlAdapter, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };
484
+ export { Mysql2Driver, MysqlAdapter, type TMysql2DriverOptions, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };