@mikro-orm/sql 7.1.0-dev.5 → 7.1.0-dev.50

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 (56) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +27 -6
  3. package/AbstractSqlDriver.d.ts +26 -1
  4. package/AbstractSqlDriver.js +294 -37
  5. package/AbstractSqlPlatform.d.ts +15 -3
  6. package/AbstractSqlPlatform.js +25 -7
  7. package/PivotCollectionPersister.d.ts +2 -2
  8. package/PivotCollectionPersister.js +19 -3
  9. package/README.md +2 -1
  10. package/SqlEntityManager.d.ts +48 -5
  11. package/SqlEntityManager.js +77 -7
  12. package/SqlMikroORM.d.ts +23 -0
  13. package/SqlMikroORM.js +23 -0
  14. package/dialects/mysql/BaseMySqlPlatform.d.ts +4 -5
  15. package/dialects/mysql/BaseMySqlPlatform.js +9 -10
  16. package/dialects/mysql/MySqlSchemaHelper.d.ts +19 -3
  17. package/dialects/mysql/MySqlSchemaHelper.js +280 -49
  18. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  19. package/dialects/oracledb/OracleDialect.js +2 -1
  20. package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
  21. package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
  22. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +11 -5
  23. package/dialects/postgresql/BasePostgreSqlPlatform.js +75 -17
  24. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +38 -1
  25. package/dialects/postgresql/PostgreSqlSchemaHelper.js +362 -28
  26. package/dialects/postgresql/index.d.ts +2 -0
  27. package/dialects/postgresql/index.js +2 -0
  28. package/dialects/postgresql/typeOverrides.d.ts +14 -0
  29. package/dialects/postgresql/typeOverrides.js +12 -0
  30. package/dialects/sqlite/SqlitePlatform.d.ts +2 -1
  31. package/dialects/sqlite/SqlitePlatform.js +4 -0
  32. package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
  33. package/dialects/sqlite/SqliteSchemaHelper.js +148 -19
  34. package/index.d.ts +2 -0
  35. package/index.js +2 -0
  36. package/package.json +4 -4
  37. package/plugin/transformer.d.ts +11 -3
  38. package/plugin/transformer.js +138 -29
  39. package/query/CriteriaNode.d.ts +1 -1
  40. package/query/CriteriaNode.js +2 -2
  41. package/query/ObjectCriteriaNode.js +1 -1
  42. package/query/QueryBuilder.d.ts +42 -1
  43. package/query/QueryBuilder.js +78 -7
  44. package/schema/DatabaseSchema.d.ts +29 -2
  45. package/schema/DatabaseSchema.js +145 -4
  46. package/schema/DatabaseTable.d.ts +20 -1
  47. package/schema/DatabaseTable.js +182 -31
  48. package/schema/SchemaComparator.d.ts +19 -0
  49. package/schema/SchemaComparator.js +250 -1
  50. package/schema/SchemaHelper.d.ts +77 -1
  51. package/schema/SchemaHelper.js +297 -25
  52. package/schema/SqlSchemaGenerator.d.ts +2 -2
  53. package/schema/SqlSchemaGenerator.js +47 -10
  54. package/schema/partitioning.d.ts +13 -0
  55. package/schema/partitioning.js +326 -0
  56. package/typings.d.ts +72 -5
@@ -1,5 +1,6 @@
1
1
  import { DeferMode, EnumType, Type, Utils, } from '@mikro-orm/core';
2
- import { SchemaHelper } from '../../schema/SchemaHelper.js';
2
+ import { SchemaHelper, stripStatementNewlines } from '../../schema/SchemaHelper.js';
3
+ import { normalizePartitionBound, normalizePartitionDefinition } from '../../schema/partitioning.js';
3
4
  /** PostGIS system views that should be automatically ignored */
4
5
  const POSTGIS_VIEWS = ['geography_columns', 'geometry_columns'];
5
6
  export class PostgreSqlSchemaHelper extends SchemaHelper {
@@ -12,23 +13,45 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
12
13
  'null::timestamp with time zone': ['null'],
13
14
  'null::timestamp without time zone': ['null'],
14
15
  };
