@atscript/db-mysql 0.1.168 → 0.1.170
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/annotation-values-CsjIErsL.mjs +34 -0
- package/dist/annotation-values-DS1rF0tw.cjs +57 -0
- package/dist/index.cjs +558 -66
- package/dist/index.d.cts +98 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +558 -66
- package/dist/plugin.cjs +112 -23
- package/dist/plugin.mjs +112 -23
- package/package.json +10 -10
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { i as mysqlOnUpdateExpr, r as mysqlName } from "./annotation-values-CsjIErsL.mjs";
|
|
1
2
|
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, NoopLogger, bucketTimeZoneUnavailable, constraintViolation, containsRelationFilter, defaultFulltextIndex, describeFulltext, fkKey, forEachResolvedRelation, isConflict, resolveFulltextIndex, searchIndexNotFoundMessage, searchTermInteger, splitFulltextFields, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
2
3
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
3
4
|
import { EMPTY_OR, SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderByList, parseRegexString, renameGeoDistance, replaceColumnsFor, searchRankOrder, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount } from "@atscript/db-sql-tools";
|
|
@@ -109,7 +110,8 @@ function mysqlIndexPrefix(mappedType, bytesPerChar) {
|
|
|
109
110
|
* default of a nullable column, and `NOT NULL DEFAULT NULL` is ER 1067);
|
|
110
111
|
* expression form for TEXT/BLOB/JSON/GEOMETRY; invented type default for a
|
|
111
112
|
* required default-less column on `add`;
|
|
112
|
-
* 5. COLLATE (native `@db.mysql.collate` or portable `@db.column.collate
|
|
113
|
+
* 5. COLLATE (native `@db.mysql.collate` or portable `@db.column.collate`;
|
|
114
|
+
* a foreign-key column's is the referenced column's);
|
|
113
115
|
* 6. ON UPDATE.
|
|
114
116
|
*
|
|
115
117
|
* A `@db.column.derived` field (since 0.1.141) renders as a VIRTUAL generated
|
|
@@ -118,12 +120,13 @@ function mysqlIndexPrefix(mappedType, bytesPerChar) {
|
|
|
118
120
|
*/
|
|
119
121
|
function buildColumnDefinition(field, ctx) {
|
|
120
122
|
const sqlType = ctx.typeMapper?.(field) ?? mysqlTypeFromField(field);
|
|
123
|
+
const { charsetClause, collateClause } = mysqlColumnCharsetClauses(field, sqlType, ctx);
|
|
121
124
|
if (field.derived) return {
|
|
122
|
-
def: `${qi(field.physicalName)} ${sqlType}${
|
|
125
|
+
def: `${qi(field.physicalName)} ${sqlType}${charsetClause}${collateClause} GENERATED ALWAYS AS (${derivedColumnExpr(mysqlDialect, field)}) VIRTUAL`,
|
|
123
126
|
inventedDefault: false
|
|
124
127
|
};
|
|
125
128
|
const increment = ctx.purpose !== "add" && (ctx.incrementFields?.has(field.path) ?? false);
|
|
126
|
-
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
129
|
+
let def = `${qi(field.physicalName)} ${sqlType}${charsetClause}`;
|
|
127
130
|
if (increment) def += " AUTO_INCREMENT";
|
|
128
131
|
if (ctx.purpose === "create") {
|
|
129
132
|
if (!field.optional && !field.isPrimaryKey && !increment) def += " NOT NULL";
|
|
@@ -137,14 +140,53 @@ function buildColumnDefinition(field, ctx) {
|
|
|
137
140
|
def += ` DEFAULT ${mysqlTypeDefault(sqlType, field)}`;
|
|
138
141
|
inventedDefault = true;
|
|
139
142
|
}
|
|
140
|
-
def +=
|
|
141
|
-
const onUpdate = ctx.onUpdateFields?.get(field.
|
|
142
|
-
if (onUpdate) def += ` ON UPDATE ${
|
|
143
|
+
def += collateClause;
|
|
144
|
+
const onUpdate = ctx.onUpdateFields?.get(field.path);
|
|
145
|
+
if (onUpdate) def += ` ON UPDATE ${mysqlOnUpdateExpr(onUpdate, currentTimestampFor(sqlType))}`;
|
|
143
146
|
return {
|
|
144
147
|
def,
|
|
145
148
|
inventedDefault
|
|
146
149
|
};
|
|
147
150
|
}
|
|
151
|
+
/** Whether a column declares its own collation (`@db.mysql.collate` or `@db.column.collate`). */
|
|
152
|
+
function declaresCollation(field) {
|
|
153
|
+
return field.collate !== void 0 || (field.type?.metadata?.has("db.mysql.collate") ?? false);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The character set a column declares (`undefined`: the table's). A
|
|
157
|
+
* foreign-key column takes the referenced column's, as MySQL requires of a
|
|
158
|
+
* string foreign key: the referenced field's own `@db.mysql.charset`, else
|
|
159
|
+
* its table's (`ctx.keyTableCharsets`); its own declarations are not used.
|
|
160
|
+
*/
|
|
161
|
+
function mysqlColumnCharset(field, ctx) {
|
|
162
|
+
const keyTarget = field.fkTargetField;
|
|
163
|
+
if (!keyTarget) return ctx.charsetFields?.get(field.path);
|
|
164
|
+
return keyTarget.type?.metadata?.get("db.mysql.charset") ?? ctx.keyTableCharsets?.get(field.path)?.charset;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The ` CHARACTER SET …` and ` COLLATE …` of a column (see
|
|
168
|
+
* {@link mysqlColumnCharset}); a foreign-key column's collation is the
|
|
169
|
+
* referenced field's own, else its table's.
|
|
170
|
+
*/
|
|
171
|
+
function mysqlColumnCharsetClauses(field, sqlType, ctx) {
|
|
172
|
+
const charset = mysqlColumnCharset(field, ctx);
|
|
173
|
+
const charsetClause = mysqlCharsetClause(sqlType, charset);
|
|
174
|
+
const keyTarget = field.fkTargetField;
|
|
175
|
+
if (!keyTarget) return {
|
|
176
|
+
charsetClause,
|
|
177
|
+
collateClause: mysqlCollateClause(field, charset)
|
|
178
|
+
};
|
|
179
|
+
const ownCharset = keyTarget.type?.metadata?.get("db.mysql.charset");
|
|
180
|
+
if (declaresCollation(keyTarget)) return {
|
|
181
|
+
charsetClause,
|
|
182
|
+
collateClause: mysqlCollateClause(keyTarget, ownCharset)
|
|
183
|
+
};
|
|
184
|
+
const tableCollation = ownCharset === void 0 ? ctx.keyTableCharsets?.get(field.path)?.collation : void 0;
|
|
185
|
+
return {
|
|
186
|
+
charsetClause,
|
|
187
|
+
collateClause: tableCollation && CHARACTER_TYPE_RE.test(sqlType) ? ` COLLATE ${mysqlName("@db.mysql.collate", tableCollation)}` : ""
|
|
188
|
+
};
|
|
189
|
+
}
|
|
148
190
|
/** Escapes a MySQL identifier by doubling backticks. */
|
|
149
191
|
function esc(name) {
|
|
150
192
|
return name.replace(/`/g, "``");
|
|
@@ -432,18 +474,39 @@ function buildAggregateSelect$1(table, where, controls) {
|
|
|
432
474
|
function buildAggregateCount$1(table, where, controls) {
|
|
433
475
|
return buildAggregateCount(mysqlDialect, table, where, controls);
|
|
434
476
|
}
|
|
477
|
+
/** Character-typed columns (the ones a `CHARACTER SET` applies to). */
|
|
478
|
+
const CHARACTER_TYPE_RE = /^(VAR)?CHAR\b|^(TINY|MEDIUM|LONG)?TEXT\b|^(ENUM|SET)\b/i;
|
|
435
479
|
/**
|
|
436
|
-
*
|
|
480
|
+
* ` CHARACTER SET <charset>` for a character-typed column with a field-level
|
|
481
|
+
* `@db.mysql.charset`, else `""`.
|
|
437
482
|
*/
|
|
483
|
+
function mysqlCharsetClause(sqlType, charset) {
|
|
484
|
+
return charset && CHARACTER_TYPE_RE.test(sqlType) ? ` CHARACTER SET ${mysqlName("@db.mysql.charset", charset)}` : "";
|
|
485
|
+
}
|
|
438
486
|
/**
|
|
439
|
-
* The `COLLATE
|
|
440
|
-
*
|
|
487
|
+
* The column's ` COLLATE …`: a native `@db.mysql.collate`, else the portable
|
|
488
|
+
* `@db.column.collate` — in the column's `charset` when it sets one
|
|
489
|
+
* (`nocase` → `<charset>_general_ci`, `binary` → `<charset>_bin`,
|
|
490
|
+
* `unicode` → `<charset>_unicode_ci`), else in `utf8mb4`.
|
|
441
491
|
*/
|
|
442
|
-
function mysqlCollateClause(field) {
|
|
492
|
+
function mysqlCollateClause(field, charset) {
|
|
443
493
|
const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
|
|
444
494
|
if (nativeCollate) return ` COLLATE ${nativeCollate}`;
|
|
445
|
-
|
|
495
|
+
if (!field.collate) return "";
|
|
496
|
+
const collation = collationToMysql(field.collate);
|
|
497
|
+
return ` COLLATE ${charset ? collation.replace(/^utf8mb4_/, `${charset}_`) : collation}`;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* The collation a table gets when only its character set is declared:
|
|
501
|
+
* `utf8mb4_unicode_ci` for `utf8mb4` (the adapter's default), else none —
|
|
502
|
+
* MySQL then applies the set's own default collation.
|
|
503
|
+
*/
|
|
504
|
+
function mysqlDefaultCollation(charset) {
|
|
505
|
+
return charset.toLowerCase() === "utf8mb4" ? "utf8mb4_unicode_ci" : void 0;
|
|
446
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Maps portable collation values to MySQL collation names.
|
|
509
|
+
*/
|
|
447
510
|
function collationToMysql(collation) {
|
|
448
511
|
switch (collation) {
|
|
449
512
|
case "binary": return "utf8mb4_bin";
|
|
@@ -452,15 +515,6 @@ function collationToMysql(collation) {
|
|
|
452
515
|
default: return "utf8mb4_unicode_ci";
|
|
453
516
|
}
|
|
454
517
|
}
|
|
455
|
-
/**
|
|
456
|
-
* Maps an Atscript field descriptor to a MySQL column type.
|
|
457
|
-
*
|
|
458
|
-
* Reads `designType`, primitive tags (via `type.type.tags`), and annotations
|
|
459
|
-
* from field metadata to produce the most specific MySQL type.
|
|
460
|
-
*
|
|
461
|
-
* For FK fields, delegates to the target PK's type via `field.fkTargetField`
|
|
462
|
-
* so the FK column type always matches the referenced column.
|
|
463
|
-
*/
|
|
464
518
|
/** Maps integer primitive tags to MySQL integer types. */
|
|
465
519
|
function intTypeFromTags(tags, unsigned) {
|
|
466
520
|
if (tags?.has("int8")) return unsigned ? "TINYINT UNSIGNED" : "TINYINT";
|
|
@@ -473,8 +527,19 @@ function intTypeFromTags(tags, unsigned) {
|
|
|
473
527
|
if (tags?.has("uint64")) return "BIGINT UNSIGNED";
|
|
474
528
|
return unsigned ? "INT UNSIGNED" : "INT";
|
|
475
529
|
}
|
|
476
|
-
|
|
477
|
-
|
|
530
|
+
/**
|
|
531
|
+
* Maps an Atscript field descriptor to a MySQL column type.
|
|
532
|
+
*
|
|
533
|
+
* Reads `designType`, primitive tags (via `type.type.tags`), and annotations
|
|
534
|
+
* from field metadata to produce the most specific MySQL type.
|
|
535
|
+
*
|
|
536
|
+
* `keyed` marks a column a foreign key references (the adapter knows it
|
|
537
|
+
* from its space); a referencing column takes its type through
|
|
538
|
+
* `field.fkTargetField`. A keyed string is never TEXT: a foreign key needs
|
|
539
|
+
* both sides indexed in full, and MySQL indexes TEXT only by a prefix.
|
|
540
|
+
*/
|
|
541
|
+
function mysqlTypeFromField(field, keyed = false) {
|
|
542
|
+
if (field.fkTargetField) return mysqlTypeFromField(field.fkTargetField, true);
|
|
478
543
|
const tags = field.type?.type?.tags;
|
|
479
544
|
const metadata = field.type?.metadata;
|
|
480
545
|
const mysqlTypeOverride = metadata?.get("db.mysql.type");
|
|
@@ -499,14 +564,15 @@ function mysqlTypeFromField(field) {
|
|
|
499
564
|
const maxLen = (metadata?.get("expect.maxLength"))?.length;
|
|
500
565
|
if (maxLen !== void 0 && maxLen <= 65535) return `VARCHAR(${maxLen})`;
|
|
501
566
|
if (maxLen !== void 0 && maxLen > 65535) return "LONGTEXT";
|
|
502
|
-
if (field.
|
|
567
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "uuid") return "CHAR(36)";
|
|
568
|
+
if (keyed || field.isPrimaryKey || field.defaultValue || field.derived) return "VARCHAR(255)";
|
|
503
569
|
return "TEXT";
|
|
504
570
|
}
|
|
505
571
|
case "json":
|
|
506
572
|
case "object":
|
|
507
573
|
case "array": return "JSON";
|
|
508
574
|
default:
|
|
509
|
-
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
575
|
+
if (keyed || field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
510
576
|
return "TEXT";
|
|
511
577
|
}
|
|
512
578
|
}
|
|
@@ -518,6 +584,8 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
518
584
|
const ctx = {
|
|
519
585
|
incrementFields: options?.incrementFields,
|
|
520
586
|
onUpdateFields: options?.onUpdateFields,
|
|
587
|
+
charsetFields: options?.charsetFields,
|
|
588
|
+
keyTableCharsets: options?.keyTableCharsets,
|
|
521
589
|
typeMapper: options?.typeMapper,
|
|
522
590
|
purpose: "create"
|
|
523
591
|
};
|
|
@@ -541,8 +609,9 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
541
609
|
let sql = `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${body})`;
|
|
542
610
|
const engine = options?.engine ?? "InnoDB";
|
|
543
611
|
const charset = options?.charset ?? "utf8mb4";
|
|
544
|
-
const collation = options?.collation ??
|
|
545
|
-
sql += ` ENGINE=${engine} DEFAULT CHARSET=${charset}
|
|
612
|
+
const collation = options?.collation ?? mysqlDefaultCollation(charset);
|
|
613
|
+
sql += ` ENGINE=${engine} DEFAULT CHARSET=${charset}`;
|
|
614
|
+
if (collation) sql += ` COLLATE=${collation}`;
|
|
546
615
|
if (options?.autoIncrementStart !== void 0) sql += ` AUTO_INCREMENT=${options.autoIncrementStart}`;
|
|
547
616
|
return sql;
|
|
548
617
|
}
|
|
@@ -565,6 +634,14 @@ function buildWhere(filter, opts) {
|
|
|
565
634
|
}
|
|
566
635
|
//#endregion
|
|
567
636
|
//#region src/mysql-adapter.ts
|
|
637
|
+
/** A column's own `@db.mysql.charset`, `@db.mysql.collate` and `@db.column.collate` (lowercased, `""` when absent). */
|
|
638
|
+
function declaredCharsetCollation(field) {
|
|
639
|
+
return [
|
|
640
|
+
field.type?.metadata?.get("db.mysql.charset"),
|
|
641
|
+
field.type?.metadata?.get("db.mysql.collate"),
|
|
642
|
+
field.collate
|
|
643
|
+
].map((v) => typeof v === "string" ? v.toLowerCase() : "");
|
|
644
|
+
}
|
|
568
645
|
/**
|
|
569
646
|
* Parses a MySQL UTC datetime string (`'YYYY-MM-DD HH:MM:SS[.ffffff]'`) to
|
|
570
647
|
* epoch ms — fractional seconds included (truncated to milliseconds; since
|
|
@@ -627,6 +704,33 @@ const nonStrictChecked = /* @__PURE__ */ new WeakSet();
|
|
|
627
704
|
* loading the time zone tables fixes a running server without a restart.
|
|
628
705
|
*/
|
|
629
706
|
const convertibleZones = /* @__PURE__ */ new WeakMap();
|
|
707
|
+
/** The `ALTER TABLE` that re-creates `fks`, all on `table`, as they were. */
|
|
708
|
+
function addForeignKeysSql(table, fks) {
|
|
709
|
+
return `ALTER TABLE ${table} ${fks.map((fk) => `ADD CONSTRAINT ${qi(fk.name)} FOREIGN KEY (${fk.columns.map(qi).join(", ")}) REFERENCES ${fk.refTable} (${fk.refColumns.map(qi).join(", ")}) ON DELETE ${fk.onDelete} ON UPDATE ${fk.onUpdate}`).join(", ")}`;
|
|
710
|
+
}
|
|
711
|
+
/** A map key of a live column. */
|
|
712
|
+
const columnKey = (c) => `${c.schema}\0${c.table}\0${c.column}`;
|
|
713
|
+
/** Which ends of `fk`'s `i`-th column pair are among `columns` (of the adapter's table). */
|
|
714
|
+
function touchedEnds(fk, i, columns) {
|
|
715
|
+
return {
|
|
716
|
+
child: fk.own && columns.has(fk.columns[i]),
|
|
717
|
+
parent: fk.ref && columns.has(fk.refColumns[i])
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const INTEGER_TYPE_RE = /^(tinyint|smallint|mediumint|int|integer|bigint)(?:\(\d+\))?(\s+unsigned)?(?:\s+zerofill)?$/;
|
|
721
|
+
const CHAR_TYPE_RE = /^(?:var)?char\(\d+\)$/i;
|
|
722
|
+
/**
|
|
723
|
+
* What the two sides of a foreign key must share: an integer's type and
|
|
724
|
+
* signedness (display width aside), a `CHAR` / `VARCHAR`'s collation (any
|
|
725
|
+
* length), anything else the type.
|
|
726
|
+
*/
|
|
727
|
+
function fkTypeKey(column) {
|
|
728
|
+
const type = column.type.trim().toLowerCase().replace(/\s+/g, " ");
|
|
729
|
+
const int = INTEGER_TYPE_RE.exec(type);
|
|
730
|
+
if (int) return `int:${int[1] === "integer" ? "int" : int[1]}:${!!int[2]}`;
|
|
731
|
+
if (CHAR_TYPE_RE.test(type)) return `char:${(column.collation ?? "").toLowerCase()}`;
|
|
732
|
+
return type;
|
|
733
|
+
}
|
|
630
734
|
/**
|
|
631
735
|
* MySQL adapter for {@link AtscriptDbTable}.
|
|
632
736
|
*
|
|
@@ -649,7 +753,8 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
649
753
|
static NATIVE_DEFAULT_FNS = new Set(["now", "increment"]);
|
|
650
754
|
_engine = "InnoDB";
|
|
651
755
|
_charset = "utf8mb4";
|
|
652
|
-
|
|
756
|
+
/** `@db.mysql.collate` on the table; none → the charset's default (see `mysqlDefaultCollation`). */
|
|
757
|
+
_collation;
|
|
653
758
|
_autoIncrementStart;
|
|
654
759
|
_incrementFields = /* @__PURE__ */ new Set();
|
|
655
760
|
/**
|
|
@@ -665,6 +770,11 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
665
770
|
*/
|
|
666
771
|
_epochAfterRekey = /* @__PURE__ */ new Map();
|
|
667
772
|
_onUpdateFields = /* @__PURE__ */ new Map();
|
|
773
|
+
/** Field path → `@db.mysql.charset` of the column. */
|
|
774
|
+
_charsetFields = /* @__PURE__ */ new Map();
|
|
775
|
+
_space;
|
|
776
|
+
/** Memo of {@link _keyTableCharsets} — it also makes its warning fire once. */
|
|
777
|
+
_keyTableCharsetsMemo;
|
|
668
778
|
/** Whether the connected MySQL instance supports native VECTOR type (MySQL 9.0+). */
|
|
669
779
|
_supportsVector;
|
|
670
780
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -674,6 +784,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
674
784
|
_replaceColumnsMemo;
|
|
675
785
|
_fulltextMemo;
|
|
676
786
|
_searchIndexesMemo;
|
|
787
|
+
_incrementPkMemo;
|
|
677
788
|
/**
|
|
678
789
|
* Schema name for INFORMATION_SCHEMA queries — `@db.schema` of the bound
|
|
679
790
|
* table, or `null` (→ `DATABASE()`, the pool's database) when the table
|
|
@@ -847,11 +958,11 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
847
958
|
onBeforeFlatten(_type) {
|
|
848
959
|
const meta = _type.metadata;
|
|
849
960
|
const engine = meta.get("db.mysql.engine");
|
|
850
|
-
if (engine) this._engine = engine;
|
|
961
|
+
if (engine) this._engine = mysqlName("@db.mysql.engine", engine);
|
|
851
962
|
const charset = meta.get("db.mysql.charset");
|
|
852
|
-
if (charset) this._charset = charset;
|
|
963
|
+
if (charset) this._charset = mysqlName("@db.mysql.charset", charset);
|
|
853
964
|
const collate = meta.get("db.mysql.collate");
|
|
854
|
-
if (collate) this._collation = collate;
|
|
965
|
+
if (collate) this._collation = mysqlName("@db.mysql.collate", collate);
|
|
855
966
|
}
|
|
856
967
|
onFieldScanned(field, _type, metadata) {
|
|
857
968
|
if (metadata.has("db.default.increment")) {
|
|
@@ -861,6 +972,8 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
861
972
|
}
|
|
862
973
|
const onUpdate = metadata.get("db.mysql.onUpdate");
|
|
863
974
|
if (onUpdate) this._onUpdateFields.set(field, onUpdate);
|
|
975
|
+
const charset = metadata.get("db.mysql.charset");
|
|
976
|
+
if (charset) this._charsetFields.set(field, mysqlName("@db.mysql.charset", charset));
|
|
864
977
|
const vectorMeta = metadata.get("db.search.vector");
|
|
865
978
|
if (vectorMeta) {
|
|
866
979
|
const indexName = vectorMeta.indexName || field;
|
|
@@ -874,6 +987,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
874
987
|
}
|
|
875
988
|
}
|
|
876
989
|
getDesiredTableOptions() {
|
|
990
|
+
const collation = this._tableCollation();
|
|
877
991
|
return [
|
|
878
992
|
{
|
|
879
993
|
key: "engine",
|
|
@@ -883,10 +997,10 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
883
997
|
key: "charset",
|
|
884
998
|
value: this._charset
|
|
885
999
|
},
|
|
886
|
-
{
|
|
1000
|
+
...collation === void 0 ? [] : [{
|
|
887
1001
|
key: "collation",
|
|
888
|
-
value:
|
|
889
|
-
}
|
|
1002
|
+
value: collation
|
|
1003
|
+
}]
|
|
890
1004
|
];
|
|
891
1005
|
}
|
|
892
1006
|
async getExistingTableOptions(tableName) {
|
|
@@ -895,18 +1009,23 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
895
1009
|
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())`, [tableName ?? this._table.tableName, this._schema]);
|
|
896
1010
|
if (!row) return [];
|
|
897
1011
|
const charset = row.TABLE_COLLATION?.split("_")[0] ?? "utf8mb4";
|
|
1012
|
+
const desired = new Map(this.getDesiredTableOptions().map((o) => [o.key, o.value]));
|
|
1013
|
+
const asDesired = (key, value) => {
|
|
1014
|
+
const want = desired.get(key);
|
|
1015
|
+
return want !== void 0 && want.toLowerCase() === value.toLowerCase() ? want : value;
|
|
1016
|
+
};
|
|
898
1017
|
return [
|
|
899
1018
|
{
|
|
900
1019
|
key: "engine",
|
|
901
|
-
value: row.ENGINE ?? "InnoDB"
|
|
1020
|
+
value: asDesired("engine", row.ENGINE ?? "InnoDB")
|
|
902
1021
|
},
|
|
903
1022
|
{
|
|
904
1023
|
key: "charset",
|
|
905
|
-
value: charset
|
|
1024
|
+
value: asDesired("charset", charset)
|
|
906
1025
|
},
|
|
907
1026
|
{
|
|
908
1027
|
key: "collation",
|
|
909
|
-
value: row.TABLE_COLLATION ?? "utf8mb4_unicode_ci"
|
|
1028
|
+
value: asDesired("collation", row.TABLE_COLLATION ?? "utf8mb4_unicode_ci")
|
|
910
1029
|
}
|
|
911
1030
|
];
|
|
912
1031
|
}
|
|
@@ -993,7 +1112,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
993
1112
|
const { sql, params } = buildInsert$1(this.resolveTableName(), data);
|
|
994
1113
|
this._log(sql, params);
|
|
995
1114
|
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
996
|
-
return { insertedId: this.
|
|
1115
|
+
return { insertedId: this._insertedIdOf(data, result.insertId, result.insertId) };
|
|
997
1116
|
}
|
|
998
1117
|
async insertMany(data) {
|
|
999
1118
|
if (data.length === 0) return {
|
|
@@ -1071,9 +1190,25 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1071
1190
|
}
|
|
1072
1191
|
/** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
|
|
1073
1192
|
_autoIncrementPk() {
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1193
|
+
const { column, composite } = this._incrementPk();
|
|
1194
|
+
return composite ? void 0 : column;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Physical column of the primary key's `@db.default.increment` part (if any)
|
|
1198
|
+
* and whether the key is composite — computed once per table.
|
|
1199
|
+
*/
|
|
1200
|
+
_incrementPk() {
|
|
1201
|
+
const src = this._table.primaryKeys;
|
|
1202
|
+
let memo = this._incrementPkMemo;
|
|
1203
|
+
if (memo?.src !== src) {
|
|
1204
|
+
const part = src.find((pk) => this._incrementFields.has(pk));
|
|
1205
|
+
memo = this._incrementPkMemo = {
|
|
1206
|
+
src,
|
|
1207
|
+
column: part === void 0 ? void 0 : this._table.physicalPath(part),
|
|
1208
|
+
composite: src.length > 1
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
return memo;
|
|
1077
1212
|
}
|
|
1078
1213
|
/**
|
|
1079
1214
|
* Splits `rows` into runs that each map to ONE statement with a derivable
|
|
@@ -1163,11 +1298,30 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1163
1298
|
async _groupInsertedIds(group, insertId, step) {
|
|
1164
1299
|
const first = Number(insertId);
|
|
1165
1300
|
const stride = group.generated && group.rows.length > 1 && first > 0 ? await step() : 1;
|
|
1301
|
+
const partId = group.rows.length === 1 ? insertId : void 0;
|
|
1166
1302
|
return group.rows.map((row, i) => {
|
|
1167
1303
|
const generatedId = first > 0 ? first + i * stride : 0;
|
|
1168
|
-
return group.generated ? generatedId : this.
|
|
1304
|
+
return group.generated ? generatedId : this._insertedIdOf(row, generatedId, partId);
|
|
1169
1305
|
});
|
|
1170
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* The inserted id of `row` (see `_resolveInsertedId`). For a COMPOSITE key
|
|
1309
|
+
* with an AUTO_INCREMENT part, `partId` is what the statement generated for
|
|
1310
|
+
* that part — pass it only when exact (a ONE-row INSERT); a row carrying the
|
|
1311
|
+
* part keeps its own value. An explicit `0` in the part counts as absent:
|
|
1312
|
+
* MySQL generates a value for it (unless `NO_AUTO_VALUE_ON_ZERO`), so only
|
|
1313
|
+
* `partId` knows it — without that the id is `undefined`, never a wrong `0`.
|
|
1314
|
+
*/
|
|
1315
|
+
_insertedIdOf(row, generatedId, partId) {
|
|
1316
|
+
const { column, composite } = this._incrementPk();
|
|
1317
|
+
if (!composite || column === void 0) return this._resolveInsertedId(row, generatedId);
|
|
1318
|
+
const source = isZero(row[column]) ? {
|
|
1319
|
+
...row,
|
|
1320
|
+
[column]: void 0
|
|
1321
|
+
} : row;
|
|
1322
|
+
const part = Number(partId) > 0 ? { [column]: partId } : void 0;
|
|
1323
|
+
return this._resolveInsertedId(source, generatedId, part);
|
|
1324
|
+
}
|
|
1171
1325
|
supportsInsertIgnore() {
|
|
1172
1326
|
return true;
|
|
1173
1327
|
}
|
|
@@ -1596,15 +1750,64 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1596
1750
|
this._log(sql);
|
|
1597
1751
|
await this._exec().exec(sql);
|
|
1598
1752
|
}
|
|
1753
|
+
/** The table's collation: `@db.mysql.collate`, else the charset's default one (`utf8mb4_unicode_ci` for `utf8mb4`). */
|
|
1754
|
+
_tableCollation() {
|
|
1755
|
+
return this._collation ?? mysqlDefaultCollation(this._charset);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Foreign-key column path → the referenced table's character set and
|
|
1759
|
+
* collation, where they differ from this table's (see
|
|
1760
|
+
* `mysqlColumnCharset`). Warns about a foreign-key column whose own
|
|
1761
|
+
* `@db.mysql.charset` / collation is replaced by the referenced column's.
|
|
1762
|
+
*/
|
|
1763
|
+
_keyTableCharsets() {
|
|
1764
|
+
return this._keyTableCharsetsMemo ??= this._buildKeyTableCharsets();
|
|
1765
|
+
}
|
|
1766
|
+
_buildKeyTableCharsets() {
|
|
1767
|
+
const out = /* @__PURE__ */ new Map();
|
|
1768
|
+
const own = `${this._charset}\0${this._tableCollation() ?? ""}`.toLowerCase();
|
|
1769
|
+
for (const fk of this._table.foreignKeys.values()) {
|
|
1770
|
+
const meta = fk.targetTypeRef?.()?.metadata;
|
|
1771
|
+
const declaredCollation = meta?.get("db.mysql.collate");
|
|
1772
|
+
const charset = meta?.get("db.mysql.charset") ?? declaredCollation?.split("_")[0] ?? "utf8mb4";
|
|
1773
|
+
const collation = declaredCollation ?? mysqlDefaultCollation(charset);
|
|
1774
|
+
if (`${charset}\0${collation ?? ""}`.toLowerCase() === own) continue;
|
|
1775
|
+
for (const path of fk.fields) out.set(path, {
|
|
1776
|
+
charset,
|
|
1777
|
+
collation
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
for (const field of this._table.fieldDescriptors) {
|
|
1781
|
+
const target = field.fkTargetField;
|
|
1782
|
+
if (!target) continue;
|
|
1783
|
+
const mine = declaredCharsetCollation(field);
|
|
1784
|
+
if (mine.some(Boolean) && mine.join("\0") !== declaredCharsetCollation(target).join("\0")) this.logger.warn?.(`[mysql] "${this._table.tableName}.${field.path}": its own character set / collation is not used — a foreign-key column takes the referenced column's ("${target.path}"), as MySQL requires`);
|
|
1785
|
+
}
|
|
1786
|
+
return out;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* The key-length prefix an index needs on `field`'s column: a function of
|
|
1790
|
+
* its MAPPED type and its character set (`mysqlColumnCharset`, else the
|
|
1791
|
+
* table's).
|
|
1792
|
+
*/
|
|
1793
|
+
_indexPrefix(field) {
|
|
1794
|
+
const charset = mysqlColumnCharset(field, {
|
|
1795
|
+
charsetFields: this._charsetFields,
|
|
1796
|
+
keyTableCharsets: this._keyTableCharsets()
|
|
1797
|
+
});
|
|
1798
|
+
return mysqlIndexPrefix(this.typeMapper(field), mysqlBytesPerChar(charset ?? this._charset));
|
|
1799
|
+
}
|
|
1599
1800
|
/** The CREATE TABLE options (engine/charset/collation/increment/ON UPDATE/type mapper). */
|
|
1600
1801
|
_tableOptions(opts) {
|
|
1601
1802
|
return {
|
|
1602
1803
|
engine: this._engine,
|
|
1603
1804
|
charset: this._charset,
|
|
1604
|
-
collation: this.
|
|
1805
|
+
collation: this._tableCollation(),
|
|
1605
1806
|
autoIncrementStart: this._autoIncrementStart,
|
|
1606
1807
|
incrementFields: this._incrementFields,
|
|
1607
1808
|
onUpdateFields: this._onUpdateFields,
|
|
1809
|
+
charsetFields: this._charsetFields,
|
|
1810
|
+
keyTableCharsets: this._keyTableCharsets(),
|
|
1608
1811
|
typeMapper: (field) => this.typeMapper(field),
|
|
1609
1812
|
deferForeignKeysTo: opts?.deferForeignKeysTo
|
|
1610
1813
|
};
|
|
@@ -1614,6 +1817,8 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1614
1817
|
return {
|
|
1615
1818
|
incrementFields: this._incrementFields,
|
|
1616
1819
|
onUpdateFields: this._onUpdateFields,
|
|
1820
|
+
charsetFields: this._charsetFields,
|
|
1821
|
+
keyTableCharsets: this._keyTableCharsets(),
|
|
1617
1822
|
typeMapper: (field) => this.typeMapper(field),
|
|
1618
1823
|
purpose
|
|
1619
1824
|
};
|
|
@@ -1798,6 +2003,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1798
2003
|
return entering.has(field.physicalName);
|
|
1799
2004
|
};
|
|
1800
2005
|
const modified = /* @__PURE__ */ new Map();
|
|
2006
|
+
const retyped = [];
|
|
1801
2007
|
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1802
2008
|
const epoch = epochSource(field, existingType);
|
|
1803
2009
|
if (epoch) {
|
|
@@ -1815,6 +2021,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1815
2021
|
continue;
|
|
1816
2022
|
}
|
|
1817
2023
|
modified.set(field.physicalName, field);
|
|
2024
|
+
retyped.push(field);
|
|
1818
2025
|
}
|
|
1819
2026
|
for (const { field } of diff.nullableChanged ?? []) {
|
|
1820
2027
|
if (rekeyed(field)) continue;
|
|
@@ -1828,17 +2035,285 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1828
2035
|
modified.set(field.physicalName, field);
|
|
1829
2036
|
}
|
|
1830
2037
|
for (const { field } of diff.defaultChanged ?? []) if (!rekeyed(field)) modified.set(field.physicalName, field);
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2038
|
+
await this._dropIndexesOutgrown(conn, [...modified.values()]);
|
|
2039
|
+
await this._withForeignKeysLifted(conn, retyped, async () => {
|
|
2040
|
+
for (const field of modified.values()) {
|
|
2041
|
+
const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
|
|
2042
|
+
this._log(ddl);
|
|
2043
|
+
await conn.exec(ddl);
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
1836
2046
|
return {
|
|
1837
2047
|
added,
|
|
1838
2048
|
renamed
|
|
1839
2049
|
};
|
|
1840
2050
|
});
|
|
1841
2051
|
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Runs `fn` (the MODIFYs of `fields`) with the live foreign keys over those
|
|
2054
|
+
* columns dropped — this table's own and those of any table referencing
|
|
2055
|
+
* them, its own included — and re-creates them, as they were, after: also
|
|
2056
|
+
* when `fn` throws. Nothing is dropped when a constraint could not be
|
|
2057
|
+
* re-created over the new types (see {@link _liftable}): MySQL then refuses
|
|
2058
|
+
* the MODIFY as it always did. A constraint that cannot be re-created is
|
|
2059
|
+
* named, with its DDL, in the error (after `fn`'s own).
|
|
2060
|
+
*/
|
|
2061
|
+
async _withForeignKeysLifted(conn, fields, fn) {
|
|
2062
|
+
if (fields.length === 0) return fn();
|
|
2063
|
+
const found = await this._foreignKeysOver(conn, fields.map((f) => f.physicalName));
|
|
2064
|
+
const lifted = found.length > 0 && await this._liftable(conn, found, fields) ? found : [];
|
|
2065
|
+
const dropped = [];
|
|
2066
|
+
let failure;
|
|
2067
|
+
try {
|
|
2068
|
+
for (const fk of lifted) {
|
|
2069
|
+
const ddl = `ALTER TABLE ${fk.table} DROP FOREIGN KEY ${qi(fk.name)}`;
|
|
2070
|
+
this._log(ddl);
|
|
2071
|
+
await conn.exec(ddl);
|
|
2072
|
+
dropped.push(fk);
|
|
2073
|
+
this.logger.info(`[mysql] foreign key ${qi(fk.name)} on ${fk.table} dropped for the type change of ${quoteTableName(this.resolveTableName())}`);
|
|
2074
|
+
}
|
|
2075
|
+
await fn();
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
failure = error;
|
|
2078
|
+
}
|
|
2079
|
+
const lost = await this._restoreForeignKeys(conn, dropped);
|
|
2080
|
+
if (lost.length > 0) {
|
|
2081
|
+
const named = lost.map((fk) => `${qi(fk.name)} on ${fk.table} (re-create it with: ${addForeignKeysSql(fk.table, [fk])})`).join("; ");
|
|
2082
|
+
const message = failure ? `${failure.message} — and foreign keys not restored: ${named}` : `Foreign keys not restored after the type change: ${named}`;
|
|
2083
|
+
throw new Error(message, { cause: failure });
|
|
2084
|
+
}
|
|
2085
|
+
if (failure) throw failure;
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Re-creates `fks` (dropped by {@link _withForeignKeysLifted}) — one
|
|
2089
|
+
* `ALTER TABLE` per table, with `foreign_key_checks` off on this connection
|
|
2090
|
+
* so MySQL adds them in place instead of rebuilding the table (the rows
|
|
2091
|
+
* met them before the change). A table whose statement fails has its
|
|
2092
|
+
* constraints re-tried one by one. Returns those that could not be
|
|
2093
|
+
* re-created.
|
|
2094
|
+
*/
|
|
2095
|
+
async _restoreForeignKeys(conn, fks) {
|
|
2096
|
+
if (fks.length === 0) return [];
|
|
2097
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
2098
|
+
for (const fk of fks) {
|
|
2099
|
+
const group = byTable.get(fk.table);
|
|
2100
|
+
if (group) group.push(fk);
|
|
2101
|
+
else byTable.set(fk.table, [fk]);
|
|
2102
|
+
}
|
|
2103
|
+
const add = async (table, group) => {
|
|
2104
|
+
const ddl = addForeignKeysSql(table, group);
|
|
2105
|
+
this._log(ddl);
|
|
2106
|
+
try {
|
|
2107
|
+
await conn.exec(ddl);
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
return error;
|
|
2110
|
+
}
|
|
2111
|
+
for (const fk of group) this.logger.info(`[mysql] foreign key ${qi(fk.name)} on ${fk.table} restored`);
|
|
2112
|
+
};
|
|
2113
|
+
const lost = [];
|
|
2114
|
+
await conn.exec("SET @atscript_fk_checks = @@SESSION.foreign_key_checks, SESSION foreign_key_checks = 0");
|
|
2115
|
+
try {
|
|
2116
|
+
for (const [table, group] of byTable) {
|
|
2117
|
+
const error = await add(table, group);
|
|
2118
|
+
if (!error) continue;
|
|
2119
|
+
for (const fk of group) {
|
|
2120
|
+
const single = group.length === 1 ? error : await add(table, [fk]);
|
|
2121
|
+
if (single) {
|
|
2122
|
+
this.logger.error(`[mysql] foreign key ${qi(fk.name)} on ${table} could not be re-created: ${single.message}`);
|
|
2123
|
+
lost.push(fk);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
} finally {
|
|
2128
|
+
await conn.exec("SET SESSION foreign_key_checks = @atscript_fk_checks");
|
|
2129
|
+
}
|
|
2130
|
+
return lost;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Whether every constraint of `fks` can be re-created once `fields` have
|
|
2134
|
+
* their new types: each column pair of a constraint that the change touches
|
|
2135
|
+
* stays compatible — the new type against the other side's live column (or
|
|
2136
|
+
* its new type, on this table): integers of the same type and signedness,
|
|
2137
|
+
* `CHAR` / `VARCHAR` either way with the same collation, anything else the
|
|
2138
|
+
* same type. Otherwise nothing is lifted (and MySQL refuses the change, as
|
|
2139
|
+
* it did before foreign keys were lifted) — a dropped constraint must
|
|
2140
|
+
* never be lost to a column outside this run.
|
|
2141
|
+
*/
|
|
2142
|
+
async _liftable(conn, fks, fields) {
|
|
2143
|
+
const changed = new Map(fields.map((f) => [f.physicalName, f]));
|
|
2144
|
+
const columns = new Set(changed.keys());
|
|
2145
|
+
const pairs = [];
|
|
2146
|
+
for (const fk of fks) fk.columns.forEach((column, i) => {
|
|
2147
|
+
const { child, parent } = touchedEnds(fk, i, columns);
|
|
2148
|
+
if (!child && !parent) return;
|
|
2149
|
+
pairs.push([{
|
|
2150
|
+
schema: fk.schema,
|
|
2151
|
+
table: fk.tableName,
|
|
2152
|
+
column,
|
|
2153
|
+
ours: child
|
|
2154
|
+
}, {
|
|
2155
|
+
schema: fk.refSchema,
|
|
2156
|
+
table: fk.refTableName,
|
|
2157
|
+
column: fk.refColumns[i],
|
|
2158
|
+
ours: parent
|
|
2159
|
+
}]);
|
|
2160
|
+
});
|
|
2161
|
+
const liveEnds = /* @__PURE__ */ new Map();
|
|
2162
|
+
for (const end of pairs.flat()) if (!end.ours) liveEnds.set(columnKey(end), end);
|
|
2163
|
+
const live = [...liveEnds.values()];
|
|
2164
|
+
const liveRows = live.length === 0 ? [] : await conn.all(`SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLLATION_NAME
|
|
2165
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
2166
|
+
WHERE ${live.map(() => "(TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?)").join(" OR ")}`, live.flatMap((end) => [
|
|
2167
|
+
end.schema,
|
|
2168
|
+
end.table,
|
|
2169
|
+
end.column
|
|
2170
|
+
]));
|
|
2171
|
+
const liveOf = new Map(liveRows.map((r) => [columnKey({
|
|
2172
|
+
schema: r.TABLE_SCHEMA,
|
|
2173
|
+
table: r.TABLE_NAME,
|
|
2174
|
+
column: r.COLUMN_NAME
|
|
2175
|
+
}), {
|
|
2176
|
+
type: r.COLUMN_TYPE,
|
|
2177
|
+
collation: r.COLLATION_NAME ?? void 0
|
|
2178
|
+
}]));
|
|
2179
|
+
const collations = await this._newCollations(conn, [...new Set(pairs.flat().flatMap((end) => end.ours ? [changed.get(end.column)] : []).filter((field) => CHAR_TYPE_RE.test(this.typeMapper(field).trim())))]);
|
|
2180
|
+
const typeOf = (end) => {
|
|
2181
|
+
if (!end.ours) return liveOf.get(columnKey(end));
|
|
2182
|
+
const field = changed.get(end.column);
|
|
2183
|
+
return {
|
|
2184
|
+
type: this.typeMapper(field),
|
|
2185
|
+
collation: collations.get(field.physicalName)
|
|
2186
|
+
};
|
|
2187
|
+
};
|
|
2188
|
+
for (const [child, parent] of pairs) {
|
|
2189
|
+
const a = typeOf(child);
|
|
2190
|
+
const b = typeOf(parent);
|
|
2191
|
+
if (!a || !b || fkTypeKey(a) !== fkTypeKey(b)) {
|
|
2192
|
+
this.logger.warn(`[mysql] ${qi(child.table)}.${qi(child.column)} → ${qi(parent.table)}.${qi(parent.column)} would not match after the type change (${a?.type ?? "?"} / ${b?.type ?? "?"}) — its foreign key stays in place, and MySQL decides on the change`);
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* The collation each of `fields` (string columns) has after its MODIFY:
|
|
2200
|
+
* the definition's own `COLLATE`, else the default of its `CHARACTER SET`,
|
|
2201
|
+
* else the table's.
|
|
2202
|
+
*/
|
|
2203
|
+
async _newCollations(conn, fields) {
|
|
2204
|
+
const out = /* @__PURE__ */ new Map();
|
|
2205
|
+
if (fields.length === 0) return out;
|
|
2206
|
+
const ctx = this._columnCtx("modify");
|
|
2207
|
+
const charsetDefaults = /* @__PURE__ */ new Map();
|
|
2208
|
+
let tableCollation = null;
|
|
2209
|
+
for (const field of fields) {
|
|
2210
|
+
const { charsetClause, collateClause } = mysqlColumnCharsetClauses(field, this.typeMapper(field), ctx);
|
|
2211
|
+
const collate = /COLLATE\s+`?(\w+)`?/i.exec(collateClause)?.[1];
|
|
2212
|
+
const charset = /CHARACTER SET\s+`?(\w+)`?/i.exec(charsetClause)?.[1];
|
|
2213
|
+
if (collate) out.set(field.physicalName, collate);
|
|
2214
|
+
else if (charset) {
|
|
2215
|
+
if (!charsetDefaults.has(charset)) {
|
|
2216
|
+
const row = await conn.get(`SELECT COLLATION_NAME FROM INFORMATION_SCHEMA.COLLATIONS
|
|
2217
|
+
WHERE CHARACTER_SET_NAME = ? AND IS_DEFAULT = 'Yes'`, [charset]);
|
|
2218
|
+
charsetDefaults.set(charset, row?.COLLATION_NAME);
|
|
2219
|
+
}
|
|
2220
|
+
out.set(field.physicalName, charsetDefaults.get(charset));
|
|
2221
|
+
} else {
|
|
2222
|
+
if (tableCollation === null) tableCollation = (await conn.get(`SELECT TABLE_COLLATION FROM INFORMATION_SCHEMA.TABLES
|
|
2223
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())`, [this._table.tableName, this._schema]))?.TABLE_COLLATION ?? void 0;
|
|
2224
|
+
out.set(field.physicalName, tableCollation);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return out;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* The live foreign keys over any of `columns` of this table: its own (by
|
|
2231
|
+
* their columns) and those referencing them (by the referenced columns),
|
|
2232
|
+
* each with every column, in key order.
|
|
2233
|
+
*/
|
|
2234
|
+
async _foreignKeysOver(conn, columns) {
|
|
2235
|
+
const target = [this._schema, this._table.tableName];
|
|
2236
|
+
const rows = await conn.all(`SELECT k.TABLE_SCHEMA, k.TABLE_NAME, k.CONSTRAINT_NAME, k.COLUMN_NAME,
|
|
2237
|
+
k.REFERENCED_TABLE_SCHEMA, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME,
|
|
2238
|
+
r.UPDATE_RULE, r.DELETE_RULE,
|
|
2239
|
+
(k.TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.TABLE_NAME = ?) AS IS_OWN,
|
|
2240
|
+
(k.REFERENCED_TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.REFERENCED_TABLE_NAME = ?) AS IS_REF
|
|
2241
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
|
|
2242
|
+
JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r
|
|
2243
|
+
ON r.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA AND r.CONSTRAINT_NAME = k.CONSTRAINT_NAME
|
|
2244
|
+
AND r.TABLE_NAME = k.TABLE_NAME
|
|
2245
|
+
WHERE k.REFERENCED_TABLE_NAME IS NOT NULL
|
|
2246
|
+
AND ((k.TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.TABLE_NAME = ?)
|
|
2247
|
+
OR (k.REFERENCED_TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.REFERENCED_TABLE_NAME = ?))
|
|
2248
|
+
ORDER BY k.TABLE_SCHEMA, k.TABLE_NAME, k.CONSTRAINT_NAME, k.ORDINAL_POSITION`, [
|
|
2249
|
+
...target,
|
|
2250
|
+
...target,
|
|
2251
|
+
...target,
|
|
2252
|
+
...target
|
|
2253
|
+
]);
|
|
2254
|
+
const byConstraint = /* @__PURE__ */ new Map();
|
|
2255
|
+
for (const r of rows) {
|
|
2256
|
+
const key = `${r.TABLE_SCHEMA}\0${r.TABLE_NAME}\0${r.CONSTRAINT_NAME}`;
|
|
2257
|
+
let fk = byConstraint.get(key);
|
|
2258
|
+
if (!fk) {
|
|
2259
|
+
fk = {
|
|
2260
|
+
schema: r.TABLE_SCHEMA,
|
|
2261
|
+
tableName: r.TABLE_NAME,
|
|
2262
|
+
table: `${qi(r.TABLE_SCHEMA)}.${qi(r.TABLE_NAME)}`,
|
|
2263
|
+
name: r.CONSTRAINT_NAME,
|
|
2264
|
+
columns: [],
|
|
2265
|
+
refSchema: r.REFERENCED_TABLE_SCHEMA,
|
|
2266
|
+
refTableName: r.REFERENCED_TABLE_NAME,
|
|
2267
|
+
refTable: `${qi(r.REFERENCED_TABLE_SCHEMA)}.${qi(r.REFERENCED_TABLE_NAME)}`,
|
|
2268
|
+
refColumns: [],
|
|
2269
|
+
onDelete: r.DELETE_RULE,
|
|
2270
|
+
onUpdate: r.UPDATE_RULE,
|
|
2271
|
+
own: Boolean(Number(r.IS_OWN)),
|
|
2272
|
+
ref: Boolean(Number(r.IS_REF))
|
|
2273
|
+
};
|
|
2274
|
+
byConstraint.set(key, fk);
|
|
2275
|
+
}
|
|
2276
|
+
fk.columns.push(r.COLUMN_NAME);
|
|
2277
|
+
fk.refColumns.push(r.REFERENCED_COLUMN_NAME);
|
|
2278
|
+
}
|
|
2279
|
+
const over = new Set(columns);
|
|
2280
|
+
return [...byConstraint.values()].filter((fk) => fk.columns.some((_, i) => {
|
|
2281
|
+
const { child, parent } = touchedEnds(fk, i, over);
|
|
2282
|
+
return child || parent;
|
|
2283
|
+
}));
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Before `fields` are modified: drops the managed indexes that key more of
|
|
2287
|
+
* a column than its new type allows — a full key on a column becoming
|
|
2288
|
+
* TEXT (ER 1170), or on a VARCHAR growing past the key-part limit (ER
|
|
2289
|
+
* 1071). Index sync re-creates them with the prefix the new type needs.
|
|
2290
|
+
* Primary keys, FULLTEXT and unmanaged indexes are left alone.
|
|
2291
|
+
*/
|
|
2292
|
+
async _dropIndexesOutgrown(conn, fields) {
|
|
2293
|
+
const limits = /* @__PURE__ */ new Map();
|
|
2294
|
+
for (const field of fields) {
|
|
2295
|
+
const prefix = this._indexPrefix(field);
|
|
2296
|
+
if (prefix !== void 0) limits.set(field.physicalName, prefix);
|
|
2297
|
+
}
|
|
2298
|
+
if (limits.size === 0) return;
|
|
2299
|
+
const columns = [...limits.keys()];
|
|
2300
|
+
const rows = await conn.all(`SELECT INDEX_NAME, COLUMN_NAME, SUB_PART, INDEX_TYPE FROM INFORMATION_SCHEMA.STATISTICS
|
|
2301
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
2302
|
+
AND COLUMN_NAME IN (${columns.map(() => "?").join(", ")})`, [
|
|
2303
|
+
this._table.tableName,
|
|
2304
|
+
this._schema,
|
|
2305
|
+
...columns
|
|
2306
|
+
]);
|
|
2307
|
+
const outgrown = /* @__PURE__ */ new Set();
|
|
2308
|
+
for (const r of rows) if (r.INDEX_TYPE !== "FULLTEXT" && r.INDEX_NAME.startsWith("atscript__") && (r.SUB_PART === null || r.SUB_PART > limits.get(r.COLUMN_NAME))) outgrown.add(r.INDEX_NAME);
|
|
2309
|
+
for (const name of outgrown) await this._dropIndex(name, conn);
|
|
2310
|
+
}
|
|
2311
|
+
/** `DROP INDEX name` on this table (logged). */
|
|
2312
|
+
async _dropIndex(name, conn = this._exec()) {
|
|
2313
|
+
const ddl = `DROP INDEX ${qi(name)} ON ${quoteTableName(this.resolveTableName())}`;
|
|
2314
|
+
this._log(ddl);
|
|
2315
|
+
await conn.exec(ddl);
|
|
2316
|
+
}
|
|
1842
2317
|
async recreateTable() {
|
|
1843
2318
|
const tableName = this.resolveTableName();
|
|
1844
2319
|
const tempName = `${this._table.tableName}__tmp_${Date.now()}`;
|
|
@@ -1913,11 +2388,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1913
2388
|
this._schema,
|
|
1914
2389
|
...columns
|
|
1915
2390
|
]);
|
|
1916
|
-
for (const row of rows)
|
|
1917
|
-
const sql = `DROP INDEX ${qi(row.name)} ON ${quoteTableName(this.resolveTableName())}`;
|
|
1918
|
-
this._log(sql);
|
|
1919
|
-
await this._exec().exec(sql);
|
|
1920
|
-
}
|
|
2391
|
+
for (const row of rows) await this._dropIndex(row.name);
|
|
1921
2392
|
}
|
|
1922
2393
|
async dropTableByName(tableName) {
|
|
1923
2394
|
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
|
|
@@ -1943,13 +2414,17 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1943
2414
|
this._log(ddl);
|
|
1944
2415
|
await this._exec().exec(ddl);
|
|
1945
2416
|
}
|
|
2417
|
+
/** Keeps the space: {@link typeMapper} asks it which fields a foreign key references. */
|
|
2418
|
+
registerSpace(space) {
|
|
2419
|
+
this._space = space;
|
|
2420
|
+
}
|
|
1946
2421
|
typeMapper(field) {
|
|
1947
2422
|
if (field.encrypted) return "TEXT";
|
|
1948
2423
|
if (this._vectorFields.has(field.path)) {
|
|
1949
2424
|
const vec = this._vectorFields.get(field.path);
|
|
1950
2425
|
return this._supportsVector ? `VECTOR(${vec.dimensions})` : "JSON";
|
|
1951
2426
|
}
|
|
1952
|
-
return mysqlTypeFromField(field);
|
|
2427
|
+
return mysqlTypeFromField(field, !field.fkTargetField && this._space !== void 0 && this._table !== void 0 && this._space.getReferencedFields(this._table.tableName, this._table.schema).has(field.path));
|
|
1953
2428
|
}
|
|
1954
2429
|
async _syncIndexes(dryRun) {
|
|
1955
2430
|
const tableName = this._table.tableName;
|
|
@@ -1957,15 +2432,14 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1957
2432
|
if (!dryRun) {
|
|
1958
2433
|
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`);
|
|
1959
2434
|
}
|
|
1960
|
-
const bytesPerChar = mysqlBytesPerChar(this._charset);
|
|
1961
2435
|
const fields = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
1962
2436
|
const mappedType = (column) => {
|
|
1963
2437
|
const field = fields.get(column);
|
|
1964
2438
|
return field ? this.typeMapper(field) : void 0;
|
|
1965
2439
|
};
|
|
1966
2440
|
const desiredPrefix = (column) => {
|
|
1967
|
-
const
|
|
1968
|
-
return
|
|
2441
|
+
const field = fields.get(column);
|
|
2442
|
+
return field && this._indexPrefix(field);
|
|
1969
2443
|
};
|
|
1970
2444
|
const renderPart = (column, prefix) => prefix === void 0 ? column : `${column}(${prefix})`;
|
|
1971
2445
|
return this.syncIndexesWithDiff({
|
|
@@ -2017,11 +2491,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
2017
2491
|
this._log(sql);
|
|
2018
2492
|
await this._exec().exec(sql);
|
|
2019
2493
|
},
|
|
2020
|
-
dropIndex:
|
|
2021
|
-
const sql = `DROP INDEX ${qi(name)} ON ${quoteTableName(this.resolveTableName())}`;
|
|
2022
|
-
this._log(sql);
|
|
2023
|
-
await this._exec().exec(sql);
|
|
2024
|
-
}
|
|
2494
|
+
dropIndex: (name) => this._dropIndex(name)
|
|
2025
2495
|
});
|
|
2026
2496
|
}
|
|
2027
2497
|
async syncForeignKeys() {
|
|
@@ -2032,10 +2502,11 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
2032
2502
|
return this._syncForeignKeys(true);
|
|
2033
2503
|
}
|
|
2034
2504
|
_syncForeignKeys(dryRun) {
|
|
2505
|
+
let existing;
|
|
2035
2506
|
return this.syncForeignKeysWithDiff({
|
|
2036
2507
|
dryRun,
|
|
2037
|
-
listExisting: () => this._getExistingFkConstraints(),
|
|
2038
|
-
dropConstraint: (name) => this._dropFkConstraint(name),
|
|
2508
|
+
listExisting: async () => existing = await this._getExistingFkConstraints(),
|
|
2509
|
+
dropConstraint: (name) => this._dropFkConstraint(name, existing.get(name)),
|
|
2039
2510
|
addConstraint: async (fk) => {
|
|
2040
2511
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${foreignKeySql(qi, fk)}`;
|
|
2041
2512
|
this._log(ddl);
|
|
@@ -2046,19 +2517,40 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
2046
2517
|
async dropForeignKeys(fkFieldKeys) {
|
|
2047
2518
|
if (fkFieldKeys.length === 0) return;
|
|
2048
2519
|
const keySet = new Set(fkFieldKeys);
|
|
2049
|
-
for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has(fkKey(cols))) await this._dropFkConstraint(constraintName);
|
|
2520
|
+
for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has(fkKey(cols))) await this._dropFkConstraint(constraintName, cols);
|
|
2050
2521
|
}
|
|
2051
|
-
|
|
2522
|
+
/**
|
|
2523
|
+
* Drops a foreign-key constraint over `columns` (in key order), then the
|
|
2524
|
+
* index InnoDB created for it: an unnamed constraint gets an index named
|
|
2525
|
+
* after its first column, which outlives the constraint — and would block
|
|
2526
|
+
* that column from becoming TEXT once it no longer references anything
|
|
2527
|
+
* (ER 1170). An index another constraint still needs is kept (ER 1553).
|
|
2528
|
+
*/
|
|
2529
|
+
async _dropFkConstraint(name, columns) {
|
|
2052
2530
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(name)}`;
|
|
2053
2531
|
this._log(ddl);
|
|
2054
2532
|
await this._exec().exec(ddl);
|
|
2533
|
+
if (columns.length === 0) return;
|
|
2534
|
+
if ((await this._exec().all(`SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.STATISTICS
|
|
2535
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE()) AND INDEX_NAME = ?
|
|
2536
|
+
ORDER BY SEQ_IN_INDEX`, [
|
|
2537
|
+
this._table.tableName,
|
|
2538
|
+
this._schema,
|
|
2539
|
+
columns[0]
|
|
2540
|
+
])).map((r) => r.COLUMN_NAME).join("\0") !== columns.join("\0")) return;
|
|
2541
|
+
try {
|
|
2542
|
+
await this._dropIndex(columns[0]);
|
|
2543
|
+
} catch (error) {
|
|
2544
|
+
if (error.errno !== 1553) throw error;
|
|
2545
|
+
}
|
|
2055
2546
|
}
|
|
2056
2547
|
/** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */
|
|
2057
2548
|
async _getExistingFkConstraints() {
|
|
2058
2549
|
const rows = await this._exec().all(`SELECT kcu.CONSTRAINT_NAME, kcu.COLUMN_NAME
|
|
2059
2550
|
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
2060
2551
|
WHERE kcu.TABLE_NAME = ? AND kcu.TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
2061
|
-
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
2552
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
2553
|
+
ORDER BY kcu.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, [this._table.tableName, this._schema]);
|
|
2062
2554
|
const byName = /* @__PURE__ */ new Map();
|
|
2063
2555
|
for (const row of rows) {
|
|
2064
2556
|
let cols = byName.get(row.CONSTRAINT_NAME);
|