@mikro-orm/sql 7.1.0-dev.3 → 7.1.0-dev.30
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 +2 -2
- package/AbstractSqlDriver.d.ts +19 -1
- package/AbstractSqlDriver.js +215 -16
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.js +13 -2
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +13 -3
- package/dialects/mysql/MySqlSchemaHelper.js +145 -21
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +28 -6
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +230 -5
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
- package/dialects/sqlite/SqliteSchemaHelper.js +148 -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 +36 -0
- package/query/QueryBuilder.js +63 -1
- package/schema/DatabaseSchema.js +26 -4
- package/schema/DatabaseTable.d.ts +20 -1
- package/schema/DatabaseTable.js +182 -31
- package/schema/SchemaComparator.d.ts +10 -0
- package/schema/SchemaComparator.js +104 -1
- package/schema/SchemaHelper.d.ts +63 -1
- package/schema/SchemaHelper.js +235 -6
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +16 -9
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +34 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DeferMode, EnumType, Type, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { SchemaHelper } 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,16 +158,76 @@ 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]);
|
|
129
177
|
}
|
|
130
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
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return ret;
|
|
230
|
+
}
|
|
131
231
|
async getAllIndexes(connection, tables, ctx) {
|
|
132
232
|
const sql = this.getIndexesSQL(tables);
|
|
133
233
|
const unquote = (str) => str.replace(/['"`]/g, '');
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -266,10 +379,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
266
379
|
is_identity,
|
|
267
380
|
identity_generation,
|
|
268
381
|
generation_expression,
|
|
269
|
-
pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment
|
|
382
|
+
pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment,
|
|
383
|
+
coll.collname as collation_name
|
|
270
384
|
from information_schema.columns cols
|
|
271
385
|
join pg_class pgc on cols.table_name = pgc.relname
|
|
272
386
|
join pg_attribute pga on pgc.oid = pga.attrelid and cols.column_name = pga.attname
|
|
387
|
+
left join pg_collation coll on pga.attcollation = coll.oid and coll.collname <> 'default'
|
|
273
388
|
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
389
|
order by ordinal_position`;
|
|
275
390
|
const allColumns = await connection.execute(sql, [], 'all', ctx);
|
|
@@ -319,6 +434,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
319
434
|
? col.generation_expression + ' stored'
|
|
320
435
|
: undefined,
|
|
321
436
|
comment: col.column_comment,
|
|
437
|
+
collation: col.collation_name ?? undefined,
|
|
322
438
|
};
|
|
323
439
|
let enumKey = column.type;
|
|
324
440
|
let enumEntry = nativeEnums?.[enumKey];
|
|
@@ -375,6 +491,104 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
375
491
|
}
|
|
376
492
|
return ret;
|
|
377
493
|
}
|
|
494
|
+
/** Generates SQL to create a PostgreSQL trigger and its associated function. */
|
|
495
|
+
createTrigger(table, trigger) {
|
|
496
|
+
if (trigger.expression) {
|
|
497
|
+
return trigger.expression;
|
|
498
|
+
}
|
|
499
|
+
const timing = trigger.timing.toUpperCase();
|
|
500
|
+
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
501
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
502
|
+
const when = trigger.when ? `\n when (${trigger.when})` : '';
|
|
503
|
+
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
504
|
+
const triggerName = this.platform.quoteIdentifier(trigger.name);
|
|
505
|
+
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${trigger.body}; end; $$ language plpgsql`;
|
|
506
|
+
const triggerSql = `create trigger ${triggerName} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} execute function ${fnName}()`;
|
|
507
|
+
return `${fnSql};\n${triggerSql}`;
|
|
508
|
+
}
|
|
509
|
+
/** Generates SQL to drop a PostgreSQL trigger and its associated function. */
|
|
510
|
+
dropTrigger(table, trigger) {
|
|
511
|
+
const triggerName = this.platform.quoteIdentifier(trigger.name);
|
|
512
|
+
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
513
|
+
return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
|
|
514
|
+
}
|
|
515
|
+
getSchemaQualifiedTriggerFnName(table, trigger) {
|
|
516
|
+
const rawName = `${table.name}_${trigger.name}_fn`;
|
|
517
|
+
const defaultSchema = this.platform.getDefaultSchemaName();
|
|
518
|
+
if (table.schema && table.schema !== defaultSchema) {
|
|
519
|
+
return `${this.platform.quoteIdentifier(table.schema)}.${this.platform.quoteIdentifier(rawName)}`;
|
|
520
|
+
}
|
|
521
|
+
return this.platform.quoteIdentifier(rawName);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Resolves the real name of the implicit 'default' collation (the DB's `datcollate`),
|
|
525
|
+
* so the comparator can treat `@Property({ collation: '<datcollate>' })` as equivalent
|
|
526
|
+
* to a column that introspects as using the default.
|
|
527
|
+
*/
|
|
528
|
+
async getDatabaseCollation(connection, ctx) {
|
|
529
|
+
const [row] = await connection.execute(`select datcollate as collation from pg_database where datname = current_database()`, [], 'all', ctx);
|
|
530
|
+
return row?.collation;
|
|
531
|
+
}
|
|
532
|
+
async getAllTriggers(connection, tablesBySchemas) {
|
|
533
|
+
const sql = this.getTriggersSQL(tablesBySchemas);
|
|
534
|
+
const allTriggers = await connection.execute(sql);
|
|
535
|
+
const ret = {};
|
|
536
|
+
const triggerMap = new Map();
|
|
537
|
+
for (const row of allTriggers) {
|
|
538
|
+
const key = this.getTableKey(row);
|
|
539
|
+
const dedupeKey = `${key}:${row.trigger_name}`;
|
|
540
|
+
if (triggerMap.has(dedupeKey)) {
|
|
541
|
+
// Same trigger with multiple events — merge events
|
|
542
|
+
const existing = triggerMap.get(dedupeKey);
|
|
543
|
+
const event = row.event.toLowerCase();
|
|
544
|
+
if (!existing.events.includes(event)) {
|
|
545
|
+
existing.events.push(event);
|
|
546
|
+
}
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
ret[key] ??= [];
|
|
550
|
+
// prosrc includes the full function body between $$ delimiters (e.g. " begin RETURN NEW; end;")
|
|
551
|
+
// Strip the begin/end wrapper to get just the trigger body for round-trip comparison
|
|
552
|
+
let body = (row.function_body ?? '').trim();
|
|
553
|
+
const beginEndMatch = /^\s*begin\s+([\s\S]*?)\s*end;?\s*$/i.exec(body);
|
|
554
|
+
if (beginEndMatch) {
|
|
555
|
+
body = beginEndMatch[1].trim().replace(/;\s*$/, '');
|
|
556
|
+
}
|
|
557
|
+
const trigger = {
|
|
558
|
+
name: row.trigger_name,
|
|
559
|
+
timing: row.timing.toLowerCase(),
|
|
560
|
+
events: [row.event.toLowerCase()],
|
|
561
|
+
forEach: row.for_each.toLowerCase(),
|
|
562
|
+
body,
|
|
563
|
+
when: row.when_clause ?? undefined,
|
|
564
|
+
};
|
|
565
|
+
ret[key].push(trigger);
|
|
566
|
+
triggerMap.set(dedupeKey, trigger);
|
|
567
|
+
}
|
|
568
|
+
return ret;
|
|
569
|
+
}
|
|
570
|
+
getTriggersSQL(tablesBySchemas) {
|
|
571
|
+
const conditions = [];
|
|
572
|
+
for (const [schema, tables] of tablesBySchemas) {
|
|
573
|
+
const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
|
|
574
|
+
const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
|
|
575
|
+
conditions.push(`(t.event_object_schema = ${schemaName} and t.event_object_table in (${names}))`);
|
|
576
|
+
}
|
|
577
|
+
// Function lookup uses the '{table}_{trigger}_fn' convention from createTrigger().
|
|
578
|
+
// External triggers with different function names will have NULL body;
|
|
579
|
+
// use the `expression` escape hatch for those.
|
|
580
|
+
return `select t.trigger_name, t.event_object_schema as schema_name, t.event_object_table as table_name,
|
|
581
|
+
t.event_manipulation as event, t.action_timing as timing,
|
|
582
|
+
t.action_orientation as for_each,
|
|
583
|
+
t.action_condition as when_clause,
|
|
584
|
+
pg_get_functiondef(p.oid) as function_def,
|
|
585
|
+
p.prosrc as function_body
|
|
586
|
+
from information_schema.triggers t
|
|
587
|
+
left join pg_namespace n on n.nspname = t.event_object_schema
|
|
588
|
+
left join pg_proc p on p.proname = t.event_object_table || '_' || t.trigger_name || '_fn' and p.pronamespace = n.oid
|
|
589
|
+
where (${conditions.join(' or ')})
|
|
590
|
+
order by t.trigger_name, t.event_manipulation`;
|
|
591
|
+
}
|
|
378
592
|
async getAllForeignKeys(connection, tablesBySchemas, ctx) {
|
|
379
593
|
const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
|
|
380
594
|
cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
|
|
@@ -505,6 +719,9 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
505
719
|
return o;
|
|
506
720
|
}, {});
|
|
507
721
|
}
|
|
722
|
+
getCollateSQL(collation) {
|
|
723
|
+
return `collate ${this.platform.quoteCollation(collation)}`;
|
|
724
|
+
}
|
|
508
725
|
createTableColumn(column, table) {
|
|
509
726
|
const pk = table.getPrimaryKey();
|
|
510
727
|
const compositePK = pk?.composite;
|
|
@@ -537,6 +754,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
537
754
|
columnType += ` generated always as ${column.generated}`;
|
|
538
755
|
}
|
|
539
756
|
col.push(columnType);
|
|
757
|
+
Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
|
|
540
758
|
Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
|
|
541
759
|
Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable);
|
|
542
760
|
}
|
|
@@ -548,6 +766,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
548
766
|
return col.join(' ');
|
|
549
767
|
}
|
|
550
768
|
getPreAlterTable(tableDiff, safe) {
|
|
769
|
+
if (tableDiff.changedPartitioning) {
|
|
770
|
+
const from = tableDiff.changedPartitioning.from?.definition;
|
|
771
|
+
const to = tableDiff.changedPartitioning.to?.definition;
|
|
772
|
+
const action = !from ? 'Adding' : !to ? 'Removing' : 'Changing';
|
|
773
|
+
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`);
|
|
774
|
+
}
|
|
551
775
|
const ret = [];
|
|
552
776
|
const parts = tableDiff.name.split('.');
|
|
553
777
|
const tableName = parts.pop();
|
|
@@ -680,7 +904,8 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
680
904
|
if (index.primary || (index.unique && index.constraint)) {
|
|
681
905
|
return `alter table ${this.quote(table)} drop constraint ${this.quote(oldIndexName)}`;
|
|
682
906
|
}
|
|
683
|
-
|
|
907
|
+
const [schemaName] = this.splitTableName(table);
|
|
908
|
+
return `drop index ${this.quote(schemaName, oldIndexName)}`;
|
|
684
909
|
}
|
|
685
910
|
/**
|
|
686
911
|
* Build the column list for a PostgreSQL index.
|
|
@@ -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;
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -15,6 +15,7 @@ const SPATIALITE_VIEWS = [
|
|
|
15
15
|
'ElementaryGeometries',
|
|
16
16
|
];
|
|
17
17
|
export class SqliteSchemaHelper extends SchemaHelper {
|
|
18
|
+
static PARTIAL_WHERE_RE = /\swhere\s+(.+?)\s*$/is;
|
|
18
19
|
disableForeignKeysSQL() {
|
|
19
20
|
return 'pragma foreign_keys = off;';
|
|
20
21
|
}
|
|
@@ -30,6 +31,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
30
31
|
getDropNamespaceSQL(name) {
|
|
31
32
|
return '';
|
|
32
33
|
}
|
|
34
|
+
async tableExists(connection, tableName, _schemaName, ctx) {
|
|
35
|
+
const rows = await connection.execute(`select name from sqlite_master where type = 'table' and name = ${this.platform.quoteValue(tableName)}`, [], 'all', ctx);
|
|
36
|
+
return rows.length > 0;
|
|
37
|
+
}
|
|
33
38
|
getListTablesSQL() {
|
|
34
39
|
return (`select name as table_name from sqlite_master where type = 'table' and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys' ` +
|
|
35
40
|
`union all select name as table_name from sqlite_temp_master where type = 'table' order by name`);
|
|
@@ -100,8 +105,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
100
105
|
const checks = await this.getChecks(connection, table.name, table.schema, ctx);
|
|
101
106
|
const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema, ctx);
|
|
102
107
|
const fks = await this.getForeignKeys(connection, table.name, table.schema, ctx);
|
|
103
|
-
const enums =
|
|
108
|
+
const enums = this.extractEnumValuesFromChecks(checks);
|
|
109
|
+
const triggers = await this.getTableTriggers(connection, table.name);
|
|
104
110
|
table.init(cols, indexes, checks, pks, fks, enums);
|
|
111
|
+
table.setTriggers(triggers);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
createTable(table, alter) {
|
|
@@ -140,6 +147,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
140
147
|
for (const index of table.getIndexes()) {
|
|
141
148
|
this.append(ret, this.createIndex(index, table));
|
|
142
149
|
}
|
|
150
|
+
for (const trigger of table.getTriggers()) {
|
|
151
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
152
|
+
}
|
|
143
153
|
return ret;
|
|
144
154
|
}
|
|
145
155
|
createTableColumn(column, table, _changedProperties) {
|
|
@@ -159,6 +169,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
159
169
|
col.push(`check (${checks[check].expression})`);
|
|
160
170
|
checks.splice(check, 1);
|
|
161
171
|
}
|
|
172
|
+
Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
|
|
162
173
|
Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
|
|
163
174
|
Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
|
|
164
175
|
Utils.runIfNotEmpty(() => col.push('primary key'), column.primary);
|
|
@@ -211,10 +222,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
211
222
|
if (index.columnNames.some(column => column.includes('.'))) {
|
|
212
223
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
213
224
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
214
|
-
return `${sqlPrefix} (${columns.join(', ')})`;
|
|
225
|
+
return `${sqlPrefix} (${columns.join(', ')})${this.getIndexWhereClause(index)}`;
|
|
215
226
|
}
|
|
216
227
|
// Use getIndexColumns to support advanced options like sort order and collation
|
|
217
|
-
return `${sqlPrefix} (${this.getIndexColumns(index)})`;
|
|
228
|
+
return `${sqlPrefix} (${this.getIndexColumns(index)})${this.getIndexWhereClause(index)}`;
|
|
218
229
|
}
|
|
219
230
|
parseTableDefinition(sql, cols) {
|
|
220
231
|
const columns = {};
|
|
@@ -285,6 +296,17 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
285
296
|
generated = `${match[2]} ${storage}`;
|
|
286
297
|
}
|
|
287
298
|
}
|
|
299
|
+
// Strip string literals first (their contents could contain unbalanced parens), then
|
|
300
|
+
// repeatedly strip the innermost balanced `(...)` until none remain — a single pass would
|
|
301
|
+
// only remove the innermost level, leaving `collate` tokens inside nested CHECK/default
|
|
302
|
+
// expressions exposed to the column-collation regex.
|
|
303
|
+
let cleanDef = (columnDefinitions[col.name]?.definition ?? '').replace(/'[^']*'/g, '').replace(/"[^"]*"/g, '');
|
|
304
|
+
let prev;
|
|
305
|
+
do {
|
|
306
|
+
prev = cleanDef;
|
|
307
|
+
cleanDef = cleanDef.replace(/\([^()]*\)/g, '');
|
|
308
|
+
} while (cleanDef !== prev);
|
|
309
|
+
const collationMatch = /\bcollate\s+([`"']?)([\w\-.]+)\1/i.exec(cleanDef);
|
|
288
310
|
return {
|
|
289
311
|
name: col.name,
|
|
290
312
|
type: col.type,
|
|
@@ -295,6 +317,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
295
317
|
unsigned: false,
|
|
296
318
|
autoincrement: !composite && col.pk && this.platform.isNumericColumn(mappedType) && hasAutoincrement,
|
|
297
319
|
generated,
|
|
320
|
+
collation: collationMatch ? collationMatch[2] : undefined,
|
|
298
321
|
};
|
|
299
322
|
});
|
|
300
323
|
}
|
|
@@ -321,23 +344,23 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
321
344
|
// everything else is an expression that had its outer parens stripped
|
|
322
345
|
return `(${value})`;
|
|
323
346
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// check constraints are defined as (note that last closing paren is missing):
|
|
331
|
-
// `type` text check (`type` in ('local', 'global')
|
|
332
|
-
const match = /[`["']([^`\]"']+)[`\]"'] text check \(.* \((.*)\)/i.exec(item);
|
|
333
|
-
/* v8 ignore next */
|
|
334
|
-
if (match) {
|
|
335
|
-
o[match[1]] = match[2]
|
|
336
|
-
.split(/,(?=\s*'(?:[^']|'')*'(?:\s*\)|$))/)
|
|
337
|
-
.map((item) => /^\(?'((?:[^']|'')*)'/.exec(item.trim())[1].replace(/''/g, "'"));
|
|
347
|
+
/** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
|
|
348
|
+
extractEnumValuesFromChecks(checks) {
|
|
349
|
+
const result = {};
|
|
350
|
+
for (const check of checks) {
|
|
351
|
+
if (!check.columnName || typeof check.expression !== 'string') {
|
|
352
|
+
continue;
|
|
338
353
|
}
|
|
339
|
-
|
|
340
|
-
|
|
354
|
+
const inClause = /\bin\s*\(([^)]*)\)/i.exec(check.expression);
|
|
355
|
+
if (!inClause) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const items = [...inClause[1].matchAll(/'((?:[^']|'')*)'/g)].map(m => m[1].replace(/''/g, "'"));
|
|
359
|
+
if (items.length > 0) {
|
|
360
|
+
result[check.columnName] = items;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
341
364
|
}
|
|
342
365
|
async getPrimaryKeys(connection, indexes, tableName, schemaName, ctx) {
|
|
343
366
|
const prefix = this.getSchemaPrefix(schemaName);
|
|
@@ -350,6 +373,16 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
350
373
|
const sql = `pragma ${prefix}table_info(\`${tableName}\`)`;
|
|
351
374
|
const cols = await connection.execute(sql, [], 'all', ctx);
|
|
352
375
|
const indexes = await connection.execute(`pragma ${prefix}index_list(\`${tableName}\`)`, [], 'all', ctx);
|
|
376
|
+
// sqlite_master.sql holds the original CREATE INDEX statement — the only place a partial
|
|
377
|
+
// index's WHERE predicate is preserved (PRAGMA index_* don't expose it).
|
|
378
|
+
const indexSqls = await connection.execute(`select name, sql from ${prefix}sqlite_master where type = 'index' and tbl_name = ?`, [tableName], 'all', ctx);
|
|
379
|
+
const wherePredicates = new Map();
|
|
380
|
+
for (const row of indexSqls) {
|
|
381
|
+
const match = row.sql && SqliteSchemaHelper.PARTIAL_WHERE_RE.exec(row.sql);
|
|
382
|
+
if (match) {
|
|
383
|
+
wherePredicates.set(row.name, match[1].trim());
|
|
384
|
+
}
|
|
385
|
+
}
|
|
353
386
|
const ret = [];
|
|
354
387
|
for (const col of cols.filter(c => c.pk)) {
|
|
355
388
|
ret.push({
|
|
@@ -362,12 +395,14 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
362
395
|
}
|
|
363
396
|
for (const index of indexes.filter(index => !this.isImplicitIndex(index.name))) {
|
|
364
397
|
const res = await connection.execute(`pragma ${prefix}index_info(\`${index.name}\`)`, [], 'all', ctx);
|
|
398
|
+
const where = wherePredicates.get(index.name);
|
|
365
399
|
ret.push(...res.map(row => ({
|
|
366
400
|
columnNames: [row.name],
|
|
367
401
|
keyName: index.name,
|
|
368
402
|
unique: !!index.unique,
|
|
369
403
|
constraint: !!index.unique,
|
|
370
404
|
primary: false,
|
|
405
|
+
...(where ? { where } : {}),
|
|
371
406
|
})));
|
|
372
407
|
}
|
|
373
408
|
return this.mapIndexes(ret);
|
|
@@ -501,8 +536,102 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
501
536
|
this.append(ret, this.getRenameIndexSQL(diff.name, index, oldIndexName));
|
|
502
537
|
}
|
|
503
538
|
}
|
|
539
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
540
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
541
|
+
}
|
|
542
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
543
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
544
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
545
|
+
}
|
|
546
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
547
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
548
|
+
}
|
|
504
549
|
return ret;
|
|
505
550
|
}
|
|
551
|
+
/** Generates SQL to create SQLite triggers. SQLite requires one trigger per event. */
|
|
552
|
+
createTrigger(table, trigger) {
|
|
553
|
+
if (trigger.expression) {
|
|
554
|
+
return trigger.expression;
|
|
555
|
+
}
|
|
556
|
+
const timing = trigger.timing.toUpperCase();
|
|
557
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
558
|
+
const ret = [];
|
|
559
|
+
for (const event of trigger.events) {
|
|
560
|
+
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
561
|
+
const when = trigger.when ? `\n when ${trigger.when}` : '';
|
|
562
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`);
|
|
563
|
+
}
|
|
564
|
+
return ret.join(';\n');
|
|
565
|
+
}
|
|
566
|
+
async getTableTriggers(connection, tableName) {
|
|
567
|
+
const rows = await connection.execute(`select name, sql from sqlite_master where type = 'trigger' and tbl_name = ?`, [tableName]);
|
|
568
|
+
// First pass: parse all triggers and collect names to detect multi-event groups
|
|
569
|
+
const parsedRows = [];
|
|
570
|
+
for (const row of rows) {
|
|
571
|
+
/* v8 ignore next 3 */
|
|
572
|
+
if (!row.sql) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const parsed = this.parseTriggerDDL(row.sql, row.name);
|
|
576
|
+
if (parsed) {
|
|
577
|
+
parsedRows.push({ name: row.name, parsed });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const allNames = parsedRows.map(r => r.name);
|
|
581
|
+
const triggers = [];
|
|
582
|
+
const triggerMap = new Map();
|
|
583
|
+
for (const { name, parsed } of parsedRows) {
|
|
584
|
+
// Only strip event suffix when another trigger with the same base exists
|
|
585
|
+
const eventLower = parsed.events[0];
|
|
586
|
+
const candidateBase = name.endsWith(`_${eventLower}`) ? name.slice(0, -eventLower.length - 1) : null;
|
|
587
|
+
const baseName = candidateBase && allNames.some(n => n !== name && n.startsWith(`${candidateBase}_`)) ? candidateBase : name;
|
|
588
|
+
if (triggerMap.has(baseName)) {
|
|
589
|
+
const existing = triggerMap.get(baseName);
|
|
590
|
+
if (!existing.events.includes(parsed.events[0])) {
|
|
591
|
+
existing.events.push(parsed.events[0]);
|
|
592
|
+
}
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const trigger = { ...parsed, name: baseName };
|
|
596
|
+
triggers.push(trigger);
|
|
597
|
+
triggerMap.set(baseName, trigger);
|
|
598
|
+
}
|
|
599
|
+
return triggers;
|
|
600
|
+
}
|
|
601
|
+
parseTriggerDDL(sql, name) {
|
|
602
|
+
// Split at the last top-level BEGIN to separate header from body,
|
|
603
|
+
// so that a WHEN clause containing the word "begin" in a string literal doesn't confuse parsing.
|
|
604
|
+
const beginIdx = sql.search(/\bbegin\b(?=[^]*$)/i);
|
|
605
|
+
/* v8 ignore next 3 */
|
|
606
|
+
if (beginIdx === -1) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
const header = sql.slice(0, beginIdx);
|
|
610
|
+
const bodyPart = sql.slice(beginIdx);
|
|
611
|
+
const headerMatch = /create\s+trigger\s+["`]?\w+["`]?\s+(before|after|instead\s+of)\s+(insert|update|delete)\s+on\s+["`]?\w+["`]?\s*(?:for\s+each\s+(row|statement))?\s*(?:when\s+([\s\S]*?))?\s*$/i.exec(header);
|
|
612
|
+
/* v8 ignore next 3 */
|
|
613
|
+
if (!headerMatch) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const bodyMatch = /^begin\s+([\s\S]*?)\s*end/i.exec(bodyPart);
|
|
617
|
+
/* v8 ignore next 3 */
|
|
618
|
+
if (!bodyMatch) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
const timing = headerMatch[1].toLowerCase();
|
|
622
|
+
const event = headerMatch[2].toLowerCase();
|
|
623
|
+
const forEach = (headerMatch[3]?.toLowerCase() ?? 'row');
|
|
624
|
+
const when = headerMatch[4]?.trim() || undefined;
|
|
625
|
+
const body = bodyMatch[1].trim().replace(/;\s*$/, '');
|
|
626
|
+
return {
|
|
627
|
+
name,
|
|
628
|
+
timing,
|
|
629
|
+
events: [event],
|
|
630
|
+
forEach,
|
|
631
|
+
body,
|
|
632
|
+
when,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
506
635
|
getAlterTempTableSQL(changedTable) {
|
|
507
636
|
const tempName = `${changedTable.toTable.name}__temp_alter`;
|
|
508
637
|
const quotedName = this.quote(changedTable.toTable.name);
|