@mikro-orm/sql 7.0.17-dev.9 → 7.0.18-dev.0

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 (50) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +27 -6
  3. package/AbstractSqlDriver.d.ts +25 -1
  4. package/AbstractSqlDriver.js +356 -20
  5. package/AbstractSqlPlatform.d.ts +13 -2
  6. package/AbstractSqlPlatform.js +16 -3
  7. package/PivotCollectionPersister.d.ts +2 -2
  8. package/PivotCollectionPersister.js +19 -3
  9. package/README.md +2 -1
  10. package/SqlEntityManager.d.ts +46 -3
  11. package/SqlEntityManager.js +77 -7
  12. package/SqlMikroORM.d.ts +4 -4
  13. package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
  14. package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
  15. package/dialects/mysql/BaseMySqlPlatform.js +3 -0
  16. package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
  17. package/dialects/mysql/MySqlSchemaHelper.d.ts +19 -3
  18. package/dialects/mysql/MySqlSchemaHelper.js +254 -21
  19. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  20. package/dialects/oracledb/OracleDialect.js +2 -1
  21. package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
  22. package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
  23. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +8 -0
  24. package/dialects/postgresql/BasePostgreSqlPlatform.js +50 -0
  25. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +38 -1
  26. package/dialects/postgresql/PostgreSqlSchemaHelper.js +341 -6
  27. package/dialects/postgresql/index.d.ts +2 -0
  28. package/dialects/postgresql/index.js +2 -0
  29. package/dialects/postgresql/typeOverrides.d.ts +14 -0
  30. package/dialects/postgresql/typeOverrides.js +12 -0
  31. package/dialects/sqlite/SqliteSchemaHelper.d.ts +7 -1
  32. package/dialects/sqlite/SqliteSchemaHelper.js +131 -2
  33. package/package.json +4 -4
  34. package/query/NativeQueryBuilder.d.ts +6 -0
  35. package/query/NativeQueryBuilder.js +16 -1
  36. package/query/QueryBuilder.d.ts +83 -1
  37. package/query/QueryBuilder.js +181 -8
  38. package/schema/DatabaseSchema.d.ts +29 -2
  39. package/schema/DatabaseSchema.js +137 -0
  40. package/schema/DatabaseTable.d.ts +20 -1
  41. package/schema/DatabaseTable.js +62 -3
  42. package/schema/SchemaComparator.d.ts +19 -0
  43. package/schema/SchemaComparator.js +250 -1
  44. package/schema/SchemaHelper.d.ts +77 -1
  45. package/schema/SchemaHelper.js +279 -5
  46. package/schema/SqlSchemaGenerator.d.ts +2 -2
  47. package/schema/SqlSchemaGenerator.js +47 -10
  48. package/schema/partitioning.d.ts +13 -0
  49. package/schema/partitioning.js +326 -0
  50. package/typings.d.ts +69 -2
@@ -7,12 +7,34 @@ export class MySqlSchemaHelper extends SchemaHelper {
7
7
  'current_timestamp(?)': ['current_timestamp(?)'],
8
8
  '0': ['0', 'false'],
9
9
  };
