@atscript/db-mysql 0.1.139 → 0.1.141

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
@@ -94,9 +94,17 @@ function mysqlIndexPrefix(mappedType, bytesPerChar) {
94
94
  * required default-less column on `add`;
95
95
  * 5. COLLATE (native `@db.mysql.collate` or portable `@db.column.collate`);
96
96
  * 6. ON UPDATE.
97
+ *
98
+ * A `@db.column.derived` field (since 0.1.141) renders as a VIRTUAL generated
99
+ * column — `type [COLLATE …] GENERATED ALWAYS AS (<json extraction>) VIRTUAL`
100
+ * — with no nullability, DEFAULT, AUTO_INCREMENT or ON UPDATE, on every purpose.
97
101
  */
98
102
  function buildColumnDefinition(field, ctx) {
99
103
  const sqlType = ctx.typeMapper?.(field) ?? mysqlTypeFromField(field);
104
+ if (field.derived) return {
105
+ def: `${qi(field.physicalName)} ${sqlType}${mysqlCollateClause(field)} GENERATED ALWAYS AS (${(0, _atscript_db_sql_tools.derivedColumnExpr)(mysqlDialect, field)}) VIRTUAL`,
106
+ inventedDefault: false
107
+ };
100
108
  const increment = ctx.purpose !== "add" && (ctx.incrementFields?.has(field.physicalName) ?? false);
101
109
  let def = `${qi(field.physicalName)} ${sqlType}`;
102
110
  if (increment) def += " AUTO_INCREMENT";
@@ -112,9 +120,7 @@ function buildColumnDefinition(field, ctx) {
112
120
  def += ` DEFAULT ${mysqlTypeDefault(sqlType, field)}`;
113
121
  inventedDefault = true;
114
122
  }
115
- const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
116
- if (nativeCollate) def += ` COLLATE ${nativeCollate}`;
117
- else if (field.collate) def += ` COLLATE ${collationToMysql(field.collate)}`;
123
+ def += mysqlCollateClause(field);
118
124
  const onUpdate = ctx.onUpdateFields?.get(field.physicalName);
119
125
  if (onUpdate) def += ` ON UPDATE ${onUpdate}`;
120
126
  return {
@@ -340,6 +346,15 @@ function buildAggregateCount(table, where, controls) {
340
346
  /**
341
347
  * Maps portable collation values to MySQL collation names.
342
348
  */
349
+ /**
350
+ * The `COLLATE` clause of a column (native `@db.mysql.collate`, else the
351
+ * portable `@db.column.collate` mapping) — `""` when none applies.
352
+ */
353
+ function mysqlCollateClause(field) {
354
+ const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
355
+ if (nativeCollate) return ` COLLATE ${nativeCollate}`;
356
+ return field.collate ? ` COLLATE ${collationToMysql(field.collate)}` : "";
357
+ }
343
358
  function collationToMysql(collation) {
344
359
  switch (collation) {
345
360
  case "binary": return "utf8mb4_bin";
@@ -395,7 +410,7 @@ function mysqlTypeFromField(field) {
395
410
  const maxLen = (metadata?.get("expect.maxLength"))?.length;
396
411
  if (maxLen !== void 0 && maxLen <= 65535) return `VARCHAR(${maxLen})`;
397
412
  if (maxLen !== void 0 && maxLen > 65535) return "LONGTEXT";
398
- if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
413
+ if (field.isPrimaryKey || field.defaultValue || field.derived) return "VARCHAR(255)";
399
414
  return "TEXT";
400
415
  }
401
416
  case "json":
@@ -578,6 +593,28 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
578
593
  _exec() {
579
594
  return this._txConnection() ?? this.driver;
580
595
  }
596
+ /**
597
+ * Runs `fn` on one connection (the transaction's, or a dedicated one) with
598
+ * `STRICT_ALL_TABLES` added to the session `sql_mode`, restoring the
599
+ * previous mode after. Schema sync's converting statements (`MODIFY
600
+ * COLUMN`, the recreate copy) then fail on a value that does not convert
601
+ * instead of coercing or truncating it — a non-strict server (RDS defaults
602
+ * to `NO_ENGINE_SUBSTITUTION`) would turn `'abc'` into `0` silently.
603
+ */
604
+ async _withStrictSession(fn) {
605
+ const tx = this._txConnection();
606
+ const conn = tx ?? await this.driver.getConnection();
607
+ try {
608
+ await conn.exec("SET @atscript_sql_mode = @@SESSION.sql_mode, SESSION sql_mode = CONCAT_WS(',', @@SESSION.sql_mode, 'STRICT_ALL_TABLES')");
609
+ try {
610
+ return await fn(conn);
611
+ } finally {
612
+ await conn.exec("SET SESSION sql_mode = @atscript_sql_mode");
613
+ }
614
+ } finally {
615
+ if (!tx) conn.release();
616
+ }
617
+ }
581
618
  /** MySQL InnoDB enforces FK constraints natively. */
582
619
  supportsNativeForeignKeys() {
583
620
  return true;
@@ -836,7 +873,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
836
873
  }
837
874
  async updateOne(filter, data, ops, expectedVersion) {
838
875
  const where = buildWhere(filter);
839
- const versionColumn = this._table.versionColumn;
876
+ const versionColumn = this._table.versionColumnPhysical;
840
877
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where, 1, ops, versionColumn, expectedVersion);
841
878
  this._log(sql, params);
842
879
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
@@ -847,7 +884,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
847
884
  }
848
885
  async updateMany(filter, data, ops) {
849
886
  const where = buildWhere(filter);
850
- const versionColumn = this._table.versionColumn;
887
+ const versionColumn = this._table.versionColumnPhysical;
851
888
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, ops, versionColumn);
852
889
  this._log(sql, params);
853
890
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
@@ -858,7 +895,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
858
895
  }
859
896
  async replaceOne(filter, data, expectedVersion) {
860
897
  const where = buildWhere(filter);
861
- const versionColumn = this._table.versionColumn;
898
+ const versionColumn = this._table.versionColumnPhysical;
862
899
  const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn);
863
900
  const { sql, params } = buildUpdate(this.resolveTableName(), full, where, 1, void 0, versionColumn, expectedVersion);
864
901
  this._log(sql, params);
@@ -870,7 +907,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
870
907
  }
871
908
  async replaceMany(filter, data) {
872
909
  const where = buildWhere(filter);
873
- const versionColumn = this._table.versionColumn;
910
+ const versionColumn = this._table.versionColumnPhysical;
874
911
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, void 0, versionColumn);
875
912
  this._log(sql, params);
876
913
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
@@ -998,7 +1035,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
998
1035
  if (clauses.length === 0) return;
999
1036
  const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ${clauses.join(", ")}`;
1000
1037
  this._log(ddl);
1001
- await this._exec().exec(ddl);
1038
+ await this._withStrictSession((conn) => conn.exec(ddl));
1002
1039
  }
1003
1040
  async _ensureView() {
1004
1041
  const view = this._table;
@@ -1010,131 +1047,130 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1010
1047
  return this.getExistingColumnsForTable(this._table.tableName);
1011
1048
  }
1012
1049
  async getExistingColumnsForTable(tableName) {
1013
- return (await this._exec().all(`SELECT c.COLUMN_NAME, c.COLUMN_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.SRS_ID,
1050
+ return this._readColumns(this._exec(), tableName);
1051
+ }
1052
+ /** Live columns of `tableName`, read through `exec` (a held connection, or the pool). */
1053
+ async _readColumns(exec, tableName) {
1054
+ return (await exec.all(`SELECT c.COLUMN_NAME, c.COLUMN_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.SRS_ID, c.EXTRA,
1014
1055
  (k.COLUMN_NAME IS NOT NULL) AS IS_PK
1015
1056
  FROM INFORMATION_SCHEMA.COLUMNS c
1016
1057
  LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
1017
1058
  ON k.TABLE_SCHEMA = c.TABLE_SCHEMA AND k.TABLE_NAME = c.TABLE_NAME
1018
1059
  AND k.COLUMN_NAME = c.COLUMN_NAME AND k.CONSTRAINT_NAME = 'PRIMARY'
1019
1060
  WHERE c.TABLE_NAME = ? AND c.TABLE_SCHEMA = COALESCE(?, DATABASE())
1020
- ORDER BY c.ORDINAL_POSITION`, [tableName, this._schema])).map((r) => ({
1021
- name: r.COLUMN_NAME,
1022
- type: r.SRS_ID == null ? r.COLUMN_TYPE.toUpperCase() : `${r.COLUMN_TYPE.toUpperCase()} SRID ${r.SRS_ID}`,
1023
- notnull: r.IS_NULLABLE === "NO",
1024
- pk: Boolean(Number(r.IS_PK ?? 0)),
1025
- dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT)
1026
- }));
1061
+ ORDER BY c.ORDINAL_POSITION`, [tableName, this._schema])).map((r) => {
1062
+ const column = {
1063
+ name: r.COLUMN_NAME,
1064
+ type: r.SRS_ID == null ? r.COLUMN_TYPE.toUpperCase() : `${r.COLUMN_TYPE.toUpperCase()} SRID ${r.SRS_ID}`,
1065
+ notnull: r.IS_NULLABLE === "NO",
1066
+ pk: Boolean(Number(r.IS_PK ?? 0)),
1067
+ dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT)
1068
+ };
1069
+ if (/\b(VIRTUAL|STORED) GENERATED\b/i.test(r.EXTRA ?? "")) column.generated = true;
1070
+ return column;
1071
+ });
1027
1072
  }
1028
1073
  async syncColumns(diff) {
1029
- const tableName = this.resolveTableName();
1030
- const added = [];
1031
- const renamed = [];
1032
- for (const { field, oldName } of diff.renamed ?? []) {
1033
- const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
1034
- this._log(ddl);
1035
- await this._exec().exec(ddl);
1036
- renamed.push(field.physicalName);
1037
- }
1038
- const quotedTable = quoteTableName(tableName);
1039
- for (const field of diff.added) {
1040
- const { def, inventedDefault } = buildColumnDefinition(field, this._columnCtx("add"));
1041
- const ddl = `ALTER TABLE ${quotedTable} ADD COLUMN ${def}`;
1042
- this._log(ddl);
1043
- await this._exec().exec(ddl);
1044
- if (inventedDefault) {
1045
- const drop = `ALTER TABLE ${quotedTable} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
1046
- this._log(drop);
1047
- await this._exec().exec(drop);
1074
+ return this._withStrictSession(async (conn) => {
1075
+ const tableName = this.resolveTableName();
1076
+ const added = [];
1077
+ const renamed = [];
1078
+ for (const { field, oldName } of diff.renamed ?? []) {
1079
+ const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
1080
+ this._log(ddl);
1081
+ await conn.exec(ddl);
1082
+ renamed.push(field.physicalName);
1048
1083
  }
1049
- added.push(field.physicalName);
1050
- }
1051
- const enteringKey = new Set(diff.primaryKeyChanged?.to ?? []);
1052
- const modified = /* @__PURE__ */ new Map();
1053
- for (const { field } of diff.typeChanged ?? []) {
1054
- if (enteringKey.has(field.physicalName)) continue;
1055
- const sqlType = this.typeMapper(field);
1056
- if (field.isGeoPoint && !field.encrypted && sqlType.startsWith("POINT")) {
1057
- await this._migrateJsonColumnToPoint(tableName, field);
1058
- continue;
1084
+ const quotedTable = quoteTableName(tableName);
1085
+ for (const field of diff.added) {
1086
+ const { def, inventedDefault } = buildColumnDefinition(field, this._columnCtx("add"));
1087
+ const ddl = `ALTER TABLE ${quotedTable} ADD COLUMN ${def}`;
1088
+ this._log(ddl);
1089
+ await conn.exec(ddl);
1090
+ if (inventedDefault) {
1091
+ const drop = `ALTER TABLE ${quotedTable} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
1092
+ this._log(drop);
1093
+ await conn.exec(drop);
1094
+ }
1095
+ added.push(field.physicalName);
1059
1096
  }
1060
- modified.set(field.physicalName, field);
1061
- }
1062
- for (const { field } of diff.nullableChanged ?? []) {
1063
- if (enteringKey.has(field.physicalName)) continue;
1064
- if (!field.optional) {
1097
+ const enteringKey = new Set(diff.primaryKeyChanged?.to ?? []);
1098
+ const modified = /* @__PURE__ */ new Map();
1099
+ for (const { field } of diff.typeChanged ?? []) {
1100
+ if (enteringKey.has(field.physicalName)) continue;
1065
1101
  const sqlType = this.typeMapper(field);
1066
- const fallback = field.defaultValue?.kind === "value" ? mysqlDefaultLiteral(sqlType, field.designType, field.defaultValue.value) : mysqlTypeDefault(sqlType, field);
1067
- const backfill = `UPDATE ${quotedTable} SET ${qi(field.physicalName)} = ${fallback} WHERE ${qi(field.physicalName)} IS NULL`;
1068
- this._log(backfill);
1069
- await this._exec().exec(backfill);
1102
+ if (field.isGeoPoint && !field.encrypted && sqlType.startsWith("POINT")) {
1103
+ await this._migrateJsonColumnToPoint(conn, tableName, field);
1104
+ continue;
1105
+ }
1106
+ modified.set(field.physicalName, field);
1070
1107
  }
1071
- modified.set(field.physicalName, field);
1072
- }
1073
- for (const { field } of diff.defaultChanged ?? []) if (!enteringKey.has(field.physicalName)) modified.set(field.physicalName, field);
1074
- for (const field of modified.values()) {
1075
- const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
1076
- this._log(ddl);
1077
- await this._exec().exec(ddl);
1078
- }
1079
- return {
1080
- added,
1081
- renamed
1082
- };
1108
+ for (const { field } of diff.nullableChanged ?? []) {
1109
+ if (enteringKey.has(field.physicalName)) continue;
1110
+ if (!field.optional) {
1111
+ const sqlType = this.typeMapper(field);
1112
+ const fallback = field.defaultValue?.kind === "value" ? mysqlDefaultLiteral(sqlType, field.designType, field.defaultValue.value) : mysqlTypeDefault(sqlType, field);
1113
+ const backfill = `UPDATE ${quotedTable} SET ${qi(field.physicalName)} = ${fallback} WHERE ${qi(field.physicalName)} IS NULL`;
1114
+ this._log(backfill);
1115
+ await conn.exec(backfill);
1116
+ }
1117
+ modified.set(field.physicalName, field);
1118
+ }
1119
+ for (const { field } of diff.defaultChanged ?? []) if (!enteringKey.has(field.physicalName)) modified.set(field.physicalName, field);
1120
+ for (const field of modified.values()) {
1121
+ const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
1122
+ this._log(ddl);
1123
+ await conn.exec(ddl);
1124
+ }
1125
+ return {
1126
+ added,
1127
+ renamed
1128
+ };
1129
+ });
1083
1130
  }
1084
1131
  async recreateTable() {
1085
1132
  const tableName = this.resolveTableName();
1086
1133
  const tempName = `${this._table.tableName}__tmp_${Date.now()}`;
1087
- const tx = this._txConnection();
1088
- const conn = tx ?? await this.driver.getConnection();
1089
- const dedicated = tx === void 0;
1090
- await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
1091
- let originalDropped = false;
1092
- try {
1093
- const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, this._tableOptions());
1094
- this._log(createSql);
1095
- await conn.exec(createSql);
1096
- const oldCols = (await this.getExistingColumns()).map((c) => c.name);
1097
- const newCols = this._table.fieldDescriptors.filter((f) => !f.ignored).map((f) => f.physicalName);
1098
- const oldColSet = new Set(oldCols);
1099
- const commonCols = newCols.filter((c) => oldColSet.has(c));
1100
- if (commonCols.length > 0) {
1101
- const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
1102
- const colNames = commonCols.map((c) => qi(c)).join(", ");
1103
- const selectExprs = commonCols.map((c) => {
1104
- const field = fieldsByName.get(c);
1105
- if (field && !field.optional && !field.isPrimaryKey) {
1106
- const fallback = field.defaultValue?.kind === "value" ? (0, _atscript_db_sql_tools.defaultValueToSqlLiteral)(field.designType, field.defaultValue.value) : (0, _atscript_db_sql_tools.defaultValueForType)(field.designType);
1107
- return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
1108
- }
1109
- return qi(c);
1110
- }).join(", ");
1111
- const copySql = `INSERT INTO ${qi(tempName)} (${colNames}) SELECT ${selectExprs} FROM ${quoteTableName(tableName)}`;
1112
- this._log(copySql);
1113
- await conn.exec(copySql);
1134
+ return this._withStrictSession(async (conn) => {
1135
+ await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
1136
+ let originalDropped = false;
1137
+ try {
1138
+ const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, this._tableOptions());
1139
+ this._log(createSql);
1140
+ await conn.exec(createSql);
1141
+ const oldCols = (await this._readColumns(conn, this._table.tableName)).map((c) => c.name);
1142
+ const newCols = this._table.storedDescriptors.map((f) => f.physicalName);
1143
+ const oldColSet = new Set(oldCols);
1144
+ const commonCols = newCols.filter((c) => oldColSet.has(c));
1145
+ if (commonCols.length > 0) {
1146
+ const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
1147
+ const colNames = commonCols.map((c) => qi(c)).join(", ");
1148
+ const selectExprs = commonCols.map((c) => {
1149
+ const field = fieldsByName.get(c);
1150
+ if (field && !field.optional && !field.isPrimaryKey) {
1151
+ const fallback = field.defaultValue?.kind === "value" ? (0, _atscript_db_sql_tools.defaultValueToSqlLiteral)(field.designType, field.defaultValue.value) : (0, _atscript_db_sql_tools.defaultValueForType)(field.designType);
1152
+ return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
1153
+ }
1154
+ return qi(c);
1155
+ }).join(", ");
1156
+ const copySql = `INSERT INTO ${qi(tempName)} (${colNames}) SELECT ${selectExprs} FROM ${quoteTableName(tableName)}`;
1157
+ this._log(copySql);
1158
+ await conn.exec(copySql);
1159
+ }
1160
+ await conn.exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)}`);
1161
+ originalDropped = true;
1162
+ await conn.exec(`RENAME TABLE ${qi(tempName)} TO ${quoteTableName(tableName)}`);
1163
+ } catch (error) {
1164
+ if (originalDropped) throw new Error(`Recreate of "${tableName}" failed after the original table was dropped; its rows are in "${tempName}"`, { cause: error });
1165
+ await conn.exec(`DROP TABLE IF EXISTS ${qi(tempName)}`).catch(() => void 0);
1166
+ throw error;
1167
+ } finally {
1168
+ await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
1114
1169
  }
1115
- await conn.exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)}`);
1116
- originalDropped = true;
1117
- await conn.exec(`RENAME TABLE ${qi(tempName)} TO ${quoteTableName(tableName)}`);
1118
- } catch (error) {
1119
- if (originalDropped) throw new Error(`Recreate of "${tableName}" failed after the original table was dropped; its rows are in "${tempName}"`, { cause: error });
1120
- await conn.exec(`DROP TABLE IF EXISTS ${qi(tempName)}`).catch(() => void 0);
1121
- throw error;
1122
- } finally {
1123
- await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
1124
- if (dedicated) conn.release();
1125
- }
1170
+ });
1126
1171
  }
1127
1172
  async dropTable() {
1128
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())}`;
1129
- this._log(ddl);
1130
- const conn = await this.driver.getConnection();
1131
- await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
1132
- try {
1133
- await conn.exec(ddl);
1134
- } finally {
1135
- await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
1136
- conn.release();
1137
- }
1173
+ return this.dropTableByName(this.resolveTableName());
1138
1174
  }
1139
1175
  async dropColumns(columns) {
1140
1176
  const tableName = this.resolveTableName();
@@ -1522,7 +1558,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1522
1558
  * Migrates a v1 JSON `[lng, lat]` column to native `POINT SRID 4326` via a
1523
1559
  * temp column (MySQL has no JSON→geometry cast for MODIFY COLUMN).
1524
1560
  */
1525
- async _migrateJsonColumnToPoint(tableName, field) {
1561
+ async _migrateJsonColumnToPoint(conn, tableName, field) {
1526
1562
  const col = qi(field.physicalName);
1527
1563
  const tmp = qi(`${field.physicalName}__geo_mig`);
1528
1564
  const quotedTable = quoteTableName(tableName);
@@ -1535,7 +1571,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1535
1571
  ];
1536
1572
  for (const ddl of steps) {
1537
1573
  this._log(ddl);
1538
- await this._exec().exec(ddl);
1574
+ await conn.exec(ddl);
1539
1575
  }
1540
1576
  }
1541
1577
  };
package/dist/index.d.cts CHANGED
@@ -127,6 +127,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
127
127
  * otherwise the pool-based driver.
128
128
  */
129
129
  private _exec;
130
+ /**
131
+ * Runs `fn` on one connection (the transaction's, or a dedicated one) with
132
+ * `STRICT_ALL_TABLES` added to the session `sql_mode`, restoring the
133
+ * previous mode after. Schema sync's converting statements (`MODIFY
134
+ * COLUMN`, the recreate copy) then fail on a value that does not convert
135
+ * instead of coercing or truncating it — a non-strict server (RDS defaults
136
+ * to `NO_ENGINE_SUBSTITUTION`) would turn `'abc'` into `0` silently.
137
+ */
138
+ private _withStrictSession;
130
139
  /** MySQL InnoDB enforces FK constraints natively. */
131
140
  supportsNativeForeignKeys(): boolean;
132
141
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -208,6 +217,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
208
217
  private _ensureView;
209
218
  getExistingColumns(): Promise<TExistingColumn[]>;
210
219
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
220
+ /** Live columns of `tableName`, read through `exec` (a held connection, or the pool). */
221
+ private _readColumns;
211
222
  syncColumns(diff: TColumnDiff): Promise<TSyncColumnResult>;
212
223
  recreateTable(): Promise<void>;
213
224
  dropTable(): Promise<void>;
package/dist/index.d.mts CHANGED
@@ -127,6 +127,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
127
127
  * otherwise the pool-based driver.
128
128
  */
129
129
  private _exec;
130
+ /**
131
+ * Runs `fn` on one connection (the transaction's, or a dedicated one) with
132
+ * `STRICT_ALL_TABLES` added to the session `sql_mode`, restoring the
133
+ * previous mode after. Schema sync's converting statements (`MODIFY
134
+ * COLUMN`, the recreate copy) then fail on a value that does not convert
135
+ * instead of coercing or truncating it — a non-strict server (RDS defaults
136
+ * to `NO_ENGINE_SUBSTITUTION`) would turn `'abc'` into `0` silently.
137
+ */
138
+ private _withStrictSession;
130
139
  /** MySQL InnoDB enforces FK constraints natively. */
131
140
  supportsNativeForeignKeys(): boolean;
132
141
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -208,6 +217,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
208
217
  private _ensureView;
209
218
  getExistingColumns(): Promise<TExistingColumn[]>;
210
219
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
220
+ /** Live columns of `tableName`, read through `exec` (a held connection, or the pool). */
221
+ private _readColumns;
211
222
  syncColumns(diff: TColumnDiff): Promise<TSyncColumnResult>;
212
223
  recreateTable(): Promise<void>;
213
224
  dropTable(): Promise<void>;
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable } from "@atscript/db";
2
2
  import { resolveAggregateSearch } from "@atscript/db/agg";
3
- import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, geoWindowFromControls, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, toSqlValue } from "@atscript/db-sql-tools";
3
+ import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, geoWindowFromControls, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, toSqlValue } from "@atscript/db-sql-tools";
4
4
  import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