16
+ static PARTIAL_WHERE_RE = /\swhere\s+(.+)$/is;
17
+ static FUNCTIONAL_COL_RE = /[(): ,"'`]/;
15
18
  getSchemaBeginning(charset, disableForeignKeys) {
16
19
  if (disableForeignKeys) {
17
20
  return `set names '${charset}';\n${this.disableForeignKeysSQL()}\n\n`;
18
21
  }
19
22
  return `set names '${charset}';\n\n`;
20
23
  }
24
+ getSetSchemaSQL(schema) {
25
+ // session-level `SET` (not `SET LOCAL`) so it also works outside a transaction; the runner
26
+ // emits `getResetSchemaSQL` afterwards to avoid leaking onto the pooled connection
27
+ return `set search_path to ${this.quote(schema)}`;
28
+ }
29
+ getResetSchemaSQL(_defaultSchema) {
30
+ return 'reset search_path';
31
+ }
32
+ supportsMigrationSchema() {
33
+ return true;
34
+ }
21
35
  getCreateDatabaseSQL(name) {
22
36
  return `create database ${this.quote(name)}`;
23
37
  }
24
38
  getListTablesSQL() {
39
+ // The `pg_inherits` anti-join compares on (schema, table) pairs so cross-schema child
40
+ // partitions are excluded even when their schema is not on the session `search_path`
41
+ // (in which case `inhrelid::regclass::text` renders as `schema.name` rather than bare `name`,
42
+ // breaking a plain `table_name not in (...)` predicate).
25
43
  return (`select table_name, table_schema as schema_name, ` +
26
44
  `(select pg_catalog.obj_description(c.oid) from pg_catalog.pg_class c
27
45
  where c.oid = (select ('"' || table_schema || '"."' || table_name || '"')::regclass::oid) and c.relname = table_name) as table_comment ` +
28
- `from information_schema.tables ` +
46
+ `from information_schema.tables t ` +
29
47
  `where ${this.getIgnoredNamespacesConditionSQL('table_schema')} ` +
30
48
  `and table_name != 'geometry_columns' and table_name != 'spatial_ref_sys' and table_type != 'VIEW' ` +
31
- `and table_name not in (select inhrelid::regclass::text from pg_inherits) ` +
49
+ `and not exists (` +
50
+ `select 1 from pg_inherits i ` +
51
+ `join pg_class c on c.oid = i.inhrelid ` +
52
+ `join pg_namespace n on n.oid = c.relnamespace ` +
53
+ `where c.relname = t.table_name and n.nspname = t.table_schema` +
54
+ `) ` +
32
55
  `order by table_name`);
33
56
  }
34
57
  getIgnoredViewsCondition() {
@@ -79,6 +102,23 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
79
102
  const dataClause = withData ? ' with data' : ' with no data';
80
103
  return `create materialized view ${viewName} as ${definition}${dataClause}`;
81
104
  }
105
+ createTable(table, alter) {
106
+ const partitioning = table.getPartitioning();
107
+ if (!partitioning) {
108
+ return super.createTable(table, alter);
109
+ }
110
+ const [createTable, ...rest] = super.createTable(table, alter);
111
+ const partitions = partitioning.partitions.map(partition => {
112
+ const partitionName = this.quote(this.getTableName(partition.name, partition.schema ?? table.schema));
113
+ return `create table ${partitionName} partition of ${table.getQuotedName()} ${partition.bound}`;
114
+ });
115
+ // SchemaHelper.append() always terminates the CREATE TABLE with `;`; we rely on that to splice
116
+ // the `partition by ...` clause in before the terminator. Use slice instead of replace() so that
117
+ // regex replacement tokens like `$$`, `$&`, or `$1` inside user-supplied expressions (e.g., a
118
+ // callback that returns a dollar-quoted literal) are not interpreted as back-references.
119
+ const spliced = `${createTable.slice(0, -1)} partition by ${partitioning.definition};`;
120
+ return [spliced, ...rest, ...partitions];
121
+ }
82
122
  dropMaterializedViewIfExists(name, schema) {
83
123
  return `drop materialized view if exists ${this.quote(this.getTableName(name, schema))} cascade`;
84
124
  }
@@ -118,15 +158,75 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
118
158
  const indexes = await this.getAllIndexes(connection, tables, ctx);
119
159
  const checks = await this.getAllChecks(connection, tablesBySchema, ctx);
120
160
  const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
161
+ const partitionings = await this.getPartitions(connection, tablesBySchema, ctx);
162
+ const triggers = await this.getAllTriggers(connection, tablesBySchema);
163
+ const dbCollation = await this.getDatabaseCollation(connection, ctx);
121
164
  for (const t of tables) {
122
165
  const key = this.getTableKey(t);
123
166
  const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
167
+ table.collation = dbCollation;
124
168
  const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
125
169
  const enums = this.getEnumDefinitions(checks[key] ?? []);
126
170
  if (columns[key]) {
127
171
  table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums);
128
172
  }
173
+ if (triggers[key]) {
174
+ table.setTriggers(triggers[key]);
175
+ }
176
+ table.setPartitioning(partitionings[key]);
177
+ }
178
+ }
179
+ /**
180
+ * Introspects direct partitions only: the `pg_inherits` join surfaces a parent's children but
181
+ * does not recurse into sub-partitioning (e.g. hash-of-range). Declarative `partitionBy`
182
+ * metadata does not express multi-level partitioning either, so grandchildren are intentionally
183
+ * invisible to schema diffing.
184
+ *
185
+ * Entries with an undefined schema bucket are resolved against `current_schema()` so they do
186
+ * not match same-named tables in unrelated schemas.
187
+ */
188
+ async getPartitions(connection, tablesBySchemas, ctx) {
189
+ // Collapse every (schema, table) pair into a single `values (...)` relation and join against
190
+ // the catalog, instead of building an OR-tree of per-schema `in (...)` predicates. This keeps
191
+ // the query size O(pairs) rather than O(schemas × predicate_overhead) and stays sargable when
192
+ // many schemas are in play.
193
+ const pairs = [...tablesBySchemas.entries()].flatMap(([schema, tables]) => tables.map(t => {
194
+ const schemaLiteral = schema == null ? 'null::text' : `${this.platform.quoteValue(schema)}::text`;
195
+ return `(${schemaLiteral}, ${this.platform.quoteValue(t.table_name)})`;
196
+ }));
197
+ if (pairs.length === 0) {
198
+ return {};
199
+ }
200
+ const sql = `with targets(schema_name, table_name) as (values ${pairs.join(', ')})
201
+ select parent_ns.nspname as schema_name,
202
+ parent.relname as table_name,
203
+ pg_get_partkeydef(parent.oid) as partition_definition,
204
+ child_ns.nspname as partition_schema_name,
205
+ child.relname as partition_name,
206
+ pg_get_expr(child.relpartbound, child.oid) as partition_bound
207
+ from targets
208
+ join pg_class parent on parent.relname = targets.table_name
209
+ join pg_namespace parent_ns on parent_ns.oid = parent.relnamespace
210
+ and parent_ns.nspname = coalesce(targets.schema_name, current_schema())
211
+ join pg_partitioned_table partitioned on partitioned.partrelid = parent.oid
212
+ left join pg_inherits inherits on inherits.inhparent = parent.oid
213
+ left join pg_class child on child.oid = inherits.inhrelid
214
+ left join pg_namespace child_ns on child_ns.oid = child.relnamespace
215
+ order by parent_ns.nspname, parent.relname, child_ns.nspname, child.relname`;
216
+ const rows = await connection.execute(sql, [], 'all', ctx);
217
+ const ret = {};
218
+ for (const row of rows) {
219
+ const key = this.getTableKey(row);
220
+ ret[key] ??= { definition: normalizePartitionDefinition(row.partition_definition), partitions: [] };
221
+ if (row.partition_name && row.partition_bound) {
222
+ ret[key].partitions.push({
223
+ name: row.partition_name,
224
+ schema: row.partition_schema_name,
225
+ bound: normalizePartitionBound(row.partition_bound),
226
+ });
227
+ }
129
228
  }
229
+ return ret;
130
230
  }
