@mikro-orm/sql 7.1.2-dev.9 → 7.1.3-dev.0

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.
@@ -510,8 +510,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
510
510
  else if (prop.fieldNames.length > 1) {
511
511
  // composite keys
512
512
  const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
513
+ // `mapFlatCompositePrimaryKey` collapses to a scalar when the target PK is a single prop
514
+ // (e.g. a relation referencing a composite unique key on a single-PK target).
513
515
  const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
514
- relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
516
+ const valid = Array.isArray(pk) ? pk.every(val => val != null) : pk != null;
517
+ relationPojo[prop.name] = valid ? pk : null;
515
518
  }
516
519
  else if (prop.runtimeType === 'Date') {
517
520
  const alias = `${relationAlias}__${prop.fieldNames[0]}`;
@@ -126,6 +126,11 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
126
126
  if (['interval'].includes(simpleType)) {
127
127
  return this.getIntervalTypeDeclarationSQL(options);
128
128
  }
129
+ // postgis reports the typmod without spaces (`geometry(point,4326)`) — collapse any
130
+ // user-supplied spacing so the metadata and introspection forms compare equal
131
+ if (['geometry', 'geography'].includes(simpleType)) {
132
+ return type.toLowerCase().replace(/\s+/g, '');
133
+ }
129
134
  // TimeType.getColumnType drops the timezone qualifier, so detect tz aliases from the original column type.
130
135
  const originalType = options.columnTypes?.[0]?.toLowerCase() ?? type;
131
136
  if (/^timetz\b/.test(originalType) || /^time\s+with\s+time\s+zone\b/.test(originalType)) {
@@ -370,6 +370,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
370
370
  is_nullable,
371
371
  udt_name,
372
372
  udt_schema,
373
+ pg_catalog.format_type(pga.atttypid, pga.atttypmod) format_type,
373
374
  coalesce(datetime_precision, character_maximum_length) length,
374
375
  atttypmod custom_length,
375
376
  numeric_precision,
@@ -404,6 +405,11 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
404
405
  if (type === 'bpchar') {
405
406
  type = 'char';
406
407
  }
408
+ // postgis stores the geometry/geography typmod (`geometry(point,4326)`) in atttypmod, which
409
+ // information_schema does not expose — recover the full type via format_type so it round-trips
410
+ if ((col.udt_name === 'geometry' || col.udt_name === 'geography') && col.format_type) {
411
+ type = col.format_type;
412
+ }
407
413
  if (type === 'vector' && col.length == null && col.custom_length != null && col.custom_length !== -1) {
408
414
  col.length = col.custom_length;
409
415
  }
@@ -11,6 +11,7 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
11
11
  usesDefaultKeyword(): boolean;
12
12
  usesReturningStatement(): boolean;
13
13
  usesEnumCheckConstraints(): boolean;
14
+ supportsComments(): boolean;
14
15
  getCurrentTimestampSQL(length: number): string;
15
16
  getDateTimeTypeDeclarationSQL(column: {
16
17
  length: number;
@@ -18,6 +18,10 @@ export class SqlitePlatform extends AbstractSqlPlatform {
18
18
  usesEnumCheckConstraints() {
19
19
  return true;
20
20
  }
21
+ // sqlite has no table/column comments, so they cannot round-trip through introspection
22
+ supportsComments() {
23
+ return false;
24
+ }
21
25
  getCurrentTimestampSQL(length) {
22
26
  return `(strftime('%s', 'now') * 1000)`;
23
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.2-dev.9",
3
+ "version": "7.1.3-dev.0",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -50,10 +50,10 @@
50
50
  "kysely": "0.29.2"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.1.1"
53
+ "@mikro-orm/core": "^7.1.2"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.2-dev.9"
56
+ "@mikro-orm/core": "7.1.3-dev.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -233,7 +233,9 @@ export class DatabaseSchema {
233
233
  ? platform.formatQuery(check.expression.sql, check.expression.params)
234
234
  : check.expression;
235
235
  table.addCheck({
236
- name: check.name,
236
+ // the engine truncates identifiers past its limit (postgres 63 bytes) on storage, so
237
+ // mirror that here — otherwise the introspected (truncated) name never matches metadata
238
+ name: check.name.substring(0, platform.getMaxIdentifierLength()),
237
239
  expression,
238
240
  definition: `check (${expression})`,
239
241
  columnName,
@@ -975,6 +975,7 @@ export class DatabaseTable {
975
975
  mappedType instanceof t.float ||
976
976
  mappedType instanceof t.double;
977
977
  const supportsUnsigned = this.#platform.supportsUnsigned();
978
+ const supportsComments = this.#platform.supportsComments();
978
979
  const columnsMapped = sortedColumnKeys.reduce((o, col) => {
979
980
  const c = columns[col];
980
981
  // omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
@@ -982,6 +983,15 @@ export class DatabaseTable {
982
983
  const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
983
984
  const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
984
985
  const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
986
+ // only emit `length` for types that actually use one — text/enum/json/etc. don't, but mysql
987
+ // information_schema still reports `character_maximum_length` for them (65535 for text, etc.)
988
+ const hasMeaningfulLength = typeof c.mappedType.getDefaultLength !== 'undefined';
989
+ // mysql stores decimal defaults padded to scale (`0` → `0.00`); collapse to canonical numeric form
990
+ // so the metadata-side (`0`) and introspection-side (`0.00`) snapshots agree
991
+ let defaultValue = c.default ?? null;
992
+ if (defaultValue != null && c.mappedType instanceof DecimalType && Number.isFinite(+defaultValue)) {
993
+ defaultValue = this.#platform.formatDecimal(defaultValue, c.scale).toString();
994
+ }
985
995
  const normalized = {
986
996
  name: c.name,
987
997
  type,
@@ -990,26 +1000,26 @@ export class DatabaseTable {
990
1000
  primary: primaryColumns.has(c.name) || !!c.primary,
991
1001
  nullable: !!c.nullable,
992
1002
  unique: uniqueColumns.has(c.name) || !!c.unique,
993
- length: c.length || null,
1003
+ length: hasMeaningfulLength ? c.length || null : null,
994
1004
  precision: fixedPrecision ? null : (c.precision ?? null),
995
1005
  scale: fixedPrecision ? null : (c.scale ?? null),
996
- default: c.default ?? null,
997
- comment: c.comment ?? null,
1006
+ default: defaultValue,
1007
+ comment: supportsComments ? c.comment || null : null,
998
1008
  collation: c.collation ?? null,
999
1009
  enumItems: c.enumItems ?? [],
1000
1010
  mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
1001
1011
  };
1002
- for (const field of [
1003
- 'generated',
1004
- 'nativeEnumName',
1005
- 'extra',
1006
- 'ignoreSchemaChanges',
1007
- 'defaultConstraint',
1008
- ]) {
1012
+ for (const field of ['generated', 'nativeEnumName', 'ignoreSchemaChanges', 'defaultConstraint']) {
1009
1013
  if (c[field]) {
1010
1014
  normalized[field] = c[field];
1011
1015
  }
1012
1016
  }
1017
+ // `extra` casing varies between metadata (user-supplied, typically uppercase like
1018
+ // `ON UPDATE CURRENT_TIMESTAMP`) and mysql introspection (returns it lowercased) — the
1019
+ // comparator already lowercases both sides when diffing, mirror that in the snapshot
1020
+ if (c.extra) {
1021
+ normalized.extra = c.extra.toLowerCase();
1022
+ }
1013
1023
  o[col] = normalized;
1014
1024
  return o;
1015
1025
  }, {});
@@ -1082,8 +1092,10 @@ export class DatabaseTable {
1082
1092
  checks: sortedChecks,
1083
1093
  triggers: sortedTriggers,
1084
1094
  foreignKeys: sortedForeignKeys,
1085
- // emit `comment` even when unset so introspection (which always reads it) matches metadata
1086
- comment: this.comment ?? null,
1095
+ // emit `comment` even when unset so introspection (which always reads it) matches metadata;
1096
+ // collapse mysql's `""` for unset comments to `null` so both sources agree. drop entirely on
1097
+ // platforms that can't read comments back (sqlite), where keeping it would flip the snapshot
1098
+ comment: supportsComments ? this.comment || null : null,
1087
1099
  };
1088
1100
  }
1089
1101
  }
@@ -1,4 +1,4 @@
1
- import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core';
1
+ import { ArrayType, BooleanType, DateTimeType, DecimalType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core';
2
2
  import { DatabaseTable } from './DatabaseTable.js';
3
3
  import { diffPartitioning } from './partitioning.js';
4
4
  /**
@@ -339,7 +339,7 @@ export class SchemaComparator {
339
339
  fromTable,
340
340
  toTable,
341
341
  };
342
- if (this.diffComment(fromTable.comment, toTable.comment)) {
342
+ if (this.#platform.supportsComments() && this.diffComment(fromTable.comment, toTable.comment)) {
343
343
  tableDifferences.changedComment = toTable.comment;
344
344
  this.log(`table comment changed for ${tableDifferences.name}`, {
345
345
  fromTableComment: fromTable.comment,
@@ -713,7 +713,7 @@ export class SchemaComparator {
713
713
  log(`'default' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
714
714
  changedProperties.add('default');
715
715
  }
716
- if (this.diffComment(fromColumn.comment, toColumn.comment)) {
716
+ if (this.#platform.supportsComments() && this.diffComment(fromColumn.comment, toColumn.comment)) {
717
717
  log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
718
718
  changedProperties.add('comment');
719
719
  }
@@ -996,6 +996,11 @@ export class SchemaComparator {
996
996
  const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
997
997
  return defaultValueFrom === defaultValueTo;
998
998
  }
999
+ // mysql stores decimal defaults padded to scale (`0` → `0.00`); compare numerically so the
1000
+ // entity-side raw literal and the introspected padded form don't churn the no-op migration
1001
+ if (to.mappedType instanceof DecimalType && Number.isFinite(+from.default) && Number.isFinite(+to.default)) {
1002
+ return (this.#platform.formatDecimal(from.default, to.scale) === this.#platform.formatDecimal(to.default, to.scale));
1003
+ }
999
1004
  if (from.default && to.default) {
1000
1005
  return from.default.toString().toLowerCase() === to.default.toString().toLowerCase();
1001
1006
  }