🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@mikro-orm/sql

Package Overview
Dependencies
Maintainers
1
Versions
774
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mikro-orm/sql - npm Package Compare versions

Comparing version
7.2.0-dev.3
to
7.2.0-dev.4
+9
-0
AbstractSqlConnection.d.ts

@@ -59,2 +59,11 @@ import { type ControlledTransaction, type Dialect, Kysely } from 'kysely';

rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
/**
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
* on that connection instead of going through its connection provider, so a rollback caused by an
* aborted query would otherwise be sent while the aborted query is still running. That not only
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
* statement is then discarded as stale and the abort never reaches the database.
*/
private waitForIdleTransaction;
private prepareQuery;

@@ -61,0 +70,0 @@ /** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */

+23
-13

@@ -113,3 +113,11 @@ import { CompiledQuery, Kysely } from 'kysely';

catch (error) {
await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
// A failing rollback must not mask why the transaction failed in the first place — the
// `'kill session'` abort strategy tears the connection down, so the rollback that follows can
// only ever report the dead connection.
try {
await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
}
catch (rollbackError) {
this.logger.warn('query', `Failed to roll back transaction: ${rollbackError.message}`);
}
throw error;

@@ -142,15 +150,5 @@ }

const trx = await trxBuilder.execute();
if (options.ctx) {
const ctx = options.ctx;
ctx.index ??= 0;
const savepointName = `trx${ctx.index + 1}`;
Reflect.defineProperty(trx, 'index', { value: ctx.index + 1 });
Reflect.defineProperty(trx, 'savepointName', { value: savepointName });
this.logQuery(this.platform.getSavepointSQL(savepointName), options.loggerContext);
for (const query of this.platform.getBeginTransactionSQL(options)) {
this.logQuery(query, options.loggerContext);
}
else {
for (const query of this.platform.getBeginTransactionSQL(options)) {
this.logQuery(query, options.loggerContext);
}
}
await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);

@@ -178,2 +176,3 @@ return trx;

await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionRollback, ctx);
await this.waitForIdleTransaction(ctx);
if ('savepointName' in ctx) {

@@ -189,2 +188,13 @@ await ctx.rollbackToSavepoint(ctx.savepointName).execute();

}
/**
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
* on that connection instead of going through its connection provider, so a rollback caused by an
* aborted query would otherwise be sent while the aborted query is still running. That not only
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
* statement is then discarded as stale and the abort never reaches the database.
*/
async waitForIdleTransaction(ctx) {
await ctx.getExecutor().provideConnection(async () => undefined);
}
prepareQuery(query, params = []) {

@@ -191,0 +201,0 @@ if (query instanceof NativeQueryBuilder) {

@@ -171,3 +171,3 @@ import { isRaw, LockMode, QueryFlag, Utils } from '@mikro-orm/core';

}
if (this.options.having) {
if (this.options.having?.sql.trim()) {
this.parts.push(`having ${this.options.having.sql}`);

@@ -174,0 +174,0 @@ this.params.push(...this.options.having.params);

@@ -56,2 +56,3 @@ import { type Dictionary, type Transaction, type Type } from '@mikro-orm/core';

getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
protected hasInlineColumnComment(): boolean;
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;

@@ -58,0 +59,0 @@ alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];

@@ -325,3 +325,3 @@ import { EnumType, StringType, TextType } from '@mikro-orm/core';

const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${trigger.body}; end`);
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${this.normalizeTriggerBody(trigger.body)} end`);
}