131
231
  async getAllIndexes(connection, tables, ctx) {
132
232
  const sql = this.getIndexesSQL(tables);
@@ -161,9 +261,22 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
161
261
  if (index.condeferrable) {
162
262
  indexDef.deferMode = index.condeferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
163
263
  }
164
- if (index.index_def.some((col) => /[(): ,"'`]/.exec(col)) || index.expression?.match(/ where /i)) {
264
+ const hasFunctionalColumns = index.index_def.some((col) => PostgreSqlSchemaHelper.FUNCTIONAL_COL_RE.exec(col));
265
+ const whereMatch = hasFunctionalColumns
266
+ ? null
267
+ : PostgreSqlSchemaHelper.PARTIAL_WHERE_RE.exec(index.expression ?? '');
268
+ if (hasFunctionalColumns) {
269
+ // Functional-column expression can't be diffed structurally — keep the whole CREATE
270
+ // statement (WHERE included) on `expression`; don't try to split the predicate.
165
271
  indexDef.expression = index.expression;
166
272
  }
273
+ else if (whereMatch) {
274
+ let where = whereMatch[1].trim();
275
+ if (where.startsWith('(') && where.endsWith(')') && this.isBalancedWrap(where)) {
276
+ where = where.slice(1, -1).trim();
277
+ }
278
+ indexDef.where = where;
279
+ }
167
280
  if (index.deferrable) {
168
281
  indexDef.deferMode = index.initially_deferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
169
282
  }
@@ -249,7 +362,6 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
249
362
  }
250
363
  }
251
364
  }
252
- /* v8 ignore next - pg_get_indexdef always returns balanced parentheses */
253
365
  return '';
254
366
  }
