@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 +113 -35
- package/dist/index.d.cts +45 -3
- package/dist/index.d.mts +45 -3
- package/dist/index.mjs +115 -37
- package/package.json +10 -10
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
|
|
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
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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
|
|
245
|
-
|
|
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
|
-
|
|
259
|
+
value = local;
|
|
249
260
|
break;
|
|
250
261
|
case "week":
|
|
251
|
-
|
|
262
|
+
value = `DATE_SUB(${local}, INTERVAL ((WEEKDAY(${local}) + 1 - ${b.weekStartIso} + 7) % 7) DAY)`;
|
|
252
263
|
break;
|
|
253
264
|
case "month":
|
|
254
|
-
|
|
265
|
+
value = `DATE_SUB(${local}, INTERVAL DAYOFMONTH(${local}) - 1 DAY)`;
|
|
255
266
|
break;
|
|
256
267
|
case "quarter":
|
|
257
|
-
|
|
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:
|
|
273
|
+
default: throw new Error(`Unhandled calendar bucket unit "${String(b.unit)}"`);
|
|
260
274
|
}
|
|
261
|
-
return `CASE WHEN ${inRange} THEN DATE_FORMAT(${
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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
|
|
244
|
-
|
|
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
|
-
|
|
258
|
+
value = local;
|
|
248
259
|
break;
|
|
249
260
|
case "week":
|
|
250
|
-
|
|
261
|
+
value = `DATE_SUB(${local}, INTERVAL ((WEEKDAY(${local}) + 1 - ${b.weekStartIso} + 7) % 7) DAY)`;
|
|
251
262
|
break;
|
|
252
263
|
case "month":
|
|
253
|
-
|
|
264
|
+
value = `DATE_SUB(${local}, INTERVAL DAYOFMONTH(${local}) - 1 DAY)`;
|
|
254
265
|
break;
|
|
255
266
|
case "quarter":
|
|
256
|
-
|
|
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:
|
|
272
|
+
default: throw new Error(`Unhandled calendar bucket unit "${String(b.unit)}"`);
|
|
259
273
|
}
|
|
260
|
-
return `CASE WHEN ${inRange} THEN DATE_FORMAT(${
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
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.
|
|
45
|
-
"@atscript/typescript": "^0.1.
|
|
46
|
-
"@uniqu/core": "^0.1.
|
|
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.
|
|
48
|
+
"unplugin-atscript": "^0.1.99"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@atscript/core": "^0.1.
|
|
52
|
-
"@atscript/typescript": "^0.1.
|
|
53
|
-
"@uniqu/core": "^0.1.
|
|
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
|
|
56
|
-
"@atscript/db": "^0.1.
|
|
55
|
+
"@atscript/db": "^0.1.147",
|
|
56
|
+
"@atscript/db-sql-tools": "^0.1.147"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"mysql2": {
|