@@ -544,2 +544,5 @@ return ret.join(';\n');

}
hasInlineColumnComment() {
return true;
}
getChangeColumnCommentSQL(tableName, to, schemaName) {

@@ -546,0 +549,0 @@ tableName = this.quote(tableName);

@@ -237,3 +237,3 @@ import { isRaw, raw, Utils } from '@mikro-orm/core';

}
if (this.options.having) {
if (this.options.having?.sql.trim()) {
this.parts.push(`having ${this.options.having.sql}`);

@@ -240,0 +240,0 @@ this.params.push(...this.options.having.params);

@@ -495,3 +495,3 @@ import { DeferMode, EnumType, Type, Utils, } from '@mikro-orm/core';

const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
const def = m ? m[1].replace(/\((.*?)\)::\w+/g, '$1') : single ? single[1] : check.expression;
const def = m ? m[1].replace(/\(([^()]*)\)::\w+/g, '$1') : single ? single[1] : check.expression;
ret[key].push({

@@ -517,3 +517,3 @@ name: check.name,

const triggerName = this.platform.quoteIdentifier(trigger.name);
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${trigger.body}; end; $$ language plpgsql`;
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${this.normalizeTriggerBody(trigger.body)} end; $$ language plpgsql`;
const triggerSql = `create trigger ${triggerName} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} execute function ${fnName}()`;

@@ -520,0 +520,0 @@ return `${fnSql};\n${triggerSql}`;

@@ -10,2 +10,5 @@ import { type Dialect } from 'kysely';

protected attachDatabases(): Promise<void>;
/** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
protected getConnectionSetupSql(): Promise<string[]>;
private getAttachDatabasesSql;
}
import { CompiledQuery } from 'kysely';
import { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
const FOREIGN_KEYS_PRAGMA = 'pragma foreign_keys = on';
export class BaseSqliteConnection extends AbstractSqlConnection {

@@ -10,17 +11,26 @@ createKyselyDialect(options) {

await super.connect(options);
await this.getClient().executeQuery(CompiledQuery.raw('pragma foreign_keys = on'));
await this.getClient().executeQuery(CompiledQuery.raw(FOREIGN_KEYS_PRAGMA));
await this.attachDatabases();
}
async attachDatabases() {
for (const sql of await this.getAttachDatabasesSql()) {
await this.execute(sql);
}
}
/** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
async getConnectionSetupSql() {
return [FOREIGN_KEYS_PRAGMA, ...(await this.getAttachDatabasesSql())];
}
async getAttachDatabasesSql() {
const attachDatabases = this.config.get('attachDatabases');
if (!attachDatabases?.length) {
return;
return [];
}
const { fs } = await import('@mikro-orm/core/fs-utils');
const baseDir = this.config.get('baseDir');
for (const db of attachDatabases) {
return attachDatabases.map(db => {
const path = fs.absolutePath(db.path, baseDir);
await this.execute(`attach database '${path}' as ${this.platform.quoteIdentifier(db.name)}`);
}
return `attach database '${path}' as ${this.platform.quoteIdentifier(db.name)}`;
});
}
}

@@ -564,3 +564,3 @@ import { Utils } from '@mikro-orm/core';

const when = trigger.when ? `\n when ${trigger.when}` : '';
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`);
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`);
}

@@ -567,0 +567,0 @@ return ret.join(';\n');

{
"name": "@mikro-orm/sql",
"version": "7.2.0-dev.3",
"version": "7.2.0-dev.4",
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",

@@ -53,6 +53,6 @@ "keywords": [

"devDependencies": {
"@mikro-orm/core": "^7.1.7"
"@mikro-orm/core": "^7.1.11"
},
"peerDependencies": {
"@mikro-orm/core": "7.2.0-dev.3"
"@mikro-orm/core": "7.2.0-dev.4"
},

@@ -59,0 +59,0 @@ "engines": {

@@ -311,3 +311,3 @@ import { isRaw, LockMode, raw, Utils, } from '@mikro-orm/core';

}
if (this.options.having) {
if (this.options.having?.sql.trim()) {
this.parts.push(`having ${this.options.having.sql}`);

@@ -314,0 +314,0 @@ this.params.push(...this.options.having.params);

@@ -417,3 +417,4 @@ import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, JsonType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';

const res = this._appendQueryCondition(type, cond[k]);
parts.push(`not (${res.sql})`);
// negating a vacuously true condition (e.g. an empty `$and`) matches nothing
parts.push(res.sql ? `not (${res.sql})` : '1 = 0');
res.params.forEach(p => params.push(p));

@@ -789,2 +790,6 @@ continue;

const params = [];
// an empty disjunction is false, same as `$in: []`, while an empty conjunction is vacuously true
if (operator === '$or' && subCondition.length === 0) {
return { sql: '1 = 0', params };
}
// single sub-condition can be ignored to reduce nesting of parens

@@ -791,0 +796,0 @@ if (subCondition.length === 1 || operator === '$and') {

@@ -52,2 +52,4 @@ import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type IndexCallback, type NamingStrategy } from '@mikro-orm/core';

private findFkIndex;
/** Advanced options require an entity-level declaration, as the property-level `index`/`unique` cannot carry them. */
private hasAdvancedIndexOptions;
private getIndexProperties;

@@ -54,0 +56,0 @@ private getSafeBaseNameForFkProp;

@@ -222,2 +222,4 @@ import { DecimalType, EntitySchema, isRaw, ReferenceKind, t, Type, UnknownType, Utils, } from '@mikro-orm/core';

index.expression ||
index.where ||
this.hasAdvancedIndexOptions(index) ||
!(index.columnNames[0] in columnFks)) && // Trivial non-composite indexes for scalar props are to be mapped to the column.

@@ -258,10 +260,3 @@ // ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)

// An index is trivial if it has no special options that require entity-level declaration
const hasAdvancedOptions = index.columns?.length ||
index.include?.length ||
index.fillFactor ||
index.type ||
index.invisible ||
index.disabled ||
index.clustered;
const isTrivial = !index.deferMode && !index.expression && !index.where && !hasAdvancedOptions;
const isTrivial = !index.deferMode && !index.expression && !index.where && !this.hasAdvancedIndexOptions(index);
if (isTrivial) {

@@ -304,2 +299,15 @@ // Index is for FK. Map to the FK prop and move on.

}
for (const check of this.getChecks()) {
// skip checks that were consumed by enum conversion — the enum property recreates an
// equivalent check under the conventional name during discovery (only on platforms that
// emulate enums via check constraints; mysql/mariadb enums are native and recreate nothing)
const enumItems = check.columnName ? this.getColumn(check.columnName)?.enumItems : undefined;
if (this.#platform.usesEnumCheckConstraints() &&
enumItems?.length &&
(check.expression === this.#platform.getEnumCheckConstraintExpression(check.columnName, enumItems) ||
check.name === this.#platform.getIndexName(this.name, [check.columnName], 'check'))) {
continue;
}
schema.meta.checks.push({ name: check.name, expression: check.expression });
}
const addedStandaloneFkPropsBasedOnColumn = new Set();

@@ -513,3 +521,5 @@ const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));

const possibleIndexes = this.#indexes.filter(index => {
return (index.columnNames.length === fkColumnsLength &&
return (!index.where &&
!this.hasAdvancedIndexOptions(index) &&
index.columnNames.length === fkColumnsLength &&
!currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName));

@@ -528,4 +538,14 @@ });

}
/** Advanced options require an entity-level declaration, as the property-level `index`/`unique` cannot carry them. */
hasAdvancedIndexOptions(index) {
return !!(index.columns?.length ||
index.include?.length ||
index.fillFactor ||
index.type ||
index.invisible ||
index.disabled ||
index.clustered);
}
getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
const propBaseNames = new Set();
const propBaseNames = new Map();
const columnNames = index.columnNames;

