@atscript/db-mysql 0.1.145 → 0.1.147

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.cjs CHANGED
@@ -175,6 +175,7 @@ const mysqlDialect = {
175
175
  calendarBucket: mysqlCalendarBucket,
176
176
  bucketAliasInHaving: true,
177
177
  jsonExtract: mysqlJsonExtract,
178
+ castDouble: (expr) => `CAST(${expr} AS DOUBLE)`,
178
179
  createViewPrefix: "CREATE OR REPLACE VIEW"
179
180
  };
180
181
  /** `JSON_TYPE()` names of JSON numbers (MariaDB reports only INTEGER / DOUBLE). */
@@ -210,7 +211,8 @@ function isMysqlTimestampColumn(fd) {
210
211
  const MYSQL_EPOCH = "CAST('1970-01-01 00:00:00' AS DATETIME)";
211
212
  /**
212
213
  * Calendar-bucket label: TEXT `'YYYY-MM-DD'` — the local date of the
213
- * bucket's first day in `b.tz` — or NULL for a NULL source or one outside
214
+ * bucket's first day in `b.tz` — or, for `hour`, `'YYYY-MM-DDTHH:00'` — the
215
+ * local wall-clock hour — or NULL for a NULL source or one outside
214
216
  * `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
215
217
  *
216
218
  * The source's UTC wall time `U` (a DATETIME) depends on storage:
@@ -222,9 +224,12 @@ const MYSQL_EPOCH = "CAST('1970-01-01 00:00:00' AS DATETIME)";
222
224
  * return whatever the server's session zone is (`UNIX_TIMESTAMP(col)` would
223
225
  * agree only under a UTC session zone).
224
226
  *
225
- * The local date is `DATE(CONVERT_TZ(U, '+00:00', '<tz>'))`, or `DATE(U)` for
226
- * `UTC` (no time zone tables needed). Truncation is calendar arithmetic on
227
- * that date. `CONVERT_TZ` returns NULL when the zone is unknown to the
227
+ * The local wall time is `CONVERT_TZ(U, '+00:00', '<tz>')`, or `U` for `UTC`
228
+ * (no time zone tables needed) — UTC → local is never ambiguous, so a
229
+ * `+05:30` zone's hour starts at :30 UTC and a DST fall-back's repeated hour
230
+ * is one label. The hour is formatted from it; the other units truncate
231
+ * its `DATE()` with calendar arithmetic. `CONVERT_TZ`
232
+ * returns NULL when the zone is unknown to the
228
233
  * server and its input unchanged outside its range — `MysqlAdapter` probes
229
234
  * each zone before running the query (`BUCKET_TZ_UNAVAILABLE`).
230
235
  *
@@ -241,24 +246,33 @@ function mysqlCalendarBucket(quotedCol, b) {
241
246
  utc = `(${MYSQL_EPOCH} + INTERVAL FLOOR(${quotedCol} / 1000) SECOND)`;
242
247
  inRange = `${quotedCol} >= ${_uniqu_core.BUCKET_MIN_INSTANT} AND ${quotedCol} < ${_uniqu_core.BUCKET_MAX_INSTANT}`;
243
248
  }
244
- const local = b.tz === "UTC" ? `DATE(${utc})` : `DATE(CONVERT_TZ(${utc}, '+00:00', ${(0, _atscript_db_sql_tools.sqlTimeZoneLiteral)(b.tz)}))`;
245
- let first;
249
+ const wall = b.tz === "UTC" ? utc : `CONVERT_TZ(${utc}, '+00:00', ${(0, _atscript_db_sql_tools.sqlTimeZoneLiteral)(b.tz)})`;
250
+ const local = `DATE(${wall})`;
251
+ let value;
252
+ let format = "%Y-%m-%d";
246
253
  switch (b.unit) {
254
+ case "hour":
255
+ value = wall;
256
+ format = "%Y-%m-%dT%H:00";
257
+ break;
247
258
  case "day":
248
- first = local;
259
+ value = local;
249
260
  break;
250
261
  case "week":
251
- first = `DATE_SUB(${local}, INTERVAL ((WEEKDAY(${local}) + 1 - ${b.weekStartIso} + 7) % 7) DAY)`;
262
+ value = `DATE_SUB(${local}, INTERVAL ((WEEKDAY(${local}) + 1 - ${b.weekStartIso} + 7) % 7) DAY)`;
252
263
  break;
253
264
  case "month":
254
- first = `DATE_SUB(${local}, INTERVAL DAYOFMONTH(${local}) - 1 DAY)`;
265
+ value = `DATE_SUB(${local}, INTERVAL DAYOFMONTH(${local}) - 1 DAY)`;
255
266
  break;
256
267
  case "quarter":
257
- first = `(MAKEDATE(YEAR(${local}), 1) + INTERVAL (QUARTER(${local}) - 1) QUARTER)`;
268
+ value = `(MAKEDATE(YEAR(${local}), 1) + INTERVAL (QUARTER(${local}) - 1) QUARTER)`;
269
+ break;
270
+ case "year":
271
+ value = `MAKEDATE(YEAR(${local}), 1)`;
258
272
  break;
259
- default: first = `MAKEDATE(YEAR(${local}), 1)`;
273
+ default: throw new Error(`Unhandled calendar bucket unit "${String(b.unit)}"`);
260
274
  }
261
- return `CASE WHEN ${inRange} THEN DATE_FORMAT(${first}, '%Y-%m-%d') END`;
275
+ return `CASE WHEN ${inRange} THEN DATE_FORMAT(${value}, '${format}') END`;
262
276
  }
263
277
  /**
264
278
  * Spherical distance in meters from a POINT column to a query point.
@@ -446,12 +460,7 @@ function buildCreateTable(table, fields, foreignKeys, options) {
446
460
  }
447
461
  if (foreignKeys) for (const fk of foreignKeys.values()) {
448
462
  if (options?.deferForeignKeysTo?.has(fk.targetTable)) continue;
449
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
450
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
451
- let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
452
- if (fk.onDelete) constraint += ` ON DELETE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
453
- if (fk.onUpdate) constraint += ` ON UPDATE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
454
- constraints.push(constraint);
463
+ constraints.push((0, _atscript_db_sql_tools.foreignKeySql)(qi, fk));
455
464
  }
456
465
  const body = [...colDefs.map((c) => c.def), ...constraints].join(", ");
457
466
  let sql = `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${body})`;
@@ -469,9 +478,15 @@ function buildCreateTable(table, fields, foreignKeys, options) {
469
478
  *
470
479
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
471
480
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
481
+ *
482
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
483
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
484
+ * source table) — a statement with an aliased FROM must pass its alias.
485
+ *
486
+ * @param opts - since 0.1.147
472
487
  */
473
- function buildWhere(filter) {
474
- return (0, _atscript_db_sql_tools.buildWhere)(mysqlDialect, filter);
488
+ function buildWhere(filter, opts) {
489
+ return (0, _atscript_db_sql_tools.buildWhere)(mysqlDialect, filter, opts);
475
490
  }
476
491
  //#endregion
477
492
  //#region src/mysql-adapter.ts
@@ -615,6 +630,17 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
615
630
  if (!tx) conn.release();
616
631
  }
617
632
  }