10
+ // Greedy `(.+)` so nested CASE expressions inside the predicate don't trip the match on
11
+ // an inner `then <col> end` — the trailing `$` anchor forces the regex engine to extend
12
+ // the capture to the outermost case-end boundary.
13
+ static PARTIAL_INDEX_RE = /^\s*\(\s*case\s+when\s+(.+)\s+then\s+`([^`]+)`\s+end\s*\)\s*$/is;
10
14
  getSchemaBeginning(charset, disableForeignKeys) {
11
15
  if (disableForeignKeys) {
12
16
  return `set names ${charset};\n${this.disableForeignKeysSQL()}\n\n`;
13
17
  }
14
18
  return `set names ${charset};\n\n`;
15
19
  }
20
+ getSetSchemaSQL(schema) {
21
+ return `use ${this.quote(schema)}`;
22
+ }
23
+ getResetSchemaSQL(defaultSchema) {
24
+ return `use ${this.quote(defaultSchema)}`;
25
+ }
26
+ supportsMigrationSchema() {
27
+ return true;
28
+ }
29
+ async tableExists(connection, tableName, schemaName, ctx) {
30
+ // MySQL "schema" = database — when none is requested, probe the connection's current DB
31
+ // via `schema()` rather than the base impl's `getDefaultSchemaName()` (which is undefined here).
32
+ const schemaClause = schemaName
33
+ ? `table_schema = ${this.platform.quoteValue(schemaName)}`
34
+ : `table_schema = schema()`;
35
+ const rows = await connection.execute(`select 1 from information_schema.tables where ${schemaClause} and table_name = ${this.platform.quoteValue(tableName)}`, [], 'all', ctx);
36
+ return rows.length > 0;
37
+ }
16
38
  disableForeignKeysSQL() {
17
39
  return 'set foreign_key_checks = 0;';
18
40
  }
@@ -31,7 +53,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
31
53
  return sql;
32
54
  }
33
55
  getListTablesSQL() {
34
- return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
56
+ return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment, table_collation as table_collation from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
35
57
  }
36
58
  getListViewsSQL() {
37
59
  return `select table_name as view_name, nullif(table_schema, schema()) as schema_name, view_definition from information_schema.views where table_schema = schema()`;
@@ -64,11 +86,16 @@ export class MySqlSchemaHelper extends SchemaHelper {
64
86
  const checks = await this.getAllChecks(connection, tables, ctx);
65
87
  const fks = await this.getAllForeignKeys(connection, tables, ctx);
66
88
  const enums = await this.getAllEnumDefinitions(connection, tables, ctx);
89
+ const triggers = await this.getAllTriggers(connection, tables);
67
90
  for (const t of tables) {
68
91
  const key = this.getTableKey(t);
69
92
  const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
93
+ table.collation = t.table_collation ?? undefined;
70
94
  const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
71
95
  table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums[key]);
96
+ if (triggers[key]) {
97
+ table.setTriggers(triggers[key]);
98
+ }
72
99
  }
73
100
  }
74
101
  async getAllIndexes(connection, tables, ctx) {
@@ -80,8 +107,11 @@ export class MySqlSchemaHelper extends SchemaHelper {
80
107
  const ret = {};
81
108
  for (const index of allIndexes) {
82
109
  const key = this.getTableKey(index);
110
+ const partialMatch = !index.column_name && typeof index.expression === 'string'
111
+ ? MySqlSchemaHelper.PARTIAL_INDEX_RE.exec(index.expression)
112
+ : null;
83
113
  const indexDef = {
84
- columnNames: [index.column_name],
114
+ columnNames: [partialMatch ? partialMatch[2] : index.column_name],
85
115
  keyName: index.index_name,
86
116
  unique: !index.non_unique,
87
117
  primary: index.index_name === 'PRIMARY',
@@ -109,7 +139,10 @@ export class MySqlSchemaHelper extends SchemaHelper {
109
139
  if (index.is_visible === 'NO') {
110
140
  indexDef.invisible = true;
111
141
  }
112
- if (!index.column_name || index.expression?.match(/ where /i)) {
142
+ if (partialMatch) {
143
+ indexDef.where = partialMatch[1].trim();
144
+ }
145
+ else if (!index.column_name || index.expression?.match(/ where /i)) {
113
146
  indexDef.expression = index.expression; // required for the `getCreateIndexSQL()` call
114
147
  indexDef.expression = this.getCreateIndexSQL(index.table_name, indexDef, !!index.expression);
115
148
  }
@@ -146,10 +179,15 @@ export class MySqlSchemaHelper extends SchemaHelper {
146
179
  return this.appendMySqlIndexSuffix(sql, index);
147
180
  }
148
181
  /**
149
- * Build the column list for a MySQL index, with MySQL-specific handling for collation.
150
- * MySQL requires collation to be specified as an expression: (column_name COLLATE collation_name)
182
+ * Build the column list for a MySQL index. MySQL requires collation via an expression:
183
+ * `(column COLLATE collation_name)`. Partial indexes (`where`) are emulated via functional
184
+ * indexes — requires MySQL 8.0.13+. MariaDB does not support inline functional indexes
185
+ * and overrides to throw at a higher level.
151
186
  */
152
187
  getIndexColumns(index) {
188
+ if (index.where) {
189
+ return this.emulatePartialIndexColumns(index);
190
+ }
153
191
  return index.columnNames
154
192
  .map(name => {
155
193
  const col = index.columns?.find(c => c.name === name);
@@ -190,22 +228,25 @@ export class MySqlSchemaHelper extends SchemaHelper {
190
228
  return sql;
191
229
  }
192
230
  async getAllColumns(connection, tables, ctx) {
193
- const sql = `select table_name as table_name,
194
- nullif(table_schema, schema()) as schema_name,
195
- column_name as column_name,
196
- column_default as column_default,
197
- nullif(column_comment, '') as column_comment,
198
- is_nullable as is_nullable,
199
- data_type as data_type,
200
- column_type as column_type,
201
- column_key as column_key,
202
- extra as extra,
203
- generation_expression as generation_expression,
204
- numeric_precision as numeric_precision,
205
- numeric_scale as numeric_scale,
206
- ifnull(datetime_precision, character_maximum_length) length
207
- from information_schema.columns where table_schema = database() and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name))})
208
- order by ordinal_position`;
231
+ const sql = `select c.table_name as table_name,
232
+ nullif(c.table_schema, schema()) as schema_name,
233
+ c.column_name as column_name,
234
+ c.column_default as column_default,
235
+ nullif(c.column_comment, '') as column_comment,
236
+ c.is_nullable as is_nullable,
237
+ c.data_type as data_type,
238
+ c.column_type as column_type,
239
+ c.column_key as column_key,
240
+ c.extra as extra,
241
+ c.generation_expression as generation_expression,
242
+ c.numeric_precision as numeric_precision,
243
+ c.numeric_scale as numeric_scale,
244
+ ifnull(c.datetime_precision, c.character_maximum_length) length,
245
+ nullif(c.collation_name, t.table_collation) as collation_name
246
+ from information_schema.columns c
247
+ join information_schema.tables t on t.table_schema = c.table_schema and t.table_name = c.table_name
248
+ where c.table_schema = database() and c.table_name in (${tables.map(t => this.platform.quoteValue(t.table_name))})
249
+ order by c.ordinal_position`;
209
250
  const allColumns = await connection.execute(sql, [], 'all', ctx);
210
251
  const str = (val) => (val != null ? '' + val : val);
211
252
  const extra = (val) => val.replace(/auto_increment|default_generated|(stored|virtual) generated/i, '').trim() || undefined;
@@ -236,6 +277,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
236
277
  precision: col.numeric_precision,
237
278
  scale: col.numeric_scale,
238
279
  comment: col.column_comment,
280
+ collation: col.collation_name ?? undefined,
239
281
  extra: extra(col.extra),
240
282
  generated,
241
283
  });
@@ -262,6 +304,194 @@ export class MySqlSchemaHelper extends SchemaHelper {
262
304
  }
263
305
  return ret;
264
306
  }
307
+ /** Generates SQL to create MySQL triggers. MySQL requires one trigger per event. */
308
+ createTrigger(table, trigger) {
309
+ if (trigger.expression) {
310
+ return trigger.expression;
311
+ }
312
+ /* v8 ignore next 3 */
313
+ if (trigger.timing === 'instead of') {
314
+ throw new Error(`MySQL does not support INSTEAD OF triggers. Use BEFORE or AFTER for trigger "${trigger.name}".`);
315
+ }
316
+ /* v8 ignore next 5 */
317
+ if (trigger.forEach === 'statement') {
318
+ throw new Error(`MySQL does not support FOR EACH STATEMENT triggers. Use FOR EACH ROW for trigger "${trigger.name}".`);
319
+ }
320
+ const timing = trigger.timing.toUpperCase();
321
+ const ret = [];
322
+ for (const event of trigger.events) {
323
+ const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
324
+ ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${trigger.body}; end`);
325
+ }
326
+ return ret.join(';\n');
327
+ }
328
+ createRoutine(routine) {
329
+ if (routine.expression) {
330
+ return routine.expression;
331
+ }
332
+ const name = this.platform.quoteIdentifier(routine.name);
333
+ // MySQL functions reject direction prefixes (function params are always IN); procedures use them.
334
+ const params = routine.params
335
+ .map(p => {
336
+ const dir = routine.type === 'procedure' ? `${p.direction.toUpperCase()} ` : '';
337
+ return `${dir}${this.platform.quoteIdentifier(p.name)} ${p.type}`;
338
+ })
339
+ .join(', ');
340
+ const determinism = routine.deterministic === true ? ' deterministic' : routine.deterministic === false ? ' not deterministic' : '';
341
+ const dataAccess = this.formatDataAccess(routine.dataAccess);
342
+ const security = routine.security === 'definer'
343
+ ? ' sql security definer'
344
+ : routine.security === 'invoker'
345
+ ? ' sql security invoker'
346
+ : '';
347
+ const comment = routine.comment ? ` comment ${this.platform.quoteValue(routine.comment)}` : '';
348
+ const body = this.wrapRoutineBody(routine.body ?? '');
349
+ if (routine.type === 'procedure') {
350
+ return `create procedure ${name}(${params})${determinism}${dataAccess}${security}${comment} ${body}`;
351
+ }
352
+ const returnType = routine.returns?.type ?? 'text';
353
+ return `create function ${name}(${params}) returns ${returnType}${determinism}${dataAccess}${security}${comment} ${body}`;
354
+ }
355
+ dropRoutine(routine) {
356
+ const kind = routine.type === 'procedure' ? 'procedure' : 'function';
357
+ return `drop ${kind} if exists ${this.platform.quoteIdentifier(routine.name)}`;
358
+ }
359
+ async getAllRoutines(connection) {
360
+ const sql = `
361
+ select
362
+ r.routine_name as name,
363
+ r.routine_schema as schema_name,
364
+ lower(r.routine_type) as type,
365
+ r.routine_definition as body,
366
+ r.dtd_identifier as return_type,
367
+ r.is_deterministic as is_deterministic,
368
+ r.sql_data_access as sql_data_access,
369
+ r.security_type as security_type,
370
+ r.routine_comment as comment
371
+ from information_schema.routines r
372
+ where r.routine_schema = database()
373
+ and r.routine_type in ('PROCEDURE', 'FUNCTION')
374
+ `;
375
+ const [rows, params] = await Promise.all([
376
+ connection.execute(sql),
377
+ this.getAllRoutineParams(connection),
378
+ ]);
379
+ return rows.map(row => ({
380
+ name: row.name,
381
+ type: row.type,
382
+ // MySQL has no schema namespace for routines — undefined matches the metadata side.
383
+ schema: undefined,
384
+ body: this.stripRoutineBody(row.body ?? ''),
385
+ deterministic: row.is_deterministic === 'YES',
386
+ dataAccess: this.parseDataAccess(row.sql_data_access),
387
+ security: row.security_type === 'DEFINER' ? 'definer' : 'invoker',
388
+ comment: row.comment || undefined,
389
+ params: params.get(row.name) ?? [],
390
+ returns: row.type === 'function' && row.return_type ? { type: row.return_type } : undefined,
391
+ }));
392
+ }
393
+ async getAllRoutineParams(connection) {
394
+ const sql = `
395
+ select
396
+ specific_name as routine_name,
397
+ parameter_name as name,
398
+ parameter_mode as direction,
399
+ dtd_identifier as type,
400
+ ordinal_position as position
401
+ from information_schema.parameters
402
+ where specific_schema = database()
403
+ and parameter_name is not null
404
+ order by specific_name, ordinal_position
405
+ `;
406
+ const rows = await connection.execute(sql);
407
+ const out = new Map();
408
+ for (const row of rows) {
409
+ if (!out.has(row.routine_name)) {
410
+ out.set(row.routine_name, []);
411
+ }
412
+ out.get(row.routine_name).push({
413
+ name: row.name,
414
+ type: row.type,
415
+ direction: row.direction.toLowerCase(),
416
+ });
417
+ }
418
+ return out;
419
+ }
420
+ formatDataAccess(access) {
421
+ switch (access) {
422
+ case 'no-sql':
423
+ return ' no sql';
424
+ case 'reads-sql-data':
425
+ return ' reads sql data';
426
+ case 'modifies-sql-data':
427
+ return ' modifies sql data';
428
+ case 'contains-sql':
429
+ return ' contains sql';
430
+ default:
431
+ return '';
432
+ }
433
+ }
434
+ parseDataAccess(access) {
435
+ return access.toLowerCase().replace(/\s+/g, '-');
436
+ }
437
+ async getAllTriggers(connection, tables) {
438
+ const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
439
+ const sql = `select trigger_name as trigger_name, event_object_table as table_name, nullif(event_object_schema, schema()) as schema_name,
440
+ event_manipulation as event, action_timing as timing,
441
+ action_orientation as for_each, action_statement as body
442
+ from information_schema.triggers
443
+ where event_object_schema = database()
444
+ and event_object_table in (${names})
445
+ order by trigger_name, event_manipulation`;
446
+ const allTriggers = await connection.execute(sql);
447
+ const ret = {};
448
+ // First pass: collect all raw trigger names per table to detect multi-event groups.
449
+ // A base name is only used for grouping if multiple triggers share it (e.g. trg_multi_insert + trg_multi_update).
450
+ const namesByTable = new Map();
451
+ for (const row of allTriggers) {
452
+ const key = this.getTableKey(row);
453
+ namesByTable.set(key, [...(namesByTable.get(key) ?? []), row.trigger_name]);
454
+ }
455
+ const triggerMap = new Map();
456
+ for (const row of allTriggers) {
457
+ const key = this.getTableKey(row);
458
+ const eventLower = row.event.toLowerCase();
459
+ const tableNames = namesByTable.get(key) ?? [];
460
+ // Only strip event suffix when another trigger with the same base exists for this table
461
+ const candidateBase = row.trigger_name.endsWith(`_${eventLower}`)
462
+ ? row.trigger_name.slice(0, -eventLower.length - 1)
463
+ : null;
464
+ const baseName = candidateBase && tableNames.some(n => n !== row.trigger_name && n.startsWith(`${candidateBase}_`))
465
+ ? candidateBase
466
+ : row.trigger_name;
467
+ const dedupeKey = `${key}:${baseName}`;
468
+ if (triggerMap.has(dedupeKey)) {
469
+ const existing = triggerMap.get(dedupeKey);
470
+ const event = eventLower;
471
+ if (!existing.events.includes(event)) {
472
+ existing.events.push(event);
473
+ }
474
+ continue;
475
+ }
476
+ ret[key] ??= [];
477
+ // Strip BEGIN/END wrapper from MySQL action_statement
478
+ let body = row.body ?? '';
479
+ const beginEndMatch = /^\s*begin\s+([\s\S]*)\s*end\s*$/i.exec(body);
480
+ if (beginEndMatch) {
481
+ body = beginEndMatch[1].trim().replace(/;\s*$/, '');
482
+ }
483
+ const trigger = {
484
+ name: baseName,
485
+ timing: row.timing.toLowerCase(),
486
+ events: [eventLower],
487
+ forEach: (row.for_each ?? 'row').toLowerCase(),
488
+ body,
489
+ };
490
+ ret[key].push(trigger);
491
+ triggerMap.set(dedupeKey, trigger);
492
+ }
493
+ return ret;
494
+ }
265
495
  async getAllForeignKeys(connection, tables, ctx) {
266
496
  const sql = `select k.constraint_name as constraint_name, nullif(k.table_schema, schema()) as schema_name, k.table_name as table_name, k.column_name as column_name, k.referenced_table_name as referenced_table_name, k.referenced_column_name as referenced_column_name, c.update_rule as update_rule, c.delete_rule as delete_rule
267
497
  from information_schema.key_column_usage k
@@ -317,10 +547,13 @@ export class MySqlSchemaHelper extends SchemaHelper {
317
547
  const col = this.createTableColumn(column, table, changedProperties);
318
548
  return [`alter table ${table.getQuotedName()} modify ${col}`];
319
549
  }
550
+ // MySQL MODIFY/CHANGE resets omitted column attributes to the table default, so collation must
551
+ // be re-emitted on rename and comment-only paths to preserve a non-default column collation.
320
552
  getColumnDeclarationSQL(col) {
321
553
  let ret = col.type;
322
554
  ret += col.unsigned ? ' unsigned' : '';
323
555
  ret += col.autoincrement ? ' auto_increment' : '';
556
+ ret += col.collation ? ` ${this.getCollateSQL(col.collation)}` : '';
324
557
  ret += ' ';
325
558
  ret += col.nullable ? 'null' : 'not null';
326
559
  ret += col.default ? ' default ' + col.default : '';
@@ -46,7 +46,7 @@ declare class OracleConnection implements DatabaseConnection {
46
46
  sql: string;
47
47
  bindParams: unknown[];
48
48
  };
49
- streamQuery<R>(compiledQuery: CompiledQuery, _chunkSize?: number): AsyncIterableIterator<QueryResult<R>>;
49
+ streamQuery<R>(compiledQuery: CompiledQuery, chunkSize?: number): AsyncIterableIterator<QueryResult<R>>;
50
50
  get connection(): OraclePoolConnection;
51
51
  }
52
52
  declare class OracleDriver implements Driver {
@@ -66,13 +66,14 @@ class OracleConnection {
66
66
  bindParams: query.parameters,
67
67
  };
68
68
  }
69
- async *streamQuery(compiledQuery, _chunkSize) {
69
+ async *streamQuery(compiledQuery, chunkSize) {
70
70
  const { sql, bindParams } = this.formatQuery(compiledQuery);
71
71
  const result = await this.#connection.execute(sql, bindParams, {
72
72
  resultSet: true,
73
73
  autoCommit: compiledQuery.autoCommit,
74
74
  outFormat: OUT_FORMAT_OBJECT,
75
75
  ...this.#executeOptions,
76
+ ...(chunkSize != null ? { fetchArraySize: chunkSize } : {}),
76
77
  });
77
78
  const rs = result.resultSet;
78
79
  try {
@@ -0,0 +1,19 @@
1
+ import { type EntityName } from '@mikro-orm/core';
2
+ import { SqlEntityManager } from '../../SqlEntityManager.js';
3
+ import type { AbstractSqlDriver } from '../../AbstractSqlDriver.js';
4
+ /**
5
+ * Shared base class for PostgreSQL-flavoured entity managers (`pg`, `pglite`).
6
+ * Adds Postgres-only helpers on top of `SqlEntityManager`.
7
+ */
8
+ export declare class BasePostgreSqlEntityManager<Driver extends AbstractSqlDriver = AbstractSqlDriver> extends SqlEntityManager<Driver> {
9
+ /**
10
+ * Refreshes a materialized view.
11
+ *
12
+ * @param entityName - The entity name or class of the materialized view
13
+ * @param options - Optional settings
14
+ * @param options.concurrently - If true, refreshes the view concurrently (requires a unique index on the view)
15
+ */
16
+ refreshMaterializedView<Entity extends object>(entityName: EntityName<Entity>, options?: {
17
+ concurrently?: boolean;
18
+ }): Promise<void>;
19
+ }
@@ -0,0 +1,24 @@
1
+ import { SqlEntityManager } from '../../SqlEntityManager.js';
2
+ /**
3
+ * Shared base class for PostgreSQL-flavoured entity managers (`pg`, `pglite`).
4
+ * Adds Postgres-only helpers on top of `SqlEntityManager`.
5
+ */
6
+ export class BasePostgreSqlEntityManager extends SqlEntityManager {
7
+ /**
8
+ * Refreshes a materialized view.
9
+ *
10
+ * @param entityName - The entity name or class of the materialized view
11
+ * @param options - Optional settings
12
+ * @param options.concurrently - If true, refreshes the view concurrently (requires a unique index on the view)
13
+ */
14
+ async refreshMaterializedView(entityName, options) {
15
+ const meta = this.getMetadata(entityName);
16
+ if (!meta.view || !meta.materialized) {
17
+ throw new Error(`Entity ${meta.className} is not a materialized view`);
18
+ }
19
+ const helper = this.getDriver().getPlatform().getSchemaHelper();
20
+ const schema = meta.schema ?? this.config.get('schema');
21
+ const sql = helper.refreshMaterializedView(meta.tableName, schema, options?.concurrently);
22
+ await this.execute(sql);
23
+ }
24
+ }
@@ -15,6 +15,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
15
15
  usesEnumCheckConstraints(): boolean;
16
16
  getEnumArrayCheckConstraintExpression(column: string, items: string[]): string;
17
17
  supportsMaterializedViews(): boolean;
18
+ supportsPartitionedTables(): boolean;
18
19
  supportsCustomPrimaryKeyNames(): boolean;
19
20
  getCurrentTimestampSQL(length: number): string;
20
21
  getDateTimeTypeDeclarationSQL(column: {
@@ -70,6 +71,12 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
70
71
  }): string[];
71
72
  marshallArray(values: string[]): string;
72
73
  unmarshallArray(value: string): string[];
74
+ escape(value: any): string;
75
+ /**
76
+ * Ported from PostgreSQL 9.2.4 source code (`src/interfaces/libpq/fe-exec.c`),
77
+ * matching `pg.Client.prototype.escapeLiteral` so we don't need a `pg` dep here.
78
+ */
79
+ private escapeLiteral;
73
80
  getVarcharTypeDeclarationSQL(column: {
74
81
  length?: number;
75
82
  }): string;
@@ -108,4 +115,5 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
108
115
  }[]): string;
109
116
  getJsonArrayElementPropertySQL(alias: string, property: string, type: string): string;
110
117
  getDefaultClientUrl(): string;
118
+ caseInsensitiveCollationNames(): boolean;
111
119
  }
@@ -30,6 +30,9 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
30
30
  supportsMaterializedViews() {
31
31
  return true;
32
32
  }
33
+ supportsPartitionedTables() {
34
+ return true;
35
+ }
33
36
  supportsCustomPrimaryKeyNames() {
34
37
  return true;
35
38
  }
@@ -209,6 +212,50 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
209
212
  return v;
210
213
  });
211
214
  }
215
+ escape(value) {
216
+ if (typeof value === 'bigint') {
217
+ value = value.toString();
218
+ }
219
+ if (typeof value === 'string') {
220
+ return this.escapeLiteral(value);
221
+ }
222
+ if (value instanceof Date) {
223
+ return `'${this.formatDate(value)}'`;
224
+ }
225
+ if (ArrayBuffer.isView(value)) {
226
+ return `E'\\\\x${value.toString('hex')}'`;
227
+ }
228
+ if (Array.isArray(value)) {
229
+ return value.map(v => this.escape(v)).join(', ');
230
+ }
231
+ return value;
232
+ }
233
+ /**
234
+ * Ported from PostgreSQL 9.2.4 source code (`src/interfaces/libpq/fe-exec.c`),
235
+ * matching `pg.Client.prototype.escapeLiteral` so we don't need a `pg` dep here.
236
+ */
237
+ escapeLiteral(str) {
238
+ let hasBackslash = false;
239
+ let escaped = `'`;
240
+ for (let i = 0; i < str.length; i++) {
241
+ const c = str[i];
242
+ if (c === `'`) {
243
+ escaped += c + c;
244
+ }
245
+ else if (c === '\\') {
246
+ escaped += c + c;
247
+ hasBackslash = true;
248
+ }
249
+ else {
250
+ escaped += c;
251
+ }
252
+ }
253
+ escaped += `'`;
254
+ if (hasBackslash) {
255
+ escaped = ` E${escaped}`;
256
+ }
257
+ return escaped;
258
+ }
212
259
  getVarcharTypeDeclarationSQL(column) {
213
260
  if (column.length === -1) {
214
261
  return 'varchar';
@@ -370,4 +417,7 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
370
417
  getDefaultClientUrl() {
371
418
  return 'postgresql://postgres@127.0.0.1:5432';
372
419
  }
420
+ caseInsensitiveCollationNames() {
421
+ return false;
422
+ }
373
423
  }
@@ -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 } from '../../typings.js';
4
+ import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, 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 {
@@ -14,7 +14,12 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
14
14
  'null::timestamp with time zone': string[];
15
15
  'null::timestamp without time zone': string[];
16
16
  };