255
367
  async getAllColumns(connection, tablesBySchemas, nativeEnums, ctx) {
@@ -266,10 +378,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
266
378
  is_identity,
267
379
  identity_generation,
268
380
  generation_expression,
269
- pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment
381
+ pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment,
382
+ coll.collname as collation_name
270
383
  from information_schema.columns cols
271
384
  join pg_class pgc on cols.table_name = pgc.relname
272
385
  join pg_attribute pga on pgc.oid = pga.attrelid and cols.column_name = pga.attname
386
+ left join pg_collation coll on pga.attcollation = coll.oid and coll.collname <> 'default'
273
387
  where (${[...tablesBySchemas.entries()].map(([schema, tables]) => `(table_schema = ${this.platform.quoteValue(schema)} and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}))`).join(' or ')})
274
388
  order by ordinal_position`;
275
389
  const allColumns = await connection.execute(sql, [], 'all', ctx);
@@ -319,6 +433,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
319
433
  ? col.generation_expression + ' stored'
320
434
  : undefined,
321
435
  comment: col.column_comment,
436
+ collation: col.collation_name ?? undefined,
322
437
  };
323
438
  let enumKey = column.type;
324
439
  let enumEntry = nativeEnums?.[enumKey];
@@ -375,6 +490,216 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
375
490
  }
376
491
  return ret;
377
492
  }
493
+ /** Generates SQL to create a PostgreSQL trigger and its associated function. */
494
+ createTrigger(table, trigger) {
495
+ if (trigger.expression) {
496
+ return trigger.expression;
497
+ }
498
+ const timing = trigger.timing.toUpperCase();
499
+ const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
500
+ const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
501
+ const when = trigger.when ? `\n when (${trigger.when})` : '';
502
+ const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
503
+ const triggerName = this.platform.quoteIdentifier(trigger.name);
504
+ const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${trigger.body}; end; $$ language plpgsql`;
505
+ const triggerSql = `create trigger ${triggerName} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} execute function ${fnName}()`;
506
+ return `${fnSql};\n${triggerSql}`;
507
+ }
508
+ /** Generates SQL to drop a PostgreSQL trigger and its associated function. */
509
+ dropTrigger(table, trigger) {
510
+ const triggerName = this.platform.quoteIdentifier(trigger.name);
511
+ const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
512
+ return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
513
+ }
514
+ createRoutine(routine) {
515
+ if (routine.expression) {
516
+ return routine.expression;
517
+ }
518
+ const qualifiedName = this.qualifiedRoutineName(routine);
519
+ const params = this.formatRoutineParams(routine);
520
+ // Default `sql` for functions, `plpgsql` for procedures (block syntax required).
521
+ const language = (routine.language ?? (routine.type === 'procedure' ? 'plpgsql' : 'sql')).toLowerCase();
522
+ const security = routine.security === 'definer' ? ' security definer' : routine.security === 'invoker' ? ' security invoker' : '';
523
+ const determinism = routine.deterministic === true ? ' immutable' : routine.deterministic === false ? ' volatile' : '';
524
+ const flatten = (s) => stripStatementNewlines(s).trim();
525
+ if (routine.type === 'procedure') {
526
+ const body = language === 'sql' ? flatten(routine.body ?? '') : this.wrapRoutineBody(routine.body ?? '');
527
+ return `create or replace procedure ${qualifiedName}(${params})${security} language ${language} as $$ ${body} $$`;
528
+ }
529
+ const returnType = routine.returns?.type ?? 'void';
530
+ const body = language === 'sql' ? flatten(routine.body ?? '') : this.wrapRoutineBody(routine.body ?? '');
531
+ return `create or replace function ${qualifiedName}(${params}) returns ${returnType}${security}${determinism} language ${language} as $$ ${body} $$`;
532
+ }
533
+ dropRoutine(routine) {
534
+ const qualifiedName = this.qualifiedRoutineName(routine);
535
+ const argTypes = routine.params.map(p => p.type).join(', ');
536
+ const kind = routine.type === 'procedure' ? 'procedure' : 'function';
537
+ return `drop ${kind} if exists ${qualifiedName}(${argTypes})`;
538
+ }
539
+ async getAllRoutines(connection, schemas = []) {
540
+ const target = (schemas.length > 0 ? schemas : [this.platform.getDefaultSchemaName()]).filter(Boolean);
541
+ const schemaList = target.map(s => this.platform.quoteValue(s)).join(', ');
542
+ const sql = `
543
+ select
544
+ n.nspname as schema_name,
545
+ p.proname as name,
546
+ case when p.prokind = 'p' then 'procedure' else 'function' end as kind,
547
+ l.lanname as language,
548
+ case when p.proisstrict then false else null end as is_strict,
549
+ case when p.provolatile = 'i' then true when p.provolatile = 'v' then false else null end as deterministic,
550
+ case when p.prosecdef then 'definer' else 'invoker' end as security,
551
+ pg_get_functiondef(p.oid) as full_def,
552
+ pg_get_function_arguments(p.oid) as arg_signature,
553
+ pg_get_function_result(p.oid) as result_type,
554
+ d.description as comment
555
+ from pg_proc p
556
+ join pg_namespace n on n.oid = p.pronamespace
557
+ join pg_language l on l.oid = p.prolang
558
+ left join pg_description d on d.objoid = p.oid and d.classoid = 'pg_proc'::regclass
559
+ where n.nspname in (${schemaList})
560
+ and l.lanname not in ('c', 'internal')
561
+ -- exclude functions owned by extensions (PostGIS et al.)
562
+ and not exists (
563
+ select 1 from pg_depend dep
564
+ where dep.objid = p.oid
565
+ and dep.classid = 'pg_proc'::regclass
566
+ and dep.deptype = 'e'
567
+ )
568
+ -- exclude trigger-helper functions; they're managed alongside their owning trigger.
569
+ and p.prorettype <> 'trigger'::regtype
570
+ `;
571
+ const rows = await connection.execute(sql);
572
+ return rows.map(row => {
573
+ const params = this.parseRoutineParams(row.arg_signature);
574
+ const body = this.extractRoutineBody(row.full_def);
575
+ return {
576
+ name: row.name,
577
+ schema: row.schema_name,
578
+ type: row.kind,
579
+ language: row.language,
580
+ comment: row.comment ?? undefined,
581
+ security: row.security,
582
+ deterministic: row.deterministic ?? undefined,
583
+ body,
584
+ params,
585
+ returns: row.kind === 'function' && row.result_type ? { type: row.result_type, nullable: true } : undefined,
586
+ };
587
+ });
588
+ }
589
+ formatRoutineParams(routine) {
590
+ return routine.params
591
+ .map(p => {
592
+ const dir = p.direction === 'in' ? '' : `${p.direction.toUpperCase()} `;
593
+ const def = p.defaultRaw ? ` default ${p.defaultRaw}` : '';
594
+ return `${dir}${this.platform.quoteIdentifier(p.name)} ${p.type}${def}`;
595
+ })
596
+ .join(', ');
597
+ }
598
+ parseRoutineParams(signature) {
599
+ if (!signature.trim()) {
600
+ return [];
601
+ }
602
+ return signature.split(/,(?![^()]*\))/).map(part => {
603
+ const trimmed = part.trim();
604
+ // INOUT before IN/OUT, with required trailing space so identifiers like `input` aren't
605
+ // mis-parsed as a direction keyword.
606
+ const match = /^(?:(INOUT|VARIADIC|IN|OUT)\s+)?(?:"((?:[^"]|"")+)"|([\w$]+))\s+(.+?)(?:\s+default\s+.+)?$/i.exec(trimmed);
607
+ /* v8 ignore next 3: defensive guard for unexpected `pg_get_function_arguments` output shapes */
608
+ if (!match) {
609
+ throw new Error(`Could not parse PostgreSQL routine parameter signature: ${JSON.stringify(trimmed)}`);
610
+ }
611
+ const dirRaw = (match[1] ?? 'in').toLowerCase();
612
+ const direction = dirRaw === 'inout' ? 'inout' : dirRaw === 'out' ? 'out' : 'in';
613
+ const name = match[2] != null ? match[2].replaceAll('""', '"') : match[3];
614
+ return {
615
+ name,
616
+ type: match[4].trim(),
617
+ direction,
618
+ };
619
+ });
620
+ }
621
+ /** PG canonicalises `$$` quoting to a tagged form like `$function$ ... $function$`; match the tag-aware form. */
622
+ extractRoutineBody(fullDef) {
623
+ const tagged = /\bAS\s+\$(\w*)\$([\s\S]*?)\$\1\$/i.exec(fullDef);
624
+ return this.stripRoutineBody(tagged ? tagged[2] : fullDef);
625
+ }
626
+ getSchemaQualifiedTriggerFnName(table, trigger) {
627
+ const rawName = `${table.name}_${trigger.name}_fn`;
628
+ const defaultSchema = this.platform.getDefaultSchemaName();
629
+ if (table.schema && table.schema !== defaultSchema) {
630
+ return `${this.platform.quoteIdentifier(table.schema)}.${this.platform.quoteIdentifier(rawName)}`;
631
+ }
632
+ return this.platform.quoteIdentifier(rawName);
633
+ }
634
+ /**
635
+ * Resolves the real name of the implicit 'default' collation (the DB's `datcollate`),
636
+ * so the comparator can treat `@Property({ collation: '<datcollate>' })` as equivalent
637
+ * to a column that introspects as using the default.
638
+ */
639
+ async getDatabaseCollation(connection, ctx) {
640
+ const [row] = await connection.execute(`select datcollate as collation from pg_database where datname = current_database()`, [], 'all', ctx);
641
+ return row?.collation;
642
+ }
643
+ async getAllTriggers(connection, tablesBySchemas) {
644
+ const sql = this.getTriggersSQL(tablesBySchemas);
645
+ const allTriggers = await connection.execute(sql);
646
+ const ret = {};
647
+ const triggerMap = new Map();
648
+ for (const row of allTriggers) {
649
+ const key = this.getTableKey(row);
650
+ const dedupeKey = `${key}:${row.trigger_name}`;
651
+ if (triggerMap.has(dedupeKey)) {
652
+ // Same trigger with multiple events — merge events
653
+ const existing = triggerMap.get(dedupeKey);
654
+ const event = row.event.toLowerCase();
655
+ if (!existing.events.includes(event)) {
656
+ existing.events.push(event);
657
+ }
658
+ continue;
659
+ }
660
+ ret[key] ??= [];
661
+ // prosrc includes the full function body between $$ delimiters (e.g. " begin RETURN NEW; end;")
662
+ // Strip the begin/end wrapper to get just the trigger body for round-trip comparison
663
+ let body = (row.function_body ?? '').trim();
664
+ const beginEndMatch = /^\s*begin\s+([\s\S]*?)\s*end;?\s*$/i.exec(body);
665
+ if (beginEndMatch) {
666
+ body = beginEndMatch[1].trim().replace(/;\s*$/, '');
667
+ }
668
+ const trigger = {
669
+ name: row.trigger_name,
670
+ timing: row.timing.toLowerCase(),
671
+ events: [row.event.toLowerCase()],
672
+ forEach: row.for_each.toLowerCase(),
673
+ body,
674
+ when: row.when_clause ?? undefined,
675
+ };
676
+ ret[key].push(trigger);
677
+ triggerMap.set(dedupeKey, trigger);
678
+ }
679
+ return ret;
680
+ }
681
+ getTriggersSQL(tablesBySchemas) {
682
+ const conditions = [];
683
+ for (const [schema, tables] of tablesBySchemas) {
684
+ const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
685
+ const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
686
+ conditions.push(`(t.event_object_schema = ${schemaName} and t.event_object_table in (${names}))`);
687
+ }
688
+ // Function lookup uses the '{table}_{trigger}_fn' convention from createTrigger().
689
+ // External triggers with different function names will have NULL body;
690
+ // use the `expression` escape hatch for those.
691
+ return `select t.trigger_name, t.event_object_schema as schema_name, t.event_object_table as table_name,
692
+ t.event_manipulation as event, t.action_timing as timing,
693
+ t.action_orientation as for_each,
694
+ t.action_condition as when_clause,
695
+ pg_get_functiondef(p.oid) as function_def,
696
+ p.prosrc as function_body
697
+ from information_schema.triggers t
698
+ left join pg_namespace n on n.nspname = t.event_object_schema
699
+ left join pg_proc p on p.proname = t.event_object_table || '_' || t.trigger_name || '_fn' and p.pronamespace = n.oid
700
+ where (${conditions.join(' or ')})
701
+ order by t.trigger_name, t.event_manipulation`;
702
+ }
378
703
  async getAllForeignKeys(connection, tablesBySchemas, ctx) {
379
704
  const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
380
705
  cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
@@ -505,6 +830,9 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
505
830
  return o;
506
831
  }, {});
