@mikro-orm/sql 7.2.0-dev.2 → 7.2.0-dev.20

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.
Files changed (46) hide show
  1. package/AbstractSqlConnection.d.ts +30 -3
  2. package/AbstractSqlConnection.js +75 -15
  3. package/AbstractSqlDriver.d.ts +1 -8
  4. package/AbstractSqlDriver.js +124 -55
  5. package/AbstractSqlPlatform.d.ts +4 -2
  6. package/AbstractSqlPlatform.js +35 -1
  7. package/SqlEntityManager.d.ts +2 -2
  8. package/SqlEntityManager.js +5 -4
  9. package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
  10. package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
  11. package/dialects/mysql/BaseMySqlPlatform.js +4 -0
  12. package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
  13. package/dialects/mysql/MySqlSchemaHelper.js +4 -1
  14. package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
  15. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
  16. package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
  17. package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
  18. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +22 -1
  19. package/dialects/postgresql/PostgreSqlSchemaHelper.js +181 -4
  20. package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
  21. package/dialects/sqlite/BaseSqliteConnection.js +15 -5
  22. package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
  23. package/dialects/sqlite/SqlitePlatform.js +4 -0
  24. package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
  25. package/package.json +4 -4
  26. package/plugin/transformer.d.ts +7 -1
  27. package/plugin/transformer.js +60 -1
  28. package/query/CriteriaNodeFactory.js +4 -0
  29. package/query/NativeQueryBuilder.js +1 -1
  30. package/query/ObjectCriteriaNode.d.ts +1 -0
  31. package/query/ObjectCriteriaNode.js +30 -5
  32. package/query/QueryBuilder.d.ts +40 -7
  33. package/query/QueryBuilder.js +181 -37
  34. package/query/QueryBuilderHelper.d.ts +5 -0
  35. package/query/QueryBuilderHelper.js +39 -9
  36. package/schema/DatabaseSchema.d.ts +4 -0
  37. package/schema/DatabaseSchema.js +107 -1
  38. package/schema/DatabaseTable.d.ts +15 -1
  39. package/schema/DatabaseTable.js +113 -19
  40. package/schema/SchemaComparator.d.ts +3 -0
  41. package/schema/SchemaComparator.js +123 -10
  42. package/schema/SchemaHelper.d.ts +26 -1
  43. package/schema/SchemaHelper.js +67 -9
  44. package/schema/SqlSchemaGenerator.d.ts +4 -0
  45. package/schema/SqlSchemaGenerator.js +59 -22
  46. package/typings.d.ts +20 -2
@@ -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;
@@ -28,7 +30,7 @@ export declare abstract class AbstractSqlPlatform extends Platform {
28
30
  getReleaseSavepointSQL(savepointName: string): string;
29
31
  quoteValue(value: any): string;
30
32
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
31
- getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
33
+ getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
32
34
  /**
33
35
  * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
34
36
  * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
@@ -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'];
@@ -75,6 +106,9 @@ export class AbstractSqlPlatform extends Platform {
75
106
  getSearchJsonPropertyKey(path, type, aliased, value) {
76
107
  const [a, ...b] = path;
77
108
  const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
109
+ if (typeof aliased === 'string') {
110
+ return raw(`json_extract(${this.quoteIdentifier(`${aliased}.${a}`)}, ${jsonPath})`);
111
+ }
78
112
  if (aliased) {
79
113
  return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, ${jsonPath})`);
80
114
  }
@@ -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
  */
@@ -68,21 +68,22 @@ 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', context.getTransactionContext(), merged);
71
+ return context.withSessionContext(context.getTransactionContext(), ctx => this.getDriver().execute(query, params, opts.method ?? 'all', ctx, merged));
72
72
  }
73
73
  /**
74
74
  * @inheritDoc
75
75
  */
