@mikro-orm/sql 7.2.0-dev.13 → 7.2.0-dev.15
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/AbstractSqlConnection.d.ts +12 -1
- package/AbstractSqlConnection.js +34 -0
- package/AbstractSqlDriver.d.ts +0 -7
- package/AbstractSqlDriver.js +62 -34
- package/AbstractSqlPlatform.d.ts +1 -1
- package/AbstractSqlPlatform.js +3 -0
- package/SqlEntityManager.js +4 -2
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +18 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +153 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
- package/package.json +3 -3
- package/plugin/transformer.d.ts +7 -1
- package/plugin/transformer.js +60 -1
- package/query/CriteriaNodeFactory.js +4 -0
- package/query/ObjectCriteriaNode.js +2 -1
- package/query/QueryBuilder.d.ts +26 -3
- package/query/QueryBuilder.js +139 -23
- package/query/QueryBuilderHelper.d.ts +5 -0
- package/query/QueryBuilderHelper.js +29 -8
- package/schema/DatabaseSchema.d.ts +4 -0
- package/schema/DatabaseSchema.js +107 -1
- package/schema/DatabaseTable.d.ts +13 -1
- package/schema/DatabaseTable.js +50 -1
- package/schema/SchemaComparator.d.ts +2 -0
- package/schema/SchemaComparator.js +94 -4
- package/schema/SchemaHelper.d.ts +6 -0
- package/schema/SchemaHelper.js +15 -0
- package/schema/SqlSchemaGenerator.js +8 -0
- package/typings.d.ts +18 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ControlledTransaction, type Dialect, Kysely } from 'kysely';
|
|
2
|
-
import { type AnyEntity, Connection, type Dictionary, type EntityData, type IsolationLevel, type LogContext, type LoggingOptions, type MaybePromise, type QueryResult, type RawQueryFragment, type Transaction, type TransactionEventBroadcaster } from '@mikro-orm/core';
|
|
2
|
+
import { type AnyEntity, Connection, type Dictionary, type EntityData, type IsolationLevel, type LogContext, type LoggingOptions, type MaybePromise, type QueryResult, type RawQueryFragment, type SessionContext, type Transaction, type TransactionEventBroadcaster } from '@mikro-orm/core';
|
|
3
3
|
import type { AbstractSqlPlatform } from './AbstractSqlPlatform.js';
|
|
4
4
|
import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
5
5
|
/** Base class for SQL database connections, built on top of Kysely. */
|
|
@@ -50,6 +50,7 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
50
50
|
ctx?: ControlledTransaction<any>;
|
|
51
51
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
52
52
|
loggerContext?: LogContext;
|
|
53
|
+
sessionContext?: SessionContext;
|
|
53
54
|
}): Promise<T>;
|
|
54
55
|
/** Begins a new transaction or creates a savepoint if a transaction context already exists. */
|
|
55
56
|
begin(options?: {
|
|
@@ -58,7 +59,17 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
58
59
|
ctx?: ControlledTransaction<any, any>;
|
|
59
60
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
60
61
|
loggerContext?: LogContext;
|
|
62
|
+
sessionContext?: SessionContext;
|
|
61
63
|
}): Promise<ControlledTransaction<any, any>>;
|
|
64
|
+
/** Applies session variables (`set_config`) and role (`set local role`) for the current transaction. */
|
|
65
|
+
private applySessionContext;
|
|
66
|
+
/**
|
|
67
|
+
* Quotes a role name as a single PostgreSQL identifier. Unlike `platform.quoteIdentifier`, which treats a dot as a
|
|
68
|
+
* schema qualifier, a role like `my.role` must be quoted whole (`"my.role"`) with embedded quotes doubled.
|
|
69
|
+
*/
|
|
70
|
+
protected static quoteRole(role: string): string;
|
|
71
|
+
/** Serializes a session variable for `set_config()`; `Date` values use ISO 8601 so casts like `::timestamptz` parse. */
|
|
72
|
+
protected stringifySessionVariable(value: unknown): string;
|
|
62
73
|
/** Commits the transaction or releases the savepoint. */
|
|
63
74
|
commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
64
75
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
package/AbstractSqlConnection.js
CHANGED
|
@@ -161,9 +161,43 @@ export class AbstractSqlConnection extends Connection {
|
|
|
161
161
|
for (const query of this.platform.getBeginTransactionSQL(options)) {
|
|
162
162
|
this.logQuery(query, options.loggerContext);
|
|
163
163
|
}
|
|
164
|
+
if (options.sessionContext) {
|
|
165
|
+
try {
|
|
166
|
+
await this.applySessionContext(trx, options.sessionContext, options.loggerContext);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
// roll back the freshly opened transaction so the pooled connection is released, not leaked
|
|
170
|
+
await trx.rollback().execute();
|
|
171
|
+
this.logQuery(this.platform.getRollbackTransactionSQL(), options.loggerContext);
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
164
175
|
await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
|
|
165
176
|
return trx;
|
|
166
177
|
}
|
|
178
|
+
/** Applies session variables (`set_config`) and role (`set local role`) for the current transaction. */
|
|
179
|
+
async applySessionContext(trx, sessionContext, loggerContext) {
|
|
180
|
+
const variables = Object.entries(sessionContext.variables ?? {});
|
|
181
|
+
if (variables.length > 0) {
|
|
182
|
+
const parts = variables.map(() => 'set_config(?, ?, true)').join(', ');
|
|
183
|
+
const params = variables.flatMap(([key, value]) => [key, this.stringifySessionVariable(value)]);
|
|
184
|
+
await this.execute(`select ${parts}`, params, 'run', trx, loggerContext);
|
|
185
|
+
}
|
|
186
|
+
if (sessionContext.role) {
|
|
187
|
+
await this.execute(`set local role ${AbstractSqlConnection.quoteRole(sessionContext.role)}`, [], 'run', trx, loggerContext);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Quotes a role name as a single PostgreSQL identifier. Unlike `platform.quoteIdentifier`, which treats a dot as a
|
|
192
|
+
* schema qualifier, a role like `my.role` must be quoted whole (`"my.role"`) with embedded quotes doubled.
|
|
193
|
+
*/
|
|
194
|
+
static quoteRole(role) {
|
|
195
|
+
return `"${role.replaceAll('"', '""')}"`;
|
|
196
|
+
}
|
|
197
|
+
/** Serializes a session variable for `set_config()`; `Date` values use ISO 8601 so casts like `::timestamptz` parse. */
|
|
198
|
+
stringifySessionVariable(value) {
|
|
199
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
200
|
+
}
|
|
167
201
|
/** Commits the transaction or releases the savepoint. */
|
|
168
202
|
async commit(ctx, eventBroadcaster, loggerContext) {
|
|
169
203
|
if (ctx.isRolledBack) {
|
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -118,13 +118,6 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
118
118
|
mergeJoinedResult<T extends object>(rawResults: EntityData<T>[], meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[]): EntityData<T>[];
|
|
119
119
|
protected shouldHaveColumn<T, U>(meta: EntityMetadata<T>, prop: EntityProperty<U>, populate: readonly PopulateOptions<U>[], fields?: readonly InternalField<U>[], exclude?: readonly InternalField<U>[]): boolean;
|
|
120
120
|
protected getFieldsForJoinedLoad<T extends object>(qb: AnyQueryBuilder<T>, meta: EntityMetadata<T>, options: FieldsForJoinedLoadOptions<T>): InternalField<T>[];
|
|
121
|
-
/**
|
|
122
|
-
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
123
|
-
* Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
|
|
124
|
-
* when filter conditions reference parent-table columns.
|
|
125
|
-
* @internal
|
|
126
|
-
*/
|
|
127
|
-
protected addTPTParentJoinsForRelation<T extends object>(qb: AnyQueryBuilder<T>, leafMeta: EntityMetadata, leafAlias: string, basePath: string): void;
|
|
128
121
|
/**
|
|
129
122
|
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
130
123
|
* @internal
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -183,7 +183,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
183
183
|
if (typeof meta.expression === 'string') {
|
|
184
184
|
return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options, type);
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
// fork the caller EM so the callback inherits its filters, filter params and session context — a fork never
|
|
187
|
+
// resolves back to the ambient RequestContext EM, which would ignore the transaction/session context set below
|
|
188
|
+
const em = (options.em?.fork() ?? this.createEntityManager(false));
|
|
187
189
|
em.setTransactionContext(options.ctx);
|
|
188
190
|
const res = meta.expression(em, where, options);
|
|
189
191
|
if (typeof res === 'string') {
|
|
@@ -209,7 +211,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
209
211
|
yield* this.wrapVirtualExpressionInSubqueryStream(meta, meta.expression, where, options, QueryType.SELECT);
|
|
210
212
|
return;
|
|
211
213
|
}
|
|
212
|
-
|
|
214
|
+
// fork the caller EM for the same reason as in `findFromVirtual`
|
|
215
|
+
const em = (options.em?.fork() ?? this.createEntityManager(false));
|
|
213
216
|
em.setTransactionContext(options.ctx);
|
|
214
217
|
const res = meta.expression(em, where, options, true);
|
|
215
218
|
if (typeof res === 'string') {
|
|
@@ -239,7 +242,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
239
242
|
const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
|
|
240
243
|
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
|
|
241
244
|
const query = native.compile();
|
|
242
|
-
const
|
|
245
|
+
const loggerContext = withAbortContext(options.loggerContext, options);
|
|
246
|
+
// virtual entities execute directly (not via QueryBuilder), so wrap in a short implicit transaction outside an
|
|
247
|
+
// existing one when a session context (row level security) needs to apply — mirrors the QueryBuilder wrap
|
|
248
|
+
const sessionContext = options.ctx ? undefined : options.em?.getTransactionSessionContext();
|
|
249
|
+
const conn = this.getConnection(this.resolveConnectionType(options));
|
|
250
|
+
const res = await (sessionContext
|
|
251
|
+
? conn.transactional(trx => this.execute(query.sql, query.params, 'all', trx, loggerContext), {
|
|
252
|
+
sessionContext,
|
|
253
|
+
loggerContext,
|
|
254
|
+
})
|
|
255
|
+
: this.execute(query.sql, query.params, 'all', options.ctx, loggerContext));
|
|
243
256
|
if (type === QueryType.COUNT) {
|
|
244
257
|
return res[0].count;
|
|
245
258
|
}
|
|
@@ -915,7 +928,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
915
928
|
if (!options.upsert && options.unionWhere?.length) {
|
|
916
929
|
where = (await this.applyUnionWhere(meta, where, options, true));
|
|
917
930
|
}
|
|
918
|
-
if (
|
|
931
|
+
if (options.upsert && meta.tptParent) {
|
|
932
|
+
res = await this.nativeUpdateMany(entityName, [where], [data], options);
|
|
933
|
+
}
|
|
934
|
+
else if (Utils.hasObjectKeys(data) || (meta.inheritanceType === 'tpt' && meta.ownsVersionProperty())) {
|
|
935
|
+
// a TPT table declaring the version property is bumped even when only other tables of the hierarchy changed
|
|
919
936
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
920
937
|
qb.setAbortOptions(pickAbortOptions(options));
|
|
921
938
|
if (options.upsert) {
|
|
@@ -941,7 +958,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
941
958
|
qb.update(data).where(where);
|
|
942
959
|
// reload generated columns and version fields
|
|
943
960
|
const returning = [];
|
|
944
|
-
meta
|
|
961
|
+
this.getTableProps(meta)
|
|
945
962
|
.filter(prop => (prop.generated && !prop.primary) || prop.version)
|
|
946
963
|
.forEach(prop => returning.push(prop.name));
|
|
947
964
|
qb.returning(returning);
|
|
@@ -957,13 +974,38 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
957
974
|
options.convertCustomTypes ??= true;
|
|
958
975
|
const meta = this.metadata.get(entityName);
|
|
959
976
|
if (options.upsert) {
|
|
977
|
+
if (meta.tptParent) {
|
|
978
|
+
// TPT parent tables go first, the PK they provide is the conflict target of this table
|
|
979
|
+
await this.nativeUpdateMany(meta.tptParent.class, where, data, options);
|
|
980
|
+
for (const [i, row] of data.entries()) {
|
|
981
|
+
if (meta.primaryKeys.some(pk => row[pk] == null)) {
|
|
982
|
+
const found = await this.findOne(meta.tptParent.class, where[i], {
|
|
983
|
+
fields: meta.primaryKeys,
|
|
984
|
+
ctx: options.ctx,
|
|
985
|
+
connectionType: 'write',
|
|
986
|
+
schema: options.schema,
|
|
987
|
+
});
|
|
988
|
+
meta.primaryKeys.forEach(pk => (row[pk] = found?.[pk]));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
options = { ...options, onConflictFields: meta.primaryKeys, onConflictWhere: undefined };
|
|
992
|
+
}
|
|
960
993
|
const uniqueFields = options.onConflictFields ??
|
|
961
994
|
(Utils.isPlainObject(where[0])
|
|
962
995
|
? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
|
|
963
996
|
: meta.primaryKeys);
|
|
964
997
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
965
998
|
qb.setAbortOptions(pickAbortOptions(options));
|
|
966
|
-
|
|
999
|
+
let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
|
|
1000
|
+
if (meta.inheritanceType === 'tpt') {
|
|
1001
|
+
// each TPT table only carries its own columns, the entity is reloaded instead of mapping the returned rows
|
|
1002
|
+
const own = (key) => meta.primaryKeys.includes(key) ||
|
|
1003
|
+
this.getTableProps(meta).some(prop => prop.name === key.split('.')[0]);
|
|
1004
|
+
data = data.map(row => Object.fromEntries(Object.entries(row).filter(([key]) => own(key))));
|
|
1005
|
+
options.onConflictMergeFields = options.onConflictMergeFields?.filter(f => own(f));
|
|
1006
|
+
options.onConflictExcludeFields = options.onConflictExcludeFields?.filter(f => own(f));
|
|
1007
|
+
returning = [];
|
|
1008
|
+
}
|
|
967
1009
|
qb.insert(data)
|
|
968
1010
|
.onConflict(uniqueFields)
|
|
969
1011
|
.returning(returning);
|
|
@@ -977,7 +1019,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
977
1019
|
if (options.onConflictWhere) {
|
|
978
1020
|
qb.where(options.onConflictWhere);
|
|
979
1021
|
}
|
|
980
|
-
|
|
1022
|
+
const res = await this.rethrow(qb.execute('run', false));
|
|
1023
|
+
return meta.inheritanceType === 'tpt' ? { ...res, row: undefined, rows: [] } : res;
|
|
981
1024
|
}
|
|
982
1025
|
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
983
1026
|
const keys = new Set();
|
|
@@ -992,7 +1035,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
992
1035
|
}
|
|
993
1036
|
}
|
|
994
1037
|
// reload generated columns and version fields
|
|
995
|
-
meta.
|
|
1038
|
+
meta.getPrimaryProps().forEach(prop => returning.add(prop.name));
|
|
1039
|
+
this.getTableProps(meta)
|
|
1040
|
+
.filter(prop => prop.generated || prop.version)
|
|
1041
|
+
.forEach(prop => returning.add(prop.name));
|
|
996
1042
|
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
|
|
997
1043
|
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
|
|
998
1044
|
.join(' and ');
|
|
@@ -1050,7 +1096,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1050
1096
|
return sql;
|
|
1051
1097
|
});
|
|
1052
1098
|
}
|
|
1053
|
-
if (meta.
|
|
1099
|
+
if (meta.ownsVersionProperty()) {
|
|
1054
1100
|
const versionProperty = meta.properties[meta.versionProperty];
|
|
1055
1101
|
const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
|
|
1056
1102
|
sql += `${quotedFieldName} = `;
|
|
@@ -1063,7 +1109,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1063
1109
|
sql += `, `;
|
|
1064
1110
|
}
|
|
1065
1111
|
sql = sql.substring(0, sql.length - 2) + ' where ';
|
|
1066
|
-
const pkProps = meta.primaryKeys.concat(...meta.
|
|
1112
|
+
const pkProps = meta.primaryKeys.concat(...meta.getOwnConcurrencyCheckKeys());
|
|
1067
1113
|
const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
|
|
1068
1114
|
const useTupleIn = pks.length <= 1 || this.platform.allowsComparingTuples();
|
|
1069
1115
|
const condTemplate = useTupleIn
|
|
@@ -1649,6 +1695,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1649
1695
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1650
1696
|
return false;
|
|
1651
1697
|
}
|
|
1698
|
+
// Polymorphic to-one flattened from an object embeddable lives inside a JSON column, so the join
|
|
1699
|
+
// conditions would need JSON extraction for both the discriminator and the FK; fall back to SELECT_IN.
|
|
1700
|
+
if (prop.polymorphic && prop.object && prop.embedded) {
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1652
1703
|
if (strategy !== LoadStrategy.JOINED) {
|
|
1653
1704
|
// force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
|
|
1654
1705
|
return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
@@ -1779,7 +1830,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1779
1830
|
// INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
|
|
1780
1831
|
// keeps the resulting query valid for rows pointing to other polymorphic targets.
|
|
1781
1832
|
if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
|
|
1782
|
-
|
|
1833
|
+
qb.addTPTParentJoins(targetMeta, tableAlias, targetPath);
|
|
1783
1834
|
}
|
|
1784
1835
|
// For polymorphic targets that are TPT base classes, also LEFT JOIN
|
|
1785
1836
|
// all descendant tables so child-specific fields can be selected.
|
|
@@ -1827,10 +1878,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1827
1878
|
: JoinType.leftJoin;
|
|
1828
1879
|
const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
|
|
1829
1880
|
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1830
|
-
// For relations to TPT child entities, INNER JOIN parent tables (GH #7469)
|
|
1831
|
-
if (meta2.inheritanceType === 'tpt' && meta2.tptParent) {
|
|
1832
|
-
this.addTPTParentJoinsForRelation(qb, meta2, tableAlias, path);
|
|
1833
|
-
}
|
|
1834
1881
|
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1835
1882
|
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
1836
1883
|
// Use the registry metadata to ensure allTPTDescendants is available
|
|
@@ -1877,25 +1924,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1877
1924
|
}
|
|
1878
1925
|
return fields;
|
|
1879
1926
|
}
|
|
1880
|
-
/**
|
|
1881
|
-
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
1882
|
-
* Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
|
|
1883
|
-
* when filter conditions reference parent-table columns.
|
|
1884
|
-
* @internal
|
|
1885
|
-
*/
|
|
1886
|
-
addTPTParentJoinsForRelation(qb, leafMeta, leafAlias, basePath) {
|
|
1887
|
-
let childAlias = leafAlias;
|
|
1888
|
-
let childMeta = leafMeta;
|
|
1889
|
-
while (childMeta.tptParent) {
|
|
1890
|
-
const parentMeta = childMeta.tptParent;
|
|
1891
|
-
const parentAlias = qb.getNextAlias(parentMeta.className);
|
|
1892
|
-
qb.createAlias(parentMeta.class, parentAlias);
|
|
1893
|
-
qb.state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
|
|
1894
|
-
qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
|
|
1895
|
-
childAlias = parentAlias;
|
|
1896
|
-
childMeta = parentMeta;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
1927
|
/**
|
|
1900
1928
|
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
1901
1929
|
* @internal
|
package/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
28
28
|
getReleaseSavepointSQL(savepointName: string): string;
|
|
29
29
|
quoteValue(value: any): string;
|
|
30
30
|
getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
|
|
31
|
-
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
|
|
31
|
+
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
|
|
32
32
|
/**
|
|
33
33
|
* Quotes a key for use inside a JSON path expression (e.g. `$.key`).
|
|
34
34
|
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -75,6 +75,9 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
75
75
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
76
76
|
const [a, ...b] = path;
|
|
77
77
|
const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
|
|
78
|
+
if (typeof aliased === 'string') {
|
|
79
|
+
return raw(`json_extract(${this.quoteIdentifier(`${aliased}.${a}`)}, ${jsonPath})`);
|
|
80
|
+
}
|
|
78
81
|
if (aliased) {
|
|
79
82
|
return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, ${jsonPath})`);
|
|
80
83
|
}
|
package/SqlEntityManager.js
CHANGED
|
@@ -68,7 +68,7 @@ export class SqlEntityManager extends EntityManager {
|
|
|
68
68
|
merged.signal = opts.signal ?? fork?.signal;
|
|
69
69
|
merged.inflightQueryAbortStrategy = opts.inflightQueryAbortStrategy ?? fork?.inflightQueryAbortStrategy;
|
|
70
70
|
}
|
|
71
|
-
return this.getDriver().execute(query, params, opts.method ?? 'all',
|
|
71
|
+
return context.withSessionContext(context.getTransactionContext(), ctx => this.getDriver().execute(query, params, opts.method ?? 'all', ctx, merged));
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* @inheritDoc
|
|
@@ -81,7 +81,9 @@ export class SqlEntityManager extends EntityManager {
|
|
|
81
81
|
const { where: rawWhere, ...countOptions } = options;
|
|
82
82
|
await em.tryFlush(entityName, options);
|
|
83
83
|
const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
|
|
84
|
-
|
|
84
|
+
// match `em.count()` semantics: an active transaction always wins over the requested connection type
|
|
85
|
+
const connectionType = em.getTransactionContext() ? 'write' : options.connectionType;
|
|
86
|
+
const qb = em.createQueryBuilder(meta.class, undefined, connectionType);
|
|
85
87
|
qb
|
|
86
88
|
.select([...fields, raw('count(*) as cnt')])
|
|
87
89
|
.where(where)
|
|
@@ -16,6 +16,8 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
16
16
|
getEnumArrayCheckConstraintExpression(column: string, items: string[]): string;
|
|
17
17
|
supportsMaterializedViews(): boolean;
|
|
18
18
|
supportsPartitionedTables(): boolean;
|
|
19
|
+
supportsRowLevelSecurity(): boolean;
|
|
20
|
+
getCurrentSettingCast(mappedType: Type<unknown>): string | null;
|
|
19
21
|
supportsCustomPrimaryKeyNames(): boolean;
|
|
20
22
|
getCurrentTimestampSQL(length: number): string;
|
|
21
23
|
getDateTimeTypeDeclarationSQL(column: {
|
|
@@ -88,7 +90,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
88
90
|
}): string;
|
|
89
91
|
getBlobDeclarationSQL(): string;
|
|
90
92
|
getJsonDeclarationSQL(): string;
|
|
91
|
-
getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean, value?: unknown): string | RawQueryFragment;
|
|
93
|
+
getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
|
|
92
94
|
getJsonIndexDefinition(index: IndexDef): string[];
|
|
93
95
|
quoteIdentifier(id: string | {
|
|
94
96
|
toString: () => string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALIAS_REPLACEMENT, ARRAY_OPERATORS, raw, RawQueryFragment, Type, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { ALIAS_REPLACEMENT, ARRAY_OPERATORS, BigIntType, BooleanType, DateTimeType, DateType, EnumType, IntegerType, raw, RawQueryFragment, SmallIntType, StringType, TextType, TimeType, TinyIntType, Type, Utils, UuidType, } from '@mikro-orm/core';
|
|
2
2
|
import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
|
|
3
3
|
import { PostgreSqlNativeQueryBuilder } from './PostgreSqlNativeQueryBuilder.js';
|
|
4
4
|
import { PostgreSqlSchemaHelper } from './PostgreSqlSchemaHelper.js';
|
|
@@ -33,6 +33,38 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
33
33
|
supportsPartitionedTables() {
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
|
+
supportsRowLevelSecurity() {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
getCurrentSettingCast(mappedType) {
|
|
40
|
+
if (mappedType instanceof UuidType) {
|
|
41
|
+
return '::uuid';
|
|
42
|
+
}
|
|
43
|
+
if (mappedType instanceof BigIntType) {
|
|
44
|
+
return '::bigint';
|
|
45
|
+
}
|
|
46
|
+
// MediumIntType extends IntegerType, so it is covered here too
|
|
47
|
+
if (mappedType instanceof IntegerType || mappedType instanceof SmallIntType || mappedType instanceof TinyIntType) {
|
|
48
|
+
return '::int';
|
|
49
|
+
}
|
|
50
|
+
if (mappedType instanceof BooleanType) {
|
|
51
|
+
return '::boolean';
|
|
52
|
+
}
|
|
53
|
+
if (mappedType instanceof DateTimeType) {
|
|
54
|
+
return '::timestamptz';
|
|
55
|
+
}
|
|
56
|
+
if (mappedType instanceof DateType) {
|
|
57
|
+
return '::date';
|
|
58
|
+
}
|
|
59
|
+
if (mappedType instanceof TimeType) {
|
|
60
|
+
return '::time';
|
|
61
|
+
}
|
|
62
|
+
// current_setting() returns text already, so string-compatible types need no cast (CharacterType extends StringType)
|
|
63
|
+
if (mappedType instanceof StringType || mappedType instanceof TextType || mappedType instanceof EnumType) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
36
68
|
supportsCustomPrimaryKeyNames() {
|
|
37
69
|
return true;
|
|
38
70
|
}
|
|
@@ -285,7 +317,8 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
285
317
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
286
318
|
const first = path.shift();
|
|
287
319
|
const last = path.pop();
|
|
288
|
-
const
|
|
320
|
+
const alias = typeof aliased === 'string' ? aliased : ALIAS_REPLACEMENT;
|
|
321
|
+
const root = this.quoteIdentifier(aliased ? `${alias}.${first}` : first);
|
|
289
322
|
type = typeof type === 'string' ? this.getMappedType(type).runtimeType : String(type);
|
|
290
323
|
const cast = (key) => raw(type in this.#jsonTypeCasts ? `(${key})::${this.#jsonTypeCasts[type]}` : key);
|
|
291
324
|
let lastOperator = '->>';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeadlockException, ExceptionConverter, ForeignKeyConstraintViolationException, InvalidFieldNameException, NonUniqueFieldNameException, NotNullConstraintViolationException, SyntaxErrorException, TableExistsException, TableNotFoundException, UniqueConstraintViolationException, CheckConstraintViolationException, } from '@mikro-orm/core';
|
|
1
|
+
import { DeadlockException, ExceptionConverter, ForeignKeyConstraintViolationException, InvalidFieldNameException, NonUniqueFieldNameException, NotNullConstraintViolationException, RowLevelSecurityViolationException, SyntaxErrorException, TableExistsException, TableNotFoundException, UniqueConstraintViolationException, CheckConstraintViolationException, } from '@mikro-orm/core';
|
|
2
2
|
export class PostgreSqlExceptionConverter extends ExceptionConverter {
|
|
3
3
|
/**
|
|
4
4
|
* @see http://www.postgresql.org/docs/9.4/static/errcodes-appendix.html
|
|
@@ -31,6 +31,13 @@ export class PostgreSqlExceptionConverter extends ExceptionConverter {
|
|
|
31
31
|
return new UniqueConstraintViolationException(exception);
|
|
32
32
|
case '23514':
|
|
33
33
|
return new CheckConstraintViolationException(exception);
|
|
34
|
+
case '42501':
|
|
35
|
+
// 42501 is generic insufficient_privilege; only RLS write violations carry this message — the `routine`
|
|
36
|
+
// field covers servers with a non-english `lc_messages`, where the message check cannot match
|
|
37
|
+
if (exception.message.includes('row-level security policy') || exception.routine === 'ExecWithCheckOptions') {
|
|
38
|
+
return new RowLevelSecurityViolationException(exception);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
34
41
|
case '42601':
|
|
35
42
|
return new SyntaxErrorException(exception);
|
|
36
43
|
case '42702':
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Dictionary, type Transaction } from '@mikro-orm/core';
|
|
2
2
|
import { SchemaHelper } from '../../schema/SchemaHelper.js';
|
|
3
3
|
import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
|
|
4
|
-
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlTriggerDef, SqlRoutineDef } from '../../typings.js';
|
|
4
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlPolicyDef, SqlTriggerDef, SqlRoutineDef } from '../../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from '../../schema/DatabaseTable.js';
|
|
7
7
|
export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
@@ -71,6 +71,17 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
71
71
|
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
72
72
|
/** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
|
|
73
73
|
private flattenDollarQuotedBodies;
|
|
74
|
+
getRlsCreateSQL(table: DatabaseTable): string[];
|
|
75
|
+
getRlsDropSQL(diff: TableDifference, safe?: boolean): string[];
|
|
76
|
+
getRlsAlterSQL(diff: TableDifference, safe?: boolean): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Quotes a policy or role name as a single identifier. Unlike `quote()`/`platform.quoteIdentifier`, which treat
|
|
79
|
+
* a dot as a schema qualifier, these names are never schema-qualified, so `my.role` must render as `"my.role"`.
|
|
80
|
+
*/
|
|
81
|
+
private quoteUnqualified;
|
|
82
|
+
private createPolicy;
|
|
83
|
+
private dropPolicy;
|
|
84
|
+
private formatPolicyRoles;
|
|
74
85
|
createRoutine(routine: SqlRoutineDef): string;
|
|
75
86
|
dropRoutine(routine: SqlRoutineDef): string;
|
|
76
87
|
getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
@@ -87,6 +98,12 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
87
98
|
getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
|
|
88
99
|
getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
|
|
89
100
|
private getTriggersSQL;
|
|
101
|
+
getAllPolicies(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<{
|
|
102
|
+
policies: SqlPolicyDef[];
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
forced: boolean;
|
|
105
|
+
}>>;
|
|
106
|
+
private parsePgRoles;
|
|
90
107
|
getAllForeignKeys(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
|
|
91
108
|
getNativeEnumDefinitions(connection: AbstractSqlConnection, schemas: string[], ctx?: Transaction): Promise<Dictionary<{
|
|
92
109
|
name: string;
|