@mikro-orm/sql 7.1.16-dev.1 → 7.1.16-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/AbstractSqlConnection.d.ts +21 -3
- package/AbstractSqlConnection.js +54 -4
- package/AbstractSqlDriver.d.ts +1 -1
- package/AbstractSqlDriver.js +17 -4
- package/AbstractSqlPlatform.d.ts +3 -1
- package/AbstractSqlPlatform.js +32 -1
- package/README.md +1 -0
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +1 -1
- package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
- package/dialects/mysql/BaseMySqlPlatform.js +4 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +2 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +33 -1
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +18 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +152 -0
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/package.json +2 -2
- package/query/ObjectCriteriaNode.d.ts +1 -0
- package/query/ObjectCriteriaNode.js +28 -4
- package/query/QueryBuilder.js +21 -7
- package/query/QueryBuilderHelper.js +4 -0
- 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 +90 -0
- package/schema/SchemaHelper.d.ts +6 -0
- package/schema/SchemaHelper.js +15 -0
- package/schema/SqlSchemaGenerator.js +8 -0
- package/typings.d.ts +16 -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. */
|
|
@@ -36,6 +36,13 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
36
36
|
getClient<T = any>(): Kysely<T>;
|
|
37
37
|
/** Ensures the Kysely client is initialized, creating it asynchronously if needed. */
|
|
38
38
|
initClient(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Guards `getNativeClient()` overrides — drivers only capture their client while building their
|
|
41
|
+
* own dialect, which `driverOptions` carrying a ready-made Kysely instance or dialect skips.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
protected requireNativeClient<T>(client: T | undefined | null): T;
|
|
39
46
|
/** Executes a callback within a transaction, committing on success and rolling back on error. */
|
|
40
47
|
transactional<T>(cb: (trx: Transaction<ControlledTransaction<any, any>>) => Promise<T>, options?: {
|
|
41
48
|
isolationLevel?: IsolationLevel;
|
|
@@ -43,6 +50,7 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
43
50
|
ctx?: ControlledTransaction<any>;
|
|
44
51
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
45
52
|
loggerContext?: LogContext;
|
|
53
|
+
sessionContext?: SessionContext;
|
|
46
54
|
}): Promise<T>;
|
|
47
55
|
/** Begins a new transaction or creates a savepoint if a transaction context already exists. */
|
|
48
56
|
begin(options?: {
|
|
@@ -51,7 +59,17 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
51
59
|
ctx?: ControlledTransaction<any, any>;
|
|
52
60
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
53
61
|
loggerContext?: LogContext;
|
|
62
|
+
sessionContext?: SessionContext;
|
|
54
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;
|
|
55
73
|
/** Commits the transaction or releases the savepoint. */
|
|
56
74
|
commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
57
75
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
|
@@ -67,9 +85,9 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
67
85
|
private waitForIdleTransaction;
|
|
68
86
|
private prepareQuery;
|
|
69
87
|
/** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
|
|
70
|
-
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[]
|
|
88
|
+
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>, method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
|
|
71
89
|
/** Executes a SQL query and returns an async iterable that yields results row by row. */
|
|
72
|
-
stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[]
|
|
90
|
+
stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>, ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
|
|
73
91
|
/** @inheritDoc */
|
|
74
92
|
executeDump(dump: string): Promise<void>;
|
|
75
93
|
protected getSql(query: string, formatted: string, context?: LogContext): string;
|
package/AbstractSqlConnection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CompiledQuery, Kysely } from 'kysely';
|
|
2
|
-
import { Connection, EventType, isRaw, Utils, } from '@mikro-orm/core';
|
|
2
|
+
import { Connection, EventType, isRaw, raw, Utils, } from '@mikro-orm/core';
|
|
3
3
|
import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
4
4
|
/**
|
|
5
5
|
* Pulls cancellation controls out of a `loggerContext` payload, returning the abort options
|
|
@@ -101,6 +101,18 @@ export class AbstractSqlConnection extends Connection {
|
|
|
101
101
|
await this.createKysely();
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Guards `getNativeClient()` overrides — drivers only capture their client while building their
|
|
106
|
+
* own dialect, which `driverOptions` carrying a ready-made Kysely instance or dialect skips.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
requireNativeClient(client) {
|
|
111
|
+
if (client == null) {
|
|
112
|
+
throw new Error('The native client is not available, as it is owned by the Kysely instance or dialect passed via `driverOptions`. Access it through the object you provided there instead.');
|
|
113
|
+
}
|
|
114
|
+
return client;
|
|
115
|
+
}
|
|
104
116
|
/** Executes a callback within a transaction, committing on success and rolling back on error. */
|
|
105
117
|
async transactional(cb, options = {}) {
|
|
106
118
|
const trx = await this.begin(options);
|
|
@@ -149,9 +161,43 @@ export class AbstractSqlConnection extends Connection {
|
|
|
149
161
|
for (const query of this.platform.getBeginTransactionSQL(options)) {
|
|
150
162
|
this.logQuery(query, options.loggerContext);
|
|
151
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
|
+
}
|
|
152
175
|
await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
|
|
153
176
|
return trx;
|
|
154
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
|
+
}
|
|
155
201
|
/** Commits the transaction or releases the savepoint. */
|
|
156
202
|
async commit(ctx, eventBroadcaster, loggerContext) {
|
|
157
203
|
if (ctx.isRolledBack) {
|
|
@@ -197,13 +243,17 @@ export class AbstractSqlConnection extends Connection {
|
|
|
197
243
|
if (query instanceof NativeQueryBuilder) {
|
|
198
244
|
query = query.toRaw();
|
|
199
245
|
}
|
|
246
|
+
if (typeof query === 'string' && !Array.isArray(params)) {
|
|
247
|
+
// plain object params hold named parameters, translate them via the `raw()` helper
|
|
248
|
+
query = raw(query, params);
|
|
249
|
+
}
|
|
200
250
|
if (isRaw(query)) {
|
|
201
251
|
params = query.params;
|
|
202
252
|
query = query.sql;
|
|
203
253
|
}
|
|
204
254
|
query = this.config.get('onQuery')(query, params);
|
|
205
255
|
const formatted = this.platform.formatQuery(query, params);
|
|
206
|
-
return { query, params, formatted };
|
|
256
|
+
return { query, params: params, formatted };
|
|
207
257
|
}
|
|
208
258
|
/** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
|
|
209
259
|
async execute(query, params = [], method = 'all', ctx, loggerContext) {
|
|
@@ -237,7 +287,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
237
287
|
.stream(compiled, chunkSize ?? 100, abort ? { signal: abort.signal } : undefined);
|
|
238
288
|
this.logQuery(sql, {
|
|
239
289
|
sql,
|
|
240
|
-
params,
|
|
290
|
+
params: q.params,
|
|
241
291
|
...cleanCtx,
|
|
242
292
|
affected: Utils.isPlainObject(res) ? res.affectedRows : undefined,
|
|
243
293
|
});
|
|
@@ -248,7 +298,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
248
298
|
}
|
|
249
299
|
}
|
|
250
300
|
catch (e) {
|
|
251
|
-
this.logQuery(sql, { sql, params, ...cleanCtx, level: 'error' });
|
|
301
|
+
this.logQuery(sql, { sql, params: q.params, ...cleanCtx, level: 'error' });
|
|
252
302
|
throw e;
|
|
253
303
|
}
|
|
254
304
|
}
|
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
100
100
|
*/
|
|
101
101
|
private convertOwnerPksForPivotQuery;
|
|
102
102
|
private getPivotOrderBy;
|
|
103
|
-
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
|
|
103
|
+
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[] | Dictionary, method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
|
|
104
104
|
stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
|
|
105
105
|
/**
|
|
106
106
|
* 1:1 owner side needs to be marked for population so QB auto-joins the owner id
|
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
|
}
|
|
@@ -1333,7 +1346,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1333
1346
|
],
|
|
1334
1347
|
populateWhere: undefined,
|
|
1335
1348
|
_populateWhere: 'infer',
|
|
1336
|
-
populateFilter: this.wrapPopulateFilter(options,
|
|
1349
|
+
populateFilter: this.wrapPopulateFilter(options, pivotProp1.name),
|
|
1337
1350
|
};
|
|
1338
1351
|
if (pivotFindOptions._partitionLimit) {
|
|
1339
1352
|
pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
|
package/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RawQueryFragment, type Constructor, type EntityManager, type EntityRepository, type IDatabaseDriver, type IsolationLevel, type MikroORM, Platform } from '@mikro-orm/core';
|
|
1
|
+
import { type RawQueryFragment, type Constructor, type EntityManager, type EntityProperty, type EntityRepository, type FormulaColumns, type IDatabaseDriver, type IsolationLevel, type MikroORM, Platform } from '@mikro-orm/core';
|
|
2
2
|
import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js';
|
|
3
3
|
import { type SchemaHelper } from './schema/SchemaHelper.js';
|
|
4
4
|
import type { IndexDef } from './typings.js';
|
|
@@ -17,6 +17,8 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
17
17
|
getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): SqlSchemaGenerator;
|
|
18
18
|
/** @internal */
|
|
19
19
|
createNativeQueryBuilder(): NativeQueryBuilder;
|
|
20
|
+
/** @inheritDoc */
|
|
21
|
+
getThroughRelationFormula(prop: EntityProperty, columns: FormulaColumns<any>): string;
|
|
20
22
|
getBeginTransactionSQL(options?: {
|
|
21
23
|
isolationLevel?: IsolationLevel;
|
|
22
24
|
readOnly?: boolean;
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRaw, JsonProperty, Platform, raw, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { isRaw, JsonProperty, Platform, raw, Utils, ValidationError, } from '@mikro-orm/core';
|
|
2
2
|
import { SqlEntityRepository } from './SqlEntityRepository.js';
|
|
3
3
|
import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js';
|
|
4
4
|
import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
@@ -39,6 +39,37 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
39
39
|
createNativeQueryBuilder() {
|
|
40
40
|
return new NativeQueryBuilder(this);
|
|
41
41
|
}
|
|
42
|
+
/** @inheritDoc */
|
|
43
|
+
getThroughRelationFormula(prop, columns) {
|
|
44
|
+
const through = prop.through;
|
|
45
|
+
const driver = this.config.getDriver();
|
|
46
|
+
const alias = `${prop.name}_through`;
|
|
47
|
+
const qb = driver.createQueryBuilder(through.entity, undefined, 'read', true, undefined, alias);
|
|
48
|
+
const throughMeta = driver.getMetadata().get(through.entity);
|
|
49
|
+
const ownerProp = throughMeta.properties[through.ownerProperty];
|
|
50
|
+
const ownerMeta = driver.getMetadata().get(ownerProp.target);
|
|
51
|
+
ownerProp.fieldNames.forEach((fieldName, idx) => {
|
|
52
|
+
const referencedColumn = ownerProp.referencedColumnNames[idx];
|
|
53
|
+
const referencedProp = ownerProp.referencedPKs
|
|
54
|
+
.map(name => ownerMeta.properties[name])
|
|
55
|
+
.find(p => p.fieldNames.includes(referencedColumn));
|
|
56
|
+
// the column mapping resolves the right alias even for TPT owners, only the column name may differ for composite FK properties
|
|
57
|
+
const ownerAlias = columns[referencedProp.name].slice(0, columns[referencedProp.name].lastIndexOf('.'));
|
|
58
|
+
qb.andWhere(raw('?? = ??', [`${alias}.${fieldName}`, `${ownerAlias}.${referencedColumn}`]));
|
|
59
|
+
});
|
|
60
|
+
if (through.where) {
|
|
61
|
+
qb.andWhere(through.where);
|
|
62
|
+
}
|
|
63
|
+
if (through.orderBy) {
|
|
64
|
+
qb.orderBy(through.orderBy);
|
|
65
|
+
}
|
|
66
|
+
qb.select(through.targetProperty ?? throughMeta.primaryKeys[0]).limit(1);
|
|
67
|
+
const sql = qb.getFormattedQuery();
|
|
68
|
+
if (Object.keys(qb.state.joins).length > 0) {
|
|
69
|
+
throw new ValidationError(`The 'where' and 'orderBy' options of the through relation ${prop.name} can only reference own columns of ${throughMeta.className}`);
|
|
70
|
+
}
|
|
71
|
+
return `(${sql})`;
|
|
72
|
+
}
|
|
42
73
|
getBeginTransactionSQL(options) {
|
|
43
74
|
if (options?.isolationLevel) {
|
|
44
75
|
return [`set transaction isolation level ${options.isolationLevel}`, 'begin'];
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ npm install @mikro-orm/mysql # MySQL
|
|
|
24
24
|
npm install @mikro-orm/mariadb # MariaDB
|
|
25
25
|
npm install @mikro-orm/sqlite # SQLite
|
|
26
26
|
npm install @mikro-orm/libsql # libSQL / Turso
|
|
27
|
+
npm install @mikro-orm/sql-js # sql.js (in-memory SQLite in WASM)
|
|
27
28
|
npm install @mikro-orm/mongodb # MongoDB
|
|
28
29
|
npm install @mikro-orm/mssql # MS SQL Server
|
|
29
30
|
npm install @mikro-orm/oracledb # Oracle
|
package/SqlEntityManager.d.ts
CHANGED
|
@@ -60,14 +60,14 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
|
|
|
60
60
|
* `signal` / `inflightQueryAbortStrategy` (set via `em.fork({ signal })`) is applied automatically.
|
|
61
61
|
* For per-call cancellation use the options-bag overload below.
|
|
62
62
|
*/
|
|
63
|
-
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
|
|
63
|
+
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[] | Dictionary, method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
|
|
64
64
|
/**
|
|
65
65
|
* Executes a raw SQL query with an options bag carrying `method`, `loggerContext`, `signal`
|
|
66
66
|
* and `inflightQueryAbortStrategy`. Per-call `signal` / `inflightQueryAbortStrategy` override
|
|
67
67
|
* the fork-level defaults set via `em.fork({ signal })`. The current transaction context is
|
|
68
68
|
* applied automatically.
|
|
69
69
|
*/
|
|
70
|
-
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params: any[], options: EmExecuteOptions): Promise<T>;
|
|
70
|
+
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params: any[] | Dictionary, options: EmExecuteOptions): Promise<T>;
|
|
71
71
|
/**
|
|
72
72
|
* @inheritDoc
|
|
73
73
|
*/
|
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
|
|
@@ -14,6 +14,8 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
14
14
|
readonly "desc nulls first": 'is not null';
|
|
15
15
|
readonly "desc nulls last": 'is null';
|
|
16
16
|
};
|
|
17
|
+
/** mysql and mariadb treat null as the lowest value when no placement is requested. */
|
|
18
|
+
sortsNullsLowest(): boolean;
|
|
17
19
|
supportsMultiColumnCountDistinct(): boolean;
|
|
18
20
|
/** @internal */
|
|
19
21
|
createNativeQueryBuilder(): MySqlNativeQueryBuilder;
|
|
@@ -18,6 +18,10 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
18
18
|
[QueryOrder.desc_nulls_first]: 'is not null',
|
|
19
19
|
[QueryOrder.desc_nulls_last]: 'is null',
|
|
20
20
|
};
|
|
21
|
+
/** mysql and mariadb treat null as the lowest value when no placement is requested. */
|
|
22
|
+
sortsNullsLowest() {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
21
25
|
supportsMultiColumnCountDistinct() {
|
|
22
26
|
return true;
|
|
23
27
|
}
|
|
@@ -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: {
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -162,6 +162,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
162
162
|
const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
|
|
163
163
|
const partitionings = await this.getPartitions(connection, tablesBySchema, ctx);
|
|
164
164
|
const triggers = await this.getAllTriggers(connection, tablesBySchema);
|
|
165
|
+
const policies = await this.getAllPolicies(connection, tablesBySchema, ctx);
|
|
165
166
|
const dbCollation = await this.getDatabaseCollation(connection, ctx);
|
|
166
167
|
for (const t of tables) {
|
|
167
168
|
const key = this.getTableKey(t);
|
|
@@ -175,6 +176,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
175
176
|
if (triggers[key]) {
|
|
176
177
|
table.setTriggers(triggers[key]);
|
|
177
178
|
}
|
|
179
|
+
const rls = policies[key];
|
|
180
|
+
if (rls) {
|
|
181
|
+
table.setPolicies(rls.policies);
|
|
182
|
+
table.rlsEnabled = rls.enabled;
|
|
183
|
+
table.rlsForced = rls.forced;
|
|
184
|
+
}
|
|
178
185
|
table.setPartitioning(partitionings[key]);
|
|
179
186
|
}
|
|
180
187
|
}
|
|
@@ -540,6 +547,83 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
540
547
|
})
|
|
541
548
|
.join('');
|
|
542
549
|
}
|
|
550
|
+
getRlsCreateSQL(table) {
|
|
551
|
+
const ret = [];
|
|
552
|
+
if (table.rlsEnabled) {
|
|
553
|
+
ret.push(`alter table ${table.getQuotedName()} enable row level security`);
|
|
554
|
+
}
|
|
555
|
+
if (table.rlsForced) {
|
|
556
|
+
ret.push(`alter table ${table.getQuotedName()} force row level security`);
|
|
557
|
+
}
|
|
558
|
+
for (const policy of table.getPolicies()) {
|
|
559
|
+
ret.push(this.createPolicy(table, policy));
|
|
560
|
+
}
|
|
561
|
+
return ret;
|
|
562
|
+
}
|
|
563
|
+
getRlsDropSQL(diff, safe) {
|
|
564
|
+
// a policy expression holds a dependency on the columns it references, so removed policies (including the
|
|
565
|
+
// old version of changed ones) must be dropped before the column drops emitted later in the diff;
|
|
566
|
+
// in safe mode only recreated policies (present in both removed + added) are dropped, so a policy that is
|
|
567
|
+
// merely removed is left untouched like removed triggers/columns
|
|
568
|
+
return Object.values(diff.removedPolicies)
|
|
569
|
+
.filter(policy => !safe || policy.name in diff.addedPolicies)
|
|
570
|
+
.map(policy => this.dropPolicy(diff.toTable, policy));
|
|
571
|
+
}
|
|
572
|
+
getRlsAlterSQL(diff, safe) {
|
|
573
|
+
const ret = [];
|
|
574
|
+
const table = diff.toTable;
|
|
575
|
+
if (diff.changedRlsEnabled === true) {
|
|
576
|
+
ret.push(`alter table ${table.getQuotedName()} enable row level security`);
|
|
577
|
+
}
|
|
578
|
+
if (diff.changedRlsForced === true) {
|
|
579
|
+
ret.push(`alter table ${table.getQuotedName()} force row level security`);
|
|
580
|
+
}
|
|
581
|
+
else if (!safe && diff.changedRlsForced === false) {
|
|
582
|
+
ret.push(`alter table ${table.getQuotedName()} no force row level security`);
|
|
583
|
+
}
|
|
584
|
+
for (const policy of Object.values(diff.addedPolicies)) {
|
|
585
|
+
ret.push(this.createPolicy(table, policy));
|
|
586
|
+
}
|
|
587
|
+
// disable only after its policies are gone (removed ones were dropped via `getRlsDropSQL`); skipped in
|
|
588
|
+
// safe mode so a safe run never lifts row level security off an existing table
|
|
589
|
+
if (!safe && diff.changedRlsEnabled === false) {
|
|
590
|
+
ret.push(`alter table ${table.getQuotedName()} disable row level security`);
|
|
591
|
+
}
|
|
592
|
+
return ret;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Quotes a policy or role name as a single identifier. Unlike `quote()`/`platform.quoteIdentifier`, which treat
|
|
596
|
+
* a dot as a schema qualifier, these names are never schema-qualified, so `my.role` must render as `"my.role"`.
|
|
597
|
+
*/
|
|
598
|
+
quoteUnqualified(name) {
|
|
599
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
600
|
+
}
|
|
601
|
+
createPolicy(table, policy) {
|
|
602
|
+
const parts = [`create policy ${this.quoteUnqualified(policy.name)} on ${table.getQuotedName()}`];
|
|
603
|
+
if (policy.type === 'restrictive') {
|
|
604
|
+
parts.push('as restrictive');
|
|
605
|
+
}
|
|
606
|
+
if (policy.command !== 'all') {
|
|
607
|
+
parts.push(`for ${policy.command}`);
|
|
608
|
+
}
|
|
609
|
+
if (policy.roles.length > 0) {
|
|
610
|
+
parts.push(`to ${this.formatPolicyRoles(policy.roles)}`);
|
|
611
|
+
}
|
|
612
|
+
if (policy.using) {
|
|
613
|
+
parts.push(`using (${policy.using})`);
|
|
614
|
+
}
|
|
615
|
+
if (policy.check) {
|
|
616
|
+
parts.push(`with check (${policy.check})`);
|
|
617
|
+
}
|
|
618
|
+
return parts.join(' ');
|
|
619
|
+
}
|
|
620
|
+
dropPolicy(table, policy) {
|
|
621
|
+
return `drop policy ${this.quoteUnqualified(policy.name)} on ${table.getQuotedName()}`;
|
|
622
|
+
}
|
|
623
|
+
// `public` is a keyword and must stay unquoted; other roles are quoted like any identifier
|
|
624
|
+
formatPolicyRoles(roles) {
|
|
625
|
+
return roles.map(role => (role === 'public' ? 'public' : this.quoteUnqualified(role))).join(', ');
|
|
626
|
+
}
|
|
543
627
|
createRoutine(routine) {
|
|
544
628
|
if (routine.expression) {
|
|
545
629
|
return this.flattenDollarQuotedBodies(routine.expression);
|
|
@@ -732,6 +816,56 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
732
816
|
where (${conditions.join(' or ')})
|
|
733
817
|
order by t.trigger_name, t.event_manipulation`;
|
|
734
818
|
}
|
|
819
|
+
async getAllPolicies(connection, tablesBySchemas, ctx) {
|
|
820
|
+
const conditionsFor = (schemaColumn, tableColumn) => [...tablesBySchemas.entries()].map(([schema, tables]) => {
|
|
821
|
+
const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
|
|
822
|
+
const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
|
|
823
|
+
return `(${schemaColumn} = ${schemaName} and ${tableColumn} in (${names}))`;
|
|
824
|
+
});
|
|
825
|
+
const flagConditions = conditionsFor('ns.nspname', 'cls.relname');
|
|
826
|
+
const policyConditions = conditionsFor('schemaname', 'tablename');
|
|
827
|
+
// pg_class carries the enable/force flags (a table can enable RLS with zero policies)
|
|
828
|
+
const flagRows = await connection.execute(`select cls.relname as table_name, ns.nspname as schema_name, cls.relrowsecurity as enabled, cls.relforcerowsecurity as forced
|
|
829
|
+
from pg_class cls
|
|
830
|
+
join pg_namespace ns on ns.oid = cls.relnamespace
|
|
831
|
+
where (${flagConditions.join(' or ')})`, [], 'all', ctx);
|
|
832
|
+
const policyRows = await connection.execute(`select tablename as table_name, schemaname as schema_name, policyname as name, permissive, roles, cmd, qual, with_check
|
|
833
|
+
from pg_policies
|
|
834
|
+
where (${policyConditions.join(' or ')})
|
|
835
|
+
order by policyname`, [], 'all', ctx);
|
|
836
|
+
const policiesByTable = {};
|
|
837
|
+
for (const row of policyRows) {
|
|
838
|
+
const key = this.getTableKey(row);
|
|
839
|
+
(policiesByTable[key] ??= []).push({
|
|
840
|
+
name: row.name,
|
|
841
|
+
command: row.cmd.toLowerCase(),
|
|
842
|
+
type: row.permissive === 'PERMISSIVE' ? 'permissive' : 'restrictive',
|
|
843
|
+
roles: this.parsePgRoles(row.roles),
|
|
844
|
+
using: row.qual ?? undefined,
|
|
845
|
+
check: row.with_check ?? undefined,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
const ret = {};
|
|
849
|
+
for (const row of flagRows) {
|
|
850
|
+
const key = this.getTableKey(row);
|
|
851
|
+
ret[key] = { policies: policiesByTable[key] ?? [], enabled: row.enabled, forced: row.forced };
|
|
852
|
+
}
|
|
853
|
+
return ret;
|
|
854
|
+
}
|
|
855
|
+
// node-postgres returns `pg_policies.roles` as an unparsed array literal (`{public}`), pglite as an array
|
|
856
|
+
parsePgRoles(value) {
|
|
857
|
+
if (Array.isArray(value)) {
|
|
858
|
+
return value;
|
|
859
|
+
}
|
|
860
|
+
// tokenize the array literal instead of splitting on commas — quoted role names can contain
|
|
861
|
+
// commas, and quoted elements escape `"` and `\` with a backslash
|
|
862
|
+
const roles = [];
|
|
863
|
+
const re = /"((?:[^"\\]|\\.)*)"|[^,]+/g;
|
|
864
|
+
for (const match of value.replace(/^\{|\}$/g, '').matchAll(re)) {
|
|
865
|
+
roles.push(match[1] != null ? match[1].replace(/\\(.)/g, '$1') : match[0]);
|
|
866
|
+
}
|
|
867
|
+
return roles;
|
|
868
|
+
}
|
|
735
869
|
async getAllForeignKeys(connection, tablesBySchemas, ctx) {
|
|
736
870
|
const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
|
|
737
871
|
cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
|
|
@@ -966,6 +1100,24 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
966
1100
|
for (const { table: localTable, foreignKey } of inboundForeignKeys) {
|
|
967
1101
|
this.append(ret, this.createForeignKey(localTable, foreignKey));
|
|
968
1102
|
}
|
|
1103
|
+
// re-enable row level security and recreate the policies (createTable skips them during a rebuild);
|
|
1104
|
+
// emitted after the data copy so `force row level security` can't block the owner's insert
|
|
1105
|
+
this.append(ret, this.getRlsCreateSQL(table));
|
|
1106
|
+
// with `ignorePolicies`, hand-written policies (and the RLS flags they imply) may exist only on the
|
|
1107
|
+
// introspected side — recreate them verbatim, or the rebuild would silently strip them
|
|
1108
|
+
if (this.options.ignorePolicies) {
|
|
1109
|
+
if (diff.fromTable.rlsEnabled && !table.rlsEnabled) {
|
|
1110
|
+
ret.push(`alter table ${table.getQuotedName()} enable row level security`);
|
|
1111
|
+
}
|
|
1112
|
+
if (diff.fromTable.rlsForced && !table.rlsForced) {
|
|
1113
|
+
ret.push(`alter table ${table.getQuotedName()} force row level security`);
|
|
1114
|
+
}
|
|
1115
|
+
for (const policy of diff.fromTable.getPolicies()) {
|
|
1116
|
+
if (!table.hasPolicy(policy.name)) {
|
|
1117
|
+
ret.push(this.createPolicy(table, policy));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
969
1121
|
}
|
|
970
1122
|
if (safe) {
|
|
971
1123
|
ret.push(`-- safe mode: original tables kept in schema "${tmpSchema}"; drop that schema manually once the data is verified`);
|