@atscript/db-mysql 0.1.169 → 0.1.171
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 +517 -61
- package/dist/index.d.cts +83 -0
- package/dist/index.d.mts +83 -0
- package/dist/index.mjs +517 -61
- package/dist/plugin.cjs +112 -23
- package/dist/plugin.mjs +112 -23
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_annotation_values = require("./annotation-values-DS1rF0tw.cjs");
|
|
2
3
|
let _atscript_db = require("@atscript/db");
|
|
3
4
|
let _atscript_db_agg = require("@atscript/db/agg");
|
|
4
5
|
let _atscript_db_sql_tools = require("@atscript/db-sql-tools");
|
|
@@ -110,7 +111,8 @@ function mysqlIndexPrefix(mappedType, bytesPerChar) {
|
|
|
110
111
|
* default of a nullable column, and `NOT NULL DEFAULT NULL` is ER 1067);
|
|
111
112
|
* expression form for TEXT/BLOB/JSON/GEOMETRY; invented type default for a
|
|
112
113
|
* required default-less column on `add`;
|
|
113
|
-
* 5. COLLATE (native `@db.mysql.collate` or portable `@db.column.collate
|
|
114
|
+
* 5. COLLATE (native `@db.mysql.collate` or portable `@db.column.collate`;
|
|
115
|
+
* a foreign-key column's is the referenced column's);
|
|
114
116
|
* 6. ON UPDATE.
|
|
115
117
|
*
|
|
116
118
|
* A `@db.column.derived` field (since 0.1.141) renders as a VIRTUAL generated
|
|
@@ -119,12 +121,13 @@ function mysqlIndexPrefix(mappedType, bytesPerChar) {
|
|
|
119
121
|
*/
|
|
120
122
|
function buildColumnDefinition(field, ctx) {
|
|
121
123
|
const sqlType = ctx.typeMapper?.(field) ?? mysqlTypeFromField(field);
|
|
124
|
+
const { charsetClause, collateClause } = mysqlColumnCharsetClauses(field, sqlType, ctx);
|
|
122
125
|
if (field.derived) return {
|
|
123
|
-
def: `${qi(field.physicalName)} ${sqlType}${
|
|
126
|
+
def: `${qi(field.physicalName)} ${sqlType}${charsetClause}${collateClause} GENERATED ALWAYS AS (${(0, _atscript_db_sql_tools.derivedColumnExpr)(mysqlDialect, field)}) VIRTUAL`,
|
|
124
127
|
inventedDefault: false
|
|
125
128
|
};
|
|
126
129
|
const increment = ctx.purpose !== "add" && (ctx.incrementFields?.has(field.path) ?? false);
|
|
127
|
-
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
130
|
+
let def = `${qi(field.physicalName)} ${sqlType}${charsetClause}`;
|
|
128
131
|
if (increment) def += " AUTO_INCREMENT";
|
|
129
132
|
if (ctx.purpose === "create") {
|
|
130
133
|
if (!field.optional && !field.isPrimaryKey && !increment) def += " NOT NULL";
|
|
@@ -138,14 +141,53 @@ function buildColumnDefinition(field, ctx) {
|
|
|
138
141
|
def += ` DEFAULT ${mysqlTypeDefault(sqlType, field)}`;
|
|
139
142
|
inventedDefault = true;
|
|
140
143
|
}
|
|
141
|
-
def +=
|
|
142
|
-
const onUpdate = ctx.onUpdateFields?.get(field.
|
|
143
|
-
if (onUpdate) def += ` ON UPDATE ${
|
|
144
|
+
def += collateClause;
|
|
145
|
+
const onUpdate = ctx.onUpdateFields?.get(field.path);
|
|
146
|
+
if (onUpdate) def += ` ON UPDATE ${require_annotation_values.mysqlOnUpdateExpr(onUpdate, currentTimestampFor(sqlType))}`;
|
|
144
147
|
return {
|
|
145
148
|
def,
|
|
146
149
|
inventedDefault
|
|
147
150
|
};
|
|
148
151
|
}
|
|
152
|
+
/** Whether a column declares its own collation (`@db.mysql.collate` or `@db.column.collate`). */
|
|
153
|
+
function declaresCollation(field) {
|
|
154
|
+
return field.collate !== void 0 || (field.type?.metadata?.has("db.mysql.collate") ?? false);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The character set a column declares (`undefined`: the table's). A
|
|
158
|
+
* foreign-key column takes the referenced column's, as MySQL requires of a
|
|
159
|
+
* string foreign key: the referenced field's own `@db.mysql.charset`, else
|
|
160
|
+
* its table's (`ctx.keyTableCharsets`); its own declarations are not used.
|
|
161
|
+
*/
|
|
162
|
+
function mysqlColumnCharset(field, ctx) {
|
|
163
|
+
const keyTarget = field.fkTargetField;
|
|
164
|
+
if (!keyTarget) return ctx.charsetFields?.get(field.path);
|
|
165
|
+
return keyTarget.type?.metadata?.get("db.mysql.charset") ?? ctx.keyTableCharsets?.get(field.path)?.charset;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The ` CHARACTER SET …` and ` COLLATE …` of a column (see
|
|
169
|
+
* {@link mysqlColumnCharset}); a foreign-key column's collation is the
|
|
170
|
+
* referenced field's own, else its table's.
|
|
171
|
+
*/
|
|
172
|
+
function mysqlColumnCharsetClauses(field, sqlType, ctx) {
|
|
173
|
+
const charset = mysqlColumnCharset(field, ctx);
|
|
174
|
+
const charsetClause = mysqlCharsetClause(sqlType, charset);
|
|
175
|
+
const keyTarget = field.fkTargetField;
|
|
176
|
+
if (!keyTarget) return {
|
|
177
|
+
charsetClause,
|
|
178
|
+
collateClause: mysqlCollateClause(field, charset)
|
|
179
|
+
};
|
|
180
|
+
const ownCharset = keyTarget.type?.metadata?.get("db.mysql.charset");
|
|
181
|
+
if (declaresCollation(keyTarget)) return {
|
|
182
|
+
charsetClause,
|
|
183
|
+
collateClause: mysqlCollateClause(keyTarget, ownCharset)
|
|
184
|
+
};
|
|
185
|
+
const tableCollation = ownCharset === void 0 ? ctx.keyTableCharsets?.get(field.path)?.collation : void 0;
|
|
186
|
+
return {
|
|
187
|
+
charsetClause,
|
|
188
|
+
collateClause: tableCollation && CHARACTER_TYPE_RE.test(sqlType) ? ` COLLATE ${require_annotation_values.mysqlName("@db.mysql.collate", tableCollation)}` : ""
|
|
189
|
+
};
|
|
190
|
+
}
|
|
149
191
|
/** Escapes a MySQL identifier by doubling backticks. */
|
|
150
192
|
function esc(name) {
|
|
151
193
|
return name.replace(/`/g, "``");
|
|
@@ -433,18 +475,39 @@ function buildAggregateSelect(table, where, controls) {
|
|
|
433
475
|
function buildAggregateCount(table, where, controls) {
|
|
434
476
|
return (0, _atscript_db_sql_tools.buildAggregateCount)(mysqlDialect, table, where, controls);
|
|
435
477
|
}
|
|
478
|
+
/** Character-typed columns (the ones a `CHARACTER SET` applies to). */
|
|
479
|
+
const CHARACTER_TYPE_RE = /^(VAR)?CHAR\b|^(TINY|MEDIUM|LONG)?TEXT\b|^(ENUM|SET)\b/i;
|
|
436
480
|
/**
|
|
437
|
-
*
|
|
481
|
+
* ` CHARACTER SET <charset>` for a character-typed column with a field-level
|
|
482
|
+
* `@db.mysql.charset`, else `""`.
|
|
438
483
|
*/
|
|
484
|
+
function mysqlCharsetClause(sqlType, charset) {
|
|
485
|
+
return charset && CHARACTER_TYPE_RE.test(sqlType) ? ` CHARACTER SET ${require_annotation_values.mysqlName("@db.mysql.charset", charset)}` : "";
|
|
486
|
+
}
|
|
439
487
|
/**
|
|
440
|
-
* The `COLLATE
|
|
441
|
-
*
|
|
488
|
+
* The column's ` COLLATE …`: a native `@db.mysql.collate`, else the portable
|
|
489
|
+
* `@db.column.collate` — in the column's `charset` when it sets one
|
|
490
|
+
* (`nocase` → `<charset>_general_ci`, `binary` → `<charset>_bin`,
|
|
491
|
+
* `unicode` → `<charset>_unicode_ci`), else in `utf8mb4`.
|
|
442
492
|
*/
|
|
443
|
-
function mysqlCollateClause(field) {
|
|
493
|
+
function mysqlCollateClause(field, charset) {
|
|
444
494
|
const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
|
|
445
495
|
if (nativeCollate) return ` COLLATE ${nativeCollate}`;
|
|
446
|
-
|
|
496
|
+
if (!field.collate) return "";
|
|
497
|
+
const collation = collationToMysql(field.collate);
|
|
498
|
+
return ` COLLATE ${charset ? collation.replace(/^utf8mb4_/, `${charset}_`) : collation}`;
|
|
447
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* The collation a table gets when only its character set is declared:
|
|
502
|
+
* `utf8mb4_unicode_ci` for `utf8mb4` (the adapter's default), else none —
|
|
503
|
+
* MySQL then applies the set's own default collation.
|
|
504
|
+
*/
|
|
505
|
+
function mysqlDefaultCollation(charset) {
|
|
506
|
+
return charset.toLowerCase() === "utf8mb4" ? "utf8mb4_unicode_ci" : void 0;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Maps portable collation values to MySQL collation names.
|
|
510
|
+
*/
|
|
448
511
|
function collationToMysql(collation) {
|
|
449
512
|
switch (collation) {
|
|
450
513
|
case "binary": return "utf8mb4_bin";
|
|
@@ -453,15 +516,6 @@ function collationToMysql(collation) {
|
|
|
453
516
|
default: return "utf8mb4_unicode_ci";
|
|
454
517
|
}
|
|
455
518
|
}
|
|
456
|
-
/**
|
|
457
|
-
* Maps an Atscript field descriptor to a MySQL column type.
|
|
458
|
-
*
|
|
459
|
-
* Reads `designType`, primitive tags (via `type.type.tags`), and annotations
|
|
460
|
-
* from field metadata to produce the most specific MySQL type.
|
|
461
|
-
*
|
|
462
|
-
* For FK fields, delegates to the target PK's type via `field.fkTargetField`
|
|
463
|
-
* so the FK column type always matches the referenced column.
|
|
464
|
-
*/
|
|
465
519
|
/** Maps integer primitive tags to MySQL integer types. */
|
|
466
520
|
function intTypeFromTags(tags, unsigned) {
|
|
467
521
|
if (tags?.has("int8")) return unsigned ? "TINYINT UNSIGNED" : "TINYINT";
|
|
@@ -474,8 +528,19 @@ function intTypeFromTags(tags, unsigned) {
|
|
|
474
528
|
if (tags?.has("uint64")) return "BIGINT UNSIGNED";
|
|
475
529
|
return unsigned ? "INT UNSIGNED" : "INT";
|
|
476
530
|
}
|
|
477
|
-
|
|
478
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Maps an Atscript field descriptor to a MySQL column type.
|
|
533
|
+
*
|
|
534
|
+
* Reads `designType`, primitive tags (via `type.type.tags`), and annotations
|
|
535
|
+
* from field metadata to produce the most specific MySQL type.
|
|
536
|
+
*
|
|
537
|
+
* `keyed` marks a column a foreign key references (the adapter knows it
|
|
538
|
+
* from its space); a referencing column takes its type through
|
|
539
|
+
* `field.fkTargetField`. A keyed string is never TEXT: a foreign key needs
|
|
540
|
+
* both sides indexed in full, and MySQL indexes TEXT only by a prefix.
|
|
541
|
+
*/
|
|
542
|
+
function mysqlTypeFromField(field, keyed = false) {
|
|
543
|
+
if (field.fkTargetField) return mysqlTypeFromField(field.fkTargetField, true);
|
|
479
544
|
const tags = field.type?.type?.tags;
|
|
480
545
|
const metadata = field.type?.metadata;
|
|
481
546
|
const mysqlTypeOverride = metadata?.get("db.mysql.type");
|
|
@@ -500,14 +565,15 @@ function mysqlTypeFromField(field) {
|
|
|
500
565
|
const maxLen = (metadata?.get("expect.maxLength"))?.length;
|
|
501
566
|
if (maxLen !== void 0 && maxLen <= 65535) return `VARCHAR(${maxLen})`;
|
|
502
567
|
if (maxLen !== void 0 && maxLen > 65535) return "LONGTEXT";
|
|
503
|
-
if (field.
|
|
568
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "uuid") return "CHAR(36)";
|
|
569
|
+
if (keyed || field.isPrimaryKey || field.defaultValue || field.derived) return "VARCHAR(255)";
|
|
504
570
|
return "TEXT";
|
|
505
571
|
}
|
|
506
572
|
case "json":
|
|
507
573
|
case "object":
|
|
508
574
|
case "array": return "JSON";
|
|
509
575
|
default:
|
|
510
|
-
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
576
|
+
if (keyed || field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
511
577
|
return "TEXT";
|
|
512
578
|
}
|
|
513
579
|
}
|
|
@@ -519,6 +585,8 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
519
585
|
const ctx = {
|
|
520
586
|
incrementFields: options?.incrementFields,
|
|
521
587
|
onUpdateFields: options?.onUpdateFields,
|
|
588
|
+
charsetFields: options?.charsetFields,
|
|
589
|
+
keyTableCharsets: options?.keyTableCharsets,
|
|
522
590
|
typeMapper: options?.typeMapper,
|
|
523
591
|
purpose: "create"
|
|
524
592
|
};
|
|
@@ -542,8 +610,9 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
542
610
|
let sql = `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${body})`;
|
|
543
611
|
const engine = options?.engine ?? "InnoDB";
|
|
544
612
|
const charset = options?.charset ?? "utf8mb4";
|
|
545
|
-
const collation = options?.collation ??
|
|
546
|
-
sql += ` ENGINE=${engine} DEFAULT CHARSET=${charset}
|
|
613
|
+
const collation = options?.collation ?? mysqlDefaultCollation(charset);
|
|
614
|
+
sql += ` ENGINE=${engine} DEFAULT CHARSET=${charset}`;
|
|
615
|
+
if (collation) sql += ` COLLATE=${collation}`;
|
|
547
616
|
if (options?.autoIncrementStart !== void 0) sql += ` AUTO_INCREMENT=${options.autoIncrementStart}`;
|
|
548
617
|
return sql;
|
|
549
618
|
}
|
|
@@ -566,6 +635,14 @@ function buildWhere(filter, opts) {
|
|
|
566
635
|
}
|
|
567
636
|
//#endregion
|
|
568
637
|
//#region src/mysql-adapter.ts
|
|
638
|
+
/** A column's own `@db.mysql.charset`, `@db.mysql.collate` and `@db.column.collate` (lowercased, `""` when absent). */
|
|
639
|
+
function declaredCharsetCollation(field) {
|
|
640
|
+
return [
|
|
641
|
+
field.type?.metadata?.get("db.mysql.charset"),
|
|
642
|
+
field.type?.metadata?.get("db.mysql.collate"),
|
|
643
|
+
field.collate
|
|
644
|
+
].map((v) => typeof v === "string" ? v.toLowerCase() : "");
|
|
645
|
+
}
|
|
569
646
|
/**
|
|
570
647
|
* Parses a MySQL UTC datetime string (`'YYYY-MM-DD HH:MM:SS[.ffffff]'`) to
|
|
571
648
|
* epoch ms — fractional seconds included (truncated to milliseconds; since
|
|
@@ -628,6 +705,33 @@ const nonStrictChecked = /* @__PURE__ */ new WeakSet();
|
|
|
628
705
|
* loading the time zone tables fixes a running server without a restart.
|
|
629
706
|
*/
|
|
630
707
|
const convertibleZones = /* @__PURE__ */ new WeakMap();
|
|
708
|
+
/** The `ALTER TABLE` that re-creates `fks`, all on `table`, as they were. */
|
|
709
|
+
function addForeignKeysSql(table, fks) {
|
|
710
|
+
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(", ")}`;
|
|
711
|
+
}
|
|
712
|
+
/** A map key of a live column. */
|
|
713
|
+
const columnKey = (c) => `${c.schema}\0${c.table}\0${c.column}`;
|
|
714
|
+
/** Which ends of `fk`'s `i`-th column pair are among `columns` (of the adapter's table). */
|
|
715
|
+
function touchedEnds(fk, i, columns) {
|
|
716
|
+
return {
|
|
717
|
+
child: fk.own && columns.has(fk.columns[i]),
|
|
718
|
+
parent: fk.ref && columns.has(fk.refColumns[i])
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const INTEGER_TYPE_RE = /^(tinyint|smallint|mediumint|int|integer|bigint)(?:\(\d+\))?(\s+unsigned)?(?:\s+zerofill)?$/;
|
|
722
|
+
const CHAR_TYPE_RE = /^(?:var)?char\(\d+\)$/i;
|
|
723
|
+
/**
|
|
724
|
+
* What the two sides of a foreign key must share: an integer's type and
|
|
725
|
+
* signedness (display width aside), a `CHAR` / `VARCHAR`'s collation (any
|
|
726
|
+
* length), anything else the type.
|
|
727
|
+
*/
|
|
728
|
+
function fkTypeKey(column) {
|
|
729
|
+
const type = column.type.trim().toLowerCase().replace(/\s+/g, " ");
|
|
730
|
+
const int = INTEGER_TYPE_RE.exec(type);
|
|
731
|
+
if (int) return `int:${int[1] === "integer" ? "int" : int[1]}:${!!int[2]}`;
|
|
732
|
+
if (CHAR_TYPE_RE.test(type)) return `char:${(column.collation ?? "").toLowerCase()}`;
|
|
733
|
+
return type;
|
|
734
|
+
}
|
|
631
735
|
/**
|
|
632
736
|
* MySQL adapter for {@link AtscriptDbTable}.
|
|
633
737
|
*
|
|
@@ -650,7 +754,8 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
650
754
|
static NATIVE_DEFAULT_FNS = new Set(["now", "increment"]);
|
|
651
755
|
_engine = "InnoDB";
|
|
652
756
|
_charset = "utf8mb4";
|
|
653
|
-
|
|
757
|
+
/** `@db.mysql.collate` on the table; none → the charset's default (see `mysqlDefaultCollation`). */
|
|
758
|
+
_collation;
|
|
654
759
|
_autoIncrementStart;
|
|
655
760
|
_incrementFields = /* @__PURE__ */ new Set();
|
|
656
761
|
/**
|
|
@@ -666,6 +771,11 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
666
771
|
*/
|
|
667
772
|
_epochAfterRekey = /* @__PURE__ */ new Map();
|
|
668
773
|
_onUpdateFields = /* @__PURE__ */ new Map();
|
|
774
|
+
/** Field path → `@db.mysql.charset` of the column. */
|
|
775
|
+
_charsetFields = /* @__PURE__ */ new Map();
|
|
776
|
+
_space;
|
|
777
|
+
/** Memo of {@link _keyTableCharsets} — it also makes its warning fire once. */
|
|
778
|
+
_keyTableCharsetsMemo;
|
|
669
779
|
/** Whether the connected MySQL instance supports native VECTOR type (MySQL 9.0+). */
|
|
670
780
|
_supportsVector;
|
|
671
781
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -849,11 +959,11 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
849
959
|
onBeforeFlatten(_type) {
|
|
850
960
|
const meta = _type.metadata;
|
|
851
961
|
const engine = meta.get("db.mysql.engine");
|
|
852
|
-
if (engine) this._engine = engine;
|
|
962
|
+
if (engine) this._engine = require_annotation_values.mysqlName("@db.mysql.engine", engine);
|
|
853
963
|
const charset = meta.get("db.mysql.charset");
|
|
854
|
-
if (charset) this._charset = charset;
|
|
964
|
+
if (charset) this._charset = require_annotation_values.mysqlName("@db.mysql.charset", charset);
|
|
855
965
|
const collate = meta.get("db.mysql.collate");
|
|
856
|
-
if (collate) this._collation = collate;
|
|
966
|
+
if (collate) this._collation = require_annotation_values.mysqlName("@db.mysql.collate", collate);
|
|
857
967
|
}
|
|
858
968
|
onFieldScanned(field, _type, metadata) {
|
|
859
969
|
if (metadata.has("db.default.increment")) {
|
|
@@ -863,6 +973,8 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
863
973
|
}
|
|
864
974
|
const onUpdate = metadata.get("db.mysql.onUpdate");
|
|
865
975
|
if (onUpdate) this._onUpdateFields.set(field, onUpdate);
|
|
976
|
+
const charset = metadata.get("db.mysql.charset");
|
|
977
|
+
if (charset) this._charsetFields.set(field, require_annotation_values.mysqlName("@db.mysql.charset", charset));
|
|
866
978
|
const vectorMeta = metadata.get("db.search.vector");
|
|
867
979
|
if (vectorMeta) {
|
|
868
980
|
const indexName = vectorMeta.indexName || field;
|
|
@@ -876,6 +988,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
876
988
|
}
|
|
877
989
|
}
|
|
878
990
|
getDesiredTableOptions() {
|
|
991
|
+
const collation = this._tableCollation();
|
|
879
992
|
return [
|
|
880
993
|
{
|
|
881
994
|
key: "engine",
|
|
@@ -885,10 +998,10 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
885
998
|
key: "charset",
|
|
886
999
|
value: this._charset
|
|
887
1000
|
},
|
|
888
|
-
{
|
|
1001
|
+
...collation === void 0 ? [] : [{
|
|
889
1002
|
key: "collation",
|
|
890
|
-
value:
|
|
891
|
-
}
|
|
1003
|
+
value: collation
|
|
1004
|
+
}]
|
|
892
1005
|
];
|
|
893
1006
|
}
|
|
894
1007
|
async getExistingTableOptions(tableName) {
|
|
@@ -897,18 +1010,23 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
897
1010
|
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())`, [tableName ?? this._table.tableName, this._schema]);
|
|
898
1011
|
if (!row) return [];
|
|
899
1012
|
const charset = row.TABLE_COLLATION?.split("_")[0] ?? "utf8mb4";
|
|
1013
|
+
const desired = new Map(this.getDesiredTableOptions().map((o) => [o.key, o.value]));
|
|
1014
|
+
const asDesired = (key, value) => {
|
|
1015
|
+
const want = desired.get(key);
|
|
1016
|
+
return want !== void 0 && want.toLowerCase() === value.toLowerCase() ? want : value;
|
|
1017
|
+
};
|
|
900
1018
|
return [
|
|
901
1019
|
{
|
|
902
1020
|
key: "engine",
|
|
903
|
-
value: row.ENGINE ?? "InnoDB"
|
|
1021
|
+
value: asDesired("engine", row.ENGINE ?? "InnoDB")
|
|
904
1022
|
},
|
|
905
1023
|
{
|
|
906
1024
|
key: "charset",
|
|
907
|
-
value: charset
|
|
1025
|
+
value: asDesired("charset", charset)
|
|
908
1026
|
},
|
|
909
1027
|
{
|
|
910
1028
|
key: "collation",
|
|
911
|
-
value: row.TABLE_COLLATION ?? "utf8mb4_unicode_ci"
|
|
1029
|
+
value: asDesired("collation", row.TABLE_COLLATION ?? "utf8mb4_unicode_ci")
|
|
912
1030
|
}
|
|
913
1031
|
];
|
|
914
1032
|
}
|
|
@@ -1633,15 +1751,64 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1633
1751
|
this._log(sql);
|
|
1634
1752
|
await this._exec().exec(sql);
|
|
1635
1753
|
}
|
|
1754
|
+
/** The table's collation: `@db.mysql.collate`, else the charset's default one (`utf8mb4_unicode_ci` for `utf8mb4`). */
|
|
1755
|
+
_tableCollation() {
|
|
1756
|
+
return this._collation ?? mysqlDefaultCollation(this._charset);
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Foreign-key column path → the referenced table's character set and
|
|
1760
|
+
* collation, where they differ from this table's (see
|
|
1761
|
+
* `mysqlColumnCharset`). Warns about a foreign-key column whose own
|
|
1762
|
+
* `@db.mysql.charset` / collation is replaced by the referenced column's.
|
|
1763
|
+
*/
|
|
1764
|
+
_keyTableCharsets() {
|
|
1765
|
+
return this._keyTableCharsetsMemo ??= this._buildKeyTableCharsets();
|
|
1766
|
+
}
|
|
1767
|
+
_buildKeyTableCharsets() {
|
|
1768
|
+
const out = /* @__PURE__ */ new Map();
|
|
1769
|
+
const own = `${this._charset}\0${this._tableCollation() ?? ""}`.toLowerCase();
|
|
1770
|
+
for (const fk of this._table.foreignKeys.values()) {
|
|
1771
|
+
const meta = fk.targetTypeRef?.()?.metadata;
|
|
1772
|
+
const declaredCollation = meta?.get("db.mysql.collate");
|
|
1773
|
+
const charset = meta?.get("db.mysql.charset") ?? declaredCollation?.split("_")[0] ?? "utf8mb4";
|
|
1774
|
+
const collation = declaredCollation ?? mysqlDefaultCollation(charset);
|
|
1775
|
+
if (`${charset}\0${collation ?? ""}`.toLowerCase() === own) continue;
|
|
1776
|
+
for (const path of fk.fields) out.set(path, {
|
|
1777
|
+
charset,
|
|
1778
|
+
collation
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
for (const field of this._table.fieldDescriptors) {
|
|
1782
|
+
const target = field.fkTargetField;
|
|
1783
|
+
if (!target) continue;
|
|
1784
|
+
const mine = declaredCharsetCollation(field);
|
|
1785
|
+
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`);
|
|
1786
|
+
}
|
|
1787
|
+
return out;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* The key-length prefix an index needs on `field`'s column: a function of
|
|
1791
|
+
* its MAPPED type and its character set (`mysqlColumnCharset`, else the
|
|
1792
|
+
* table's).
|
|
1793
|
+
*/
|
|
1794
|
+
_indexPrefix(field) {
|
|
1795
|
+
const charset = mysqlColumnCharset(field, {
|
|
1796
|
+
charsetFields: this._charsetFields,
|
|
1797
|
+
keyTableCharsets: this._keyTableCharsets()
|
|
1798
|
+
});
|
|
1799
|
+
return mysqlIndexPrefix(this.typeMapper(field), mysqlBytesPerChar(charset ?? this._charset));
|
|
1800
|
+
}
|
|
1636
1801
|
/** The CREATE TABLE options (engine/charset/collation/increment/ON UPDATE/type mapper). */
|
|
1637
1802
|
_tableOptions(opts) {
|
|
1638
1803
|
return {
|
|
1639
1804
|
engine: this._engine,
|
|
1640
1805
|
charset: this._charset,
|
|
1641
|
-
collation: this.
|
|
1806
|
+
collation: this._tableCollation(),
|
|
1642
1807
|
autoIncrementStart: this._autoIncrementStart,
|
|
1643
1808
|
incrementFields: this._incrementFields,
|
|
1644
1809
|
onUpdateFields: this._onUpdateFields,
|
|
1810
|
+
charsetFields: this._charsetFields,
|
|
1811
|
+
keyTableCharsets: this._keyTableCharsets(),
|
|
1645
1812
|
typeMapper: (field) => this.typeMapper(field),
|
|
1646
1813
|
deferForeignKeysTo: opts?.deferForeignKeysTo
|
|
1647
1814
|
};
|
|
@@ -1651,6 +1818,8 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1651
1818
|
return {
|
|
1652
1819
|
incrementFields: this._incrementFields,
|
|
1653
1820
|
onUpdateFields: this._onUpdateFields,
|
|
1821
|
+
charsetFields: this._charsetFields,
|
|
1822
|
+
keyTableCharsets: this._keyTableCharsets(),
|
|
1654
1823
|
typeMapper: (field) => this.typeMapper(field),
|
|
1655
1824
|
purpose
|
|
1656
1825
|
};
|
|
@@ -1835,6 +2004,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1835
2004
|
return entering.has(field.physicalName);
|
|
1836
2005
|
};
|
|
1837
2006
|
const modified = /* @__PURE__ */ new Map();
|
|
2007
|
+
const retyped = [];
|
|
1838
2008
|
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1839
2009
|
const epoch = epochSource(field, existingType);
|
|
1840
2010
|
if (epoch) {
|
|
@@ -1852,6 +2022,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1852
2022
|
continue;
|
|
1853
2023
|
}
|
|
1854
2024
|
modified.set(field.physicalName, field);
|
|
2025
|
+
retyped.push(field);
|
|
1855
2026
|
}
|
|
1856
2027
|
for (const { field } of diff.nullableChanged ?? []) {
|
|
1857
2028
|
if (rekeyed(field)) continue;
|
|
@@ -1865,17 +2036,285 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1865
2036
|
modified.set(field.physicalName, field);
|
|
1866
2037
|
}
|
|
1867
2038
|
for (const { field } of diff.defaultChanged ?? []) if (!rekeyed(field)) modified.set(field.physicalName, field);
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2039
|
+
await this._dropIndexesOutgrown(conn, [...modified.values()]);
|
|
2040
|
+
await this._withForeignKeysLifted(conn, retyped, async () => {
|
|
2041
|
+
for (const field of modified.values()) {
|
|
2042
|
+
const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
|
|
2043
|
+
this._log(ddl);
|
|
2044
|
+
await conn.exec(ddl);
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
1873
2047
|
return {
|
|
1874
2048
|
added,
|
|
1875
2049
|
renamed
|
|
1876
2050
|
};
|
|
1877
2051
|
});
|
|
1878
2052
|
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Runs `fn` (the MODIFYs of `fields`) with the live foreign keys over those
|
|
2055
|
+
* columns dropped — this table's own and those of any table referencing
|
|
2056
|
+
* them, its own included — and re-creates them, as they were, after: also
|
|
2057
|
+
* when `fn` throws. Nothing is dropped when a constraint could not be
|
|
2058
|
+
* re-created over the new types (see {@link _liftable}): MySQL then refuses
|
|
2059
|
+
* the MODIFY as it always did. A constraint that cannot be re-created is
|
|
2060
|
+
* named, with its DDL, in the error (after `fn`'s own).
|
|
2061
|
+
*/
|
|
2062
|
+
async _withForeignKeysLifted(conn, fields, fn) {
|
|
2063
|
+
if (fields.length === 0) return fn();
|
|
2064
|
+
const found = await this._foreignKeysOver(conn, fields.map((f) => f.physicalName));
|
|
2065
|
+
const lifted = found.length > 0 && await this._liftable(conn, found, fields) ? found : [];
|
|
2066
|
+
const dropped = [];
|
|
2067
|
+
let failure;
|
|
2068
|
+
try {
|
|
2069
|
+
for (const fk of lifted) {
|
|
2070
|
+
const ddl = `ALTER TABLE ${fk.table} DROP FOREIGN KEY ${qi(fk.name)}`;
|
|
2071
|
+
this._log(ddl);
|
|
2072
|
+
await conn.exec(ddl);
|
|
2073
|
+
dropped.push(fk);
|
|
2074
|
+
this.logger.info(`[mysql] foreign key ${qi(fk.name)} on ${fk.table} dropped for the type change of ${quoteTableName(this.resolveTableName())}`);
|
|
2075
|
+
}
|
|
2076
|
+
await fn();
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
failure = error;
|
|
2079
|
+
}
|
|
2080
|
+
const lost = await this._restoreForeignKeys(conn, dropped);
|
|
2081
|
+
if (lost.length > 0) {
|
|
2082
|
+
const named = lost.map((fk) => `${qi(fk.name)} on ${fk.table} (re-create it with: ${addForeignKeysSql(fk.table, [fk])})`).join("; ");
|
|
2083
|
+
const message = failure ? `${failure.message} — and foreign keys not restored: ${named}` : `Foreign keys not restored after the type change: ${named}`;
|
|
2084
|
+
throw new Error(message, { cause: failure });
|
|
2085
|
+
}
|
|
2086
|
+
if (failure) throw failure;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Re-creates `fks` (dropped by {@link _withForeignKeysLifted}) — one
|
|
2090
|
+
* `ALTER TABLE` per table, with `foreign_key_checks` off on this connection
|
|
2091
|
+
* so MySQL adds them in place instead of rebuilding the table (the rows
|
|
2092
|
+
* met them before the change). A table whose statement fails has its
|
|
2093
|
+
* constraints re-tried one by one. Returns those that could not be
|
|
2094
|
+
* re-created.
|
|
2095
|
+
*/
|
|
2096
|
+
async _restoreForeignKeys(conn, fks) {
|
|
2097
|
+
if (fks.length === 0) return [];
|
|
2098
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
2099
|
+
for (const fk of fks) {
|
|
2100
|
+
const group = byTable.get(fk.table);
|
|
2101
|
+
if (group) group.push(fk);
|
|
2102
|
+
else byTable.set(fk.table, [fk]);
|
|
2103
|
+
}
|
|
2104
|
+
const add = async (table, group) => {
|
|
2105
|
+
const ddl = addForeignKeysSql(table, group);
|
|
2106
|
+
this._log(ddl);
|
|
2107
|
+
try {
|
|
2108
|
+
await conn.exec(ddl);
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
return error;
|
|
2111
|
+
}
|
|
2112
|
+
for (const fk of group) this.logger.info(`[mysql] foreign key ${qi(fk.name)} on ${fk.table} restored`);
|
|
2113
|
+
};
|
|
2114
|
+
const lost = [];
|
|
2115
|
+
await conn.exec("SET @atscript_fk_checks = @@SESSION.foreign_key_checks, SESSION foreign_key_checks = 0");
|
|
2116
|
+
try {
|
|
2117
|
+
for (const [table, group] of byTable) {
|
|
2118
|
+
const error = await add(table, group);
|
|
2119
|
+
if (!error) continue;
|
|
2120
|
+
for (const fk of group) {
|
|
2121
|
+
const single = group.length === 1 ? error : await add(table, [fk]);
|
|
2122
|
+
if (single) {
|
|
2123
|
+
this.logger.error(`[mysql] foreign key ${qi(fk.name)} on ${table} could not be re-created: ${single.message}`);
|
|
2124
|
+
lost.push(fk);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
} finally {
|
|
2129
|
+
await conn.exec("SET SESSION foreign_key_checks = @atscript_fk_checks");
|
|
2130
|
+
}
|
|
2131
|
+
return lost;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Whether every constraint of `fks` can be re-created once `fields` have
|
|
2135
|
+
* their new types: each column pair of a constraint that the change touches
|
|
2136
|
+
* stays compatible — the new type against the other side's live column (or
|
|
2137
|
+
* its new type, on this table): integers of the same type and signedness,
|
|
2138
|
+
* `CHAR` / `VARCHAR` either way with the same collation, anything else the
|
|
2139
|
+
* same type. Otherwise nothing is lifted (and MySQL refuses the change, as
|
|
2140
|
+
* it did before foreign keys were lifted) — a dropped constraint must
|
|
2141
|
+
* never be lost to a column outside this run.
|
|
2142
|
+
*/
|
|
2143
|
+
async _liftable(conn, fks, fields) {
|
|
2144
|
+
const changed = new Map(fields.map((f) => [f.physicalName, f]));
|
|
2145
|
+
const columns = new Set(changed.keys());
|
|
2146
|
+
const pairs = [];
|
|
2147
|
+
for (const fk of fks) fk.columns.forEach((column, i) => {
|
|
2148
|
+
const { child, parent } = touchedEnds(fk, i, columns);
|
|
2149
|
+
if (!child && !parent) return;
|
|
2150
|
+
pairs.push([{
|
|
2151
|
+
schema: fk.schema,
|
|
2152
|
+
table: fk.tableName,
|
|
2153
|
+
column,
|
|
2154
|
+
ours: child
|
|
2155
|
+
}, {
|
|
2156
|
+
schema: fk.refSchema,
|
|
2157
|
+
table: fk.refTableName,
|
|
2158
|
+
column: fk.refColumns[i],
|
|
2159
|
+
ours: parent
|
|
2160
|
+
}]);
|
|
2161
|
+
});
|
|
2162
|
+
const liveEnds = /* @__PURE__ */ new Map();
|
|
2163
|
+
for (const end of pairs.flat()) if (!end.ours) liveEnds.set(columnKey(end), end);
|
|
2164
|
+
const live = [...liveEnds.values()];
|
|
2165
|
+
const liveRows = live.length === 0 ? [] : await conn.all(`SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLLATION_NAME
|
|
2166
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
2167
|
+
WHERE ${live.map(() => "(TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?)").join(" OR ")}`, live.flatMap((end) => [
|
|
2168
|
+
end.schema,
|
|
2169
|
+
end.table,
|
|
2170
|
+
end.column
|
|
2171
|
+
]));
|
|
2172
|
+
const liveOf = new Map(liveRows.map((r) => [columnKey({
|
|
2173
|
+
schema: r.TABLE_SCHEMA,
|
|
2174
|
+
table: r.TABLE_NAME,
|
|
2175
|
+
column: r.COLUMN_NAME
|
|
2176
|
+
}), {
|
|
2177
|
+
type: r.COLUMN_TYPE,
|
|
2178
|
+
collation: r.COLLATION_NAME ?? void 0
|
|
2179
|
+
}]));
|
|
2180
|
+
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())))]);
|
|
2181
|
+
const typeOf = (end) => {
|
|
2182
|
+
if (!end.ours) return liveOf.get(columnKey(end));
|
|
2183
|
+
const field = changed.get(end.column);
|
|
2184
|
+
return {
|
|
2185
|
+
type: this.typeMapper(field),
|
|
2186
|
+
collation: collations.get(field.physicalName)
|
|
2187
|
+
};
|
|
2188
|
+
};
|
|
2189
|
+
for (const [child, parent] of pairs) {
|
|
2190
|
+
const a = typeOf(child);
|
|
2191
|
+
const b = typeOf(parent);
|
|
2192
|
+
if (!a || !b || fkTypeKey(a) !== fkTypeKey(b)) {
|
|
2193
|
+
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`);
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return true;
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* The collation each of `fields` (string columns) has after its MODIFY:
|
|
2201
|
+
* the definition's own `COLLATE`, else the default of its `CHARACTER SET`,
|
|
2202
|
+
* else the table's.
|
|
2203
|
+
*/
|
|
2204
|
+
async _newCollations(conn, fields) {
|
|
2205
|
+
const out = /* @__PURE__ */ new Map();
|
|
2206
|
+
if (fields.length === 0) return out;
|
|
2207
|
+
const ctx = this._columnCtx("modify");
|
|
2208
|
+
const charsetDefaults = /* @__PURE__ */ new Map();
|
|
2209
|
+
let tableCollation = null;
|
|
2210
|
+
for (const field of fields) {
|
|
2211
|
+
const { charsetClause, collateClause } = mysqlColumnCharsetClauses(field, this.typeMapper(field), ctx);
|
|
2212
|
+
const collate = /COLLATE\s+`?(\w+)`?/i.exec(collateClause)?.[1];
|
|
2213
|
+
const charset = /CHARACTER SET\s+`?(\w+)`?/i.exec(charsetClause)?.[1];
|
|
2214
|
+
if (collate) out.set(field.physicalName, collate);
|
|
2215
|
+
else if (charset) {
|
|
2216
|
+
if (!charsetDefaults.has(charset)) {
|
|
2217
|
+
const row = await conn.get(`SELECT COLLATION_NAME FROM INFORMATION_SCHEMA.COLLATIONS
|
|
2218
|
+
WHERE CHARACTER_SET_NAME = ? AND IS_DEFAULT = 'Yes'`, [charset]);
|
|
2219
|
+
charsetDefaults.set(charset, row?.COLLATION_NAME);
|
|
2220
|
+
}
|
|
2221
|
+
out.set(field.physicalName, charsetDefaults.get(charset));
|
|
2222
|
+
} else {
|
|
2223
|
+
if (tableCollation === null) tableCollation = (await conn.get(`SELECT TABLE_COLLATION FROM INFORMATION_SCHEMA.TABLES
|
|
2224
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())`, [this._table.tableName, this._schema]))?.TABLE_COLLATION ?? void 0;
|
|
2225
|
+
out.set(field.physicalName, tableCollation);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return out;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* The live foreign keys over any of `columns` of this table: its own (by
|
|
2232
|
+
* their columns) and those referencing them (by the referenced columns),
|
|
2233
|
+
* each with every column, in key order.
|
|
2234
|
+
*/
|
|
2235
|
+
async _foreignKeysOver(conn, columns) {
|
|
2236
|
+
const target = [this._schema, this._table.tableName];
|
|
2237
|
+
const rows = await conn.all(`SELECT k.TABLE_SCHEMA, k.TABLE_NAME, k.CONSTRAINT_NAME, k.COLUMN_NAME,
|
|
2238
|
+
k.REFERENCED_TABLE_SCHEMA, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME,
|
|
2239
|
+
r.UPDATE_RULE, r.DELETE_RULE,
|
|
2240
|
+
(k.TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.TABLE_NAME = ?) AS IS_OWN,
|
|
2241
|
+
(k.REFERENCED_TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.REFERENCED_TABLE_NAME = ?) AS IS_REF
|
|
2242
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
|
|
2243
|
+
JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r
|
|
2244
|
+
ON r.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA AND r.CONSTRAINT_NAME = k.CONSTRAINT_NAME
|
|
2245
|
+
AND r.TABLE_NAME = k.TABLE_NAME
|
|
2246
|
+
WHERE k.REFERENCED_TABLE_NAME IS NOT NULL
|
|
2247
|
+
AND ((k.TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.TABLE_NAME = ?)
|
|
2248
|
+
OR (k.REFERENCED_TABLE_SCHEMA = COALESCE(?, DATABASE()) AND k.REFERENCED_TABLE_NAME = ?))
|
|
2249
|
+
ORDER BY k.TABLE_SCHEMA, k.TABLE_NAME, k.CONSTRAINT_NAME, k.ORDINAL_POSITION`, [
|
|
2250
|
+
...target,
|
|
2251
|
+
...target,
|
|
2252
|
+
...target,
|
|
2253
|
+
...target
|
|
2254
|
+
]);
|
|
2255
|
+
const byConstraint = /* @__PURE__ */ new Map();
|
|
2256
|
+
for (const r of rows) {
|
|
2257
|
+
const key = `${r.TABLE_SCHEMA}\0${r.TABLE_NAME}\0${r.CONSTRAINT_NAME}`;
|
|
2258
|
+
let fk = byConstraint.get(key);
|
|
2259
|
+
if (!fk) {
|
|
2260
|
+
fk = {
|
|
2261
|
+
schema: r.TABLE_SCHEMA,
|
|
2262
|
+
tableName: r.TABLE_NAME,
|
|
2263
|
+
table: `${qi(r.TABLE_SCHEMA)}.${qi(r.TABLE_NAME)}`,
|
|
2264
|
+
name: r.CONSTRAINT_NAME,
|
|
2265
|
+
columns: [],
|
|
2266
|
+
refSchema: r.REFERENCED_TABLE_SCHEMA,
|
|
2267
|
+
refTableName: r.REFERENCED_TABLE_NAME,
|
|
2268
|
+
refTable: `${qi(r.REFERENCED_TABLE_SCHEMA)}.${qi(r.REFERENCED_TABLE_NAME)}`,
|
|
2269
|
+
refColumns: [],
|
|
2270
|
+
onDelete: r.DELETE_RULE,
|
|
2271
|
+
onUpdate: r.UPDATE_RULE,
|
|
2272
|
+
own: Boolean(Number(r.IS_OWN)),
|
|
2273
|
+
ref: Boolean(Number(r.IS_REF))
|
|
2274
|
+
};
|
|
2275
|
+
byConstraint.set(key, fk);
|
|
2276
|
+
}
|
|
2277
|
+
fk.columns.push(r.COLUMN_NAME);
|
|
2278
|
+
fk.refColumns.push(r.REFERENCED_COLUMN_NAME);
|
|
2279
|
+
}
|
|
2280
|
+
const over = new Set(columns);
|
|
2281
|
+
return [...byConstraint.values()].filter((fk) => fk.columns.some((_, i) => {
|
|
2282
|
+
const { child, parent } = touchedEnds(fk, i, over);
|
|
2283
|
+
return child || parent;
|
|
2284
|
+
}));
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Before `fields` are modified: drops the managed indexes that key more of
|
|
2288
|
+
* a column than its new type allows — a full key on a column becoming
|
|
2289
|
+
* TEXT (ER 1170), or on a VARCHAR growing past the key-part limit (ER
|
|
2290
|
+
* 1071). Index sync re-creates them with the prefix the new type needs.
|
|
2291
|
+
* Primary keys, FULLTEXT and unmanaged indexes are left alone.
|
|
2292
|
+
*/
|
|
2293
|
+
async _dropIndexesOutgrown(conn, fields) {
|
|
2294
|
+
const limits = /* @__PURE__ */ new Map();
|
|
2295
|
+
for (const field of fields) {
|
|
2296
|
+
const prefix = this._indexPrefix(field);
|
|
2297
|
+
if (prefix !== void 0) limits.set(field.physicalName, prefix);
|
|
2298
|
+
}
|
|
2299
|
+
if (limits.size === 0) return;
|
|
2300
|
+
const columns = [...limits.keys()];
|
|
2301
|
+
const rows = await conn.all(`SELECT INDEX_NAME, COLUMN_NAME, SUB_PART, INDEX_TYPE FROM INFORMATION_SCHEMA.STATISTICS
|
|
2302
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
2303
|
+
AND COLUMN_NAME IN (${columns.map(() => "?").join(", ")})`, [
|
|
2304
|
+
this._table.tableName,
|
|
2305
|
+
this._schema,
|
|
2306
|
+
...columns
|
|
2307
|
+
]);
|
|
2308
|
+
const outgrown = /* @__PURE__ */ new Set();
|
|
2309
|
+
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);
|
|
2310
|
+
for (const name of outgrown) await this._dropIndex(name, conn);
|
|
2311
|
+
}
|
|
2312
|
+
/** `DROP INDEX name` on this table (logged). */
|
|
2313
|
+
async _dropIndex(name, conn = this._exec()) {
|
|
2314
|
+
const ddl = `DROP INDEX ${qi(name)} ON ${quoteTableName(this.resolveTableName())}`;
|
|
2315
|
+
this._log(ddl);
|
|
2316
|
+
await conn.exec(ddl);
|
|
2317
|
+
}
|
|
1879
2318
|
async recreateTable() {
|
|
1880
2319
|
const tableName = this.resolveTableName();
|
|
1881
2320
|
const tempName = `${this._table.tableName}__tmp_${Date.now()}`;
|
|
@@ -1950,11 +2389,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1950
2389
|
this._schema,
|
|
1951
2390
|
...columns
|
|
1952
2391
|
]);
|
|
1953
|
-
for (const row of rows)
|
|
1954
|
-
const sql = `DROP INDEX ${qi(row.name)} ON ${quoteTableName(this.resolveTableName())}`;
|
|
1955
|
-
this._log(sql);
|
|
1956
|
-
await this._exec().exec(sql);
|
|
1957
|
-
}
|
|
2392
|
+
for (const row of rows) await this._dropIndex(row.name);
|
|
1958
2393
|
}
|
|
1959
2394
|
async dropTableByName(tableName) {
|
|
1960
2395
|
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
|
|
@@ -1980,13 +2415,17 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1980
2415
|
this._log(ddl);
|
|
1981
2416
|
await this._exec().exec(ddl);
|
|
1982
2417
|
}
|
|
2418
|
+
/** Keeps the space: {@link typeMapper} asks it which fields a foreign key references. */
|
|
2419
|
+
registerSpace(space) {
|
|
2420
|
+
this._space = space;
|
|
2421
|
+
}
|
|
1983
2422
|
typeMapper(field) {
|
|
1984
2423
|
if (field.encrypted) return "TEXT";
|
|
1985
2424
|
if (this._vectorFields.has(field.path)) {
|
|
1986
2425
|
const vec = this._vectorFields.get(field.path);
|
|
1987
2426
|
return this._supportsVector ? `VECTOR(${vec.dimensions})` : "JSON";
|
|
1988
2427
|
}
|
|
1989
|
-
return mysqlTypeFromField(field);
|
|
2428
|
+
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));
|
|
1990
2429
|
}
|
|
1991
2430
|
async _syncIndexes(dryRun) {
|
|
1992
2431
|
const tableName = this._table.tableName;
|
|
@@ -1994,15 +2433,14 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1994
2433
|
if (!dryRun) {
|
|
1995
2434
|
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
2435
|
}
|
|
1997
|
-
const bytesPerChar = mysqlBytesPerChar(this._charset);
|
|
1998
2436
|
const fields = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
1999
2437
|
const mappedType = (column) => {
|
|
2000
2438
|
const field = fields.get(column);
|
|
2001
2439
|
return field ? this.typeMapper(field) : void 0;
|
|
2002
2440
|
};
|
|
2003
2441
|
const desiredPrefix = (column) => {
|
|
2004
|
-
const
|
|
2005
|
-
return
|
|
2442
|
+
const field = fields.get(column);
|
|
2443
|
+
return field && this._indexPrefix(field);
|
|
2006
2444
|
};
|
|
2007
2445
|
const renderPart = (column, prefix) => prefix === void 0 ? column : `${column}(${prefix})`;
|
|
2008
2446
|
return this.syncIndexesWithDiff({
|
|
@@ -2054,11 +2492,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2054
2492
|
this._log(sql);
|
|
2055
2493
|
await this._exec().exec(sql);
|
|
2056
2494
|
},
|
|
2057
|
-
dropIndex:
|
|
2058
|
-
const sql = `DROP INDEX ${qi(name)} ON ${quoteTableName(this.resolveTableName())}`;
|
|
2059
|
-
this._log(sql);
|
|
2060
|
-
await this._exec().exec(sql);
|
|
2061
|
-
}
|
|
2495
|
+
dropIndex: (name) => this._dropIndex(name)
|
|
2062
2496
|
});
|
|
2063
2497
|
}
|
|
2064
2498
|
async syncForeignKeys() {
|
|
@@ -2069,10 +2503,11 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2069
2503
|
return this._syncForeignKeys(true);
|
|
2070
2504
|
}
|
|
2071
2505
|
_syncForeignKeys(dryRun) {
|
|
2506
|
+
let existing;
|
|
2072
2507
|
return this.syncForeignKeysWithDiff({
|
|
2073
2508
|
dryRun,
|
|
2074
|
-
listExisting: () => this._getExistingFkConstraints(),
|
|
2075
|
-
dropConstraint: (name) => this._dropFkConstraint(name),
|
|
2509
|
+
listExisting: async () => existing = await this._getExistingFkConstraints(),
|
|
2510
|
+
dropConstraint: (name) => this._dropFkConstraint(name, existing.get(name)),
|
|
2076
2511
|
addConstraint: async (fk) => {
|
|
2077
2512
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${(0, _atscript_db_sql_tools.foreignKeySql)(qi, fk)}`;
|
|
2078
2513
|
this._log(ddl);
|
|
@@ -2083,19 +2518,40 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2083
2518
|
async dropForeignKeys(fkFieldKeys) {
|
|
2084
2519
|
if (fkFieldKeys.length === 0) return;
|
|
2085
2520
|
const keySet = new Set(fkFieldKeys);
|
|
2086
|
-
for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has((0, _atscript_db.fkKey)(cols))) await this._dropFkConstraint(constraintName);
|
|
2521
|
+
for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has((0, _atscript_db.fkKey)(cols))) await this._dropFkConstraint(constraintName, cols);
|
|
2087
2522
|
}
|
|
2088
|
-
|
|
2523
|
+
/**
|
|
2524
|
+
* Drops a foreign-key constraint over `columns` (in key order), then the
|
|
2525
|
+
* index InnoDB created for it: an unnamed constraint gets an index named
|
|
2526
|
+
* after its first column, which outlives the constraint — and would block
|
|
2527
|
+
* that column from becoming TEXT once it no longer references anything
|
|
2528
|
+
* (ER 1170). An index another constraint still needs is kept (ER 1553).
|
|
2529
|
+
*/
|
|
2530
|
+
async _dropFkConstraint(name, columns) {
|
|
2089
2531
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(name)}`;
|
|
2090
2532
|
this._log(ddl);
|
|
2091
2533
|
await this._exec().exec(ddl);
|
|
2534
|
+
if (columns.length === 0) return;
|
|
2535
|
+
if ((await this._exec().all(`SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.STATISTICS
|
|
2536
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE()) AND INDEX_NAME = ?
|
|
2537
|
+
ORDER BY SEQ_IN_INDEX`, [
|
|
2538
|
+
this._table.tableName,
|
|
2539
|
+
this._schema,
|
|
2540
|
+
columns[0]
|
|
2541
|
+
])).map((r) => r.COLUMN_NAME).join("\0") !== columns.join("\0")) return;
|
|
2542
|
+
try {
|
|
2543
|
+
await this._dropIndex(columns[0]);
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
if (error.errno !== 1553) throw error;
|
|
2546
|
+
}
|
|
2092
2547
|
}
|
|
2093
2548
|
/** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */
|
|
2094
2549
|
async _getExistingFkConstraints() {
|
|
2095
2550
|
const rows = await this._exec().all(`SELECT kcu.CONSTRAINT_NAME, kcu.COLUMN_NAME
|
|
2096
2551
|
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
2097
2552
|
WHERE kcu.TABLE_NAME = ? AND kcu.TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
2098
|
-
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
2553
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
2554
|
+
ORDER BY kcu.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, [this._table.tableName, this._schema]);
|
|
2099
2555
|
const byName = /* @__PURE__ */ new Map();
|
|
2100
2556
|
for (const row of rows) {
|
|
2101
2557
|
let cols = byName.get(row.CONSTRAINT_NAME);
|