5
5
  //#region src/sql-builder.ts
6
6
  /** TEXT / BLOB families (`TINYTEXT` … `LONGBLOB`). */
@@ -93,9 +93,17 @@ function mysqlIndexPrefix(mappedType, bytesPerChar) {
93
93
  * required default-less column on `add`;
94
94
  * 5. COLLATE (native `@db.mysql.collate` or portable `@db.column.collate`);
95
95
  * 6. ON UPDATE.
96
+ *
97
+ * A `@db.column.derived` field (since 0.1.141) renders as a VIRTUAL generated
98
+ * column — `type [COLLATE …] GENERATED ALWAYS AS (<json extraction>) VIRTUAL`
99
+ * — with no nullability, DEFAULT, AUTO_INCREMENT or ON UPDATE, on every purpose.
96
100
  */
97
101
  function buildColumnDefinition(field, ctx) {
98
102
  const sqlType = ctx.typeMapper?.(field) ?? mysqlTypeFromField(field);
103
+ if (field.derived) return {
104
+ def: `${qi(field.physicalName)} ${sqlType}${mysqlCollateClause(field)} GENERATED ALWAYS AS (${derivedColumnExpr(mysqlDialect, field)}) VIRTUAL`,
105
+ inventedDefault: false
106
+ };
99
107
  const increment = ctx.purpose !== "add" && (ctx.incrementFields?.has(field.physicalName) ?? false);
100
108
  let def = `${qi(field.physicalName)} ${sqlType}`;
101
109
  if (increment) def += " AUTO_INCREMENT";
@@ -111,9 +119,7 @@ function buildColumnDefinition(field, ctx) {
111
119
  def += ` DEFAULT ${mysqlTypeDefault(sqlType, field)}`;
112
120
  inventedDefault = true;
113
121
  }
114
- const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
115
- if (nativeCollate) def += ` COLLATE ${nativeCollate}`;
116
- else if (field.collate) def += ` COLLATE ${collationToMysql(field.collate)}`;
122
+ def += mysqlCollateClause(field);
117
123
  const onUpdate = ctx.onUpdateFields?.get(field.physicalName);
118
124
  if (onUpdate) def += ` ON UPDATE ${onUpdate}`;
119
125
  return {
@@ -339,6 +345,15 @@ function buildAggregateCount$1(table, where, controls) {
339
345
  /**
340
346
  * Maps portable collation values to MySQL collation names.
341
347
  */
348
+ /**
349
+ * The `COLLATE` clause of a column (native `@db.mysql.collate`, else the
350
+ * portable `@db.column.collate` mapping) — `""` when none applies.
351
+ */
352
+ function mysqlCollateClause(field) {
353
+ const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
354
+ if (nativeCollate) return ` COLLATE ${nativeCollate}`;
355
+ return field.collate ? ` COLLATE ${collationToMysql(field.collate)}` : "";
356
+ }
342
357
  function collationToMysql(collation) {
343
358
  switch (collation) {
344
359
  case "binary": return "utf8mb4_bin";
@@ -394,7 +409,7 @@ function mysqlTypeFromField(field) {
394
409
  const maxLen = (metadata?.get("expect.maxLength"))?.length;
395
410
  if (maxLen !== void 0 && maxLen <= 65535) return `VARCHAR(${maxLen})`;
396
411
  if (maxLen !== void 0 && maxLen > 65535) return "LONGTEXT";
397
- if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
412
+ if (field.isPrimaryKey || field.defaultValue || field.derived) return "VARCHAR(255)";
398
413
  return "TEXT";
399
414
  }
400
415
  case "json":
@@ -577,6 +592,28 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
577
592
  _exec() {
578
593
  return this._txConnection() ?? this.driver;
579
594
  }
595
+ /**
596
+ * Runs `fn` on one connection (the transaction's, or a dedicated one) with
597
+ * `STRICT_ALL_TABLES` added to the session `sql_mode`, restoring the
598
+ * previous mode after. Schema sync's converting statements (`MODIFY
599
+ * COLUMN`, the recreate copy) then fail on a value that does not convert
600
+ * instead of coercing or truncating it — a non-strict server (RDS defaults
601
+ * to `NO_ENGINE_SUBSTITUTION`) would turn `'abc'` into `0` silently.
602
+ */
603
+ async _withStrictSession(fn) {
604
+ const tx = this._txConnection();
605
+ const conn = tx ?? await this.driver.getConnection();
606
+ try {
607
+ await conn.exec("SET @atscript_sql_mode = @@SESSION.sql_mode, SESSION sql_mode = CONCAT_WS(',', @@SESSION.sql_mode, 'STRICT_ALL_TABLES')");
608
+ try {
609
+ return await fn(conn);
610
+ } finally {
611
+ await conn.exec("SET SESSION sql_mode = @atscript_sql_mode");
612
+ }
613
+ } finally {
614
+ if (!tx) conn.release();
615
+ }
616
+ }
580
617
  /** MySQL InnoDB enforces FK constraints natively. */
581
618
  supportsNativeForeignKeys() {
582
619
  return true;
@@ -835,7 +872,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
835
872
  }
836
873
  async updateOne(filter, data, ops, expectedVersion) {
837
874
  const where = buildWhere(filter);
838
- const versionColumn = this._table.versionColumn;
875
+ const versionColumn = this._table.versionColumnPhysical;
839
876
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, 1, ops, versionColumn, expectedVersion);
840
877
  this._log(sql, params);
841
878
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
@@ -846,7 +883,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
846
883
  }
847
884
  async updateMany(filter, data, ops) {
848
885
  const where = buildWhere(filter);
849
- const versionColumn = this._table.versionColumn;
886
+ const versionColumn = this._table.versionColumnPhysical;
850
887
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, ops, versionColumn);
851
888
  this._log(sql, params);
852
889
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
@@ -857,7 +894,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
857
894
  }
858
895
  async replaceOne(filter, data, expectedVersion) {
859
896
  const where = buildWhere(filter);
860
- const versionColumn = this._table.versionColumn;
897
+ const versionColumn = this._table.versionColumnPhysical;
861
898
  const full = fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn);
862
899
  const { sql, params } = buildUpdate$1(this.resolveTableName(), full, where, 1, void 0, versionColumn, expectedVersion);
863
900
  this._log(sql, params);
@@ -869,7 +906,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
869
906
  }
870
907
  async replaceMany(filter, data) {
871
908
  const where = buildWhere(filter);
872
- const versionColumn = this._table.versionColumn;
909
+ const versionColumn = this._table.versionColumnPhysical;
873
910
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, void 0, versionColumn);
874
911
  this._log(sql, params);
875
912
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
@@ -997,7 +1034,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
997
1034
  if (clauses.length === 0) return;
998
1035
  const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ${clauses.join(", ")}`;
999
1036
  this._log(ddl);
1000
- await this._exec().exec(ddl);
1037
+ await this._withStrictSession((conn) => conn.exec(ddl));
1001
1038
  }
1002
1039
  async _ensureView() {
1003
1040
  const view = this._table;
@@ -1009,131 +1046,130 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1009
1046
  return this.getExistingColumnsForTable(this._table.tableName);
1010
1047
  }
1011
1048
  async getExistingColumnsForTable(tableName) {
1012
- return (await this._exec().all(`SELECT c.COLUMN_NAME, c.COLUMN_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.SRS_ID,
1049
+ return this._readColumns(this._exec(), tableName);
1050
+ }
1051
+ /** Live columns of `tableName`, read through `exec` (a held connection, or the pool). */
1052
+ async _readColumns(exec, tableName) {
1053
+ return (await exec.all(`SELECT c.COLUMN_NAME, c.COLUMN_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT, c.SRS_ID, c.EXTRA,
1013
1054
  (k.COLUMN_NAME IS NOT NULL) AS IS_PK
1014
1055
  FROM INFORMATION_SCHEMA.COLUMNS c
1015
1056
  LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
1016
1057
  ON k.TABLE_SCHEMA = c.TABLE_SCHEMA AND k.TABLE_NAME = c.TABLE_NAME
1017
1058
  AND k.COLUMN_NAME = c.COLUMN_NAME AND k.CONSTRAINT_NAME = 'PRIMARY'
1018
1059
  WHERE c.TABLE_NAME = ? AND c.TABLE_SCHEMA = COALESCE(?, DATABASE())
1019
- ORDER BY c.ORDINAL_POSITION`, [tableName, this._schema])).map((r) => ({
1020
- name: r.COLUMN_NAME,
1021
- type: r.SRS_ID == null ? r.COLUMN_TYPE.toUpperCase() : `${r.COLUMN_TYPE.toUpperCase()} SRID ${r.SRS_ID}`,
1022
- notnull: r.IS_NULLABLE === "NO",
1023
- pk: Boolean(Number(r.IS_PK ?? 0)),
1024
- dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT)
1025
- }));
1060
+ ORDER BY c.ORDINAL_POSITION`, [tableName, this._schema])).map((r) => {
1061
+ const column = {
1062
+ name: r.COLUMN_NAME,
1063
+ type: r.SRS_ID == null ? r.COLUMN_TYPE.toUpperCase() : `${r.COLUMN_TYPE.toUpperCase()} SRID ${r.SRS_ID}`,
1064
+ notnull: r.IS_NULLABLE === "NO",
1065
+ pk: Boolean(Number(r.IS_PK ?? 0)),
1066
+ dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT)
1067
+ };
1068
+ if (/\b(VIRTUAL|STORED) GENERATED\b/i.test(r.EXTRA ?? "")) column.generated = true;
1069
+ return column;
1070
+ });
1026
1071
  }
1027
1072
  async syncColumns(diff) {
1028
- const tableName = this.resolveTableName();
1029
- const added = [];
1030
- const renamed = [];
1031
- for (const { field, oldName } of diff.renamed ?? []) {
1032
- const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
1033
- this._log(ddl);
1034
- await this._exec().exec(ddl);
1035
- renamed.push(field.physicalName);
1036
- }
1037
- const quotedTable = quoteTableName(tableName);
1038
- for (const field of diff.added) {
1039
- const { def, inventedDefault } = buildColumnDefinition(field, this._columnCtx("add"));
1040
- const ddl = `ALTER TABLE ${quotedTable} ADD COLUMN ${def}`;
1041
- this._log(ddl);
1042
- await this._exec().exec(ddl);
1043
- if (inventedDefault) {
1044
- const drop = `ALTER TABLE ${quotedTable} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
1045
- this._log(drop);
1046
- await this._exec().exec(drop);
1073
+ return this._withStrictSession(async (conn) => {
1074
+ const tableName = this.resolveTableName();
1075
+ const added = [];
1076
+ const renamed = [];
1077
+ for (const { field, oldName } of diff.renamed ?? []) {
1078
+ const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
1079
+ this._log(ddl);
1080
+ await conn.exec(ddl);
1081
+ renamed.push(field.physicalName);
1047
1082
  }
1048
- added.push(field.physicalName);
1049
- }
1050
- const enteringKey = new Set(diff.primaryKeyChanged?.to ?? []);
1051
- const modified = /* @__PURE__ */ new Map();
1052
- for (const { field } of diff.typeChanged ?? []) {
1053
- if (enteringKey.has(field.physicalName)) continue;
1054
- const sqlType = this.typeMapper(field);
1055
- if (field.isGeoPoint && !field.encrypted && sqlType.startsWith("POINT")) {
1056
- await this._migrateJsonColumnToPoint(tableName, field);
1057
- continue;
1083
+ const quotedTable = quoteTableName(tableName);
1084
+ for (const field of diff.added) {
1085
+ const { def, inventedDefault } = buildColumnDefinition(field, this._columnCtx("add"));
1086
+ const ddl = `ALTER TABLE ${quotedTable} ADD COLUMN ${def}`;
1087
+ this._log(ddl);
1088
+ await conn.exec(ddl);
1089
+ if (inventedDefault) {
1090
+ const drop = `ALTER TABLE ${quotedTable} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
1091
+ this._log(drop);
1092
+ await conn.exec(drop);
1093
+ }
1094
+ added.push(field.physicalName);
1058
1095
  }
1059
- modified.set(field.physicalName, field);
1060
- }
1061
- for (const { field } of diff.nullableChanged ?? []) {
1062
- if (enteringKey.has(field.physicalName)) continue;
1063
- if (!field.optional) {
1096
+ const enteringKey = new Set(diff.primaryKeyChanged?.to ?? []);
1097
+ const modified = /* @__PURE__ */ new Map();
1098
+ for (const { field } of diff.typeChanged ?? []) {
1099
+ if (enteringKey.has(field.physicalName)) continue;
1064
1100
  const sqlType = this.typeMapper(field);
1065
- const fallback = field.defaultValue?.kind === "value" ? mysqlDefaultLiteral(sqlType, field.designType, field.defaultValue.value) : mysqlTypeDefault(sqlType, field);
1066
- const backfill = `UPDATE ${quotedTable} SET ${qi(field.physicalName)} = ${fallback} WHERE ${qi(field.physicalName)} IS NULL`;
1067
- this._log(backfill);
1068
- await this._exec().exec(backfill);
1101
+ if (field.isGeoPoint && !field.encrypted && sqlType.startsWith("POINT")) {
1102
+ await this._migrateJsonColumnToPoint(conn, tableName, field);
1103
+ continue;
1104
+ }
1105
+ modified.set(field.physicalName, field);
1069
1106
  }
1070
- modified.set(field.physicalName, field);
1071
- }
1072
- for (const { field } of diff.defaultChanged ?? []) if (!enteringKey.has(field.physicalName)) modified.set(field.physicalName, field);
1073
- for (const field of modified.values()) {
1074
- const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
1075
- this._log(ddl);
1076
- await this._exec().exec(ddl);
1077
- }
1078
- return {
1079
- added,
1080
- renamed
1081
- };
1107
+ for (const { field } of diff.nullableChanged ?? []) {
1108
+ if (enteringKey.has(field.physicalName)) continue;
1109
+ if (!field.optional) {
1110
+ const sqlType = this.typeMapper(field);
1111
+ const fallback = field.defaultValue?.kind === "value" ? mysqlDefaultLiteral(sqlType, field.designType, field.defaultValue.value) : mysqlTypeDefault(sqlType, field);
1112
+ const backfill = `UPDATE ${quotedTable} SET ${qi(field.physicalName)} = ${fallback} WHERE ${qi(field.physicalName)} IS NULL`;
1113
+ this._log(backfill);
1114
+ await conn.exec(backfill);
1115
+ }
1116
+ modified.set(field.physicalName, field);
1117
+ }
1118
+ for (const { field } of diff.defaultChanged ?? []) if (!enteringKey.has(field.physicalName)) modified.set(field.physicalName, field);
1119
+ for (const field of modified.values()) {
1120
+ const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
1121
+ this._log(ddl);
1122
+ await conn.exec(ddl);
1123
+ }
1124
+ return {
1125
+ added,
1126
+ renamed
1127
+ };
1128
+ });
1082
1129
  }
1083
1130
  async recreateTable() {
1084
1131
  const tableName = this.resolveTableName();
1085
1132
  const tempName = `${this._table.tableName}__tmp_${Date.now()}`;
1086
- const tx = this._txConnection();
1087
- const conn = tx ?? await this.driver.getConnection();
1088
- const dedicated = tx === void 0;
1089
- await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
1090
- let originalDropped = false;
1091
- try {
1092
- const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, this._tableOptions());
1093
- this._log(createSql);
1094
- await conn.exec(createSql);
1095
- const oldCols = (await this.getExistingColumns()).map((c) => c.name);
1096
- const newCols = this._table.fieldDescriptors.filter((f) => !f.ignored).map((f) => f.physicalName);
1097
- const oldColSet = new Set(oldCols);
1098
- const commonCols = newCols.filter((c) => oldColSet.has(c));
1099
- if (commonCols.length > 0) {
1100
- const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
1101
- const colNames = commonCols.map((c) => qi(c)).join(", ");
1102
- const selectExprs = commonCols.map((c) => {
1103
- const field = fieldsByName.get(c);
1104
- if (field && !field.optional && !field.isPrimaryKey) {
1105
- const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
1106
- return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
1107
- }
1108
- return qi(c);
1109
- }).join(", ");
1110
- const copySql = `INSERT INTO ${qi(tempName)} (${colNames}) SELECT ${selectExprs} FROM ${quoteTableName(tableName)}`;
1111
- this._log(copySql);
1112
- await conn.exec(copySql);
1133
+ return this._withStrictSession(async (conn) => {
1134
+ await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
1135
+ let originalDropped = false;
1136
+ try {
1137
+ const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, this._tableOptions());
1138
+ this._log(createSql);
1139
+ await conn.exec(createSql);
1140
+ const oldCols = (await this._readColumns(conn, this._table.tableName)).map((c) => c.name);
1141
+ const newCols = this._table.storedDescriptors.map((f) => f.physicalName);
1142
+ const oldColSet = new Set(oldCols);
1143
+ const commonCols = newCols.filter((c) => oldColSet.has(c));
1144
+ if (commonCols.length > 0) {
1145
+ const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
1146
+ const colNames = commonCols.map((c) => qi(c)).join(", ");
1147
+ const selectExprs = commonCols.map((c) => {
1148
+ const field = fieldsByName.get(c);
1149
+ if (field && !field.optional && !field.isPrimaryKey) {
1150
+ const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
1151
+ return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
1152
+ }
1153
+ return qi(c);
1154
+ }).join(", ");
1155
+ const copySql = `INSERT INTO ${qi(tempName)} (${colNames}) SELECT ${selectExprs} FROM ${quoteTableName(tableName)}`;
1156
+ this._log(copySql);
1157
+ await conn.exec(copySql);
1158
+ }
1159
+ await conn.exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)}`);
1160
+ originalDropped = true;
1161
+ await conn.exec(`RENAME TABLE ${qi(tempName)} TO ${quoteTableName(tableName)}`);
1162
+ } catch (error) {
1163
+ if (originalDropped) throw new Error(`Recreate of "${tableName}" failed after the original table was dropped; its rows are in "${tempName}"`, { cause: error });
1164
+ await conn.exec(`DROP TABLE IF EXISTS ${qi(tempName)}`).catch(() => void 0);
1165
+ throw error;
1166
+ } finally {
1167
+ await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
1113
1168
  }
1114
- await conn.exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)}`);
1115
- originalDropped = true;
1116
- await conn.exec(`RENAME TABLE ${qi(tempName)} TO ${quoteTableName(tableName)}`);
1117
- } catch (error) {
1118
- if (originalDropped) throw new Error(`Recreate of "${tableName}" failed after the original table was dropped; its rows are in "${tempName}"`, { cause: error });
1119
- await conn.exec(`DROP TABLE IF EXISTS ${qi(tempName)}`).catch(() => void 0);
1120
- throw error;
1121
- } finally {
1122
- await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
1123
- if (dedicated) conn.release();
1124
- }
1169
+ });
1125
1170
  }
1126
1171
  async dropTable() {
1127
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())}`;
1128
- this._log(ddl);
1129
- const conn = await this.driver.getConnection();
1130
- await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
1131
- try {
1132
- await conn.exec(ddl);
1133
- } finally {
1134
- await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
1135
- conn.release();
1136
- }
1172
+ return this.dropTableByName(this.resolveTableName());
1137
1173
  }
1138
1174
  async dropColumns(columns) {
1139
1175
  const tableName = this.resolveTableName();
@@ -1521,7 +1557,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1521
1557
  * Migrates a v1 JSON `[lng, lat]` column to native `POINT SRID 4326` via a
1522
1558
  * temp column (MySQL has no JSON→geometry cast for MODIFY COLUMN).
1523
1559
  */
1524
- async _migrateJsonColumnToPoint(tableName, field) {
1560
+ async _migrateJsonColumnToPoint(conn, tableName, field) {
1525
1561
  const col = qi(field.physicalName);
1526
1562
  const tmp = qi(`${field.physicalName}__geo_mig`);
1527
1563
  const quotedTable = quoteTableName(tableName);
@@ -1534,7 +1570,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1534
1570
  ];
1535
1571
  for (const ddl of steps) {
1536
1572
  this._log(ddl);
1537
- await this._exec().exec(ddl);
1573
+ await conn.exec(ddl);
1538
1574
  }
1539
1575
  }
1540
1576
  };
package/dist/plugin.cjs CHANGED
@@ -14,6 +14,7 @@ const annotations = {
14
14
  engine: new _atscript_core.AnnotationSpec({
15
15
  description: "Specifies the MySQL storage engine.\n\n**Default:** `\"InnoDB\"`\n\n```atscript\n@db.mysql.engine \"MyISAM\"\nexport interface Logs { ... }\n```",
16
16
  nodeType: ["interface"],
17
+ passedWhenReferred: false,
17
18
  multiple: false,
18
19
  argument: {
19
20
  name: "engine",
package/dist/plugin.mjs CHANGED
@@ -13,6 +13,7 @@ const annotations = {
13
13
  engine: new AnnotationSpec({
14
14
  description: "Specifies the MySQL storage engine.\n\n**Default:** `\"InnoDB\"`\n\n```atscript\n@db.mysql.engine \"MyISAM\"\nexport interface Logs { ... }\n```",
15
15
  nodeType: ["interface"],
16
+ passedWhenReferred: false,
16
17
  multiple: false,
17
18
  argument: {
18
19
  name: "engine",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mysql",
3
- "version": "0.1.139",
3
+ "version": "0.1.141",
4
4
  "description": "MySQL adapter for @atscript/db with mysql2 driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -41,19 +41,19 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@atscript/core": "^0.1.92",
45
- "@atscript/typescript": "^0.1.92",
44
+ "@atscript/core": "^0.1.95",
45
+ "@atscript/typescript": "^0.1.95",
46
46
  "@uniqu/core": "^0.1.11",
47
47
  "mysql2": "^3.11.0",
48
- "unplugin-atscript": "^0.1.92"
48
+ "unplugin-atscript": "^0.1.95"
49
49
  },
50
50
  "peerDependencies": {
51
- "@atscript/core": "^0.1.92",
52
- "@atscript/typescript": "^0.1.92",
51
+ "@atscript/core": "^0.1.95",
52
+ "@atscript/typescript": "^0.1.95",
53
53
  "@uniqu/core": "^0.1.11",
54
54
  "mysql2": ">=3.0.0",
55
- "@atscript/db": "^0.1.139",
56
- "@atscript/db-sql-tools": "^0.1.139"
55
+ "@atscript/db": "^0.1.141",
56
+ "@atscript/db-sql-tools": "^0.1.141"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "mysql2": {