@atscript/db-mysql 0.1.167 → 0.1.169
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 +99 -11
- package/dist/index.d.cts +21 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +99 -11
- package/package.json +12 -10
package/dist/index.cjs
CHANGED
|
@@ -212,6 +212,8 @@ const mysqlDialect = {
|
|
|
212
212
|
},
|
|
213
213
|
jsonFromText: (quotedCol) => `IF(JSON_VALID(${quotedCol}), ${quotedCol}, JSON_QUOTE(${quotedCol}))`,
|
|
214
214
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE)`,
|
|
215
|
+
conditionalAggregate: "case",
|
|
216
|
+
countDistinctColumns: (columns, when) => `COUNT(DISTINCT ${columns.map((c) => when ? `CASE WHEN ${when} THEN ${c} END` : c).join(", ")})`,
|
|
215
217
|
createViewPrefix: "CREATE OR REPLACE VIEW"
|
|
216
218
|
};
|
|
217
219
|
/** `JSON_TYPE()` names of JSON numbers (MariaDB reports only INTEGER / DOUBLE). */
|
|
@@ -673,6 +675,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
673
675
|
_replaceColumnsMemo;
|
|
674
676
|
_fulltextMemo;
|
|
675
677
|
_searchIndexesMemo;
|
|
678
|
+
_incrementPkMemo;
|
|
676
679
|
/**
|
|
677
680
|
* Schema name for INFORMATION_SCHEMA queries — `@db.schema` of the bound
|
|
678
681
|
* table, or `null` (→ `DATABASE()`, the pool's database) when the table
|
|
@@ -811,6 +814,18 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
811
814
|
supportsAggregateExpressions() {
|
|
812
815
|
return true;
|
|
813
816
|
}
|
|
817
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
818
|
+
supportsConditionalAggregates() {
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
/** `countDistinct` over several fields. */
|
|
822
|
+
supportsDistinctFieldLists() {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
826
|
+
supportsNestedAggregates() {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
814
829
|
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
815
830
|
supportsNullsPlacement() {
|
|
816
831
|
return true;
|
|
@@ -980,7 +995,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
980
995
|
const { sql, params } = buildInsert(this.resolveTableName(), data);
|
|
981
996
|
this._log(sql, params);
|
|
982
997
|
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
983
|
-
return { insertedId: this.
|
|
998
|
+
return { insertedId: this._insertedIdOf(data, result.insertId, result.insertId) };
|
|
984
999
|
}
|
|
985
1000
|
async insertMany(data) {
|
|
986
1001
|
if (data.length === 0) return {
|
|
@@ -1058,9 +1073,25 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1058
1073
|
}
|
|
1059
1074
|
/** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
|
|
1060
1075
|
_autoIncrementPk() {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1076
|
+
const { column, composite } = this._incrementPk();
|
|
1077
|
+
return composite ? void 0 : column;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Physical column of the primary key's `@db.default.increment` part (if any)
|
|
1081
|
+
* and whether the key is composite — computed once per table.
|
|
1082
|
+
*/
|
|
1083
|
+
_incrementPk() {
|
|
1084
|
+
const src = this._table.primaryKeys;
|
|
1085
|
+
let memo = this._incrementPkMemo;
|
|
1086
|
+
if (memo?.src !== src) {
|
|
1087
|
+
const part = src.find((pk) => this._incrementFields.has(pk));
|
|
1088
|
+
memo = this._incrementPkMemo = {
|
|
1089
|
+
src,
|
|
1090
|
+
column: part === void 0 ? void 0 : this._table.physicalPath(part),
|
|
1091
|
+
composite: src.length > 1
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return memo;
|
|
1064
1095
|
}
|
|
1065
1096
|
/**
|
|
1066
1097
|
* Splits `rows` into runs that each map to ONE statement with a derivable
|
|
@@ -1150,11 +1181,30 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1150
1181
|
async _groupInsertedIds(group, insertId, step) {
|
|
1151
1182
|
const first = Number(insertId);
|
|
1152
1183
|
const stride = group.generated && group.rows.length > 1 && first > 0 ? await step() : 1;
|
|
1184
|
+
const partId = group.rows.length === 1 ? insertId : void 0;
|
|
1153
1185
|
return group.rows.map((row, i) => {
|
|
1154
1186
|
const generatedId = first > 0 ? first + i * stride : 0;
|
|
1155
|
-
return group.generated ? generatedId : this.
|
|
1187
|
+
return group.generated ? generatedId : this._insertedIdOf(row, generatedId, partId);
|
|
1156
1188
|
});
|
|
1157
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* The inserted id of `row` (see `_resolveInsertedId`). For a COMPOSITE key
|
|
1192
|
+
* with an AUTO_INCREMENT part, `partId` is what the statement generated for
|
|
1193
|
+
* that part — pass it only when exact (a ONE-row INSERT); a row carrying the
|
|
1194
|
+
* part keeps its own value. An explicit `0` in the part counts as absent:
|
|
1195
|
+
* MySQL generates a value for it (unless `NO_AUTO_VALUE_ON_ZERO`), so only
|
|
1196
|
+
* `partId` knows it — without that the id is `undefined`, never a wrong `0`.
|
|
1197
|
+
*/
|
|
1198
|
+
_insertedIdOf(row, generatedId, partId) {
|
|
1199
|
+
const { column, composite } = this._incrementPk();
|
|
1200
|
+
if (!composite || column === void 0) return this._resolveInsertedId(row, generatedId);
|
|
1201
|
+
const source = isZero(row[column]) ? {
|
|
1202
|
+
...row,
|
|
1203
|
+
[column]: void 0
|
|
1204
|
+
} : row;
|
|
1205
|
+
const part = Number(partId) > 0 ? { [column]: partId } : void 0;
|
|
1206
|
+
return this._resolveInsertedId(source, generatedId, part);
|
|
1207
|
+
}
|
|
1158
1208
|
supportsInsertIgnore() {
|
|
1159
1209
|
return true;
|
|
1160
1210
|
}
|
|
@@ -1421,7 +1471,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1421
1471
|
const search = (0, _atscript_db_agg.resolveAggregateSearch)(query.controls);
|
|
1422
1472
|
const where = search ? this._buildSearchWhere(search.text, query, search.indexName) : buildWhere(query.filter);
|
|
1423
1473
|
const tableName = search ? this.resolveTableName() : this._readSource(query, "aggregate");
|
|
1424
|
-
for (const
|
|
1474
|
+
for (const select of [query.controls.$select, query.controls.$from?.$select]) for (const bucket of select?.buckets ?? []) await this._ensureBucketZone(bucket.tz);
|
|
1425
1475
|
if (query.controls.$count) {
|
|
1426
1476
|
const { sql, params } = buildAggregateCount(tableName, where, query.controls);
|
|
1427
1477
|
this._log(sql, params);
|
|
@@ -1925,6 +1975,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1925
1975
|
}
|
|
1926
1976
|
async renameTable(oldName) {
|
|
1927
1977
|
const newName = this.resolveTableName();
|
|
1978
|
+
if (!await this.getObjectKind(oldName) && await this.getObjectKind(this._table.tableName)) return;
|
|
1928
1979
|
const ddl = `RENAME TABLE ${quoteTableName(oldName)} TO ${quoteTableName(newName)}`;
|
|
1929
1980
|
this._log(ddl);
|
|
1930
1981
|
await this._exec().exec(ddl);
|
|
@@ -1940,6 +1991,9 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1940
1991
|
async _syncIndexes(dryRun) {
|
|
1941
1992
|
const tableName = this._table.tableName;
|
|
1942
1993
|
const schema = this._schema;
|
|
1994
|
+
if (!dryRun) {
|
|
1995
|
+
for (const index of this._table.indexes.values()) if (index.type === "fulltext" && index.language !== void 0) this.logger.warn(`[mysql] fulltext index "${index.name}": @db.index.fulltext.language "${index.language}" is ignored — MySQL FULLTEXT has no per-index language`);
|
|
1996
|
+
}
|
|
1943
1997
|
const bytesPerChar = mysqlBytesPerChar(this._charset);
|
|
1944
1998
|
const fields = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
1945
1999
|
const mappedType = (column) => {
|
|
@@ -2123,13 +2177,16 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2123
2177
|
const { text: textFields, integer: integerFields } = (0, _atscript_db.splitFulltextFields)(fulltextIndex);
|
|
2124
2178
|
const where = buildWhere(query.filter);
|
|
2125
2179
|
const n = integerFields.length > 0 ? (0, _atscript_db.searchTermInteger)(text) : void 0;
|
|
2126
|
-
const
|
|
2180
|
+
const matchCols = textFields.map((f) => qi(f.name)).join(", ");
|
|
2181
|
+
const all = fulltextIndex.match === "all";
|
|
2182
|
+
const matchClause = `MATCH(${matchCols}) AGAINST(? IN ${all ? "BOOLEAN" : "NATURAL LANGUAGE"} MODE)`;
|
|
2183
|
+
const term = all ? mysqlRequireEveryTerm(text) : text;
|
|
2127
2184
|
const rank = (0, _atscript_db_sql_tools.searchRankOrder)(mysqlDialect, {
|
|
2128
2185
|
exactEqualities: n === void 0 ? [] : integerFields.map((f) => `${qi(f.name)} = ?`),
|
|
2129
2186
|
n,
|
|
2130
2187
|
textRank: textFields.length > 0 ? {
|
|
2131
2188
|
sql: `${matchClause} DESC`,
|
|
2132
|
-
params: [
|
|
2189
|
+
params: [term]
|
|
2133
2190
|
} : void 0,
|
|
2134
2191
|
pkColumns: this._table.primaryKeys.map((key) => this._table.physicalPath(key))
|
|
2135
2192
|
});
|
|
@@ -2138,10 +2195,10 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2138
2195
|
params: [...where.params, ...params],
|
|
2139
2196
|
rank
|
|
2140
2197
|
});
|
|
2141
|
-
if (n === void 0) return textFields.length === 0 ? withWhere(_atscript_db_sql_tools.EMPTY_OR.sql, []) : withWhere(matchClause, [
|
|
2198
|
+
if (n === void 0) return textFields.length === 0 ? withWhere(_atscript_db_sql_tools.EMPTY_OR.sql, []) : withWhere(matchClause, [term]);
|
|
2142
2199
|
const keys = this._table.primaryKeys.map((key) => qi(this._table.physicalPath(key)));
|
|
2143
2200
|
if (keys.length === 0) {
|
|
2144
|
-
const or = (0, _atscript_db_sql_tools.orFragment)([...textFields.length > 0 ? [matchClause] : [], ...integerFields.map((f) => `${qi(f.name)} = ?`)], [...textFields.length > 0 ? [
|
|
2201
|
+
const or = (0, _atscript_db_sql_tools.orFragment)([...textFields.length > 0 ? [matchClause] : [], ...integerFields.map((f) => `${qi(f.name)} = ?`)], [...textFields.length > 0 ? [term] : [], ...integerFields.map(() => n)]);
|
|
2145
2202
|
return withWhere(or.sql, or.params);
|
|
2146
2203
|
}
|
|
2147
2204
|
const table = quoteTableName(this.resolveTableName());
|
|
@@ -2150,7 +2207,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2150
2207
|
const params = [];
|
|
2151
2208
|
if (textFields.length > 0) {
|
|
2152
2209
|
arms.push(`SELECT ${keyList} FROM ${table} WHERE ${matchClause}`);
|
|
2153
|
-
params.push(
|
|
2210
|
+
params.push(term);
|
|
2154
2211
|
}
|
|
2155
2212
|
for (const f of integerFields) {
|
|
2156
2213
|
arms.push(`SELECT ${keyList} FROM ${table} WHERE ${qi(f.name)} = ?`);
|
|
@@ -2441,6 +2498,37 @@ function similarityToMysqlFn(similarity) {
|
|
|
2441
2498
|
function vectorToString(vector) {
|
|
2442
2499
|
return `[${vector.join(",")}]`;
|
|
2443
2500
|
}
|
|
2501
|
+
/** InnoDB's default full-text stop words (`INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD`). */
|
|
2502
|
+
const INNODB_DEFAULT_STOPWORDS = new Set("a about an are as at be by com de en for from how i in is it la of on or that the this to was what when where who will with und www".split(" "));
|
|
2503
|
+
/**
|
|
2504
|
+
* A search text as a boolean-mode `AGAINST` term with every word required
|
|
2505
|
+
* (`@db.index.fulltext.match "all"`, since 0.1.168): `+word` per word, a
|
|
2506
|
+
* `"quoted phrase"` stays one required phrase, a trailing `*` keeps its prefix
|
|
2507
|
+
* meaning. The user's own boolean operators (`+ - < > ( ) ~ * " @`) are text,
|
|
2508
|
+
* not syntax. A word InnoDB does not index by default — shorter than 3
|
|
2509
|
+
* characters (`innodb_ft_min_token_size`) or on its default stop-word list —
|
|
2510
|
+
* stays optional: requiring it would match no row.
|
|
2511
|
+
*/
|
|
2512
|
+
function mysqlRequireEveryTerm(text) {
|
|
2513
|
+
const parts = [];
|
|
2514
|
+
for (const match of text.matchAll(/"([^"]*)"|(\S+)/g)) {
|
|
2515
|
+
const phrase = match[1];
|
|
2516
|
+
if (phrase !== void 0) {
|
|
2517
|
+
const clean = phrase.trim();
|
|
2518
|
+
if (clean) parts.push(`+"${clean}"`);
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
const word = match[2];
|
|
2522
|
+
const core = word.replace(/\*+$/, "");
|
|
2523
|
+
const pieces = core.split(/[+\-<>()~*"@]+/).filter(Boolean);
|
|
2524
|
+
pieces.forEach((piece, i) => {
|
|
2525
|
+
const star = i === pieces.length - 1 && core.length < word.length ? "*" : "";
|
|
2526
|
+
const required = [...piece].length >= 3 && !INNODB_DEFAULT_STOPWORDS.has(piece.toLowerCase());
|
|
2527
|
+
parts.push(`${required ? "+" : ""}${piece}${star}`);
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
return parts.join(" ");
|
|
2531
|
+
}
|
|
2444
2532
|
//#endregion
|
|
2445
2533
|
//#region src/mysql2-driver.ts
|
|
2446
2534
|
const NO_PARAMS = [];
|
package/dist/index.d.cts
CHANGED
|
@@ -141,6 +141,7 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
141
141
|
private _replaceColumnsMemo?;
|
|
142
142
|
private _fulltextMemo?;
|
|
143
143
|
private _searchIndexesMemo?;
|
|
144
|
+
private _incrementPkMemo?;
|
|
144
145
|
/**
|
|
145
146
|
* Schema name for INFORMATION_SCHEMA queries — `@db.schema` of the bound
|
|
146
147
|
* table, or `null` (→ `DATABASE()`, the pool's database) when the table
|
|
@@ -201,6 +202,12 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
201
202
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
202
203
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
203
204
|
supportsAggregateExpressions(): boolean;
|
|
205
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
206
|
+
supportsConditionalAggregates(): boolean;
|
|
207
|
+
/** `countDistinct` over several fields. */
|
|
208
|
+
supportsDistinctFieldLists(): boolean;
|
|
209
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
210
|
+
supportsNestedAggregates(): boolean;
|
|
204
211
|
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
205
212
|
supportsNullsPlacement(): boolean;
|
|
206
213
|
/**
|
|
@@ -254,6 +261,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
254
261
|
private _uniformIdGroup;
|
|
255
262
|
/** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
|
|
256
263
|
private _autoIncrementPk;
|
|
264
|
+
/**
|
|
265
|
+
* Physical column of the primary key's `@db.default.increment` part (if any)
|
|
266
|
+
* and whether the key is composite — computed once per table.
|
|
267
|
+
*/
|
|
268
|
+
private _incrementPk;
|
|
257
269
|
/**
|
|
258
270
|
* Splits `rows` into runs that each map to ONE statement with a derivable
|
|
259
271
|
* id sequence. MySQL reports only the first GENERATED id of a statement, and
|
|
@@ -292,6 +304,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
292
304
|
* count — mode 2, the 8.0 default), explicit rows keep their own value.
|
|
293
305
|
*/
|
|
294
306
|
private _groupInsertedIds;
|
|
307
|
+
/**
|
|
308
|
+
* The inserted id of `row` (see `_resolveInsertedId`). For a COMPOSITE key
|
|
309
|
+
* with an AUTO_INCREMENT part, `partId` is what the statement generated for
|
|
310
|
+
* that part — pass it only when exact (a ONE-row INSERT); a row carrying the
|
|
311
|
+
* part keeps its own value. An explicit `0` in the part counts as absent:
|
|
312
|
+
* MySQL generates a value for it (unless `NO_AUTO_VALUE_ON_ZERO`), so only
|
|
313
|
+
* `partId` knows it — without that the id is `undefined`, never a wrong `0`.
|
|
314
|
+
*/
|
|
315
|
+
private _insertedIdOf;
|
|
295
316
|
supportsInsertIgnore(): boolean;
|
|
296
317
|
/**
|
|
297
318
|
* Standalone (no caller transaction): per chunk ONE optimistic multi-row
|
package/dist/index.d.mts
CHANGED
|
@@ -141,6 +141,7 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
141
141
|
private _replaceColumnsMemo?;
|
|
142
142
|
private _fulltextMemo?;
|
|
143
143
|
private _searchIndexesMemo?;
|
|
144
|
+
private _incrementPkMemo?;
|
|
144
145
|
/**
|
|
145
146
|
* Schema name for INFORMATION_SCHEMA queries — `@db.schema` of the bound
|
|
146
147
|
* table, or `null` (→ `DATABASE()`, the pool's database) when the table
|
|
@@ -201,6 +202,12 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
201
202
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
202
203
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
203
204
|
supportsAggregateExpressions(): boolean;
|
|
205
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
206
|
+
supportsConditionalAggregates(): boolean;
|
|
207
|
+
/** `countDistinct` over several fields. */
|
|
208
|
+
supportsDistinctFieldLists(): boolean;
|
|
209
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
210
|
+
supportsNestedAggregates(): boolean;
|
|
204
211
|
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
205
212
|
supportsNullsPlacement(): boolean;
|
|
206
213
|
/**
|
|
@@ -254,6 +261,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
254
261
|
private _uniformIdGroup;
|
|
255
262
|
/** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
|
|
256
263
|
private _autoIncrementPk;
|
|
264
|
+
/**
|
|
265
|
+
* Physical column of the primary key's `@db.default.increment` part (if any)
|
|
266
|
+
* and whether the key is composite — computed once per table.
|
|
267
|
+
*/
|
|
268
|
+
private _incrementPk;
|
|
257
269
|
/**
|
|
258
270
|
* Splits `rows` into runs that each map to ONE statement with a derivable
|
|
259
271
|
* id sequence. MySQL reports only the first GENERATED id of a statement, and
|
|
@@ -292,6 +304,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
292
304
|
* count — mode 2, the 8.0 default), explicit rows keep their own value.
|
|
293
305
|
*/
|
|
294
306
|
private _groupInsertedIds;
|
|
307
|
+
/**
|
|
308
|
+
* The inserted id of `row` (see `_resolveInsertedId`). For a COMPOSITE key
|
|
309
|
+
* with an AUTO_INCREMENT part, `partId` is what the statement generated for
|
|
310
|
+
* that part — pass it only when exact (a ONE-row INSERT); a row carrying the
|
|
311
|
+
* part keeps its own value. An explicit `0` in the part counts as absent:
|
|
312
|
+
* MySQL generates a value for it (unless `NO_AUTO_VALUE_ON_ZERO`), so only
|
|
313
|
+
* `partId` knows it — without that the id is `undefined`, never a wrong `0`.
|
|
314
|
+
*/
|
|
315
|
+
private _insertedIdOf;
|
|
295
316
|
supportsInsertIgnore(): boolean;
|
|
296
317
|
/**
|
|
297
318
|
* Standalone (no caller transaction): per chunk ONE optimistic multi-row
|
package/dist/index.mjs
CHANGED
|
@@ -211,6 +211,8 @@ const mysqlDialect = {
|
|
|
211
211
|
},
|
|
212
212
|
jsonFromText: (quotedCol) => `IF(JSON_VALID(${quotedCol}), ${quotedCol}, JSON_QUOTE(${quotedCol}))`,
|
|
213
213
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE)`,
|
|
214
|
+
conditionalAggregate: "case",
|
|
215
|
+
countDistinctColumns: (columns, when) => `COUNT(DISTINCT ${columns.map((c) => when ? `CASE WHEN ${when} THEN ${c} END` : c).join(", ")})`,
|
|
214
216
|
createViewPrefix: "CREATE OR REPLACE VIEW"
|
|
215
217
|
};
|
|
216
218
|
/** `JSON_TYPE()` names of JSON numbers (MariaDB reports only INTEGER / DOUBLE). */
|
|
@@ -672,6 +674,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
672
674
|
_replaceColumnsMemo;
|
|
673
675
|
_fulltextMemo;
|
|
674
676
|
_searchIndexesMemo;
|
|
677
|
+
_incrementPkMemo;
|
|
675
678
|
/**
|
|
676
679
|
* Schema name for INFORMATION_SCHEMA queries — `@db.schema` of the bound
|
|
677
680
|
* table, or `null` (→ `DATABASE()`, the pool's database) when the table
|
|
@@ -810,6 +813,18 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
810
813
|
supportsAggregateExpressions() {
|
|
811
814
|
return true;
|
|
812
815
|
}
|
|
816
|
+
/** Conditional aggregates (`$where` on an aggregate `$select` entry). */
|
|
817
|
+
supportsConditionalAggregates() {
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
/** `countDistinct` over several fields. */
|
|
821
|
+
supportsDistinctFieldLists() {
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
/** Two-level aggregates (`$from`): the inner level is a derived table. */
|
|
825
|
+
supportsNestedAggregates() {
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
813
828
|
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
814
829
|
supportsNullsPlacement() {
|
|
815
830
|
return true;
|
|
@@ -979,7 +994,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
979
994
|
const { sql, params } = buildInsert$1(this.resolveTableName(), data);
|
|
980
995
|
this._log(sql, params);
|
|
981
996
|
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
982
|
-
return { insertedId: this.
|
|
997
|
+
return { insertedId: this._insertedIdOf(data, result.insertId, result.insertId) };
|
|
983
998
|
}
|
|
984
999
|
async insertMany(data) {
|
|
985
1000
|
if (data.length === 0) return {
|
|
@@ -1057,9 +1072,25 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1057
1072
|
}
|
|
1058
1073
|
/** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
|
|
1059
1074
|
_autoIncrementPk() {
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1075
|
+
const { column, composite } = this._incrementPk();
|
|
1076
|
+
return composite ? void 0 : column;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Physical column of the primary key's `@db.default.increment` part (if any)
|
|
1080
|
+
* and whether the key is composite — computed once per table.
|
|
1081
|
+
*/
|
|
1082
|
+
_incrementPk() {
|
|
1083
|
+
const src = this._table.primaryKeys;
|
|
1084
|
+
let memo = this._incrementPkMemo;
|
|
1085
|
+
if (memo?.src !== src) {
|
|
1086
|
+
const part = src.find((pk) => this._incrementFields.has(pk));
|
|
1087
|
+
memo = this._incrementPkMemo = {
|
|
1088
|
+
src,
|
|
1089
|
+
column: part === void 0 ? void 0 : this._table.physicalPath(part),
|
|
1090
|
+
composite: src.length > 1
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return memo;
|
|
1063
1094
|
}
|
|
1064
1095
|
/**
|
|
1065
1096
|
* Splits `rows` into runs that each map to ONE statement with a derivable
|
|
@@ -1149,11 +1180,30 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1149
1180
|
async _groupInsertedIds(group, insertId, step) {
|
|
1150
1181
|
const first = Number(insertId);
|
|
1151
1182
|
const stride = group.generated && group.rows.length > 1 && first > 0 ? await step() : 1;
|
|
1183
|
+
const partId = group.rows.length === 1 ? insertId : void 0;
|
|
1152
1184
|
return group.rows.map((row, i) => {
|
|
1153
1185
|
const generatedId = first > 0 ? first + i * stride : 0;
|
|
1154
|
-
return group.generated ? generatedId : this.
|
|
1186
|
+
return group.generated ? generatedId : this._insertedIdOf(row, generatedId, partId);
|
|
1155
1187
|
});
|
|
1156
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* The inserted id of `row` (see `_resolveInsertedId`). For a COMPOSITE key
|
|
1191
|
+
* with an AUTO_INCREMENT part, `partId` is what the statement generated for
|
|
1192
|
+
* that part — pass it only when exact (a ONE-row INSERT); a row carrying the
|
|
1193
|
+
* part keeps its own value. An explicit `0` in the part counts as absent:
|
|
1194
|
+
* MySQL generates a value for it (unless `NO_AUTO_VALUE_ON_ZERO`), so only
|
|
1195
|
+
* `partId` knows it — without that the id is `undefined`, never a wrong `0`.
|
|
1196
|
+
*/
|
|
1197
|
+
_insertedIdOf(row, generatedId, partId) {
|
|
1198
|
+
const { column, composite } = this._incrementPk();
|
|
1199
|
+
if (!composite || column === void 0) return this._resolveInsertedId(row, generatedId);
|
|
1200
|
+
const source = isZero(row[column]) ? {
|
|
1201
|
+
...row,
|
|
1202
|
+
[column]: void 0
|
|
1203
|
+
} : row;
|
|
1204
|
+
const part = Number(partId) > 0 ? { [column]: partId } : void 0;
|
|
1205
|
+
return this._resolveInsertedId(source, generatedId, part);
|
|
1206
|
+
}
|
|
1157
1207
|
supportsInsertIgnore() {
|
|
1158
1208
|
return true;
|
|
1159
1209
|
}
|
|
@@ -1420,7 +1470,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1420
1470
|
const search = resolveAggregateSearch(query.controls);
|
|
1421
1471
|
const where = search ? this._buildSearchWhere(search.text, query, search.indexName) : buildWhere(query.filter);
|
|
1422
1472
|
const tableName = search ? this.resolveTableName() : this._readSource(query, "aggregate");
|
|
1423
|
-
for (const
|
|
1473
|
+
for (const select of [query.controls.$select, query.controls.$from?.$select]) for (const bucket of select?.buckets ?? []) await this._ensureBucketZone(bucket.tz);
|
|
1424
1474
|
if (query.controls.$count) {
|
|
1425
1475
|
const { sql, params } = buildAggregateCount$1(tableName, where, query.controls);
|
|
1426
1476
|
this._log(sql, params);
|
|
@@ -1924,6 +1974,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1924
1974
|
}
|
|
1925
1975
|
async renameTable(oldName) {
|
|
1926
1976
|
const newName = this.resolveTableName();
|
|
1977
|
+
if (!await this.getObjectKind(oldName) && await this.getObjectKind(this._table.tableName)) return;
|
|
1927
1978
|
const ddl = `RENAME TABLE ${quoteTableName(oldName)} TO ${quoteTableName(newName)}`;
|
|
1928
1979
|
this._log(ddl);
|
|
1929
1980
|
await this._exec().exec(ddl);
|
|
@@ -1939,6 +1990,9 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1939
1990
|
async _syncIndexes(dryRun) {
|
|
1940
1991
|
const tableName = this._table.tableName;
|
|
1941
1992
|
const schema = this._schema;
|
|
1993
|
+
if (!dryRun) {
|
|
1994
|
+
for (const index of this._table.indexes.values()) if (index.type === "fulltext" && index.language !== void 0) this.logger.warn(`[mysql] fulltext index "${index.name}": @db.index.fulltext.language "${index.language}" is ignored — MySQL FULLTEXT has no per-index language`);
|
|
1995
|
+
}
|
|
1942
1996
|
const bytesPerChar = mysqlBytesPerChar(this._charset);
|
|
1943
1997
|
const fields = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
1944
1998
|
const mappedType = (column) => {
|
|
@@ -2122,13 +2176,16 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
2122
2176
|
const { text: textFields, integer: integerFields } = splitFulltextFields(fulltextIndex);
|
|
2123
2177
|
const where = buildWhere(query.filter);
|
|
2124
2178
|
const n = integerFields.length > 0 ? searchTermInteger(text) : void 0;
|
|
2125
|
-
const
|
|
2179
|
+
const matchCols = textFields.map((f) => qi(f.name)).join(", ");
|
|
2180
|
+
const all = fulltextIndex.match === "all";
|
|
2181
|
+
const matchClause = `MATCH(${matchCols}) AGAINST(? IN ${all ? "BOOLEAN" : "NATURAL LANGUAGE"} MODE)`;
|
|
2182
|
+
const term = all ? mysqlRequireEveryTerm(text) : text;
|
|
2126
2183
|
const rank = searchRankOrder(mysqlDialect, {
|
|
2127
2184
|
exactEqualities: n === void 0 ? [] : integerFields.map((f) => `${qi(f.name)} = ?`),
|
|
2128
2185
|
n,
|
|
2129
2186
|
textRank: textFields.length > 0 ? {
|
|
2130
2187
|
sql: `${matchClause} DESC`,
|
|
2131
|
-
params: [
|
|
2188
|
+
params: [term]
|
|
2132
2189
|
} : void 0,
|
|
2133
2190
|
pkColumns: this._table.primaryKeys.map((key) => this._table.physicalPath(key))
|
|
2134
2191
|
});
|
|
@@ -2137,10 +2194,10 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
2137
2194
|
params: [...where.params, ...params],
|
|
2138
2195
|
rank
|
|
2139
2196
|
});
|
|
2140
|
-
if (n === void 0) return textFields.length === 0 ? withWhere(EMPTY_OR.sql, []) : withWhere(matchClause, [
|
|
2197
|
+
if (n === void 0) return textFields.length === 0 ? withWhere(EMPTY_OR.sql, []) : withWhere(matchClause, [term]);
|
|
2141
2198
|
const keys = this._table.primaryKeys.map((key) => qi(this._table.physicalPath(key)));
|
|
2142
2199
|
if (keys.length === 0) {
|
|
2143
|
-
const or = orFragment([...textFields.length > 0 ? [matchClause] : [], ...integerFields.map((f) => `${qi(f.name)} = ?`)], [...textFields.length > 0 ? [
|
|
2200
|
+
const or = orFragment([...textFields.length > 0 ? [matchClause] : [], ...integerFields.map((f) => `${qi(f.name)} = ?`)], [...textFields.length > 0 ? [term] : [], ...integerFields.map(() => n)]);
|
|
2144
2201
|
return withWhere(or.sql, or.params);
|
|
2145
2202
|
}
|
|
2146
2203
|
const table = quoteTableName(this.resolveTableName());
|
|
@@ -2149,7 +2206,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
2149
2206
|
const params = [];
|
|
2150
2207
|
if (textFields.length > 0) {
|
|
2151
2208
|
arms.push(`SELECT ${keyList} FROM ${table} WHERE ${matchClause}`);
|
|
2152
|
-
params.push(
|
|
2209
|
+
params.push(term);
|
|
2153
2210
|
}
|
|
2154
2211
|
for (const f of integerFields) {
|
|
2155
2212
|
arms.push(`SELECT ${keyList} FROM ${table} WHERE ${qi(f.name)} = ?`);
|
|
@@ -2440,6 +2497,37 @@ function similarityToMysqlFn(similarity) {
|
|
|
2440
2497
|
function vectorToString(vector) {
|
|
2441
2498
|
return `[${vector.join(",")}]`;
|
|
2442
2499
|
}
|
|
2500
|
+
/** InnoDB's default full-text stop words (`INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD`). */
|
|
2501
|
+
const INNODB_DEFAULT_STOPWORDS = new Set("a about an are as at be by com de en for from how i in is it la of on or that the this to was what when where who will with und www".split(" "));
|
|
2502
|
+
/**
|
|
2503
|
+
* A search text as a boolean-mode `AGAINST` term with every word required
|
|
2504
|
+
* (`@db.index.fulltext.match "all"`, since 0.1.168): `+word` per word, a
|
|
2505
|
+
* `"quoted phrase"` stays one required phrase, a trailing `*` keeps its prefix
|
|
2506
|
+
* meaning. The user's own boolean operators (`+ - < > ( ) ~ * " @`) are text,
|
|
2507
|
+
* not syntax. A word InnoDB does not index by default — shorter than 3
|
|
2508
|
+
* characters (`innodb_ft_min_token_size`) or on its default stop-word list —
|
|
2509
|
+
* stays optional: requiring it would match no row.
|
|
2510
|
+
*/
|
|
2511
|
+
function mysqlRequireEveryTerm(text) {
|
|
2512
|
+
const parts = [];
|
|
2513
|
+
for (const match of text.matchAll(/"([^"]*)"|(\S+)/g)) {
|
|
2514
|
+
const phrase = match[1];
|
|
2515
|
+
if (phrase !== void 0) {
|
|
2516
|
+
const clean = phrase.trim();
|
|
2517
|
+
if (clean) parts.push(`+"${clean}"`);
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
const word = match[2];
|
|
2521
|
+
const core = word.replace(/\*+$/, "");
|
|
2522
|
+
const pieces = core.split(/[+\-<>()~*"@]+/).filter(Boolean);
|
|
2523
|
+
pieces.forEach((piece, i) => {
|
|
2524
|
+
const star = i === pieces.length - 1 && core.length < word.length ? "*" : "";
|
|
2525
|
+
const required = [...piece].length >= 3 && !INNODB_DEFAULT_STOPWORDS.has(piece.toLowerCase());
|
|
2526
|
+
parts.push(`${required ? "+" : ""}${piece}${star}`);
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
return parts.join(" ");
|
|
2530
|
+
}
|
|
2443
2531
|
//#endregion
|
|
2444
2532
|
//#region src/mysql2-driver.ts
|
|
2445
2533
|
const NO_PARAMS = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mysql",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.169",
|
|
4
4
|
"description": "MySQL adapter for @atscript/db with mysql2 driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -40,20 +40,22 @@
|
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@atscript/db-sql-tools": "^0.1.169"
|
|
45
|
+
},
|
|
43
46
|
"devDependencies": {
|
|
44
|
-
"@atscript/core": "^0.2.
|
|
45
|
-
"@atscript/typescript": "^0.2.
|
|
46
|
-
"@uniqu/core": "^0.
|
|
47
|
+
"@atscript/core": "^0.2.4",
|
|
48
|
+
"@atscript/typescript": "^0.2.4",
|
|
49
|
+
"@uniqu/core": "^0.2.0",
|
|
47
50
|
"mysql2": "^3.11.0",
|
|
48
|
-
"unplugin-atscript": "^0.2.
|
|
51
|
+
"unplugin-atscript": "^0.2.4"
|
|
49
52
|
},
|
|
50
53
|
"peerDependencies": {
|
|
51
|
-
"@atscript/core": "^0.2.
|
|
52
|
-
"@atscript/typescript": "^0.2.
|
|
53
|
-
"@uniqu/core": "^0.
|
|
54
|
+
"@atscript/core": "^0.2.4",
|
|
55
|
+
"@atscript/typescript": "^0.2.4",
|
|
56
|
+
"@uniqu/core": "^0.2.0",
|
|
54
57
|
"mysql2": ">=3.0.0",
|
|
55
|
-
"@atscript/db": "^0.1.
|
|
56
|
-
"@atscript/db-sql-tools": "^0.1.167"
|
|
58
|
+
"@atscript/db": "^0.1.169"
|
|
57
59
|
},
|
|
58
60
|
"peerDependenciesMeta": {
|
|
59
61
|
"mysql2": {
|