@mikro-orm/sql 7.1.0-dev.20 → 7.1.0-dev.21

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.
@@ -47,6 +47,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
47
47
  precision?: number;
48
48
  scale?: number;
49
49
  autoincrement?: boolean;
50
+ columnTypes?: string[];
50
51
  }): string;
51
52
  getMappedType(type: string): Type<unknown>;
52
53
  getRegExpOperator(val?: unknown, flags?: string): string;
@@ -93,18 +93,27 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
93
93
  }
94
94
  normalizeColumnType(type, options) {
95
95
  const simpleType = this.extractSimpleType(type);
96
- if (['int', 'int4', 'integer'].includes(simpleType)) {
96
+ if (['int', 'int4', 'integer', 'serial'].includes(simpleType)) {
97
97
  return this.getIntegerTypeDeclarationSQL({});
98
98
  }
99
- if (['bigint', 'int8'].includes(simpleType)) {
99
+ if (['bigint', 'int8', 'bigserial'].includes(simpleType)) {
100
100
  return this.getBigIntTypeDeclarationSQL({});
101
101
  }
102
- if (['smallint', 'int2'].includes(simpleType)) {
102
+ if (['smallint', 'int2', 'smallserial'].includes(simpleType)) {
103
103
  return this.getSmallIntTypeDeclarationSQL({});
104
104
  }
105
105
  if (['boolean', 'bool'].includes(simpleType)) {
106
106
  return this.getBooleanTypeDeclarationSQL();
107
107
  }
108
+ if (['double', 'double precision', 'float8'].includes(simpleType)) {
109
+ return this.getDoubleDeclarationSQL();
110
+ }
111
+ if (['real', 'float4'].includes(simpleType)) {
112
+ return this.getFloatDeclarationSQL();
113
+ }
114
+ if (['timestamptz', 'timestamp with time zone'].includes(simpleType)) {
115
+ return this.getDateTimeTypeDeclarationSQL(options);
116
+ }
108
117
  if (['varchar', 'character varying'].includes(simpleType)) {
109
118
  return this.getVarcharTypeDeclarationSQL(options);
110
119
  }
@@ -117,6 +126,12 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
117
126
  if (['interval'].includes(simpleType)) {
118
127
  return this.getIntervalTypeDeclarationSQL(options);
119
128
  }
129
+ // TimeType.getColumnType drops the timezone qualifier, so detect tz aliases from the original column type.
130
+ const originalType = options.columnTypes?.[0]?.toLowerCase() ?? type;
131
+ if (/^timetz\b/.test(originalType) || /^time\s+with\s+time\s+zone\b/.test(originalType)) {
132
+ const length = options.length ?? this.getDefaultDateTimeLength();
133
+ return `timetz(${length})`;
134
+ }
120
135
  return super.normalizeColumnType(type, options);
121
136
  }
122
137
  getMappedType(type) {
@@ -15,6 +15,7 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
15
15
  getDateTimeTypeDeclarationSQL(column: {
16
16
  length: number;
17
17
  }): string;
18
+ getDefaultVersionLength(): number;
18
19
  getBeginTransactionSQL(options?: {
19
20
  isolationLevel?: IsolationLevel;
20
21
  readOnly?: boolean;
@@ -24,6 +24,10 @@ export class SqlitePlatform extends AbstractSqlPlatform {
24
24
  getDateTimeTypeDeclarationSQL(column) {
25
25
  return 'datetime';
26
26
  }
27
+ // sqlite's datetime DDL drops precision and the current-ts expression hardcodes ms scaling
28
+ getDefaultVersionLength() {
29
+ return 0;
30
+ }
27
31
  getBeginTransactionSQL(options) {
28
32
  return ['begin'];
29
33
  }
@@ -45,7 +45,8 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
45
45
  * We need to add them back so they match what we generate in DDL.
46
46
  */
47
47
  private wrapExpressionDefault;
48
- private getEnumDefinitions;
48
+ /** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
49
+ private extractEnumValuesFromChecks;
49
50
  getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[], tableName: string, schemaName?: string, ctx?: Transaction): Promise<string[]>;
50
51
  private getIndexes;
51
52
  private getChecks;
@@ -101,7 +101,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
101
101
  const checks = await this.getChecks(connection, table.name, table.schema, ctx);
102
102
  const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema, ctx);
103
103
  const fks = await this.getForeignKeys(connection, table.name, table.schema, ctx);
104
- const enums = await this.getEnumDefinitions(connection, table.name, table.schema, ctx);
104
+ const enums = this.extractEnumValuesFromChecks(checks);
105
105
  const triggers = await this.getTableTriggers(connection, table.name);
106
106
  table.init(cols, indexes, checks, pks, fks, enums);
107
107
  table.setTriggers(triggers);
@@ -327,23 +327,23 @@ export class SqliteSchemaHelper extends SchemaHelper {
327
327
  // everything else is an expression that had its outer parens stripped
328
328
  return `(${value})`;
329
329
  }
330
- async getEnumDefinitions(connection, tableName, schemaName, ctx) {
331
- const prefix = this.getSchemaPrefix(schemaName);
332
- const sql = `select sql from ${prefix}sqlite_master where type = ? and name = ?`;
333
- const tableDefinition = await connection.execute(sql, ['table', tableName], 'get', ctx);
334
- const checkConstraints = [...(tableDefinition.sql.match(/[`["'][^`\]"']+[`\]"'] text check \(.*?\)/gi) ?? [])];
335
- return checkConstraints.reduce((o, item) => {
336
- // check constraints are defined as (note that last closing paren is missing):
337
- // `type` text check (`type` in ('local', 'global')
338
- const match = /[`["']([^`\]"']+)[`\]"'] text check \(.* \((.*)\)/i.exec(item);
339
- /* v8 ignore next */
340
- if (match) {
341
- o[match[1]] = match[2]
342
- .split(/,(?=\s*'(?:[^']|'')*'(?:\s*\)|$))/)
343
- .map((item) => /^\(?'((?:[^']|'')*)'/.exec(item.trim())[1].replace(/''/g, "'"));
330
+ /** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
331
+ extractEnumValuesFromChecks(checks) {
332
+ const result = {};
333
+ for (const check of checks) {
334
+ if (!check.columnName || typeof check.expression !== 'string') {
335
+ continue;
344
336
  }
345
- return o;
346
- }, {});
337
+ const inClause = /\bin\s*\(([^)]*)\)/i.exec(check.expression);
338
+ if (!inClause) {
339
+ continue;
340
+ }
341
+ const items = [...inClause[1].matchAll(/'((?:[^']|'')*)'/g)].map(m => m[1].replace(/''/g, "'"));
342
+ if (items.length > 0) {
343
+ result[check.columnName] = items;
344
+ }
345
+ }
346
+ return result;
347
347
  }
348
348
  async getPrimaryKeys(connection, indexes, tableName, schemaName, ctx) {
349
349
  const prefix = this.getSchemaPrefix(schemaName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.0-dev.20",
3
+ "version": "7.1.0-dev.21",
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",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.0.12"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.0-dev.20"
56
+ "@mikro-orm/core": "7.1.0-dev.21"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -341,12 +341,16 @@ export class DatabaseSchema {
341
341
  (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner));
342
342
  }
343
343
  toJSON() {
344
+ // locale-independent comparison so the snapshot is stable across machines
345
+ const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
346
+ const tableKey = (t) => `${t.schema ?? ''}.${t.name}`;
347
+ const byTable = (a, b) => byString(tableKey(a), tableKey(b));
344
348
  return {
345
349
  name: this.name,
346
- namespaces: [...this.#namespaces],
347
- tables: this.#tables,
348
- views: this.#views,
349
- nativeEnums: this.#nativeEnums,
350
+ namespaces: [...this.#namespaces].sort(),
351
+ tables: [...this.#tables].sort(byTable),
352
+ views: [...this.#views].sort(byTable),
353
+ nativeEnums: Object.fromEntries(Object.entries(this.#nativeEnums).sort(([a], [b]) => byString(a, b))),
350
354
  };
351
355
  }
352
356
  prune(schema, wildcardSchemaTables) {
@@ -68,6 +68,7 @@ export class DatabaseTable {
68
68
  this.#indexes = indexes;
69
69
  this.#checks = checks;
70
70
  this.#foreignKeys = fks;
71
+ const helper = this.#platform.getSchemaHelper();
71
72
  this.#columns = cols.reduce((o, v) => {
72
73
  const index = indexes.filter(i => i.columnNames[0] === v.name);
73
74
  v.primary = v.primary || pks.includes(v.name);
@@ -76,6 +77,11 @@ export class DatabaseTable {
76
77
  v.mappedType = this.#platform.getMappedType(type);
77
78
  v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
78
79
  v.enumItems ??= enums[v.name] || [];
80
+ // recover length from the declared type so introspection matches `addColumnFromProperty`;
81
+ // scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
82
+ if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
83
+ v.length = helper.inferLengthFromColumnType(v.type);
84
+ }
79
85
  o[v.name] = v;
80
86
  return o;
81
87
  }, {});
@@ -85,7 +91,9 @@ export class DatabaseTable {
85
91
  }
86
92
  addColumnFromProperty(prop, meta, config) {
87
93
  prop.fieldNames?.forEach((field, idx) => {
88
- const type = prop.enum ? 'enum' : prop.columnTypes[idx];
94
+ // numeric enums fall through to the underlying numeric type no platform emits a CHECK we could parse back
95
+ const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
96
+ const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
89
97
  const mappedType = this.#platform.getMappedType(type);
90
98
  if (mappedType instanceof DecimalType) {
91
99
  const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
@@ -933,52 +941,137 @@ export class DatabaseTable {
933
941
  }
934
942
  toJSON() {
935
943
  const columns = this.#columns;
936
- const columnsMapped = Utils.keys(columns).reduce((o, col) => {
944
+ // locale-independent comparison so the snapshot is stable across machines
945
+ const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
946
+ const sortedColumnKeys = Utils.keys(columns).sort(byString);
947
+ // mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
948
+ // so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
949
+ const primaryColumns = new Set();
950
+ const uniqueColumns = new Set();
951
+ for (const idx of this.#indexes) {
952
+ if (idx.primary) {
953
+ idx.columnNames.forEach(c => primaryColumns.add(c));
954
+ }
955
+ if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
956
+ uniqueColumns.add(idx.columnNames[0]);
957
+ }
958
+ }
959
+ // integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
960
+ const isFixedPrecisionFamily = (mappedType) => mappedType instanceof t.integer ||
961
+ mappedType instanceof t.smallint ||
962
+ mappedType instanceof t.tinyint ||
963
+ mappedType instanceof t.mediumint ||
964
+ mappedType instanceof t.bigint ||
965
+ mappedType instanceof t.float ||
966
+ mappedType instanceof t.double;
967
+ const supportsUnsigned = this.#platform.supportsUnsigned();
968
+ const columnsMapped = sortedColumnKeys.reduce((o, col) => {
937
969
  const c = columns[col];
970
+ // omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
971
+ const rawType = c.type?.toLowerCase();
972
+ const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
973
+ const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
974
+ const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
938
975
  const normalized = {
939
976
  name: c.name,
940
- type: c.type,
941
- unsigned: !!c.unsigned,
977
+ type,
978
+ unsigned: supportsUnsigned && !!c.unsigned,
942
979
  autoincrement: !!c.autoincrement,
943
- primary: !!c.primary,
980
+ primary: primaryColumns.has(c.name) || !!c.primary,
944
981
  nullable: !!c.nullable,
945
- unique: !!c.unique,
946
- length: c.length ?? null,
947
- precision: c.precision ?? null,
948
- scale: c.scale ?? null,
982
+ unique: uniqueColumns.has(c.name) || !!c.unique,
983
+ length: c.length || null,
984
+ precision: fixedPrecision ? null : (c.precision ?? null),
985
+ scale: fixedPrecision ? null : (c.scale ?? null),
949
986
  default: c.default ?? null,
950
987
  comment: c.comment ?? null,
951
988
  enumItems: c.enumItems ?? [],
952
989
  mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
953
990
  };
954
- if (c.generated) {
955
- normalized.generated = c.generated;
956
- }
957
- if (c.nativeEnumName) {
958
- normalized.nativeEnumName = c.nativeEnumName;
959
- }
960
- if (c.extra) {
961
- normalized.extra = c.extra;
962
- }
963
- if (c.ignoreSchemaChanges) {
964
- normalized.ignoreSchemaChanges = c.ignoreSchemaChanges;
965
- }
966
- if (c.defaultConstraint) {
967
- normalized.defaultConstraint = c.defaultConstraint;
991
+ for (const field of [
992
+ 'generated',
993
+ 'nativeEnumName',
994
+ 'extra',
995
+ 'ignoreSchemaChanges',
996
+ 'defaultConstraint',
997
+ ]) {
998
+ if (c[field]) {
999
+ normalized[field] = c[field];
1000
+ }
968
1001
  }
969
1002
  o[col] = normalized;
970
1003
  return o;
971
1004
  }, {});
1005
+ const normalizeIndex = (idx) => {
1006
+ const out = {
1007
+ columnNames: idx.columnNames,
1008
+ composite: !!idx.composite,
1009
+ // PK indexes are always backed by a constraint — force it so postgres introspection matches
1010
+ constraint: !!idx.constraint || !!idx.primary,
1011
+ keyName: idx.keyName,
1012
+ primary: !!idx.primary,
1013
+ unique: !!idx.unique,
1014
+ };
1015
+ const optional = [
1016
+ 'expression',
1017
+ 'type',
1018
+ 'deferMode',
1019
+ 'columns',
1020
+ 'include',
1021
+ 'fillFactor',
1022
+ 'invisible',
1023
+ 'disabled',
1024
+ 'clustered',
1025
+ ];
1026
+ for (const field of optional) {
1027
+ if (idx[field] != null && idx[field] !== false) {
1028
+ out[field] = idx[field];
1029
+ }
1030
+ }
1031
+ return out;
1032
+ };
1033
+ const normalizeFk = (fk) => {
1034
+ const isNoAction = (rule) => !rule || rule.toLowerCase() === 'no action';
1035
+ // JSON.stringify drops undefined properties — let them through instead of guarding
1036
+ return {
1037
+ columnNames: fk.columnNames,
1038
+ constraintName: fk.constraintName,
1039
+ localTableName: fk.localTableName,
1040
+ referencedColumnNames: fk.referencedColumnNames,
1041
+ referencedTableName: fk.referencedTableName,
1042
+ updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
1043
+ deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
1044
+ deferMode: fk.deferMode,
1045
+ };
1046
+ };
1047
+ const normalizeCheck = (check) => {
1048
+ const out = { name: check.name };
1049
+ if (typeof check.expression === 'string') {
1050
+ out.expression = check.expression;
1051
+ }
1052
+ for (const field of ['definition', 'columnName']) {
1053
+ if (check[field]) {
1054
+ out[field] = check[field];
1055
+ }
1056
+ }
1057
+ return out;
1058
+ };
1059
+ const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
1060
+ const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
1061
+ const sortedTriggers = [...this.#triggers].sort((a, b) => byString(a.name, b.name));
1062
+ const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
1063
+ .sort(([a], [b]) => byString(a, b))
1064
+ .map(([k, v]) => [k, normalizeFk(v)]));
972
1065
  return {
973
1066
  name: this.name,
974
1067
  schema: this.schema,
975
1068
  columns: columnsMapped,
976
- indexes: this.#indexes,
977
- checks: this.#checks,
978
- triggers: this.#triggers,
979
- foreignKeys: this.#foreignKeys,
980
- nativeEnums: this.nativeEnums,
981
- comment: this.comment,
1069
+ indexes: sortedIndexes,
1070
+ checks: sortedChecks,
1071
+ triggers: sortedTriggers,
1072
+ foreignKeys: sortedForeignKeys,
1073
+ // emit `comment` even when unset so introspection (which always reads it) matches metadata
1074
+ comment: this.comment ?? null,
982
1075
  };
983
1076
  }
984
1077
  }
@@ -42,7 +42,7 @@ export class SchemaHelper {
42
42
  }
43
43
  inferLengthFromColumnType(type) {
44
44
  const match = /^\w+\s*(?:\(\s*(\d+)\s*\)|$)/.exec(type);
45
- if (!match) {
45
+ if (match?.[1] == null) {
46
46
  return;
47
47
  }
48
48
  return +match[1];