@@ -536,2 +556,10 @@ const l = columnNames.length;

}
const addPropBaseName = (baseName, position) => {
const positions = propBaseNames.get(baseName);
if (positions) {
positions.last = position;
return;
}
propBaseNames.set(baseName, { first: position, last: position });
};
for (let i = 0; i < l; ++i) {

@@ -547,3 +575,3 @@ const columnName = columnNames[i];

// Add it and move on.
propBaseNames.add(columnName);
addPropBaseName(columnName, i);
continue;

@@ -555,3 +583,3 @@ }

if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
propBaseNames.add(columnName);
addPropBaseName(columnName, i);
continue;

@@ -568,3 +596,3 @@ }

if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
propBaseNames.add(propName);
addPropBaseName(propName, i);
propAdded = true;

@@ -580,3 +608,6 @@ }

}
return Array.from(propBaseNames).map(baseName => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
// Props sharing their first column would otherwise follow FK discovery order, so break ties on the last one.
return Array.from(propBaseNames)
.sort(([, a], [, b]) => a.first - b.first || a.last - b.last)
.map(([baseName]) => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
}

@@ -707,5 +738,15 @@ getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {

const index = compositeFkIndexes[prop] ||
this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && !idx.unique && !idx.primary);
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
!idx.composite &&
!idx.unique &&
!idx.primary &&
!idx.where &&
!this.hasAdvancedIndexOptions(idx));
const unique = compositeFkUniques[prop] ||
this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && idx.unique && !idx.primary);
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
!idx.composite &&
idx.unique &&
!idx.primary &&
!idx.where &&
!this.hasAdvancedIndexOptions(idx));
const kind = this.getReferenceKind(fk, unique);

@@ -712,0 +753,0 @@ const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);