633
+ /**
634
+ * Relational predicates (`$some` / `$none`) render as correlated
635
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
636
+ * An UPDATE / DELETE whose predicate reads the mutated table itself is
637
+ * rewritten through a materialized derived table (MySQL error 1093).
638
+ *
639
+ * @since 0.1.147
640
+ */
641
+ supportsRelationFilters(_mode) {
642
+ return true;
643
+ }
618
644
  /** MySQL InnoDB enforces FK constraints natively. */
619
645
  supportsNativeForeignKeys() {
620
646
  return true;
@@ -639,6 +665,10 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
639
665
  aggregateFns() {
640
666
  return _atscript_db.ALL_AGGREGATE_FNS;
641
667
  }
668
+ /** Computed view columns and first-row joins. */
669
+ viewCapabilities() {
670
+ return _atscript_db.ALL_VIEW_CAPABILITIES;
671
+ }
642
672
  onBeforeFlatten(_type) {
643
673
  const meta = _type.metadata;
644
674
  const engine = meta.get("db.mysql.engine");
@@ -827,6 +857,16 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
827
857
  this._log(sql, params);
828
858
  return this._exec().all(sql, params);
829
859
  }
860
+ /**
861
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
862
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
863
+ */
864
+ async findManyPerPartition(query, partitionBy) {
865
+ const where = buildWhere(query.filter);
866
+ const { sql, params } = (0, _atscript_db_sql_tools.buildPartitionedSelect)(mysqlDialect, this.resolveTableName(), where, query.controls, partitionBy);
867
+ this._log(sql, params);
868
+ return (0, _atscript_db_sql_tools.stripPartitionRowNumber)(await this._exec().all(sql, params));
869
+ }
830
870
  async count(query) {
831
871
  const where = buildWhere(query.filter);
832
872
  const sql = `SELECT COUNT(*) as cnt FROM ${quoteTableName(this.resolveTableName())} WHERE ${where.sql}`;
@@ -871,8 +911,50 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
871
911
  }
872
912
  zones.add(tz);
873
913
  }