76
76
  async countBy(entityName, groupBy, options = {}) {
77
77
  const em = this.getContext(false);
78
- options = { ...options };
79
- em.prepareOptions(options);
78
+ options = em.prepareOptions(options);
80
79
  const meta = em.getMetadata().find(entityName);
81
80
  const fields = Utils.asArray(groupBy);
82
81
  const { where: rawWhere, ...countOptions } = options;
83
82
  await em.tryFlush(entityName, options);
84
83
  const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
85
- const qb = em.createQueryBuilder(meta.class);
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);
86
87
  qb
87
88
  .select([...fields, raw('count(*) as cnt')])
88
89
  .where(where)
@@ -168,7 +168,7 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
168
168
  const fields = this.options.groupBy.map(field => this.quote(field));
169
169
  this.parts.push(`group by ${fields.join(', ')}`);
170
170
  }
171
- if (this.options.having) {
171
+ if (this.options.having?.sql.trim()) {
172
172
  this.parts.push(`having ${this.options.having.sql}`);
173
173
  this.params.push(...this.options.having.params);
174
174
  }
@@ -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
  }
@@ -53,6 +53,7 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
53
53
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
54
54
  getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column): string;
55
55
  getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
56
+ protected hasInlineColumnComment(): boolean;
56
57
  getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
57
58
  alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
58
59
  private getColumnDeclarationSQL;
@@ -322,7 +322,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
322
322
  const ret = [];
323
323
  for (const event of trigger.events) {
324
324
  const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
325
- ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${trigger.body}; end`);
325
+ ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${this.normalizeTriggerBody(trigger.body)} end`);
326
326
  }
327
327
  return ret.join(';\n');
328
328
  }
@@ -540,6 +540,9 @@ export class MySqlSchemaHelper extends SchemaHelper {
540
540
  const keyName = this.quote(index.keyName);
541
541
  return [`alter table ${tableName} rename index ${oldIndexName} to ${keyName}`];
542
542
  }
