@mikro-orm/sql 7.1.2-dev.1 → 7.1.2-dev.11

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.
@@ -479,8 +479,11 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
479
479
  }
480
480
  seen.add(dedupeKey);
481
481
  ret[key] ??= [];
482
+ // CHECK: unwrap the `CHECK ((<predicate>))` shell and drop pg-added `::type` casts so the
483
+ // inner predicate matches the user's metadata. EXCLUDE bodies are kept verbatim — the user's
484
+ // `@Check` expression is the full body (see SchemaHelper.createCheck).
482
485
  const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
483
- const def = m?.[1].replace(/\((.*?)\)::\w+/g, '$1');
486
+ const def = m ? m[1].replace(/\((.*?)\)::\w+/g, '$1') : check.expression;
484
487
  ret[key].push({
485
488
  name: check.name,
486
489
  columnName: check.column_name,
@@ -1073,16 +1076,31 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
1073
1076
  join pg_am as am on am.oid = i.relam
1074
1077
  left join pg_constraint as c on c.conname = i.relname
1075
1078
  where indrelid in (${tables.map(t => `${this.platform.quoteValue(`${this.quote(t.schema_name)}.${this.quote(t.table_name)}`)}::regclass`).join(', ')})
1079
+ and (c.contype is null or c.contype <> 'x')
1076
1080
  order by relname`;
1077
1081
  }
1078
1082
  getChecksSQL(tablesBySchemas) {
1083
+ const checkFilter = [...tablesBySchemas.entries()]
1084
+ .map(([schema, tables]) => `ccu.table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}) and ccu.table_schema = ${this.platform.quoteValue(schema)}`)
1085
+ .join(' or ');
1086
+ // EXCLUDE constraints don't appear in information_schema.constraint_column_usage, so the second
1087
+ // branch resolves the table from pg_class/pg_namespace directly.
1088
+ const excludeFilter = [...tablesBySchemas.entries()]
1089
+ .map(([schema, tables]) => `cls.relname in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}) and nsp.nspname = ${this.platform.quoteValue(schema)}`)
1090
+ .join(' or ');
1079
1091
  return `select ccu.table_name as table_name, ccu.table_schema as schema_name, pgc.conname as name, conrelid::regclass as table_from, ccu.column_name as column_name, pg_get_constraintdef(pgc.oid) as expression
1080
1092
  from pg_constraint pgc
1081
1093
  join pg_namespace nsp on nsp.oid = pgc.connamespace
1082
1094
  join pg_class cls on pgc.conrelid = cls.oid
1083
1095
  join information_schema.constraint_column_usage ccu on pgc.conname = ccu.constraint_name and nsp.nspname = ccu.constraint_schema and cls.relname = ccu.table_name
1084
- where contype = 'c' and (${[...tablesBySchemas.entries()].map(([schema, tables]) => `ccu.table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}) and ccu.table_schema = ${this.platform.quoteValue(schema)}`).join(' or ')})
1085
- order by pgc.conname`;
1096
+ where pgc.contype = 'c' and (${checkFilter})
1097
+ union all
1098
+ select cls.relname as table_name, nsp.nspname as schema_name, pgc.conname as name, conrelid::regclass as table_from, null as column_name, pg_get_constraintdef(pgc.oid) as expression
1099
+ from pg_constraint pgc
1100
+ join pg_namespace nsp on nsp.oid = pgc.connamespace
1101
+ join pg_class cls on pgc.conrelid = cls.oid
1102
+ where pgc.contype = 'x' and (${excludeFilter})
1103
+ order by name`;
1086
1104
  }