874
- async updateOne(filter, data, ops, expectedVersion) {
914
+ /**
915
+ * The WHERE of an UPDATE / DELETE on this table. MySQL rejects a statement
916
+ * whose WHERE reads the mutated table in a subquery (error 1093,
917
+ * `ER_UPDATE_TABLE_USED`) — what a relational predicate (`$some` / `$none`)
918
+ * does when its target or junction table, at any nesting level, is this
919
+ * table (a self relation such as `parent`). Such a filter is re-keyed on the
920
+ * primary key through a materialized derived table:
921
+ *
922
+ * ```sql
923
+ * WHERE (<pk…>) IN (SELECT * FROM (SELECT DISTINCT <pk…> FROM t WHERE <where>) AS `_rfm`)
924
+ * ```
925
+ *
926
+ * `DISTINCT` keeps the optimizer from merging the derived table back into
927
+ * the statement. Every other filter renders as-is.
928
+ *
929
+ * @since 0.1.147
930
+ */
931
+ _mutationWhere(filter) {
875
932
  const where = buildWhere(filter);
933
+ if (!this._filterReadsOwnTable(filter)) return where;
934
+ const tableName = this.resolveTableName();
935
+ const keys = this._table.primaryKeys.map((key) => this._table.physicalPath(key));
936
+ if (keys.length === 0) throw new _atscript_db.DbError("REL_FILTER_NOT_SUPPORTED", [{
937
+ path: "",
938
+ message: "MySQL cannot update or delete by a relational predicate that reads the table itself when the table has no primary key"
939
+ }]);
940
+ const cols = keys.map((key) => qi(key)).join(", ");
941
+ return {
942
+ sql: `${keys.length === 1 ? cols : `(${cols})`} IN (SELECT * FROM (SELECT DISTINCT ${cols} FROM ${quoteTableName(tableName)} WHERE ${where.sql}) AS ${qi("_rfm")})`,
943
+ params: where.params
944
+ };
945
+ }
946
+ /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
947
+ _filterReadsOwnTable(filter) {
948
+ if (!(0, _atscript_db.containsRelationFilter)(filter)) return false;
949
+ const own = this.resolveTableName();
950
+ let hit = false;
951
+ (0, _atscript_db.forEachResolvedRelation)(filter, (node) => {
952
+ if (node.target.table === own || node.junction?.table === own) hit = true;
953
+ });
954
+ return hit;
955
+ }
956
+ async updateOne(filter, data, ops, expectedVersion) {
957
+ const where = this._mutationWhere(filter);
876
958
  const versionColumn = this._table.versionColumnPhysical;
877
959
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where, 1, ops, versionColumn, expectedVersion);
878
960
  this._log(sql, params);
@@ -883,7 +965,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
883
965
  };
884
966
  }
885
967
  async updateMany(filter, data, ops) {
886
- const where = buildWhere(filter);
968
+ const where = this._mutationWhere(filter);
887
969
  const versionColumn = this._table.versionColumnPhysical;
888
970
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, ops, versionColumn);
889
971
  this._log(sql, params);
@@ -894,7 +976,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
894
976
  };
895
977
  }
896
978
  async replaceOne(filter, data, expectedVersion) {
897
- const where = buildWhere(filter);
979
+ const where = this._mutationWhere(filter);
898
980
  const versionColumn = this._table.versionColumnPhysical;
899
981
  const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn);
900
982
  const { sql, params } = buildUpdate(this.resolveTableName(), full, where, 1, void 0, versionColumn, expectedVersion);
@@ -906,7 +988,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
906
988
  };
907
989
  }
908
990
  async replaceMany(filter, data) {
909
- const where = buildWhere(filter);
991
+ const where = this._mutationWhere(filter);
910
992
  const versionColumn = this._table.versionColumnPhysical;
911
993
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, void 0, versionColumn);
912
994
  this._log(sql, params);
@@ -917,13 +999,13 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
917
999
  };
918
1000
  }
919
1001
  async deleteOne(filter) {
920
- const where = buildWhere(filter);
1002
+ const where = this._mutationWhere(filter);
921
1003
  const { sql, params } = buildDelete(this.resolveTableName(), where, 1);
922
1004
  this._log(sql, params);
923
1005
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
924
1006
  }
