@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/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}${mysqlCollateClause(field)} GENERATED ALWAYS AS (${(0, _atscript_db_sql_tools.derivedColumnExpr)(mysqlDialect, field)}) VIRTUAL`,
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 += mysqlCollateClause(field);
142
- const onUpdate = ctx.onUpdateFields?.get(field.physicalName);
143
- if (onUpdate) def += ` ON UPDATE ${/^current_timestamp(\(\))?$/i.test(onUpdate) ? currentTimestampFor(sqlType) : onUpdate}`;
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
- * Maps portable collation values to MySQL collation names.
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` clause of a column (native `@db.mysql.collate`, else the
441
- * portable `@db.column.collate` mapping) — `""` when none applies.
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
- return field.collate ? ` COLLATE ${collationToMysql(field.collate)}` : "";
496
+ if (!field.collate) return "";
497
+ const collation = collationToMysql(field.collate);
498
+ return ` COLLATE ${charset ? collation.replace(/^utf8mb4_/, `${charset}_`) : collation}`;
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;
447
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
- function mysqlTypeFromField(field) {
478
- if (field.fkTargetField) return mysqlTypeFromField(field.fkTargetField);
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.isPrimaryKey || field.defaultValue || field.derived) return "VARCHAR(255)";
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 ?? "utf8mb4_unicode_ci";
546
- sql += ` ENGINE=${engine} DEFAULT CHARSET=${charset} COLLATE=${collation}`;
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
- _collation = "utf8mb4_unicode_ci";
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 }. */
@@ -675,6 +785,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
675
785
  _replaceColumnsMemo;
676
786
  _fulltextMemo;
677
787
  _searchIndexesMemo;
788
+ _incrementPkMemo;
678
789
  /**
679
790
  * Schema name for INFORMATION_SCHEMA queries — `@db.schema` of the bound
680
791
  * table, or `null` (→ `DATABASE()`, the pool's database) when the table
@@ -848,11 +959,11 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
848
959
  onBeforeFlatten(_type) {
849
960
  const meta = _type.metadata;
850
961
  const engine = meta.get("db.mysql.engine");
851
- if (engine) this._engine = engine;
962
+ if (engine) this._engine = require_annotation_values.mysqlName("@db.mysql.engine", engine);
852
963
  const charset = meta.get("db.mysql.charset");
853
- if (charset) this._charset = charset;
964
+ if (charset) this._charset = require_annotation_values.mysqlName("@db.mysql.charset", charset);
854
965
  const collate = meta.get("db.mysql.collate");
855
- if (collate) this._collation = collate;
966
+ if (collate) this._collation = require_annotation_values.mysqlName("@db.mysql.collate", collate);
856
967
  }
857
968
  onFieldScanned(field, _type, metadata) {
858
969
  if (metadata.has("db.default.increment")) {
@@ -862,6 +973,8 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
862
973
  }
863
974
  const onUpdate = metadata.get("db.mysql.onUpdate");
864
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));
865
978
  const vectorMeta = metadata.get("db.search.vector");
866
979
  if (vectorMeta) {
867
980
  const indexName = vectorMeta.indexName || field;
@@ -875,6 +988,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
875
988
  }
876
989
  }
877
990
  getDesiredTableOptions() {
991
+ const collation = this._tableCollation();
878
992
  return [
879
993
  {
880
994
  key: "engine",
@@ -884,10 +998,10 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
884
998
  key: "charset",
885
999
  value: this._charset
886
1000
  },
887
- {
1001
+ ...collation === void 0 ? [] : [{
888
1002
  key: "collation",
889
- value: this._collation
890
- }
1003
+ value: collation
1004
+ }]
891
1005
  ];
892
1006
  }
893
1007
  async getExistingTableOptions(tableName) {
@@ -896,18 +1010,23 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
896
1010
  WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())`, [tableName ?? this._table.tableName, this._schema]);
897
1011
  if (!row) return [];
898
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
+ };
899
1018
  return [
900
1019
  {
901
1020
  key: "engine",
902
- value: row.ENGINE ?? "InnoDB"
1021
+ value: asDesired("engine", row.ENGINE ?? "InnoDB")
903
1022
  },
904
1023
  {
905
1024
  key: "charset",
906
- value: charset
1025
+ value: asDesired("charset", charset)
907
1026
  },
908
1027
  {
909
1028
  key: "collation",
910
- value: row.TABLE_COLLATION ?? "utf8mb4_unicode_ci"
1029
+ value: asDesired("collation", row.TABLE_COLLATION ?? "utf8mb4_unicode_ci")
911
1030
  }
912
1031
  ];
913
1032
  }
@@ -994,7 +1113,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
994
1113
  const { sql, params } = buildInsert(this.resolveTableName(), data);
995
1114
  this._log(sql, params);
996
1115
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
997
- return { insertedId: this._resolveInsertedId(data, result.insertId) };
1116
+ return { insertedId: this._insertedIdOf(data, result.insertId, result.insertId) };
998
1117
  }
999
1118
  async insertMany(data) {
1000
1119
  if (data.length === 0) return {
@@ -1072,9 +1191,25 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1072
1191
  }
1073
1192
  /** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
1074
1193
  _autoIncrementPk() {
1075
- const pks = this._table.primaryKeys;
1076
- if (pks.length !== 1 || !this._incrementFields.has(pks[0])) return void 0;
1077
- return this._table.getMetadata().physicalPath(pks[0]);
1194
+ const { column, composite } = this._incrementPk();
1195
+ return composite ? void 0 : column;
1196
+ }
1197
+ /**
1198
+ * Physical column of the primary key's `@db.default.increment` part (if any)
1199
+ * and whether the key is composite — computed once per table.
1200
+ */
1201
+ _incrementPk() {
1202
+ const src = this._table.primaryKeys;
1203
+ let memo = this._incrementPkMemo;
1204
+ if (memo?.src !== src) {
1205
+ const part = src.find((pk) => this._incrementFields.has(pk));
1206
+ memo = this._incrementPkMemo = {
1207
+ src,
1208
+ column: part === void 0 ? void 0 : this._table.physicalPath(part),
1209
+ composite: src.length > 1
1210
+ };
1211
+ }
1212
+ return memo;
1078
1213
  }