507
832
  }
833
+ getCollateSQL(collation) {
834
+ return `collate ${this.platform.quoteCollation(collation)}`;
835
+ }
508
836
  createTableColumn(column, table) {
509
837
  const pk = table.getPrimaryKey();
510
838
  const compositePK = pk?.composite;
@@ -537,6 +865,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
537
865
  columnType += ` generated always as ${column.generated}`;
538
866
  }
539
867
  col.push(columnType);
868
+ Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
540
869
  Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
541
870
  Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable);
542
871
  }
@@ -548,6 +877,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
548
877
  return col.join(' ');
549
878
  }
550
879
  getPreAlterTable(tableDiff, safe) {
880
+ if (tableDiff.changedPartitioning) {
881
+ const from = tableDiff.changedPartitioning.from?.definition;
882
+ const to = tableDiff.changedPartitioning.to?.definition;
883
+ const action = !from ? 'Adding' : !to ? 'Removing' : 'Changing';
884
+ throw new Error(`${action} partition definitions for existing PostgreSQL tables is not supported automatically (${tableDiff.name}: '${from ?? '<none>'}' -> '${to ?? '<none>'}'); create a manual migration instead`);
885
+ }
551
886
  const ret = [];
552
887
  const parts = tableDiff.name.split('.');
553
888
  const tableName = parts.pop();
@@ -680,33 +1015,32 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
680
1015
  if (index.primary || (index.unique && index.constraint)) {
681
1016
  return `alter table ${this.quote(table)} drop constraint ${this.quote(oldIndexName)}`;
682
1017
  }
683
- return `drop index ${this.quote(oldIndexName)}`;
1018
+ const [schemaName] = this.splitTableName(table);
1019
+ return `drop index ${this.quote(schemaName, oldIndexName)}`;
684
1020
  }