925
1007
  async deleteMany(filter) {
926
- const where = buildWhere(filter);
1008
+ const where = this._mutationWhere(filter);
927
1009
  const { sql, params } = buildDelete(this.resolveTableName(), where);
928
1010
  this._log(sql, params);
929
1011
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
@@ -1295,7 +1377,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1295
1377
  async syncForeignKeys() {
1296
1378
  const existingByName = await this._getExistingFkConstraints();
1297
1379
  const desiredFkKeys = /* @__PURE__ */ new Set();
1298
- for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].toSorted().join(","));
1380
+ for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(","));
1299
1381
  for (const [constraintName, columns] of existingByName) {
1300
1382
  const key = columns.toSorted().join(",");
1301
1383
  if (!desiredFkKeys.has(key)) {
@@ -1306,13 +1388,9 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1306
1388
  }
1307
1389
  const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
1308
1390
  for (const fk of this._table.foreignKeys.values()) {
1309
- const key = [...fk.fields].toSorted().join(",");
1391
+ const key = [...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(",");
1310
1392
  if (!existingKeys.has(key)) {
1311
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
1312
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
1313
- let ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
1314
- if (fk.onDelete) ddl += ` ON DELETE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
1315
- if (fk.onUpdate) ddl += ` ON UPDATE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
1393
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${(0, _atscript_db_sql_tools.foreignKeySql)(qi, fk)}`;
1316
1394
  this._log(ddl);
1317
1395
  await this._exec().exec(ddl);
1318
1396
  }
@@ -1472,7 +1550,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1472
1550
  vec = first.value[1];
1473
1551
  }
1474
1552
  const distanceFn = similarityToMysqlFn(vec.similarity);
1475
- const where = buildWhere(query.filter);
1553
+ const where = buildWhere(query.filter, { qualifier: mysqlDialect.quoteTable(_atscript_db_sql_tools.SEARCH_SOURCE_ALIAS) });
1476
1554
  const controls = query.controls || {};
1477
1555
  const threshold = this._resolveVectorThreshold(controls, vec.indexName);
1478
1556
  return {
@@ -1550,7 +1628,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1550
1628
  const controls = query.controls ?? {};
1551
1629
  return {
1552
1630
  tableName: this.resolveTableName(),
1553
- where: buildWhere(query.filter),
1631
+ where: buildWhere(query.filter, { qualifier: mysqlDialect.quoteTable(_atscript_db_sql_tools.SEARCH_SOURCE_ALIAS) }),
1554
1632
  dist: mysqlGeoDistanceExpr(qi(column), point),
1555
1633
  window: (0, _atscript_db_sql_tools.geoWindowFromControls)(controls),
1556
1634
  controls
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, 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;
@@ -148,6 +157,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
148
157
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
149
158
  /** Every aggregate function, `countDistinct` included. */
150
159
  aggregateFns(): ReadonlySet<AggregateFn>;
160
+ /** Computed view columns and first-row joins. */
161
+ viewCapabilities(): ReadonlySet<TViewCapability>;
151
162
  onBeforeFlatten(_type: unknown): void;
152
163
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
153
164
  getDesiredTableOptions(): TExistingTableOption[];
@@ -174,6 +185,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
174
185
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
175
186
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
176
187
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
188
+ /**
189
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
190
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
191
+ */
192
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
177
193
  count(query: DbQuery): Promise<number>;
178
194
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
179
195
  /**
@@ -186,6 +202,26 @@ declare class MysqlAdapter extends BaseDbAdapter {
186
202
  * expression skips `CONVERT_TZ`. Successes are cached per driver.
187
203
  */
188
204
  private _ensureBucketZone;
205
+ /**
206
+ * The WHERE of an UPDATE / DELETE on this table. MySQL rejects a statement
207
+ * whose WHERE reads the mutated table in a subquery (error 1093,
208
+ * `ER_UPDATE_TABLE_USED`) — what a relational predicate (`$some` / `$none`)
209
+ * does when its target or junction table, at any nesting level, is this
210
+ * table (a self relation such as `parent`). Such a filter is re-keyed on the
211
+ * primary key through a materialized derived table:
212
+ *
213
+ * ```sql
214
+ * WHERE (<pk…>) IN (SELECT * FROM (SELECT DISTINCT <pk…> FROM t WHERE <where>) AS `_rfm`)
215
+ * ```
216
+ *
217
+ * `DISTINCT` keeps the optimizer from merging the derived table back into
218
+ * the statement. Every other filter renders as-is.
219
+ *
220
+ * @since 0.1.147
221
+ */
222
+ private _mutationWhere;
223
+ /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
224
+ private _filterReadsOwnTable;
189
225
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
190
226
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
191
227
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
@@ -335,8 +371,14 @@ declare class Mysql2Driver implements TMysqlDriver {
335
371
  *
336
372
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
337
373
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
374
+ *
375
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
376
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
377
+ * source table) — a statement with an aliased FROM must pass its alias.
378
+ *
379
+ * @param opts - since 0.1.147
338
380
  */
339
- declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
381
+ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions): TSqlFragment$1;
340
382
  //#endregion
341
383
  //#region src/index.d.ts
342
384
  /**
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, 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;
@@ -148,6 +157,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
148
157
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
149
158
  /** Every aggregate function, `countDistinct` included. */
150
159
  aggregateFns(): ReadonlySet<AggregateFn>;
160
+ /** Computed view columns and first-row joins. */
161
+ viewCapabilities(): ReadonlySet<TViewCapability>;
151
162
  onBeforeFlatten(_type: unknown): void;
152
163
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
153
164
  getDesiredTableOptions(): TExistingTableOption[];
@@ -174,6 +185,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
174
185
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
175
186
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
176
187
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
188
+ /**
189
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
190
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
191
+ */
192
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
177
193
  count(query: DbQuery): Promise<number>;
178
194
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
179
195
  /**
@@ -186,6 +202,26 @@ declare class MysqlAdapter extends BaseDbAdapter {
186
202
  * expression skips `CONVERT_TZ`. Successes are cached per driver.
187
203
  */
188
204
  private _ensureBucketZone;
205
+ /**
206
+ * The WHERE of an UPDATE / DELETE on this table. MySQL rejects a statement
207
+ * whose WHERE reads the mutated table in a subquery (error 1093,
208
+ * `ER_UPDATE_TABLE_USED`) — what a relational predicate (`$some` / `$none`)
209
+ * does when its target or junction table, at any nesting level, is this
210
+ * table (a self relation such as `parent`). Such a filter is re-keyed on the
211
+ * primary key through a materialized derived table:
212
+ *
213
+ * ```sql
214
+ * WHERE (<pk…>) IN (SELECT * FROM (SELECT DISTINCT <pk…> FROM t WHERE <where>) AS `_rfm`)
215
+ * ```
216
+ *
217
+ * `DISTINCT` keeps the optimizer from merging the derived table back into
218
+ * the statement. Every other filter renders as-is.
219
+ *
220
+ * @since 0.1.147
221
+ */
222
+ private _mutationWhere;
223
+ /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
224
+ private _filterReadsOwnTable;
189
225
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
190
226
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
191
227
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
@@ -335,8 +371,14 @@ declare class Mysql2Driver implements TMysqlDriver {
335
371
  *
336
372
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
337
373
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
374
+ *
375
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
376
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
377
+ * source table) — a statement with an aliased FROM must pass its alias.
378
+ *
379
+ * @param opts - since 0.1.147
338
380
  */
339
- declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
381
+ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions): TSqlFragment$1;
340
382
  //#endregion
341
383
  //#region src/index.d.ts
342
384
  /**
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, vectorIndexNotFoundMessage } from "@atscript/db";
1
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, containsRelationFilter, fkColumns, forEachResolvedRelation, vectorIndexNotFoundMessage } from "@atscript/db";
2
2
  import { resolveAggregateSearch } from "@atscript/db/agg";
3
- import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, geoWindowFromControls, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource } from "@atscript/db-sql-tools";
3
+ import { SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, foreignKeySql, geoWindowFromControls, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource } from "@atscript/db-sql-tools";
4
4
  import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
5
5
  //#region src/sql-builder.ts
6
6
  /** TEXT / BLOB families (`TINYTEXT` … `LONGBLOB`). */
@@ -174,6 +174,7 @@ const mysqlDialect = {
174
174
  calendarBucket: mysqlCalendarBucket,
175
175
  bucketAliasInHaving: true,
176
176
  jsonExtract: mysqlJsonExtract,
177
+ castDouble: (expr) => `CAST(${expr} AS DOUBLE)`,
177
178
  createViewPrefix: "CREATE OR REPLACE VIEW"
178
179
  };
179
180
  /** `JSON_TYPE()` names of JSON numbers (MariaDB reports only INTEGER / DOUBLE). */
@@ -209,7 +210,8 @@ function isMysqlTimestampColumn(fd) {
209
210
  const MYSQL_EPOCH = "CAST('1970-01-01 00:00:00' AS DATETIME)";
210
211
  /**
211
212
  * Calendar-bucket label: TEXT `'YYYY-MM-DD'` — the local date of the
212
- * bucket's first day in `b.tz` — or NULL for a NULL source or one outside
213
+ * bucket's first day in `b.tz` — or, for `hour`, `'YYYY-MM-DDTHH:00'` — the
214
+ * local wall-clock hour — or NULL for a NULL source or one outside
213
215
  * `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
214
216
  *
215
217
  * The source's UTC wall time `U` (a DATETIME) depends on storage:
@@ -221,9 +223,12 @@ const MYSQL_EPOCH = "CAST('1970-01-01 00:00:00' AS DATETIME)";
221
223
  * return whatever the server's session zone is (`UNIX_TIMESTAMP(col)` would
222
224
  * agree only under a UTC session zone).
223
225
  *
224
- * The local date is `DATE(CONVERT_TZ(U, '+00:00', '<tz>'))`, or `DATE(U)` for
225
- * `UTC` (no time zone tables needed). Truncation is calendar arithmetic on
226
- * that date. `CONVERT_TZ` returns NULL when the zone is unknown to the
226
+ * The local wall time is `CONVERT_TZ(U, '+00:00', '<tz>')`, or `U` for `UTC`
227
+ * (no time zone tables needed) — UTC → local is never ambiguous, so a
228
+ * `+05:30` zone's hour starts at :30 UTC and a DST fall-back's repeated hour
229
+ * is one label. The hour is formatted from it; the other units truncate
230
+ * its `DATE()` with calendar arithmetic. `CONVERT_TZ`
231
+ * returns NULL when the zone is unknown to the
227
232
  * server and its input unchanged outside its range — `MysqlAdapter` probes
228
233
  * each zone before running the query (`BUCKET_TZ_UNAVAILABLE`).
229
234
  *
@@ -240,24 +245,33 @@ function mysqlCalendarBucket(quotedCol, b) {
240
245
  utc = `(${MYSQL_EPOCH} + INTERVAL FLOOR(${quotedCol} / 1000) SECOND)`;
241
246
  inRange = `${quotedCol} >= ${BUCKET_MIN_INSTANT} AND ${quotedCol} < ${BUCKET_MAX_INSTANT}`;
242
247
  }
243
- const local = b.tz === "UTC" ? `DATE(${utc})` : `DATE(CONVERT_TZ(${utc}, '+00:00', ${sqlTimeZoneLiteral(b.tz)}))`;
244
- let first;
248
+ const wall = b.tz === "UTC" ? utc : `CONVERT_TZ(${utc}, '+00:00', ${sqlTimeZoneLiteral(b.tz)})`;
249
+ const local = `DATE(${wall})`;
250
+ let value;
251
+ let format = "%Y-%m-%d";
245
252
  switch (b.unit) {
253
+ case "hour":
254
+ value = wall;
255
+ format = "%Y-%m-%dT%H:00";
256
+ break;
246
257
  case "day":
247
- first = local;
258
+ value = local;
248
259
  break;
249
260
  case "week":
250
- first = `DATE_SUB(${local}, INTERVAL ((WEEKDAY(${local}) + 1 - ${b.weekStartIso} + 7) % 7) DAY)`;
261
+ value = `DATE_SUB(${local}, INTERVAL ((WEEKDAY(${local}) + 1 - ${b.weekStartIso} + 7) % 7) DAY)`;
251
262
  break;
252
263
  case "month":
253
- first = `DATE_SUB(${local}, INTERVAL DAYOFMONTH(${local}) - 1 DAY)`;
264
+ value = `DATE_SUB(${local}, INTERVAL DAYOFMONTH(${local}) - 1 DAY)`;
254
265
  break;
255
266
  case "quarter":
256
- first = `(MAKEDATE(YEAR(${local}), 1) + INTERVAL (QUARTER(${local}) - 1) QUARTER)`;
267
+ value = `(MAKEDATE(YEAR(${local}), 1) + INTERVAL (QUARTER(${local}) - 1) QUARTER)`;
268
+ break;
269
+ case "year":
270
+ value = `MAKEDATE(YEAR(${local}), 1)`;
257
271
  break;
258
- default: first = `MAKEDATE(YEAR(${local}), 1)`;
272
+ default: throw new Error(`Unhandled calendar bucket unit "${String(b.unit)}"`);
259
273
  }
260
- return `CASE WHEN ${inRange} THEN DATE_FORMAT(${first}, '%Y-%m-%d') END`;
274
+ return `CASE WHEN ${inRange} THEN DATE_FORMAT(${value}, '${format}') END`;
261
275
  }
262
276
  /**
263
277
  * Spherical distance in meters from a POINT column to a query point.
@@ -445,12 +459,7 @@ function buildCreateTable(table, fields, foreignKeys, options) {
445
459
  }
446
460
  if (foreignKeys) for (const fk of foreignKeys.values()) {
447
461
  if (options?.deferForeignKeysTo?.has(fk.targetTable)) continue;
448
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
449
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
450
- let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
451
- if (fk.onDelete) constraint += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
452
- if (fk.onUpdate) constraint += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
453
- constraints.push(constraint);
462
+ constraints.push(foreignKeySql(qi, fk));
454
463
  }
455
464
  const body = [...colDefs.map((c) => c.def), ...constraints].join(", ");
456
465
  let sql = `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${body})`;
@@ -468,9 +477,15 @@ function buildCreateTable(table, fields, foreignKeys, options) {
468
477
  *
469
478
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
470
479
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
480
+ *
481
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
482
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
483
+ * source table) — a statement with an aliased FROM must pass its alias.
484
+ *
485
+ * @param opts - since 0.1.147
471
486
  */
472
- function buildWhere(filter) {
473
- return buildWhere$1(mysqlDialect, filter);
487
+ function buildWhere(filter, opts) {
488
+ return buildWhere$1(mysqlDialect, filter, opts);
474
489
  }
475
490
  //#endregion
476
491
  //#region src/mysql-adapter.ts
@@ -614,6 +629,17 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
614
629
  if (!tx) conn.release();
615
630
  }
616
631
  }
632
+ /**
633
+ * Relational predicates (`$some` / `$none`) render as correlated
634
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
635
+ * An UPDATE / DELETE whose predicate reads the mutated table itself is
636
+ * rewritten through a materialized derived table (MySQL error 1093).
637
+ *
638
+ * @since 0.1.147
639
+ */
640
+ supportsRelationFilters(_mode) {
641
+ return true;
642
+ }
617
643
  /** MySQL InnoDB enforces FK constraints natively. */
618
644
  supportsNativeForeignKeys() {
619
645
  return true;
@@ -638,6 +664,10 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
638
664
  aggregateFns() {
639
665
  return ALL_AGGREGATE_FNS;
640
666
  }
667
+ /** Computed view columns and first-row joins. */
668
+ viewCapabilities() {
669
+ return ALL_VIEW_CAPABILITIES;
670
+ }
641
671
  onBeforeFlatten(_type) {
642
672
  const meta = _type.metadata;
643
673
  const engine = meta.get("db.mysql.engine");
@@ -826,6 +856,16 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
826
856
  this._log(sql, params);
827
857
  return this._exec().all(sql, params);
828
858
  }
859
+ /**
860
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
861
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
862
+ */
863
+ async findManyPerPartition(query, partitionBy) {
864
+ const where = buildWhere(query.filter);
865
+ const { sql, params } = buildPartitionedSelect(mysqlDialect, this.resolveTableName(), where, query.controls, partitionBy);
866
+ this._log(sql, params);
867
+ return stripPartitionRowNumber(await this._exec().all(sql, params));
868
+ }
829
869
  async count(query) {
830
870
  const where = buildWhere(query.filter);
831
871
  const sql = `SELECT COUNT(*) as cnt FROM ${quoteTableName(this.resolveTableName())} WHERE ${where.sql}`;
@@ -870,8 +910,50 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
870
910
  }
871
911
  zones.add(tz);
872
912
  }
873
- async updateOne(filter, data, ops, expectedVersion) {
913
+ /**
914
+ * The WHERE of an UPDATE / DELETE on this table. MySQL rejects a statement
915
+ * whose WHERE reads the mutated table in a subquery (error 1093,
916
+ * `ER_UPDATE_TABLE_USED`) — what a relational predicate (`$some` / `$none`)
917
+ * does when its target or junction table, at any nesting level, is this
918
+ * table (a self relation such as `parent`). Such a filter is re-keyed on the
919
+ * primary key through a materialized derived table:
920
+ *
921
+ * ```sql
922
+ * WHERE (<pk…>) IN (SELECT * FROM (SELECT DISTINCT <pk…> FROM t WHERE <where>) AS `_rfm`)
923
+ * ```
924
+ *
925
+ * `DISTINCT` keeps the optimizer from merging the derived table back into
926
+ * the statement. Every other filter renders as-is.
927
+ *
928
+ * @since 0.1.147
929
+ */
930
+ _mutationWhere(filter) {
874
931
  const where = buildWhere(filter);
932
+ if (!this._filterReadsOwnTable(filter)) return where;
933
+ const tableName = this.resolveTableName();
934
+ const keys = this._table.primaryKeys.map((key) => this._table.physicalPath(key));
935
+ if (keys.length === 0) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
936
+ path: "",
937
+ message: "MySQL cannot update or delete by a relational predicate that reads the table itself when the table has no primary key"
938
+ }]);
939
+ const cols = keys.map((key) => qi(key)).join(", ");
940
+ return {
941
+ sql: `${keys.length === 1 ? cols : `(${cols})`} IN (SELECT * FROM (SELECT DISTINCT ${cols} FROM ${quoteTableName(tableName)} WHERE ${where.sql}) AS ${qi("_rfm")})`,
942
+ params: where.params
943
+ };
944
+ }
945
+ /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
946
+ _filterReadsOwnTable(filter) {
947
+ if (!containsRelationFilter(filter)) return false;
948
+ const own = this.resolveTableName();
949
+ let hit = false;
950
+ forEachResolvedRelation(filter, (node) => {
951
+ if (node.target.table === own || node.junction?.table === own) hit = true;
952
+ });
953
+ return hit;
954
+ }
955
+ async updateOne(filter, data, ops, expectedVersion) {
956
+ const where = this._mutationWhere(filter);
875
957
  const versionColumn = this._table.versionColumnPhysical;
876
958
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, 1, ops, versionColumn, expectedVersion);
877
959
  this._log(sql, params);