1079
1214
  /**
1080
1215
  * Splits `rows` into runs that each map to ONE statement with a derivable
@@ -1164,11 +1299,30 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1164
1299
  async _groupInsertedIds(group, insertId, step) {
1165
1300
  const first = Number(insertId);
1166
1301
  const stride = group.generated && group.rows.length > 1 && first > 0 ? await step() : 1;
1302
+ const partId = group.rows.length === 1 ? insertId : void 0;
1167
1303
  return group.rows.map((row, i) => {
1168
1304
  const generatedId = first > 0 ? first + i * stride : 0;
1169
- return group.generated ? generatedId : this._resolveInsertedId(row, generatedId);
1305
+ return group.generated ? generatedId : this._insertedIdOf(row, generatedId, partId);
1170
1306
  });
1171
1307
  }
1308
+ /**
1309
+ * The inserted id of `row` (see `_resolveInsertedId`). For a COMPOSITE key
1310
+ * with an AUTO_INCREMENT part, `partId` is what the statement generated for
1311
+ * that part — pass it only when exact (a ONE-row INSERT); a row carrying the
1312
+ * part keeps its own value. An explicit `0` in the part counts as absent:
1313
+ * MySQL generates a value for it (unless `NO_AUTO_VALUE_ON_ZERO`), so only
1314
+ * `partId` knows it — without that the id is `undefined`, never a wrong `0`.
1315
+ */
1316
+ _insertedIdOf(row, generatedId, partId) {
1317
+ const { column, composite } = this._incrementPk();
1318
+ if (!composite || column === void 0) return this._resolveInsertedId(row, generatedId);
1319
+ const source = isZero(row[column]) ? {
1320
+ ...row,
1321
+ [column]: void 0
1322
+ } : row;
1323
+ const part = Number(partId) > 0 ? { [column]: partId } : void 0;
1324
+ return this._resolveInsertedId(source, generatedId, part);
1325
+ }
1172
1326
  supportsInsertIgnore() {
1173
1327
  return true;
1174
1328
  }