543
+ hasInlineColumnComment() {
544
+ return true;
545
+ }
543
546
  getChangeColumnCommentSQL(tableName, to, schemaName) {
544
547
  tableName = this.quote(tableName);
545
548
  const columnName = this.quote(to.name);
@@ -234,7 +234,7 @@ export class OracleNativeQueryBuilder extends NativeQueryBuilder {
234
234
  const fields = this.options.groupBy.map(field => this.quote(field));
235
235
  this.parts.push(`group by ${fields.join(', ')}`);
236
236
  }
237
- if (this.options.having) {
237
+ if (this.options.having?.sql.trim()) {
238
238
  this.parts.push(`having ${this.options.having.sql}`);
239
239
  this.params.push(...this.options.having.params);
240
240
  }
@@ -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 root = this.quoteIdentifier(aliased ? `${ALIAS_REPLACEMENT}.${first}` : first);
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 {
@@ -69,6 +69,19 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
69
69
  createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
70
70
  /** Generates SQL to drop a PostgreSQL trigger and its associated function. */
71
71
  dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
72
+ /** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
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;
72
85
  createRoutine(routine: SqlRoutineDef): string;
73
86
  dropRoutine(routine: SqlRoutineDef): string;
74
87
  getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
@@ -85,6 +98,12 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
85
98
  getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
86
99
  getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
87
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;
88
107
  getAllForeignKeys(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
89
108
  getNativeEnumDefinitions(connection: AbstractSqlConnection, schemas: string[], ctx?: Transaction): Promise<Dictionary<{
90
109
  name: string;
@@ -131,6 +150,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
131
150
  * Build the column list for a PostgreSQL index.
132
151
  */
133
152
  protected getIndexColumns(index: IndexDef): string;
153
+ /** Non-default index access methods (gin, gist, brin, hash, ...), normalized to lower case. */
154
+ getIndexAccessMethod(index: IndexDef): string;
134
155
  /**
135
156
  * PostgreSQL-specific index options like fill factor.
136
157
  */
@@ -3,6 +3,8 @@ import { SchemaHelper, stripStatementNewlines } from '../../schema/SchemaHelper.
3
3
  import { normalizePartitionBound, normalizePartitionDefinition } from '../../schema/partitioning.js';
4
4
  /** PostGIS system views that should be automatically ignored */
5
5
  const POSTGIS_VIEWS = ['geography_columns', 'geometry_columns'];
6
+ /** Dollar-quote delimiter, e.g. `$$` or `$body$`, captured so `split` keeps it. */
7
+ const DOLLAR_QUOTE_TAG = /(\$(?:[A-Za-z_]\w*)?\$)/;
6
8
  export class PostgreSqlSchemaHelper extends SchemaHelper {
7
9
  static DEFAULT_VALUES = {
8
10
  'now()': ['now()', 'current_timestamp'],
@@ -160,6 +162,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
160
162
  const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
161
163
  const partitionings = await this.getPartitions(connection, tablesBySchema, ctx);
162
164
  const triggers = await this.getAllTriggers(connection, tablesBySchema);
165
+ const policies = await this.getAllPolicies(connection, tablesBySchema, ctx);
163
166
  const dbCollation = await this.getDatabaseCollation(connection, ctx);
164
167
  for (const t of tables) {
165
168
  const key = this.getTableKey(t);
@@ -173,6 +176,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
173
176
  if (triggers[key]) {
174
177
  table.setTriggers(triggers[key]);
175
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
+ }
176
185
  table.setPartitioning(partitionings[key]);
177
186
  }
178
187
  }
@@ -492,7 +501,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
492
501
  // SchemaHelper.createCheck).
493
502
  const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
494
503
  const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
495
- const def = m ? m[1].replace(/\((.*?)\)::\w+/g, '$1') : single ? single[1] : check.expression;
504
+ const def = m ? m[1].replace(/\(([^()]*)\)::\w+(?:\[\])?/g, '$1') : single ? single[1] : check.expression;
496
505
  ret[key].push({
497
506
  name: check.name,
498
507
  columnName: check.column_name,
@@ -505,7 +514,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
505
514
  /** Generates SQL to create a PostgreSQL trigger and its associated function. */
506
515
  createTrigger(table, trigger) {
507
516
  if (trigger.expression) {
508
- return trigger.expression;
517
+ return this.flattenDollarQuotedBodies(trigger.expression);
509
518
  }
510
519
  const timing = trigger.timing.toUpperCase();
511
520
  const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
@@ -513,7 +522,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
513
522
  const when = trigger.when ? `\n when (${trigger.when})` : '';
514
523
  const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
515
524
  const triggerName = this.platform.quoteIdentifier(trigger.name);
516
- const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${trigger.body}; end; $$ language plpgsql`;
525
+ const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${this.normalizeTriggerBody(trigger.body)} end; $$ language plpgsql`;
517
526
  const triggerSql = `create trigger ${triggerName} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} execute function ${fnName}()`;
518
527
  return `${fnSql};\n${triggerSql}`;
519
528
  }
@@ -523,9 +532,101 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
523
532
  const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
524
533
  return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
525
534
  }
535
+ /** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
536
+ flattenDollarQuotedBodies(ddl) {
537
+ let openTag = '';
538
+ return ddl
539
+ .split(DOLLAR_QUOTE_TAG)
540
+ .map((part, i) => {
541
+ // the capture group puts the delimiters on the odd indexes
542
+ if (i % 2 === 1) {
543
+ openTag = openTag === part ? '' : openTag || part;
544
+ return part;
545
+ }
546
+ return openTag ? stripStatementNewlines(part) : part;
547
+ })
548
+ .join('');
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
+ }
526
627
  createRoutine(routine) {
527
628
  if (routine.expression) {
528
- return routine.expression;
629
+ return this.flattenDollarQuotedBodies(routine.expression);
529
630
  }
530
631
  const qualifiedName = this.qualifiedRoutineName(routine);
531
632
  const params = this.formatRoutineParams(routine);
@@ -715,6 +816,56 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
715
816
  where (${conditions.join(' or ')})
716
817
  order by t.trigger_name, t.event_manipulation`;
717
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
+ }
718
869
  async getAllForeignKeys(connection, tablesBySchemas, ctx) {
719
870
  const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
720
871
  cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
@@ -949,6 +1100,24 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
949
1100
  for (const { table: localTable, foreignKey } of inboundForeignKeys) {
950
1101
  this.append(ret, this.createForeignKey(localTable, foreignKey));
951
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
+ }
952
1121
  }
953
1122
  if (safe) {
954
1123
  ret.push(`-- safe mode: original tables kept in schema "${tmpSchema}"; drop that schema manually once the data is verified`);
@@ -1118,6 +1287,14 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
1118
1287
  })
1119
1288
  .join(', ');
1120
1289
  }
1290
+ /** Non-default index access methods (gin, gist, brin, hash, ...), normalized to lower case. */
1291
+ getIndexAccessMethod(index) {
1292
+ // `fulltext` is a cross-dialect alias handled via `getFullTextIndexExpression`, not a pg access method
1293
+ if (typeof index.type !== 'string' || ['', 'btree', 'fulltext'].includes(index.type.toLowerCase())) {
1294
+ return '';
1295
+ }
1296
+ return index.type.toLowerCase();
1297
+ }
1121
1298
  /**
1122
1299
  * PostgreSQL-specific index options like fill factor.
1123
1300
  */
@@ -7,4 +7,7 @@ export declare class BaseSqliteConnection extends AbstractSqlConnection {
7
7
  skipOnConnect?: boolean;
8
8
  }): Promise<void>;
9
9
  protected attachDatabases(): Promise<void>;
10
+ /** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
11
+ protected getConnectionSetupSql(): Promise<string[]>;
12
+ private getAttachDatabasesSql;
10
13
  }
@@ -1,5 +1,6 @@
1
1
  import { CompiledQuery } from 'kysely';
2
2
  import { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
3
+ const FOREIGN_KEYS_PRAGMA = 'pragma foreign_keys = on';
3
4
  export class BaseSqliteConnection extends AbstractSqlConnection {
4
5
  createKyselyDialect(options) {
5
6
  throw new Error('No SQLite dialect configured. Pass a Kysely dialect via the `driverOptions` config option, ' +
@@ -7,19 +8,28 @@ export class BaseSqliteConnection extends AbstractSqlConnection {
7
8
  }
8
9
  async connect(options) {
9
10
  await super.connect(options);
10
- await this.getClient().executeQuery(CompiledQuery.raw('pragma foreign_keys = on'));
11
+ await this.getClient().executeQuery(CompiledQuery.raw(FOREIGN_KEYS_PRAGMA));
11
12
  await this.attachDatabases();
12
13
  }
13
14
  async attachDatabases() {
15
+ for (const sql of await this.getAttachDatabasesSql()) {
16
+ await this.execute(sql);
17
+ }
18
+ }
19
+ /** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
20
+ async getConnectionSetupSql() {
21
+ return [FOREIGN_KEYS_PRAGMA, ...(await this.getAttachDatabasesSql())];
22
+ }
23
+ async getAttachDatabasesSql() {
14
24
  const attachDatabases = this.config.get('attachDatabases');
15
25
  if (!attachDatabases?.length) {
16
- return;
26
+ return [];
17
27
  }
18
28
  const { fs } = await import('@mikro-orm/core/fs-utils');
19
29
  const baseDir = this.config.get('baseDir');
20
- for (const db of attachDatabases) {
30
+ return attachDatabases.map(db => {
21
31
  const path = fs.absolutePath(db.path, baseDir);
22
- await this.execute(`attach database '${path}' as ${this.platform.quoteIdentifier(db.name)}`);
23
- }
32
+ return `attach database '${path}' as ${this.platform.quoteIdentifier(db.name)}`;
33
+ });
24
34
  }
25
35
  }