685
1021
  /**
686
1022
  * Build the column list for a PostgreSQL index.
687
1023
  */
688
1024
  getIndexColumns(index) {
689
- if (index.columns?.length) {
690
- return index.columns
691
- .map(col => {
692
- let colDef = this.quote(col.name);
693
- // PostgreSQL supports collation with double quotes
694
- if (col.collation) {
695
- colDef += ` collate ${this.quote(col.collation)}`;
696
- }
697
- // PostgreSQL supports sort order
698
- if (col.sort) {
699
- colDef += ` ${col.sort}`;
700
- }
701
- // PostgreSQL supports NULLS FIRST/LAST
702
- if (col.nulls) {
703
- colDef += ` nulls ${col.nulls}`;
704
- }
705
- return colDef;
706
- })
707
- .join(', ');
708
- }
709
- return index.columnNames.map(c => this.quote(c)).join(', ');
1025
+ return index.columnNames
1026
+ .map(name => {
1027
+ const col = index.columns?.find(c => c.name === name);
1028
+ let colDef = this.quote(name);
1029
+ // PostgreSQL supports collation with double quotes
1030
+ if (col?.collation) {
1031
+ colDef += ` collate ${this.quote(col.collation)}`;
1032
+ }
1033
+ // PostgreSQL supports sort order
1034
+ if (col?.sort) {
1035
+ colDef += ` ${col.sort}`;
1036
+ }
1037
+ // PostgreSQL supports NULLS FIRST/LAST
1038
+ if (col?.nulls) {
1039
+ colDef += ` nulls ${col.nulls}`;
1040
+ }
1041
+ return colDef;
1042
+ })
1043
+ .join(', ');
710
1044
  }
