@mikro-orm/sql 7.1.0-dev.9 → 7.1.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.
- package/AbstractSqlConnection.d.ts +1 -1
- package/AbstractSqlConnection.js +27 -6
- package/AbstractSqlDriver.d.ts +15 -1
- package/AbstractSqlDriver.js +143 -26
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.d.ts +2 -2
- package/PivotCollectionPersister.js +6 -1
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +44 -5
- package/SqlEntityManager.js +41 -6
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +3 -5
- package/dialects/mysql/BaseMySqlPlatform.js +6 -10
- package/dialects/mysql/MySqlSchemaHelper.d.ts +16 -3
- package/dialects/mysql/MySqlSchemaHelper.js +197 -49
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
- package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +11 -5
- package/dialects/postgresql/BasePostgreSqlPlatform.js +75 -17
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +269 -28
- package/dialects/postgresql/index.d.ts +2 -0
- package/dialects/postgresql/index.js +2 -0
- package/dialects/postgresql/typeOverrides.d.ts +14 -0
- package/dialects/postgresql/typeOverrides.js +12 -0
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -1
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +4 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +49 -19
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +4 -4
- package/plugin/transformer.d.ts +11 -3
- package/plugin/transformer.js +138 -29
- package/query/CriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +2 -2
- package/query/ObjectCriteriaNode.js +1 -1
- package/query/QueryBuilder.d.ts +42 -1
- package/query/QueryBuilder.js +78 -7
- package/schema/DatabaseSchema.d.ts +29 -2
- package/schema/DatabaseSchema.js +131 -4
- package/schema/DatabaseTable.d.ts +14 -1
- package/schema/DatabaseTable.js +165 -32
- package/schema/SchemaComparator.d.ts +18 -0
- package/schema/SchemaComparator.js +196 -1
- package/schema/SchemaHelper.d.ts +67 -1
- package/schema/SchemaHelper.js +255 -25
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +40 -10
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +59 -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
|
|
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,10 +158,13 @@ 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);
|
|
121
162
|
const triggers = await this.getAllTriggers(connection, tablesBySchema);
|
|
163
|
+
const dbCollation = await this.getDatabaseCollation(connection, ctx);
|
|
122
164
|
for (const t of tables) {
|
|
123
165
|
const key = this.getTableKey(t);
|
|
124
166
|
const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
|
|
167
|
+
table.collation = dbCollation;
|
|
125
168
|
const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
|
|
126
169
|
const enums = this.getEnumDefinitions(checks[key] ?? []);
|
|
127
170
|
if (columns[key]) {
|
|
@@ -130,7 +173,60 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
130
173
|
if (triggers[key]) {
|
|
131
174
|
table.setTriggers(triggers[key]);
|
|
132
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
|
+
}
|
|
133
228
|
}
|
|
229
|
+
return ret;
|
|
134
230
|
}
|
|
135
231
|
async getAllIndexes(connection, tables, ctx) {
|
|
136
232
|
const sql = this.getIndexesSQL(tables);
|
|
@@ -165,9 +261,22 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
165
261
|
if (index.condeferrable) {
|
|
166
262
|
indexDef.deferMode = index.condeferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
|
|
167
263
|
}
|
|
168
|
-
|
|
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.
|
|
169
271
|
indexDef.expression = index.expression;
|
|
170
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
|
+
}
|
|
171
280
|
if (index.deferrable) {
|
|
172
281
|
indexDef.deferMode = index.initially_deferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
|
|
173
282
|
}
|
|
@@ -253,7 +362,6 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
253
362
|
}
|
|
254
363
|
}
|
|
255
364
|
}
|
|
256
|
-
/* v8 ignore next - pg_get_indexdef always returns balanced parentheses */
|
|
257
365
|
return '';
|
|
258
366
|
}
|
|
259
367
|
async getAllColumns(connection, tablesBySchemas, nativeEnums, ctx) {
|
|
@@ -270,10 +378,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
270
378
|
is_identity,
|
|
271
379
|
identity_generation,
|
|
272
380
|
generation_expression,
|
|
273
|
-
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
|
|
274
383
|
from information_schema.columns cols
|
|
275
384
|
join pg_class pgc on cols.table_name = pgc.relname
|
|
276
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'
|
|
277
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 ')})
|
|
278
388
|
order by ordinal_position`;
|
|
279
389
|
const allColumns = await connection.execute(sql, [], 'all', ctx);
|
|
@@ -323,6 +433,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
323
433
|
? col.generation_expression + ' stored'
|
|
324
434
|
: undefined,
|
|
325
435
|
comment: col.column_comment,
|
|
436
|
+
collation: col.collation_name ?? undefined,
|
|
326
437
|
};
|
|
327
438
|
let enumKey = column.type;
|
|
328
439
|
let enumEntry = nativeEnums?.[enumKey];
|
|
@@ -400,6 +511,118 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
400
511
|
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
401
512
|
return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
|
|
402
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
|
+
}
|
|
403
626
|
getSchemaQualifiedTriggerFnName(table, trigger) {
|
|
404
627
|
const rawName = `${table.name}_${trigger.name}_fn`;
|
|
405
628
|
const defaultSchema = this.platform.getDefaultSchemaName();
|
|
@@ -408,6 +631,15 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
408
631
|
}
|
|
409
632
|
return this.platform.quoteIdentifier(rawName);
|
|
410
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
|
+
}
|
|
411
643
|
async getAllTriggers(connection, tablesBySchemas) {
|
|
412
644
|
const sql = this.getTriggersSQL(tablesBySchemas);
|
|
413
645
|
const allTriggers = await connection.execute(sql);
|
|
@@ -598,6 +830,9 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
598
830
|
return o;
|
|
599
831
|
}, {});
|
|
600
832
|
}
|
|
833
|
+
getCollateSQL(collation) {
|
|
834
|
+
return `collate ${this.platform.quoteCollation(collation)}`;
|
|
835
|
+
}
|
|
601
836
|
createTableColumn(column, table) {
|
|
602
837
|
const pk = table.getPrimaryKey();
|
|
603
838
|
const compositePK = pk?.composite;
|
|
@@ -630,6 +865,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
630
865
|
columnType += ` generated always as ${column.generated}`;
|
|
631
866
|
}
|
|
632
867
|
col.push(columnType);
|
|
868
|
+
Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
|
|
633
869
|
Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
|
|
634
870
|
Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable);
|
|
635
871
|
}
|
|
@@ -641,6 +877,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
641
877
|
return col.join(' ');
|
|
642
878
|
}
|
|
643
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
|
+
}
|
|
644
886
|
const ret = [];
|
|
645
887
|
const parts = tableDiff.name.split('.');
|
|
646
888
|
const tableName = parts.pop();
|
|
@@ -773,33 +1015,32 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
773
1015
|
if (index.primary || (index.unique && index.constraint)) {
|
|
774
1016
|
return `alter table ${this.quote(table)} drop constraint ${this.quote(oldIndexName)}`;
|
|
775
1017
|
}
|
|
776
|
-
|
|
1018
|
+
const [schemaName] = this.splitTableName(table);
|
|
1019
|
+
return `drop index ${this.quote(schemaName, oldIndexName)}`;
|
|
777
1020
|
}
|
|
778
1021
|
/**
|
|
779
1022
|
* Build the column list for a PostgreSQL index.
|
|
780
1023
|
*/
|
|
781
1024
|
getIndexColumns(index) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
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(', ');
|
|
803
1044
|
}
|
|
804
1045
|
/**
|
|
805
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
|
}
|
|
@@ -5,11 +5,13 @@ import type { Column, IndexDef, Table, TableDifference, SqlTriggerDef } from '..
|
|
|
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
|
-
|
|
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;
|