@@ -900,5 +900,9 @@ import { ArrayType, BooleanType, DateTimeType, DecimalType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core';

.replace(/!=/g, '<>')
.replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
// `\b` keeps this from firing inside identifiers like `min(...)`
.replace(/\bin\s*\((.*?)\)/gi, '= any (array[$1])')
// MySQL normalizes count(*) to count(0)
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
// multi word type names in casts, the generic `::\w+` below only covers single word ones
// the precision is kept, so `timestamptz(3)` and `timestamp(3) with time zone` leave the same residue
.replace(/::\s*(?:character\s+varying|bit\s+varying|double\s+precision|(?:timestamp|time)\b(\s*\(\d+\))?(?:\s+with(?:out)?\s+time\s+zone)?)/gi, '$1')
// Remove quotes first so we can process identifiers

@@ -911,2 +915,6 @@ .replace(/['"`]/g, '')

.replace(/\binner\s+join\b/gi, 'join')
// PostgreSQL names an unaliased bare function call after the function itself,
// so `max(created_at)` comes back as `max(created_at) AS max`
// the lookahead skips table function column alias lists like `unnest(a) AS unnest(c)`, which are meaningful
.replace(/\b(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)\s+as\s+\1\b(?!\s*\()/gi, '$1($2)')
// Remove redundant column aliases like `title AS title` -> `title`

@@ -917,3 +925,5 @@ .replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')

// Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
.replace(/[()\n[\]*]|::\w+| +/g, '')
// tabs and CRs included — the schema generator trims every line before executing the DDL,
// so indentation and CRLF endings can never come back from introspection
.replace(/[()\n\r\t[\]*]|::\w+| +/g, '')
.replace(/anyarray\[(.*)]/gi, '$1')

@@ -920,0 +930,0 @@ .toLowerCase()

@@ -7,3 +7,9 @@ import { type Connection, type Dictionary, type Options, type Transaction, type RawQueryFragment } from '@mikro-orm/core';

import type { DatabaseTable } from './DatabaseTable.js';
/** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
/**
* Flattens `;\n` boundaries and drops blank lines so the schema-generator's statement splitter
* doesn't break the routine or trigger DDL apart — it treats both as statement/group separators.
* Blank lines go first, otherwise a `;` followed by one would keep its newline. Like
* `normalizeViewDefinition`, this is not string-literal aware, so a blank line inside a multi-line
* literal is dropped too. Other whitespace is preserved.
*/
export declare function stripStatementNewlines(body: string): string;

@@ -122,2 +128,4 @@ /**

hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
/** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
protected getPrimaryKeyConstraintPrefix(table: DatabaseTable, index: IndexDef): string;
castColumn(name: string, type: string): string;

@@ -142,2 +150,4 @@ alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];

getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
protected hasInlineColumnComment(): boolean;
getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;

@@ -182,2 +192,4 @@ protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;

getAllRoutines(_connection: AbstractSqlConnection, _schemas?: string[]): Promise<SqlRoutineDef[]>;
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
protected normalizeTriggerBody(body: string): string;
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */

@@ -184,0 +196,0 @@ protected wrapRoutineBody(body: string): string;

import { isRaw, Utils, } from '@mikro-orm/core';
/** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
/**
* Flattens `;\n` boundaries and drops blank lines so the schema-generator's statement splitter
* doesn't break the routine or trigger DDL apart — it treats both as statement/group separators.
* Blank lines go first, otherwise a `;` followed by one would keep its newline. Like
* `normalizeViewDefinition`, this is not string-literal aware, so a blank line inside a multi-line
* literal is dropped too. Other whitespace is preserved.
*/
export function stripStatementNewlines(body) {
return body.replace(/;[\t ]*\r?\n/g, '; ');
return body
.split('\n')
.filter(line => line.trim() !== '')
.join('\n')
.replace(/;[\t ]*\r?\n/g, '; ');
}

@@ -439,3 +449,4 @@ /**

for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
if (this.hasInlineColumnComment() &&
['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
continue; // will be handled via column update

@@ -493,3 +504,9 @@ }

.join(', ');
return [`alter table ${table.getQuotedName()} ${adds}`];
const ret = [`alter table ${table.getQuotedName()} ${adds}`];
if (!this.hasInlineColumnComment()) {
for (const column of columns.filter(column => column.comment)) {
ret.push(this.getChangeColumnCommentSQL(table.name, column, table.schema));
}
}
return ret;
}

@@ -509,2 +526,6 @@ getDropColumnsSQL(tableName, columns, schemaName) {

}
/** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
getPrimaryKeyConstraintPrefix(table, index) {
return this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(index.keyName)} ` : '';
}
/* v8 ignore next */

@@ -614,2 +635,6 @@ castColumn(name, type) {

}
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
hasInlineColumnComment() {
return false;
}
async getNamespaces(connection, ctx) {

@@ -752,3 +777,3 @@ return [];

if (createPrimary && primaryKey) {
const name = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(primaryKey.keyName)} ` : '';
const name = this.getPrimaryKeyConstraintPrefix(table, primaryKey);
sql += `, ${name}primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`;

@@ -836,3 +861,3 @@ }

if (index.primary) {
const keyName = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${index.keyName} ` : '';
const keyName = this.getPrimaryKeyConstraintPrefix(table, index);
return `alter table ${table.getQuotedName()} add ${keyName}primary key (${columns})${defer}`;

@@ -870,3 +895,3 @@ }

const when = trigger.when ? ` when (${trigger.when})` : '';
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`;
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`;
}

@@ -895,2 +920,7 @@ /**

}
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
normalizeTriggerBody(body) {
const trimmed = stripStatementNewlines(body).trim();
return /;\s*$/.test(trimmed) ? trimmed : `${trimmed};`;
}
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */

@@ -897,0 +927,0 @@ wrapRoutineBody(body) {

@@ -57,2 +57,6 @@ import { type ClearDatabaseOptions, type CreateSchemaOptions, type DropSchemaOptions, type EnsureDatabaseOptions, type EntityMetadata, type ISchemaGenerator, type MikroORM, type Options, type Transaction, type UpdateSchemaOptions } from '@mikro-orm/core';

}): Promise<void>;
/** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
private splitOutsideLiterals;
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
protected startsBatch(_statement: string): boolean;
dropTableIfExists(name: string, schema?: string): Promise<void>;

@@ -59,0 +63,0 @@ private wrapSchema;

@@ -442,7 +442,8 @@ import { CommitOrderCalculator, TableNotFoundException, Utils, } from '@mikro-orm/core';

options.wrap ??= false;
const lines = this.wrapSchema(sql, options).split('\n');
const lines = this.splitOutsideLiterals(this.wrapSchema(sql, options), '\n');
const groups = [];
let i = 0;
for (const line of lines) {
if (line.trim() === '') {
const stmt = line.trim();
if (stmt === '') {
if (groups[i]?.length > 0) {

@@ -453,4 +454,8 @@ i++;

}
// same boundary an empty line creates, for statements that have to start their own batch
if (groups[i]?.length > 0 && this.startsBatch(stmt)) {
i++;
}
groups[i] ??= [];
groups[i].push(line.trim());
groups[i].push(stmt);
}

@@ -468,5 +473,3 @@ if (groups.length === 0) {

const statements = groups.flatMap(group => {
return group
.join('\n')
.split(';\n')
return this.splitOutsideLiterals(group.join('\n'), ';\n')
.map(s => s.trim())

@@ -477,2 +480,27 @@ .filter(s => s);

}
/** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
splitOutsideLiterals(sql, separator) {
const [idOpen, idClose] = this.platform.quoteIdentifier('');
// mysql escapes quotes as `\'`, the other dialects double them, which pairs up on its own
const esc = this.platform.quoteValue(`'`).includes(`\\'`) ? '\\\\.|' : '';
// complete literals, quoted identifiers and `--` comments, so that an apostrophe inside an
// identifier or a comment is not mistaken for one opening a literal
const tokens = new RegExp(`'(?:${esc}[^'])*'|\\${idOpen}[^\\${idClose}]*\\${idClose}|--[^\n]*`, 'g');
const parts = [];
for (const chunk of sql.split(separator)) {
const prev = parts.at(-1);
// whatever quote is left once the complete tokens are gone opened a literal we are still inside of
if (prev?.replace(tokens, '').includes(`'`)) {
parts[parts.length - 1] = prev + separator + chunk;
}
else {
parts.push(chunk);
}
}
return parts;
}
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
startsBatch(_statement) {
return false;
}
async dropTableIfExists(name, schema) {

@@ -479,0 +507,0 @@ const sql = this.helper.dropTableIfExists(name, schema);

@@ -371,5 +371,5 @@ import type { Generated, Kysely } from 'kysely';

} ? Generated<TValue> : TOptions extends {
default: true;
default: unknown;
} ? Generated<TValue> : TOptions extends {
defaultRaw: true;
defaultRaw: unknown;
} ? Generated<TValue> : TProcessOnCreate extends false ? TValue : TOptions extends {

@@ -376,0 +376,0 @@ onCreate: Function;

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display