711
1045
  /**
712
1046
  * PostgreSQL-specific index options like fill factor.
@@ -1,4 +1,6 @@
1
1
  export * from './PostgreSqlNativeQueryBuilder.js';
2
2
  export * from './BasePostgreSqlPlatform.js';
3
+ export * from './BasePostgreSqlEntityManager.js';
3
4
  export * from './FullTextType.js';
4
5
  export * from './PostgreSqlSchemaHelper.js';
6
+ export * from './typeOverrides.js';
@@ -1,4 +1,6 @@
1
1
  export * from './PostgreSqlNativeQueryBuilder.js';
2
2
  export * from './BasePostgreSqlPlatform.js';
3
+ export * from './BasePostgreSqlEntityManager.js';
3
4
  export * from './FullTextType.js';
4
5
  export * from './PostgreSqlSchemaHelper.js';
6
+ export * from './typeOverrides.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MikroORM keeps PostgreSQL date/timestamp/interval values as raw strings
3
+ * (and array variants as `string[]`); both `pg` and `pglite` would otherwise
4
+ * eagerly parse them via `pg-types`. Centralizing the OID list here keeps the
5
+ * postgres and pglite drivers in lockstep, while leaving the actual array
6
+ * parsing implementation to the leaf driver (so `@mikro-orm/sql` stays free of
7
+ * postgres-array / postgres-date / postgres-interval dependencies).
8
+ *
9
+ * Use `select typname, oid, typarray from pg_type order by oid` to look up OIDs.
10
+ */
11
+ type PostgreSqlArrayParser = (value: string) => string[];
12
+ type PostgreSqlValueParser = (value: string) => unknown;
13
+ export declare function createPostgreSqlTypeParsers(arrayParse: PostgreSqlArrayParser): Record<number, PostgreSqlValueParser>;
14
+ export {};
@@ -0,0 +1,12 @@
1
+ export function createPostgreSqlTypeParsers(arrayParse) {
2
+ const parsers = {};
3
+ for (const oid of [1082, 1114, 1184, 1186]) {
4
+ // date, timestamp, timestamptz, interval — kept as raw strings
5
+ parsers[oid] = str => str;
6
+ }
7
+ for (const oid of [1182, 1115, 1185, 1187]) {
8
+ // date[], timestamp[], timestamptz[], interval[]
9
+ parsers[oid] = arrayParse;
10
+ }
11
+ return parsers;
12
+ }
@@ -15,6 +15,7 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
15
15
  getDateTimeTypeDeclarationSQL(column: {
16
16
  length: number;
17
17
  }): string;
