@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.
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +21 -3
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/package.json +2 -2
- package/query/QueryBuilder.d.ts +17 -5
- package/schema/DatabaseTable.js +24 -12
- package/schema/SchemaComparator.js +8 -3
- package/schema/SchemaHelper.d.ts +2 -0
- package/schema/SchemaHelper.js +7 -1
|
@@ -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
|
|
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 (${
|
|
1085
|
-
|
|
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.
|
|
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.
|
|
56
|
+
"@mikro-orm/core": "7.1.2-dev.11"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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> {
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -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:
|
|
997
|
-
comment: c.comment
|
|
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
|
-
|
|
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
|
}
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -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).
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -813,7 +813,13 @@ export class SchemaHelper {
|
|
|
813
813
|
return this.getCreateIndexSQL(table.getShortestName(), index);
|
|
814
814
|
}
|
|
815
815
|
createCheck(table, check) {
|
|
816
|
-
|
|
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.
|