@mikro-orm/sql 7.0.13-dev.7 → 7.0.13-dev.8

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.
@@ -90,18 +90,27 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
90
90
  }
91
91
  normalizeColumnType(type, options) {
92
92
  const simpleType = this.extractSimpleType(type);
93
- if (['int', 'int4', 'integer'].includes(simpleType)) {
93
+ if (['int', 'int4', 'integer', 'serial'].includes(simpleType)) {
94
94
  return this.getIntegerTypeDeclarationSQL({});
95
95
  }
96
- if (['bigint', 'int8'].includes(simpleType)) {
96
+ if (['bigint', 'int8', 'bigserial'].includes(simpleType)) {
97
97
  return this.getBigIntTypeDeclarationSQL({});
98
98
  }
99
- if (['smallint', 'int2'].includes(simpleType)) {
99
+ if (['smallint', 'int2', 'smallserial'].includes(simpleType)) {
100
100
  return this.getSmallIntTypeDeclarationSQL({});
101
101
  }
102
102
  if (['boolean', 'bool'].includes(simpleType)) {
103
103
  return this.getBooleanTypeDeclarationSQL();
104
104
  }
105
+ if (['double', 'double precision', 'float8'].includes(simpleType)) {
106
+ return this.getDoubleDeclarationSQL();
107
+ }
108
+ if (['real', 'float4'].includes(simpleType)) {
109
+ return this.getFloatDeclarationSQL();
110
+ }
111
+ if (['timestamptz', 'timestamp with time zone'].includes(simpleType)) {
112
+ return this.getDateTimeTypeDeclarationSQL(options);
113
+ }
105
114
  if (['varchar', 'character varying'].includes(simpleType)) {
106
115
  return this.getVarcharTypeDeclarationSQL(options);
107
116
  }
@@ -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
  }
@@ -44,7 +44,8 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
44
44
  * We need to add them back so they match what we generate in DDL.
45
45
  */
46
46
  private wrapExpressionDefault;
47
- private getEnumDefinitions;
47
+ /** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
48
+ private extractEnumValuesFromChecks;
48
49
  getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[], tableName: string, schemaName?: string, ctx?: Transaction): Promise<string[]>;
49
50
  private getIndexes;
50
51
  private getChecks;
@@ -100,7 +100,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
100
100
  const checks = await this.getChecks(connection, table.name, table.schema, ctx);
101
101
  const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema, ctx);
102
102
  const fks = await this.getForeignKeys(connection, table.name, table.schema, ctx);
103
- const enums = await this.getEnumDefinitions(connection, table.name, table.schema, ctx);
103
+ const enums = this.extractEnumValuesFromChecks(checks);
104
104
  table.init(cols, indexes, checks, pks, fks, enums);
105
105
  }
106
106
  }
@@ -321,23 +321,23 @@ export class SqliteSchemaHelper extends SchemaHelper {
321
321
  // everything else is an expression that had its outer parens stripped
322
322
  return `(${value})`;
323
323
  }
324
- async getEnumDefinitions(connection, tableName, schemaName, ctx) {
325
- const prefix = this.getSchemaPrefix(schemaName);
326
- const sql = `select sql from ${prefix}sqlite_master where type = ? and name = ?`;
327
- const tableDefinition = await connection.execute(sql, ['table', tableName], 'get', ctx);
328
- const checkConstraints = [...(tableDefinition.sql.match(/[`["'][^`\]"']+[`\]"'] text check \(.*?\)/gi) ?? [])];
329
- return checkConstraints.reduce((o, item) => {
330
- // check constraints are defined as (note that last closing paren is missing):
331
- // `type` text check (`type` in ('local', 'global')
332
- const match = /[`["']([^`\]"']+)[`\]"'] text check \(.* \((.*)\)/i.exec(item);
333
- /* v8 ignore next */
334
- if (match) {
335
- o[match[1]] = match[2]
336
- .split(/,(?=\s*'(?:[^']|'')*'(?:\s*\)|$))/)
337
- .map((item) => /^\(?'((?:[^']|'')*)'/.exec(item.trim())[1].replace(/''/g, "'"));
324
+ /** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
325
+ extractEnumValuesFromChecks(checks) {
326
+ const result = {};
327
+ for (const check of checks) {
328
+ if (!check.columnName || typeof check.expression !== 'string') {
329
+ continue;
338
330
  }
339
- return o;
340
- }, {});
331
+ const inClause = /\bin\s*\(([^)]*)\)/i.exec(check.expression);
332
+ if (!inClause) {
333
+ continue;
334
+ }
335
+ const items = [...inClause[1].matchAll(/'((?:[^']|'')*)'/g)].map(m => m[1].replace(/''/g, "'"));
336
+ if (items.length > 0) {
337
+ result[check.columnName] = items;
338
+ }
339
+ }
340
+ return result;
341
341
  }
342
342
  async getPrimaryKeys(connection, indexes, tableName, schemaName, ctx) {
343
343
  const prefix = this.getSchemaPrefix(schemaName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.13-dev.7",
3
+ "version": "7.0.13-dev.8",
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.0.13-dev.7"
56
+ "@mikro-orm/core": "7.0.13-dev.8"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -323,12 +323,16 @@ export class DatabaseSchema {
323
323
  (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner));
324
324
  }
325
325
  toJSON() {
326
+ // locale-independent comparison so the snapshot is stable across machines
327
+ const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
328
+ const tableKey = (t) => `${t.schema ?? ''}.${t.name}`;
329
+ const byTable = (a, b) => byString(tableKey(a), tableKey(b));
326
330
  return {
327
331
  name: this.name,
328
- namespaces: [...this.#namespaces],
329
- tables: this.#tables,
330
- views: this.#views,
331
- nativeEnums: this.#nativeEnums,
332
+ namespaces: [...this.#namespaces].sort(),
333
+ tables: [...this.#tables].sort(byTable),
334
+ views: [...this.#views].sort(byTable),
335
+ nativeEnums: Object.fromEntries(Object.entries(this.#nativeEnums).sort(([a], [b]) => byString(a, b))),
332
336
  };
333
337
  }
334
338
  prune(schema, wildcardSchemaTables) {
@@ -51,6 +51,7 @@ export class DatabaseTable {
51
51
  this.#indexes = indexes;
52
52
  this.#checks = checks;
53
53
  this.#foreignKeys = fks;
54
+ const helper = this.#platform.getSchemaHelper();
54
55
  this.#columns = cols.reduce((o, v) => {
55
56
  const index = indexes.filter(i => i.columnNames[0] === v.name);
56
57
  v.primary = v.primary || pks.includes(v.name);
@@ -59,6 +60,11 @@ export class DatabaseTable {
59
60
  v.mappedType = this.#platform.getMappedType(type);
60
61
  v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
61
62
  v.enumItems ??= enums[v.name] || [];
63
+ // recover length from the declared type so introspection matches `addColumnFromProperty`;
64
+ // scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
65
+ if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
66
+ v.length = helper.inferLengthFromColumnType(v.type);
67
+ }
62
68
  o[v.name] = v;
63
69
  return o;
64
70
  }, {});
@@ -68,7 +74,9 @@ export class DatabaseTable {
68
74
  }
69
75
  addColumnFromProperty(prop, meta, config) {
70
76
  prop.fieldNames?.forEach((field, idx) => {
71
- const type = prop.enum ? 'enum' : prop.columnTypes[idx];
77
+ // numeric enums fall through to the underlying numeric type no platform emits a CHECK we could parse back
78
+ const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
79
+ const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
72
80
  const mappedType = this.#platform.getMappedType(type);
73
81
  if (mappedType instanceof DecimalType) {
74
82
  const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
@@ -887,51 +895,135 @@ export class DatabaseTable {
887
895
  }
888
896
  toJSON() {
889
897
  const columns = this.#columns;
890
- const columnsMapped = Utils.keys(columns).reduce((o, col) => {
898
+ // locale-independent comparison so the snapshot is stable across machines
899
+ const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
900
+ const sortedColumnKeys = Utils.keys(columns).sort(byString);
901
+ // mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
902
+ // so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
903
+ const primaryColumns = new Set();
904
+ const uniqueColumns = new Set();
905
+ for (const idx of this.#indexes) {
906
+ if (idx.primary) {
907
+ idx.columnNames.forEach(c => primaryColumns.add(c));
908
+ }
909
+ if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
910
+ uniqueColumns.add(idx.columnNames[0]);
911
+ }
912
+ }
913
+ // integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
914
+ const isFixedPrecisionFamily = (mappedType) => mappedType instanceof t.integer ||
915
+ mappedType instanceof t.smallint ||
916
+ mappedType instanceof t.tinyint ||
917
+ mappedType instanceof t.mediumint ||
918
+ mappedType instanceof t.bigint ||
919
+ mappedType instanceof t.float ||
920
+ mappedType instanceof t.double;
921
+ const supportsUnsigned = this.#platform.supportsUnsigned();
922
+ const columnsMapped = sortedColumnKeys.reduce((o, col) => {
891
923
  const c = columns[col];
924
+ // omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
925
+ const rawType = c.type?.toLowerCase();
926
+ const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
927
+ const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
928
+ const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
892
929
  const normalized = {
893
930
  name: c.name,
894
- type: c.type,
895
- unsigned: !!c.unsigned,
931
+ type,
932
+ unsigned: supportsUnsigned && !!c.unsigned,
896
933
  autoincrement: !!c.autoincrement,
897
- primary: !!c.primary,
934
+ primary: primaryColumns.has(c.name) || !!c.primary,
898
935
  nullable: !!c.nullable,
899
- unique: !!c.unique,
900
- length: c.length ?? null,
901
- precision: c.precision ?? null,
902
- scale: c.scale ?? null,
936
+ unique: uniqueColumns.has(c.name) || !!c.unique,
937
+ length: c.length || null,
938
+ precision: fixedPrecision ? null : (c.precision ?? null),
939
+ scale: fixedPrecision ? null : (c.scale ?? null),
903
940
  default: c.default ?? null,
904
941
  comment: c.comment ?? null,
905
942
  enumItems: c.enumItems ?? [],
906
943
  mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
907
944
  };
908
- if (c.generated) {
909
- normalized.generated = c.generated;
910
- }
911
- if (c.nativeEnumName) {
912
- normalized.nativeEnumName = c.nativeEnumName;
913
- }
914
- if (c.extra) {
915
- normalized.extra = c.extra;
916
- }
917
- if (c.ignoreSchemaChanges) {
918
- normalized.ignoreSchemaChanges = c.ignoreSchemaChanges;
919
- }
920
- if (c.defaultConstraint) {
921
- normalized.defaultConstraint = c.defaultConstraint;
945
+ for (const field of [
946
+ 'generated',
947
+ 'nativeEnumName',
948
+ 'extra',
949
+ 'ignoreSchemaChanges',
950
+ 'defaultConstraint',
951
+ ]) {
952
+ if (c[field]) {
953
+ normalized[field] = c[field];
954
+ }
922
955
  }
923
956
  o[col] = normalized;
924
957
  return o;
925
958
  }, {});
959
+ const normalizeIndex = (idx) => {
960
+ const out = {
961
+ columnNames: idx.columnNames,
962
+ composite: !!idx.composite,
963
+ // PK indexes are always backed by a constraint — force it so postgres introspection matches
964
+ constraint: !!idx.constraint || !!idx.primary,
965
+ keyName: idx.keyName,
966
+ primary: !!idx.primary,
967
+ unique: !!idx.unique,
968
+ };
969
+ const optional = [
970
+ 'expression',
971
+ 'type',
972
+ 'deferMode',
973
+ 'columns',
974
+ 'include',
975
+ 'fillFactor',
976
+ 'invisible',
977
+ 'disabled',
978
+ 'clustered',
979
+ ];
980
+ for (const field of optional) {
981
+ if (idx[field] != null && idx[field] !== false) {
982
+ out[field] = idx[field];
983
+ }
984
+ }
985
+ return out;
986
+ };
987
+ const normalizeFk = (fk) => {
988
+ const isNoAction = (rule) => !rule || rule.toLowerCase() === 'no action';
989
+ // JSON.stringify drops undefined properties — let them through instead of guarding
990
+ return {
991
+ columnNames: fk.columnNames,
992
+ constraintName: fk.constraintName,
993
+ localTableName: fk.localTableName,
994
+ referencedColumnNames: fk.referencedColumnNames,
995
+ referencedTableName: fk.referencedTableName,
996
+ updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
997
+ deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
998
+ deferMode: fk.deferMode,
999
+ };
1000
+ };
1001
+ const normalizeCheck = (check) => {
1002
+ const out = { name: check.name };
1003
+ if (typeof check.expression === 'string') {
1004
+ out.expression = check.expression;
1005
+ }
1006
+ for (const field of ['definition', 'columnName']) {
1007
+ if (check[field]) {
1008
+ out[field] = check[field];
1009
+ }
1010
+ }
1011
+ return out;
1012
+ };
1013
+ const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
1014
+ const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
1015
+ const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
1016
+ .sort(([a], [b]) => byString(a, b))
1017
+ .map(([k, v]) => [k, normalizeFk(v)]));
926
1018
  return {
927
1019
  name: this.name,
928
1020
  schema: this.schema,
929
1021
  columns: columnsMapped,
930
- indexes: this.#indexes,
931
- checks: this.#checks,
932
- foreignKeys: this.#foreignKeys,
933
- nativeEnums: this.nativeEnums,
934
- comment: this.comment,
1022
+ indexes: sortedIndexes,
1023
+ checks: sortedChecks,
1024
+ foreignKeys: sortedForeignKeys,
1025
+ // emit `comment` even when unset so introspection (which always reads it) matches metadata
1026
+ comment: this.comment ?? null,
935
1027
  };
936
1028
  }
937
1029
  }
@@ -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];