18
+ getDefaultVersionLength(): number;
18
19
  getBeginTransactionSQL(options?: {
19
20
  isolationLevel?: IsolationLevel;
20
21
  readOnly?: boolean;
@@ -62,7 +63,7 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
62
63
  * including all Date properties, as we would be comparing Date object with timestamp.
63
64
  */
64
65
  processDateProperty(value: unknown): string | number | Date;
65
- getIndexName(tableName: string, columns: string[], type: 'index' | 'unique' | 'foreign' | 'primary' | 'sequence'): string;
66
+ getIndexName(tableName: string, columns: string[], type: 'index' | 'unique' | 'foreign' | 'primary' | 'sequence' | 'check'): string;
66
67
  supportsDeferredUniqueConstraints(): boolean;
67
68
  /**
68
69
  * SQLite supports schemas via ATTACH DATABASE. Returns true when there are
@@ -24,6 +24,10 @@ export class SqlitePlatform extends AbstractSqlPlatform {
24
24
  getDateTimeTypeDeclarationSQL(column) {
25
25
  return 'datetime';
26
26
  }
27
+ // sqlite's datetime DDL drops precision and the current-ts expression hardcodes ms scaling
28
+ getDefaultVersionLength() {
29
+ return 0;
30
+ }
27
31
  getBeginTransactionSQL(options) {
28
32
  return ['begin'];
29
33
  }
@@ -1,15 +1,17 @@
1
1
  import { type Connection, type Transaction } from '@mikro-orm/core';
2
2
  import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
3
3
  import { SchemaHelper } from '../../schema/SchemaHelper.js';
4
- import type { Column, IndexDef, Table, TableDifference } from '../../typings.js';
4
+ import type { Column, IndexDef, Table, TableDifference, SqlTriggerDef } from '../../typings.js';
5
5
  import type { DatabaseTable } from '../../schema/DatabaseTable.js';
6
6
  import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
7
7
  export declare class SqliteSchemaHelper extends SchemaHelper {
8
+ private static readonly PARTIAL_WHERE_RE;
8
9
  disableForeignKeysSQL(): string;
9
10
  enableForeignKeysSQL(): string;
10
11
  supportsSchemaConstraints(): boolean;
11
12
  getCreateNamespaceSQL(name: string): string;
12
13
  getDropNamespaceSQL(name: string): string;
14
+ tableExists(connection: AbstractSqlConnection, tableName: string, _schemaName: string | undefined, ctx?: Transaction): Promise<boolean>;
13
15
  getListTablesSQL(): string;
14
16
  getAllTables(connection: AbstractSqlConnection, schemas?: string[], ctx?: Transaction): Promise<Table[]>;
15
17
  getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
@@ -44,7 +46,8 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
44
46
  * We need to add them back so they match what we generate in DDL.
45
47
  */
46
48
  private wrapExpressionDefault;
47
- private getEnumDefinitions;
49
+ /** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
50
+ private extractEnumValuesFromChecks;
48
51
  getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[], tableName: string, schemaName?: string, ctx?: Transaction): Promise<string[]>;
49
52
  private getIndexes;
50
53
  private getChecks;
@@ -64,5 +67,9 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
64
67
  */
65
68
  getReferencedTableName(referencedTableName: string, schema?: string): string;
66
69
  alterTable(diff: TableDifference, safe?: boolean): string[];
70
+ /** Generates SQL to create SQLite triggers. SQLite requires one trigger per event. */
71
+ createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
72
+ private getTableTriggers;
73
+ private parseTriggerDDL;
67
74
  private getAlterTempTableSQL;
68
75
  }