17
+ private static readonly PARTIAL_WHERE_RE;
18
+ private static readonly FUNCTIONAL_COL_RE;
17
19
  getSchemaBeginning(charset: string, disableForeignKeys?: boolean): string;
20
+ getSetSchemaSQL(schema: string): string;
21
+ getResetSchemaSQL(_defaultSchema: string): string;
22
+ supportsMigrationSchema(): boolean;
18
23
  getCreateDatabaseSQL(name: string): string;
19
24
  getListTablesSQL(): string;
20
25
  private getIgnoredViewsCondition;
@@ -23,11 +28,22 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
23
28
  getListMaterializedViewsSQL(): string;
24
29
  loadMaterializedViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string, ctx?: Transaction): Promise<void>;
25
30
  createMaterializedView(name: string, schema: string | undefined, definition: string, withData?: boolean): string;
31
+ createTable(table: DatabaseTable, alter?: boolean): string[];
26
32
  dropMaterializedViewIfExists(name: string, schema?: string): string;
27
33
  refreshMaterializedView(name: string, schema?: string, concurrently?: boolean): string;
28
34
  getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
29
35
  private getIgnoredNamespacesConditionSQL;
30
36
  loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[], ctx?: Transaction): Promise<void>;
37
+ /**
38
+ * Introspects direct partitions only: the `pg_inherits` join surfaces a parent's children but
39
+ * does not recurse into sub-partitioning (e.g. hash-of-range). Declarative `partitionBy`
40
+ * metadata does not express multi-level partitioning either, so grandchildren are intentionally
41
+ * invisible to schema diffing.
42
+ *
43
+ * Entries with an undefined schema bucket are resolved against `current_schema()` so they do
44
+ * not match same-named tables in unrelated schemas.
45
+ */
46
+ getPartitions(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<TablePartitioning>>;
31
47
  getAllIndexes(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<IndexDef[]>>;
32
48
  /**
33
49
  * Parses column definitions from the full CREATE INDEX expression.
@@ -49,6 +65,26 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
49
65
  items: string[];
50
66
  }>, ctx?: Transaction): Promise<Dictionary<Column[]>>;
51
67
  getAllChecks(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<CheckDef[]>>;
68
+ /** Generates SQL to create a PostgreSQL trigger and its associated function. */
69
+ createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
70
+ /** Generates SQL to drop a PostgreSQL trigger and its associated function. */
71
+ dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
72
+ createRoutine(routine: SqlRoutineDef): string;
73
+ dropRoutine(routine: SqlRoutineDef): string;
74
+ getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
75
+ private formatRoutineParams;
76
+ private parseRoutineParams;
77
+ /** PG canonicalises `$$` quoting to a tagged form like `$function$ ... $function$`; match the tag-aware form. */
78
+ private extractRoutineBody;
79
+ private getSchemaQualifiedTriggerFnName;
80
+ /**
81
+ * Resolves the real name of the implicit 'default' collation (the DB's `datcollate`),
82
+ * so the comparator can treat `@Property({ collation: '<datcollate>' })` as equivalent
83
+ * to a column that introspects as using the default.
84
+ */
85
+ getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
86
+ getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
87
+ private getTriggersSQL;
52
88
  getAllForeignKeys(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
53
89
  getNativeEnumDefinitions(connection: AbstractSqlConnection, schemas: string[], ctx?: Transaction): Promise<Dictionary<{
54
90
  name: string;
@@ -59,6 +95,7 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
59
95
  getDropNativeEnumSQL(name: string, schema?: string): string;
60
96
  getAlterNativeEnumSQL(name: string, schema?: string, value?: string, items?: string[], oldItems?: string[]): string;
61
97
  private getEnumDefinitions;
98
+ protected getCollateSQL(collation: string): string;
62
99
  createTableColumn(column: Column, table: DatabaseTable): string | undefined;
63
100
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
64
101
  castColumn(name: string, type: string): string;