@@ -882,7 +964,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
882
964
  };
883
965
  }
884
966
  async updateMany(filter, data, ops) {
885
- const where = buildWhere(filter);
967
+ const where = this._mutationWhere(filter);
886
968
  const versionColumn = this._table.versionColumnPhysical;
887
969
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, ops, versionColumn);
888
970
  this._log(sql, params);
@@ -893,7 +975,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
893
975
  };
894
976
  }
895
977
  async replaceOne(filter, data, expectedVersion) {
896
- const where = buildWhere(filter);
978
+ const where = this._mutationWhere(filter);
897
979
  const versionColumn = this._table.versionColumnPhysical;
898
980
  const full = fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn);
899
981
  const { sql, params } = buildUpdate$1(this.resolveTableName(), full, where, 1, void 0, versionColumn, expectedVersion);
@@ -905,7 +987,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
905
987
  };
906
988
  }
907
989
  async replaceMany(filter, data) {
908
- const where = buildWhere(filter);
990
+ const where = this._mutationWhere(filter);
909
991
  const versionColumn = this._table.versionColumnPhysical;
910
992
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, void 0, versionColumn);
911
993
  this._log(sql, params);
@@ -916,13 +998,13 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
916
998
  };
917
999
  }
918
1000
  async deleteOne(filter) {
919
- const where = buildWhere(filter);
1001
+ const where = this._mutationWhere(filter);
920
1002
  const { sql, params } = buildDelete$1(this.resolveTableName(), where, 1);
921
1003
  this._log(sql, params);
922
1004
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
923
1005
  }