1087
1105
  inferLengthFromColumnType(type) {
1088
1106
  const match = /^(\w+(?:\s+\w+)*)\s*(?:\(\s*(\d+)\s*\)|$)/.exec(type);
@@ -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.1",
3
+ "version": "7.1.2-dev.11",
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.1.1"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.2-dev.1"
56
+ "@mikro-orm/core": "7.1.2-dev.11"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -826,8 +826,9 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
826
826
  getResultAndCount(): Promise<[Loaded<Entity, Hint, Fields>[], number]>;
827
827
  /**
828
828
  * Returns native query builder instance with sub-query aliased with given alias.
829
+ * The alias literal is preserved in the type so `addSelect()` exposes it on `execute()` results.
829
830
  */
830
- as(alias: string): NativeQueryBuilder;
831
+ as<Alias extends string>(alias: Alias): NativeQueryBuilder & RawQueryFragment<Alias>;
831
832
  /**
832
833
  * Returns native query builder instance with sub-query aliased with given alias.
833
834
  * You can provide the target entity name as the first parameter and use the second parameter to point to an existing property to infer its field name.
@@ -1050,17 +1051,28 @@ type PopulatedDTO<T, K extends keyof T> = NonNullable<T[K]> extends Collection<i
1050
1051
  type SubFields<F extends string, Rel extends string> = F extends `${Rel}.${infer Sub}` ? Sub : never;
1051
1052
  type RootFields<F extends string, H extends string> = F extends `${string}.${string}` ? F extends `${H}.${string}` ? never : F : F;
1052
1053
  type JoinDTO<T, K extends keyof T, F extends string> = NonNullable<T[K]> extends Collection<infer U> ? SubFields<F, K & string> extends never ? EntityDTOProp<T, Collection<U>> : DirectDTO<U, (SubFields<F, K & string> | PrimaryProperty<U>) & keyof U>[] : SubFields<F, K & string> extends never ? EntityDTOProp<T, T[K]> : DirectDTO<NonNullable<T[K]>, (SubFields<F, K & string> | PrimaryProperty<NonNullable<T[K]>>) & keyof NonNullable<T[K]>> | Extract<T[K], null | undefined>;
1053
- type ExecuteDTO<T, H extends string, F extends string> = [H] extends [never] ? [F] extends ['*'] ? EntityDTOFlat<T> : DirectDTO<T, F & keyof T> : [F] extends ['*'] ? true extends (H extends `${string}.${string}` ? true : false) ? SerializeDTO<T, H> : Omit<EntityDTOFlat<T>, H & keyof EntityDTOFlat<T>> & {
1054
+ /**
1055
+ * Raw aliases (`sql`...`.as('x')`, `'col as x'`, or aliased sub-queries) are not entity keys,
1056
+ * so they never appear in the selected-fields DTO. They are tracked in the `RawAliases` generic
1057
+ * (preserved across joins), so expose them on the result as `unknown` — their value type is not
1058
+ * known at the type level. Only intersected when raw aliases are present, to keep the plain DTO
1059
+ * untouched when there are none.
1060
+ */
1061
+ type WithRawAliases<DTO, RawAliases extends string> = [RawAliases] extends [never] ? DTO : DTO & {
1062
+ [K in RawAliases]: unknown;
1063
+ };
1064
+ type ExecuteDTOInner<T, H extends string, F extends string> = [H] extends [never] ? [F] extends ['*'] ? EntityDTOFlat<T> : DirectDTO<T, F & keyof T> : [F] extends ['*'] ? true extends (H extends `${string}.${string}` ? true : false) ? SerializeDTO<T, H> : Omit<EntityDTOFlat<T>, H & keyof EntityDTOFlat<T>> & {
1054
1065
  [K in H & keyof T as K & keyof EntityDTOFlat<T>]: PopulatedDTO<T, K> | Extract<T[K], null | undefined>;
1055
1066
  } : true extends (H extends `${string}.${string}` ? true : false) ? EntityDTOFlat<Loaded<T, H, F>> : DirectDTO<T, (RootFields<F, H> | PrimaryProperty<T>) & keyof T> & {
1056
1067
  [K in H & keyof T]: JoinDTO<T, K, F>;
1057
1068
  };
1069
+ type ExecuteDTO<T, H extends string, F extends string, RawAliases extends string = never> = WithRawAliases<ExecuteDTOInner<T, H, F>, RawAliases>;
1058
1070
  /** Shorthand for `QueryBuilder` with all generic parameters set to `any`. */
1059
1071
  export type AnyQueryBuilder<T extends object = AnyEntity> = QueryBuilder<T, any, any, any, any, any, any>;
1060
1072
  export interface SelectQueryBuilder<Entity extends object = AnyEntity, RootAlias extends string = never, Hint extends string = never, Context extends object = never, RawAliases extends string = never, Fields extends string = '*', CTEs extends Record<string, object> = {}> extends QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields, CTEs> {
1061
- execute<Result = ExecuteDTO<Entity, Hint, Fields>[]>(method?: 'all' | 'get' | 'run', mapResults?: boolean): Promise<Result>;
1062
- execute<Result = ExecuteDTO<Entity, Hint, Fields>[]>(method: 'all', mapResults?: boolean): Promise<Result>;
1063
- execute<Result = ExecuteDTO<Entity, Hint, Fields>>(method: 'get', mapResults?: boolean): Promise<Result>;
1073
+ execute<Result = ExecuteDTO<Entity, Hint, Fields, RawAliases>[]>(method?: 'all' | 'get' | 'run', mapResults?: boolean): Promise<Result>;
1074
+ execute<Result = ExecuteDTO<Entity, Hint, Fields, RawAliases>[]>(method: 'all', mapResults?: boolean): Promise<Result>;
1075
+ execute<Result = ExecuteDTO<Entity, Hint, Fields, RawAliases>>(method: 'get', mapResults?: boolean): Promise<Result>;
1064
1076
  execute<Result = QueryResult<Entity>>(method: 'run', mapResults?: boolean): Promise<Result>;
1065
1077
  }
1066
1078
  export interface CountQueryBuilder<Entity extends object> extends QueryBuilder<Entity, any, any> {
@@ -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
  }
@@ -138,6 +138,8 @@ export declare abstract class SchemaHelper {
138
138
  getReferencedTableName(referencedTableName: string, schema?: string): string;
139
139
  createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
140
140
  createCheck(table: DatabaseTable, check: CheckDef): string;
141
+ /** True for `@Check` expressions that are a full table-constraint body (e.g. PostgreSQL `exclude using gist (...)`) and must be emitted verbatim instead of wrapped in `check (...)`. */
142
+ private isRawConstraintBody;
141
143
  /**
142
144
  * Generates SQL to create a database trigger on a table.
143
145
  * Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
@@ -813,7 +813,13 @@ export class SchemaHelper {
813
813
  return this.getCreateIndexSQL(table.getShortestName(), index);
814
814
  }
815
815
  createCheck(table, check) {
816
- return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`;
816
+ const expression = check.expression;
817
+ const body = this.isRawConstraintBody(expression) ? expression : `check (${expression})`;
818
+ return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} ${body}`;
819
+ }
820
+ /** True for `@Check` expressions that are a full table-constraint body (e.g. PostgreSQL `exclude using gist (...)`) and must be emitted verbatim instead of wrapped in `check (...)`. */
821
+ isRawConstraintBody(expression) {
822
+ return /^\s*exclude\b/i.test(expression);
817
823
  }
818
824
  /**
819
825
  * Generates SQL to create a database trigger on a table.