@@ -1597,15 +1751,64 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1597
1751
  this._log(sql);
1598
1752
  await this._exec().exec(sql);
1599
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
+ }
1600
1801
  /** The CREATE TABLE options (engine/charset/collation/increment/ON UPDATE/type mapper). */
1601
1802
  _tableOptions(opts) {
1602
1803
  return {
1603
1804
  engine: this._engine,
1604
1805
  charset: this._charset,
1605
- collation: this._collation,
1806
+ collation: this._tableCollation(),
1606
1807
  autoIncrementStart: this._autoIncrementStart,
1607
1808
  incrementFields: this._incrementFields,
1608
1809
  onUpdateFields: this._onUpdateFields,
1810
+ charsetFields: this._charsetFields,
1811
+ keyTableCharsets: this._keyTableCharsets(),
1609
1812
  typeMapper: (field) => this.typeMapper(field),
1610
1813
  deferForeignKeysTo: opts?.deferForeignKeysTo
1611
1814
  };
@@ -1615,6 +1818,8 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1615
1818
  return {
1616
1819
  incrementFields: this._incrementFields,
1617
1820
  onUpdateFields: this._onUpdateFields,
1821
+ charsetFields: this._charsetFields,
1822
+ keyTableCharsets: this._keyTableCharsets(),
1618
1823
  typeMapper: (field) => this.typeMapper(field),
1619
1824
  purpose
1620
1825
  };
@@ -1799,6 +2004,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1799
2004
  return entering.has(field.physicalName);
1800
2005
  };
1801
2006
  const modified = /* @__PURE__ */ new Map();
2007
+ const retyped = [];
1802
2008
  for (const { field, existingType } of diff.typeChanged ?? []) {
1803
2009
  const epoch = epochSource(field, existingType);
1804
2010
  if (epoch) {
@@ -1816,6 +2022,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1816
2022
  continue;
1817
2023
  }
1818
2024
  modified.set(field.physicalName, field);
2025
+ retyped.push(field);
1819
2026
  }
1820
2027
  for (const { field } of diff.nullableChanged ?? []) {
1821
2028
  if (rekeyed(field)) continue;
@@ -1829,17 +2036,285 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1829
2036
  modified.set(field.physicalName, field);
1830
2037
  }
1831
2038
  for (const { field } of diff.defaultChanged ?? []) if (!rekeyed(field)) modified.set(field.physicalName, field);
1832
- for (const field of modified.values()) {
1833
- const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
1834
- this._log(ddl);
1835
- await conn.exec(ddl);
1836
- }
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
+ });
1837
2047
  return {
1838
2048
  added,
1839
2049
  renamed
1840
2050
  };
1841
2051
  });
1842
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
+ }
1843
2318
  async recreateTable() {
1844
2319
  const tableName = this.resolveTableName();
1845
2320
  const tempName = `${this._table.tableName}__tmp_${Date.now()}`;
@@ -1914,11 +2389,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1914
2389
  this._schema,
1915
2390
  ...columns
1916
2391
  ]);
1917
- for (const row of rows) {
1918
- const sql = `DROP INDEX ${qi(row.name)} ON ${quoteTableName(this.resolveTableName())}`;
1919
- this._log(sql);
1920
- await this._exec().exec(sql);
1921
- }
2392
+ for (const row of rows) await this._dropIndex(row.name);
1922
2393
  }
1923
2394
  async dropTableByName(tableName) {
1924
2395
  const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
@@ -1944,13 +2415,17 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1944
2415
  this._log(ddl);
1945
2416
  await this._exec().exec(ddl);
1946
2417
  }