924
1006
  async deleteMany(filter) {
925
- const where = buildWhere(filter);
1007
+ const where = this._mutationWhere(filter);
926
1008
  const { sql, params } = buildDelete$1(this.resolveTableName(), where);
927
1009
  this._log(sql, params);
928
1010
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
@@ -1294,7 +1376,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1294
1376
  async syncForeignKeys() {
1295
1377
  const existingByName = await this._getExistingFkConstraints();
1296
1378
  const desiredFkKeys = /* @__PURE__ */ new Set();
1297
- for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].toSorted().join(","));
1379
+ for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fkColumns(fk).fields].toSorted().join(","));
1298
1380
  for (const [constraintName, columns] of existingByName) {
1299
1381
  const key = columns.toSorted().join(",");
1300
1382
  if (!desiredFkKeys.has(key)) {
@@ -1305,13 +1387,9 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1305
1387
  }
1306
1388
  const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
1307
1389
  for (const fk of this._table.foreignKeys.values()) {
1308
- const key = [...fk.fields].toSorted().join(",");
1390
+ const key = [...fkColumns(fk).fields].toSorted().join(",");
1309
1391
  if (!existingKeys.has(key)) {
1310
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
1311
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
1312
- let ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
1313
- if (fk.onDelete) ddl += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
1314
- if (fk.onUpdate) ddl += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
1392
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${foreignKeySql(qi, fk)}`;
1315
1393
  this._log(ddl);
1316
1394
  await this._exec().exec(ddl);
1317
1395
  }
@@ -1471,7 +1549,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1471
1549
  vec = first.value[1];
1472
1550
  }
1473
1551
  const distanceFn = similarityToMysqlFn(vec.similarity);
1474
- const where = buildWhere(query.filter);
1552
+ const where = buildWhere(query.filter, { qualifier: mysqlDialect.quoteTable(SEARCH_SOURCE_ALIAS) });
1475
1553
  const controls = query.controls || {};
1476
1554
  const threshold = this._resolveVectorThreshold(controls, vec.indexName);
1477
1555
  return {
@@ -1549,7 +1627,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1549
1627
  const controls = query.controls ?? {};
1550
1628
  return {
1551
1629
  tableName: this.resolveTableName(),
1552
- where: buildWhere(query.filter),
1630
+ where: buildWhere(query.filter, { qualifier: mysqlDialect.quoteTable(SEARCH_SOURCE_ALIAS) }),
1553
1631
  dist: mysqlGeoDistanceExpr(qi(column), point),
1554
1632
  window: geoWindowFromControls(controls),
1555
1633
  controls
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mysql",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "MySQL adapter for @atscript/db with mysql2 driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -41,19 +41,19 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@atscript/core": "^0.1.97",
45
- "@atscript/typescript": "^0.1.97",
46
- "@uniqu/core": "^0.1.11",
44
+ "@atscript/core": "^0.1.99",
45
+ "@atscript/typescript": "^0.1.99",
46
+ "@uniqu/core": "^0.1.12",
47
47
  "mysql2": "^3.11.0",
48
- "unplugin-atscript": "^0.1.97"
48
+ "unplugin-atscript": "^0.1.99"
49
49
  },
50
50
  "peerDependencies": {
51
- "@atscript/core": "^0.1.97",
52
- "@atscript/typescript": "^0.1.97",
53
- "@uniqu/core": "^0.1.11",
51
+ "@atscript/core": "^0.1.99",
52
+ "@atscript/typescript": "^0.1.99",
53
+ "@uniqu/core": "^0.1.12",
54
54
  "mysql2": ">=3.0.0",
55
- "@atscript/db-sql-tools": "^0.1.145",
56
- "@atscript/db": "^0.1.145"
55
+ "@atscript/db": "^0.1.147",
56
+ "@atscript/db-sql-tools": "^0.1.147"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "mysql2": {