2418
+ /** Keeps the space: {@link typeMapper} asks it which fields a foreign key references. */
2419
+ registerSpace(space) {
2420
+ this._space = space;
2421
+ }
1947
2422
  typeMapper(field) {
1948
2423
  if (field.encrypted) return "TEXT";
1949
2424
  if (this._vectorFields.has(field.path)) {
1950
2425
  const vec = this._vectorFields.get(field.path);
1951
2426
  return this._supportsVector ? `VECTOR(${vec.dimensions})` : "JSON";
1952
2427
  }
1953
- 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));
1954
2429
  }
1955
2430
  async _syncIndexes(dryRun) {
1956
2431
  const tableName = this._table.tableName;
@@ -1958,15 +2433,14 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1958
2433
  if (!dryRun) {
1959
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`);
1960
2435
  }
1961
- const bytesPerChar = mysqlBytesPerChar(this._charset);
1962
2436
  const fields = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
1963
2437
  const mappedType = (column) => {
1964
2438
  const field = fields.get(column);
1965
2439
  return field ? this.typeMapper(field) : void 0;
1966
2440
  };
1967
2441
  const desiredPrefix = (column) => {
1968
- const type = mappedType(column);
1969
- return type === void 0 ? void 0 : mysqlIndexPrefix(type, bytesPerChar);
2442
+ const field = fields.get(column);
2443
+ return field && this._indexPrefix(field);
1970
2444
  };
1971
2445
  const renderPart = (column, prefix) => prefix === void 0 ? column : `${column}(${prefix})`;
1972
2446
  return this.syncIndexesWithDiff({
@@ -2018,11 +2492,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
2018
2492
  this._log(sql);
2019
2493
  await this._exec().exec(sql);
2020
2494
  },
2021
- dropIndex: async (name) => {
2022
- const sql = `DROP INDEX ${qi(name)} ON ${quoteTableName(this.resolveTableName())}`;
2023
- this._log(sql);
2024
- await this._exec().exec(sql);
2025
- }
2495
+ dropIndex: (name) => this._dropIndex(name)
2026
2496
  });
2027
2497
  }
2028
2498
  async syncForeignKeys() {
@@ -2033,10 +2503,11 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
2033
2503
  return this._syncForeignKeys(true);
2034
2504
  }
2035
2505
  _syncForeignKeys(dryRun) {
2506
+ let existing;
2036
2507
  return this.syncForeignKeysWithDiff({
2037
2508
  dryRun,
2038
- listExisting: () => this._getExistingFkConstraints(),
2039
- dropConstraint: (name) => this._dropFkConstraint(name),
2509
+ listExisting: async () => existing = await this._getExistingFkConstraints(),
2510
+ dropConstraint: (name) => this._dropFkConstraint(name, existing.get(name)),
2040
2511
  addConstraint: async (fk) => {
2041
2512
  const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${(0, _atscript_db_sql_tools.foreignKeySql)(qi, fk)}`;
2042
2513
  this._log(ddl);
@@ -2047,19 +2518,40 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
2047
2518
  async dropForeignKeys(fkFieldKeys) {
2048
2519
  if (fkFieldKeys.length === 0) return;
2049
2520
  const keySet = new Set(fkFieldKeys);
2050
- 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);
2051
2522
  }
2052
- async _dropFkConstraint(name) {
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) {
2053
2531
  const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(name)}`;
2054
2532
  this._log(ddl);
2055
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
+ }
2056
2547
  }
2057
2548
  /** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */
2058
2549
  async _getExistingFkConstraints() {
2059
2550
  const rows = await this._exec().all(`SELECT kcu.CONSTRAINT_NAME, kcu.COLUMN_NAME
2060
2551
  FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
2061
2552
  WHERE kcu.TABLE_NAME = ? AND kcu.TABLE_SCHEMA = COALESCE(?, DATABASE())
2062
- AND kcu.REFERENCED_TABLE_NAME IS NOT NULL`, [this._table.tableName, this._schema]);
2553
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
2554
+ ORDER BY kcu.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, [this._table.tableName, this._schema]);
2063
2555
  const byName = /* @__PURE__ */ new Map();
2064
2556
  for (const row of rows) {
2065
2557
  let cols = byName.get(row.CONSTRAINT_NAME);