@mikro-orm/sql
Advanced tools
| import { type ControlledTransaction, type Dialect, Kysely } from 'kysely'; | ||
| import { type AnyEntity, Connection, type Dictionary, type EntityData, type IsolationLevel, type LogContext, type LoggingOptions, type MaybePromise, type QueryResult, RawQueryFragment, type Transaction, type TransactionEventBroadcaster } from '@mikro-orm/core'; | ||
| import type { AbstractSqlPlatform } from './AbstractSqlPlatform.js'; | ||
| import { NativeQueryBuilder } from './query/NativeQueryBuilder.js'; | ||
| export declare abstract class AbstractSqlConnection extends Connection { | ||
| protected platform: AbstractSqlPlatform; | ||
| protected client: Kysely<any>; | ||
| abstract createKyselyDialect(overrides: Dictionary): MaybePromise<Dialect>; | ||
| connect(options?: { | ||
| skipOnConnect?: boolean; | ||
| }): Promise<void>; | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| close(force?: boolean): Promise<void>; | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| isConnected(): Promise<boolean>; | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| checkConnection(): Promise<{ | ||
| ok: true; | ||
| } | { | ||
| ok: false; | ||
| reason: string; | ||
| error?: Error; | ||
| }>; | ||
| getClient<T = any>(): Kysely<T>; | ||
| transactional<T>(cb: (trx: Transaction<ControlledTransaction<any, any>>) => Promise<T>, options?: { | ||
| isolationLevel?: IsolationLevel; | ||
| readOnly?: boolean; | ||
| ctx?: ControlledTransaction<any>; | ||
| eventBroadcaster?: TransactionEventBroadcaster; | ||
| loggerContext?: LogContext; | ||
| }): Promise<T>; | ||
| begin(options?: { | ||
| isolationLevel?: IsolationLevel; | ||
| readOnly?: boolean; | ||
| ctx?: ControlledTransaction<any, any>; | ||
| eventBroadcaster?: TransactionEventBroadcaster; | ||
| loggerContext?: LogContext; | ||
| }): Promise<ControlledTransaction<any, any>>; | ||
| commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>; | ||
| rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>; | ||
| private prepareQuery; | ||
| execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>; | ||
| stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions): AsyncIterableIterator<T>; | ||
| /** | ||
| * Execute raw SQL queries from file | ||
| */ | ||
| loadFile(path: string): Promise<void>; | ||
| private getSql; | ||
| protected transformRawResult<T>(res: any, method?: 'all' | 'get' | 'run'): T; | ||
| } |
| import { CompiledQuery, Kysely } from 'kysely'; | ||
| import { Connection, EventType, RawQueryFragment, Utils, } from '@mikro-orm/core'; | ||
| import { NativeQueryBuilder } from './query/NativeQueryBuilder.js'; | ||
| export class AbstractSqlConnection extends Connection { | ||
| client; | ||
| async connect(options) { | ||
| let driverOptions = this.options.driverOptions ?? this.config.get('driverOptions'); | ||
| if (typeof driverOptions === 'function') { | ||
| driverOptions = await driverOptions(); | ||
| } | ||
| if (driverOptions instanceof Kysely) { | ||
| this.logger.log('info', 'Reusing Kysely client provided via `driverOptions`'); | ||
| this.client = driverOptions; | ||
| } | ||
| else if ('createDriver' in driverOptions) { | ||
| this.logger.log('info', 'Reusing Kysely dialect provided via `driverOptions`'); | ||
| this.client = new Kysely({ dialect: driverOptions }); | ||
| } | ||
| else { | ||
| this.client = new Kysely({ | ||
| dialect: await this.createKyselyDialect(driverOptions), | ||
| }); | ||
| } | ||
| this.connected = true; | ||
| if (options?.skipOnConnect !== true) { | ||
| await this.onConnect(); | ||
| } | ||
| } | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| async close(force) { | ||
| await super.close(force); | ||
| await this.client?.destroy(); | ||
| this.connected = false; | ||
| } | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| async isConnected() { | ||
| const check = await this.checkConnection(); | ||
| return check.ok; | ||
| } | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| async checkConnection() { | ||
| if (!this.connected) { | ||
| return { ok: false, reason: 'Connection not established' }; | ||
| } | ||
| try { | ||
| await this.client.executeQuery(CompiledQuery.raw('select 1')); | ||
| return { ok: true }; | ||
| } | ||
| catch (error) { | ||
| return { ok: false, reason: error.message, error }; | ||
| } | ||
| } | ||
| getClient() { | ||
| return this.client; | ||
| } | ||
| async transactional(cb, options = {}) { | ||
| const trx = await this.begin(options); | ||
| try { | ||
| const ret = await cb(trx); | ||
| await this.commit(trx, options.eventBroadcaster, options.loggerContext); | ||
| return ret; | ||
| } | ||
| catch (error) { | ||
| await this.rollback(trx, options.eventBroadcaster, options.loggerContext); | ||
| throw error; | ||
| } | ||
| } | ||
| async begin(options = {}) { | ||
| if (options.ctx) { | ||
| const ctx = options.ctx; | ||
| await options.eventBroadcaster?.dispatchEvent(EventType.beforeTransactionStart, ctx); | ||
| ctx.index ??= 0; | ||
| const savepointName = `trx${ctx.index + 1}`; | ||
| const trx = await options.ctx.savepoint(savepointName).execute(); | ||
| Reflect.defineProperty(trx, 'index', { value: ctx.index + 1 }); | ||
| Reflect.defineProperty(trx, 'savepointName', { value: savepointName }); | ||
| this.logQuery(this.platform.getSavepointSQL(savepointName), options.loggerContext); | ||
| await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx); | ||
| return trx; | ||
| } | ||
| await this.ensureConnection(); | ||
| await options.eventBroadcaster?.dispatchEvent(EventType.beforeTransactionStart); | ||
| let trxBuilder = this.client.startTransaction(); | ||
| if (options.isolationLevel) { | ||
| trxBuilder = trxBuilder.setIsolationLevel(options.isolationLevel); | ||
| } | ||
| if (options.readOnly) { | ||
| trxBuilder = trxBuilder.setAccessMode('read only'); | ||
| } | ||
| 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); | ||
| } | ||
| else { | ||
| for (const query of this.platform.getBeginTransactionSQL(options)) { | ||
| this.logQuery(query, options.loggerContext); | ||
| } | ||
| } | ||
| await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx); | ||
| return trx; | ||
| } | ||
| async commit(ctx, eventBroadcaster, loggerContext) { | ||
| if (ctx.isRolledBack) { | ||
| return; | ||
| } | ||
| await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionCommit, ctx); | ||
| if ('savepointName' in ctx) { | ||
| await ctx.releaseSavepoint(ctx.savepointName).execute(); | ||
| this.logQuery(this.platform.getReleaseSavepointSQL(ctx.savepointName), loggerContext); | ||
| } | ||
| else { | ||
| await ctx.commit().execute(); | ||
| this.logQuery(this.platform.getCommitTransactionSQL(), loggerContext); | ||
| } | ||
| await eventBroadcaster?.dispatchEvent(EventType.afterTransactionCommit, ctx); | ||
| } | ||
| async rollback(ctx, eventBroadcaster, loggerContext) { | ||
| await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionRollback, ctx); | ||
| if ('savepointName' in ctx) { | ||
| await ctx.rollbackToSavepoint(ctx.savepointName).execute(); | ||
| this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName), loggerContext); | ||
| } | ||
| else { | ||
| await ctx.rollback().execute(); | ||
| this.logQuery(this.platform.getRollbackTransactionSQL(), loggerContext); | ||
| } | ||
| await eventBroadcaster?.dispatchEvent(EventType.afterTransactionRollback, ctx); | ||
| } | ||
| prepareQuery(query, params = []) { | ||
| if (query instanceof NativeQueryBuilder) { | ||
| query = query.toRaw(); | ||
| } | ||
| if (query instanceof RawQueryFragment) { | ||
| params = query.params; | ||
| query = query.sql; | ||
| } | ||
| query = this.config.get('onQuery')(query, params); | ||
| const formatted = this.platform.formatQuery(query, params); | ||
| return { query, params, formatted }; | ||
| } | ||
| async execute(query, params = [], method = 'all', ctx, loggerContext) { | ||
| await this.ensureConnection(); | ||
| const q = this.prepareQuery(query, params); | ||
| const sql = this.getSql(q.query, q.formatted, loggerContext); | ||
| return this.executeQuery(sql, async () => { | ||
| const compiled = CompiledQuery.raw(q.formatted); | ||
| const res = await (ctx ?? this.client).executeQuery(compiled); | ||
| return this.transformRawResult(res, method); | ||
| }, { ...q, ...loggerContext }); | ||
| } | ||
| async *stream(query, params = [], ctx, loggerContext) { | ||
| await this.ensureConnection(); | ||
| const q = this.prepareQuery(query, params); | ||
| const sql = this.getSql(q.query, q.formatted, loggerContext); | ||
| // construct the compiled query manually with `kind: 'SelectQueryNode'` to avoid sqlite validation for select queries when streaming | ||
| const compiled = { | ||
| query: { | ||
| kind: 'SelectQueryNode', | ||
| }, | ||
| sql: q.formatted, | ||
| parameters: [], | ||
| }; | ||
| try { | ||
| const res = (ctx ?? this.client).getExecutor().stream(compiled, 1); | ||
| this.logQuery(sql, { | ||
| sql, params, | ||
| ...loggerContext, | ||
| affected: Utils.isPlainObject(res) ? res.affectedRows : undefined, | ||
| }); | ||
| for await (const items of res) { | ||
| for (const row of this.transformRawResult(items, 'all')) { | ||
| yield row; | ||
| } | ||
| } | ||
| } | ||
| catch (e) { | ||
| this.logQuery(sql, { sql, params, ...loggerContext, level: 'error' }); | ||
| throw e; | ||
| } | ||
| } | ||
| /** | ||
| * Execute raw SQL queries from file | ||
| */ | ||
| async loadFile(path) { | ||
| await this.ensureConnection(); | ||
| const { readFile } = globalThis.process.getBuiltinModule('node:fs/promises'); | ||
| const buf = await readFile(path); | ||
| try { | ||
| const raw = CompiledQuery.raw(buf.toString()); | ||
| await this.client.executeQuery(raw); | ||
| } | ||
| catch (e) { | ||
| /* v8 ignore next */ | ||
| throw this.platform.getExceptionConverter().convertException(e); | ||
| } | ||
| } | ||
| getSql(query, formatted, context) { | ||
| const logger = this.config.getLogger(); | ||
| if (!logger.isEnabled('query', context)) { | ||
| return query; | ||
| } | ||
| if (logger.isEnabled('query-params', context)) { | ||
| return formatted; | ||
| } | ||
| return query; | ||
| } | ||
| transformRawResult(res, method) { | ||
| if (method === 'get') { | ||
| return res.rows[0]; | ||
| } | ||
| if (method === 'all') { | ||
| return res.rows; | ||
| } | ||
| return { | ||
| affectedRows: Number(res.numAffectedRows ?? res.rows.length), | ||
| insertId: res.insertId != null ? Number(res.insertId) : res.insertId, | ||
| row: res.rows[0], | ||
| rows: res.rows, | ||
| }; | ||
| } | ||
| } |
| import { type AnyEntity, type Collection, type Configuration, type ConnectionType, type Constructor, type CountOptions, DatabaseDriver, type DeleteOptions, type Dictionary, type DriverMethodOptions, type EntityData, type EntityDictionary, type EntityField, EntityManagerType, type EntityMetadata, type EntityName, type EntityProperty, type FilterQuery, type FindOneOptions, type FindOptions, type LockOptions, type LoggingOptions, type NativeInsertUpdateManyOptions, type NativeInsertUpdateOptions, type ObjectQuery, type Options, type OrderDefinition, type PopulateOptions, type PopulatePath, type Primary, type QueryOrderMap, type QueryResult, RawQueryFragment, type StreamOptions, type Transaction, type UpsertManyOptions, type UpsertOptions } from '@mikro-orm/core'; | ||
| import type { AbstractSqlConnection } from './AbstractSqlConnection.js'; | ||
| import type { AbstractSqlPlatform } from './AbstractSqlPlatform.js'; | ||
| import { QueryBuilder } from './query/QueryBuilder.js'; | ||
| import { type NativeQueryBuilder } from './query/NativeQueryBuilder.js'; | ||
| import { QueryType } from './query/enums.js'; | ||
| import { SqlEntityManager } from './SqlEntityManager.js'; | ||
| import type { Field } from './typings.js'; | ||
| export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlConnection = AbstractSqlConnection, Platform extends AbstractSqlPlatform = AbstractSqlPlatform> extends DatabaseDriver<Connection> { | ||
| [EntityManagerType]: SqlEntityManager<this>; | ||
| protected readonly connection: Connection; | ||
| protected readonly replicas: Connection[]; | ||
| protected readonly platform: Platform; | ||
| protected constructor(config: Configuration, platform: Platform, connection: Constructor<Connection>, connector: string[]); | ||
| getPlatform(): Platform; | ||
| createEntityManager(useContext?: boolean): this[typeof EntityManagerType]; | ||
| private createQueryBuilderFromOptions; | ||
| find<T extends object, P extends string = never, F extends string = PopulatePath.ALL, E extends string = never>(entityName: string, where: ObjectQuery<T>, options?: FindOptions<T, P, F, E>): Promise<EntityData<T>[]>; | ||
| findOne<T extends object, P extends string = never, F extends string = PopulatePath.ALL, E extends string = never>(entityName: string, where: ObjectQuery<T>, options?: FindOneOptions<T, P, F, E>): Promise<EntityData<T> | null>; | ||
| protected hasToManyJoins<T extends object>(hint: PopulateOptions<T>, meta: EntityMetadata<T>): boolean; | ||
| findVirtual<T extends object>(entityName: string, where: ObjectQuery<T>, options: FindOptions<T, any, any, any>): Promise<EntityData<T>[]>; | ||
| countVirtual<T extends object>(entityName: string, where: ObjectQuery<T>, options: CountOptions<T, any>): Promise<number>; | ||
| protected findFromVirtual<T extends object>(entityName: string, where: ObjectQuery<T>, options: FindOptions<T, any> | CountOptions<T, any>, type: QueryType): Promise<EntityData<T>[] | number>; | ||
| protected streamFromVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any>): AsyncIterableIterator<EntityData<T>>; | ||
| protected wrapVirtualExpressionInSubquery<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any>, type: QueryType): Promise<T[] | number>; | ||
| protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>; | ||
| mapResult<T extends object>(result: EntityData<T>, meta: EntityMetadata<T>, populate?: PopulateOptions<T>[], qb?: QueryBuilder<T, any, any, any>, map?: Dictionary): EntityData<T> | null; | ||
| private mapJoinedProps; | ||
| count<T extends object>(entityName: string, where: any, options?: CountOptions<T>): Promise<number>; | ||
| nativeInsert<T extends object>(entityName: string, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>; | ||
| nativeInsertMany<T extends object>(entityName: string, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>; | ||
| nativeUpdate<T extends object>(entityName: string, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T> & UpsertOptions<T>): Promise<QueryResult<T>>; | ||
| nativeUpdateMany<T extends object>(entityName: string, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T> & UpsertManyOptions<T>): Promise<QueryResult<T>>; | ||
| nativeDelete<T extends object>(entityName: string, where: FilterQuery<T> | string | any, options?: DeleteOptions<T>): Promise<QueryResult<T>>; | ||
| /** | ||
| * Fast comparison for collection snapshots that are represented by PK arrays. | ||
| * Compares scalars via `===` and fallbacks to Utils.equals()` for more complex types like Buffer. | ||
| * Always expects the same length of the arrays, since we only compare PKs of the same entity type. | ||
| */ | ||
| private comparePrimaryKeyArrays; | ||
| syncCollections<T extends object, O extends object>(collections: Iterable<Collection<T, O>>, options?: DriverMethodOptions): Promise<void>; | ||
| loadFromPivotTable<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where?: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>, pivotJoin?: boolean): Promise<Dictionary<T[]>>; | ||
| private getPivotOrderBy; | ||
| execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>; | ||
| stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>; | ||
| /** | ||
| * 1:1 owner side needs to be marked for population so QB auto-joins the owner id | ||
| */ | ||
| protected autoJoinOneToOneOwner<T extends object>(meta: EntityMetadata<T>, populate: PopulateOptions<T>[], fields?: readonly EntityField<T, any>[]): PopulateOptions<T>[]; | ||
| /** | ||
| * @internal | ||
| */ | ||
| joinedProps<T>(meta: EntityMetadata, populate: readonly PopulateOptions<T>[], options?: { | ||
| strategy?: Options['loadStrategy']; | ||
| }): PopulateOptions<T>[]; | ||
| /** | ||
| * @internal | ||
| */ | ||
| mergeJoinedResult<T extends object>(rawResults: EntityData<T>[], meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[]): EntityData<T>[]; | ||
| protected shouldHaveColumn<T, U>(meta: EntityMetadata<T>, prop: EntityProperty<U>, populate: readonly PopulateOptions<U>[], fields?: readonly Field<U>[], exclude?: readonly Field<U>[]): boolean; | ||
| protected getFieldsForJoinedLoad<T extends object>(qb: QueryBuilder<T, any, any, any>, meta: EntityMetadata<T>, options: FieldsForJoinedLoadOptions<T>): Field<T>[]; | ||
| /** | ||
| * @internal | ||
| */ | ||
| mapPropToFieldNames<T extends object>(qb: QueryBuilder<T, any, any, any>, prop: EntityProperty<T>, tableAlias: string, explicitFields?: readonly Field<T>[]): Field<T>[]; | ||
| /** @internal */ | ||
| createQueryBuilder<T extends object>(entityName: EntityName<T> | QueryBuilder<T, any, any, any>, ctx?: Transaction, preferredConnectionType?: ConnectionType, convertCustomTypes?: boolean, loggerContext?: LoggingOptions, alias?: string, em?: SqlEntityManager): QueryBuilder<T, any, any, any>; | ||
| protected resolveConnectionType(args: { | ||
| ctx?: Transaction; | ||
| connectionType?: ConnectionType; | ||
| }): ConnectionType; | ||
| protected extractManyToMany<T>(entityName: string, data: EntityDictionary<T>): EntityData<T>; | ||
| protected processManyToMany<T extends object>(meta: EntityMetadata<T> | undefined, pks: Primary<T>[], collections: EntityData<T>, clear: boolean, options?: DriverMethodOptions): Promise<void>; | ||
| lockPessimistic<T extends object>(entity: T, options: LockOptions): Promise<void>; | ||
| protected buildPopulateWhere<T extends object>(meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'populateWhere'>): ObjectQuery<T>; | ||
| protected buildOrderBy<T extends object>(qb: QueryBuilder<T, any, any, any>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'strategy' | 'orderBy' | 'populateOrderBy'>): QueryOrderMap<T>[]; | ||
| protected buildPopulateOrderBy<T extends object>(qb: QueryBuilder<T, any, any, any>, meta: EntityMetadata<T>, populateOrderBy: QueryOrderMap<T>[], parentPath: string, explicit: boolean, parentAlias?: string): QueryOrderMap<T>[]; | ||
| protected buildJoinedPropsOrderBy<T extends object>(qb: QueryBuilder<T, any, any, any>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[], options?: Pick<FindOptions<any>, 'strategy' | 'orderBy' | 'populateOrderBy'>, parentPath?: string): QueryOrderMap<T>[]; | ||
| protected normalizeFields<T extends object>(fields: Field<T>[], prefix?: string): string[]; | ||
| protected processField<T extends object>(meta: EntityMetadata<T>, prop: EntityProperty<T> | undefined, field: string, ret: Field<T>[]): void; | ||
| protected buildFields<T extends object>(meta: EntityMetadata<T>, populate: PopulateOptions<T>[], joinedProps: PopulateOptions<T>[], qb: QueryBuilder<T, any, any, any>, alias: string, options: Pick<FindOptions<T, any, any, any>, 'strategy' | 'fields' | 'exclude'>): Field<T>[]; | ||
| } | ||
| interface FieldsForJoinedLoadOptions<T extends object> { | ||
| explicitFields?: readonly Field<T>[]; | ||
| exclude?: readonly Field<T>[]; | ||
| populate?: readonly PopulateOptions<T>[]; | ||
| strategy?: Options['loadStrategy']; | ||
| populateWhere?: FindOptions<any>['populateWhere']; | ||
| populateFilter?: FindOptions<any>['populateFilter']; | ||
| parentTableAlias: string; | ||
| parentJoinPath?: string; | ||
| count?: boolean; | ||
| } | ||
| export {}; |
Sorry, the diff of this file is too big to display
| import { type Constructor, type EntityManager, type EntityRepository, type IDatabaseDriver, type IsolationLevel, type MikroORM, Platform } from '@mikro-orm/core'; | ||
| import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js'; | ||
| import { type SchemaHelper } from './schema/SchemaHelper.js'; | ||
| import type { IndexDef } from './typings.js'; | ||
| import { NativeQueryBuilder } from './query/NativeQueryBuilder.js'; | ||
| export declare abstract class AbstractSqlPlatform extends Platform { | ||
| protected readonly schemaHelper?: SchemaHelper; | ||
| usesPivotTable(): boolean; | ||
| indexForeignKeys(): boolean; | ||
| getRepositoryClass<T extends object>(): Constructor<EntityRepository<T>>; | ||
| getSchemaHelper(): SchemaHelper | undefined; | ||
| /** @inheritDoc */ | ||
| lookupExtensions(orm: MikroORM): void; | ||
| getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): SqlSchemaGenerator; | ||
| /** @internal */ | ||
| createNativeQueryBuilder(): NativeQueryBuilder; | ||
| getBeginTransactionSQL(options?: { | ||
| isolationLevel?: IsolationLevel; | ||
| readOnly?: boolean; | ||
| }): string[]; | ||
| getCommitTransactionSQL(): string; | ||
| getRollbackTransactionSQL(): string; | ||
| getSavepointSQL(savepointName: string): string; | ||
| getRollbackToSavepointSQL(savepointName: string): string; | ||
| getReleaseSavepointSQL(savepointName: string): string; | ||
| quoteValue(value: any): string; | ||
| escape(value: any): string; | ||
| getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string; | ||
| getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string; | ||
| getJsonIndexDefinition(index: IndexDef): string[]; | ||
| supportsSchemas(): boolean; | ||
| /** @inheritDoc */ | ||
| generateCustomOrder(escapedColumn: string, values: unknown[]): string; | ||
| /** | ||
| * @internal | ||
| */ | ||
| getOrderByExpression(column: string, direction: string): string[]; | ||
| } |
| import SqlString from 'sqlstring'; | ||
| import { isRaw, JsonProperty, Platform, raw, Utils, } from '@mikro-orm/core'; | ||
| import { SqlEntityRepository } from './SqlEntityRepository.js'; | ||
| import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js'; | ||
| import { NativeQueryBuilder } from './query/NativeQueryBuilder.js'; | ||
| export class AbstractSqlPlatform extends Platform { | ||
| schemaHelper; | ||
| usesPivotTable() { | ||
| return true; | ||
| } | ||
| indexForeignKeys() { | ||
| return true; | ||
| } | ||
| getRepositoryClass() { | ||
| return SqlEntityRepository; | ||
| } | ||
| getSchemaHelper() { | ||
| return this.schemaHelper; | ||
| } | ||
| /** @inheritDoc */ | ||
| lookupExtensions(orm) { | ||
| SqlSchemaGenerator.register(orm); | ||
| } | ||
| /* v8 ignore next: kept for type inference only */ | ||
| getSchemaGenerator(driver, em) { | ||
| return new SqlSchemaGenerator(em ?? driver); | ||
| } | ||
| /** @internal */ | ||
| /* v8 ignore next */ | ||
| createNativeQueryBuilder() { | ||
| return new NativeQueryBuilder(this); | ||
| } | ||
| getBeginTransactionSQL(options) { | ||
| if (options?.isolationLevel) { | ||
| return [`set transaction isolation level ${options.isolationLevel}`, 'begin']; | ||
| } | ||
| return ['begin']; | ||
| } | ||
| getCommitTransactionSQL() { | ||
| return 'commit'; | ||
| } | ||
| getRollbackTransactionSQL() { | ||
| return 'rollback'; | ||
| } | ||
| getSavepointSQL(savepointName) { | ||
| return `savepoint ${this.quoteIdentifier(savepointName)}`; | ||
| } | ||
| getRollbackToSavepointSQL(savepointName) { | ||
| return `rollback to savepoint ${this.quoteIdentifier(savepointName)}`; | ||
| } | ||
| getReleaseSavepointSQL(savepointName) { | ||
| return `release savepoint ${this.quoteIdentifier(savepointName)}`; | ||
| } | ||
| quoteValue(value) { | ||
| if (isRaw(value)) { | ||
| return this.formatQuery(value.sql, value.params); | ||
| } | ||
| if (Utils.isPlainObject(value) || value?.[JsonProperty]) { | ||
| return this.escape(JSON.stringify(value)); | ||
| } | ||
| return this.escape(value); | ||
| } | ||
| escape(value) { | ||
| return SqlString.escape(value, true, this.timezone); | ||
| } | ||
| getSearchJsonPropertySQL(path, type, aliased) { | ||
| return this.getSearchJsonPropertyKey(path.split('->'), type, aliased); | ||
| } | ||
| getSearchJsonPropertyKey(path, type, aliased, value) { | ||
| const [a, ...b] = path; | ||
| const quoteKey = (key) => key.match(/^[a-z]\w*$/i) ? key : `"${key}"`; | ||
| if (aliased) { | ||
| return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, '$.${b.map(quoteKey).join('.')}')`); | ||
| } | ||
| return raw(`json_extract(${this.quoteIdentifier(a)}, '$.${b.map(quoteKey).join('.')}')`); | ||
| } | ||
| getJsonIndexDefinition(index) { | ||
| return index.columnNames | ||
| .map(column => { | ||
| if (!column.includes('.')) { | ||
| return column; | ||
| } | ||
| const [root, ...path] = column.split('.'); | ||
| return `(json_extract(${root}, '$.${path.join('.')}'))`; | ||
| }); | ||
| } | ||
| supportsSchemas() { | ||
| return false; | ||
| } | ||
| /** @inheritDoc */ | ||
| generateCustomOrder(escapedColumn, values) { | ||
| let ret = '(case '; | ||
| values.forEach((v, i) => { | ||
| ret += `when ${escapedColumn} = ${this.quoteValue(v)} then ${i} `; | ||
| }); | ||
| return ret + 'else null end)'; | ||
| } | ||
| /** | ||
| * @internal | ||
| */ | ||
| getOrderByExpression(column, direction) { | ||
| return [`${column} ${direction.toLowerCase()}`]; | ||
| } | ||
| } |
| export * from './mssql/index.js'; | ||
| export * from './mysql/index.js'; | ||
| export * from './postgresql/index.js'; | ||
| export * from './sqlite/index.js'; |
| export * from './mssql/index.js'; | ||
| export * from './mysql/index.js'; | ||
| export * from './postgresql/index.js'; | ||
| export * from './sqlite/index.js'; |
| export * from './MsSqlNativeQueryBuilder.js'; |
| export * from './MsSqlNativeQueryBuilder.js'; |
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| /** @internal */ | ||
| export declare class MsSqlNativeQueryBuilder extends NativeQueryBuilder { | ||
| compile(): { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| protected compileInsert(): void; | ||
| private appendOutputTable; | ||
| private compileUpsert; | ||
| protected compileSelect(): void; | ||
| protected addLockClause(): void; | ||
| protected compileTruncate(): void; | ||
| } |
| import { LockMode, QueryFlag, RawQueryFragment, Utils } from '@mikro-orm/core'; | ||
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| import { QueryType } from '../../query/enums.js'; | ||
| /** @internal */ | ||
| export class MsSqlNativeQueryBuilder extends NativeQueryBuilder { | ||
| compile() { | ||
| if (!this.type) { | ||
| throw new Error('No query type provided'); | ||
| } | ||
| this.parts.length = 0; | ||
| this.params.length = 0; | ||
| if (this.options.flags?.has(QueryFlag.IDENTITY_INSERT)) { | ||
| this.parts.push(`set identity_insert ${this.getTableName()} on;`); | ||
| } | ||
| const { prefix, suffix } = this.appendOutputTable(); | ||
| if (prefix) { | ||
| this.parts.push(prefix); | ||
| } | ||
| if (this.options.comment) { | ||
| this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`)); | ||
| } | ||
| if (this.options.onConflict && !Utils.isEmpty(Utils.asArray(this.options.data)[0])) { | ||
| this.compileUpsert(); | ||
| } | ||
| else { | ||
| switch (this.type) { | ||
| case QueryType.SELECT: | ||
| case QueryType.COUNT: | ||
| this.compileSelect(); | ||
| break; | ||
| case QueryType.INSERT: | ||
| this.compileInsert(); | ||
| break; | ||
| case QueryType.UPDATE: | ||
| this.compileUpdate(); | ||
| break; | ||
| case QueryType.DELETE: | ||
| this.compileDelete(); | ||
| break; | ||
| case QueryType.TRUNCATE: | ||
| this.compileTruncate(); | ||
| break; | ||
| } | ||
| if (suffix) { | ||
| this.parts[this.parts.length - 1] += ';'; | ||
| this.parts.push(suffix); | ||
| } | ||
| else if ([QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE].includes(this.type)) { | ||
| this.parts[this.parts.length - 1] += '; select @@rowcount;'; | ||
| } | ||
| } | ||
| if (this.options.flags?.has(QueryFlag.IDENTITY_INSERT)) { | ||
| this.parts.push(`set identity_insert ${this.getTableName()} off;`); | ||
| } | ||
| return this.combineParts(); | ||
| } | ||
| compileInsert() { | ||
| if (!this.options.data) { | ||
| throw new Error('No data provided'); | ||
| } | ||
| this.parts.push('insert'); | ||
| this.addHintComment(); | ||
| this.parts.push(`into ${this.getTableName()}`); | ||
| if (Object.keys(this.options.data).length === 0) { | ||
| this.addOutputClause('inserted'); | ||
| this.parts.push('default values'); | ||
| return; | ||
| } | ||
| const parts = this.processInsertData(); | ||
| if (this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) { | ||
| this.parts[this.parts.length - 2] += ' into #out '; | ||
| } | ||
| this.parts.push(parts.join(', ')); | ||
| } | ||
| appendOutputTable() { | ||
| if (!this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) { | ||
| return { prefix: '', suffix: '' }; | ||
| } | ||
| const returningFields = this.options.returning; | ||
| const selections = returningFields | ||
| .map(field => `[t].${this.platform.quoteIdentifier(field)}`) | ||
| .join(','); | ||
| return { | ||
| prefix: `select top(0) ${selections} into #out from ${this.getTableName()} as t left join ${this.getTableName()} on 0 = 1;`, | ||
| suffix: `select ${selections} from #out as t; drop table #out`, | ||
| }; | ||
| } | ||
| compileUpsert() { | ||
| const clause = this.options.onConflict; | ||
| const dataAsArray = Utils.asArray(this.options.data); | ||
| const keys = Object.keys(dataAsArray[0]); | ||
| const values = keys.map(() => '?'); | ||
| const parts = []; | ||
| for (const data of dataAsArray) { | ||
| for (const key of keys) { | ||
| this.params.push(data[key]); | ||
| } | ||
| parts.push(`(${values.join(', ')})`); | ||
| } | ||
| this.parts.push(`merge into ${this.getTableName()}`); | ||
| this.parts.push(`using (values ${parts.join(', ')}) as tsource(${keys.map(key => this.quote(key)).join(', ')})`); | ||
| if (clause.fields instanceof RawQueryFragment) { | ||
| this.parts.push(clause.fields.sql); | ||
| this.params.push(...clause.fields.params); | ||
| } | ||
| else if (clause.fields.length > 0) { | ||
| const fields = clause.fields.map(field => { | ||
| const col = this.quote(field); | ||
| return `${this.getTableName()}.${col} = tsource.${col}`; | ||
| }); | ||
| this.parts.push(`on ${fields.join(' and ')}`); | ||
| } | ||
| const sourceColumns = keys.map(field => `tsource.${this.quote(field)}`).join(', '); | ||
| const destinationColumns = keys.map(field => this.quote(field)).join(', '); | ||
| this.parts.push(`when not matched then insert (${destinationColumns}) values (${sourceColumns})`); | ||
| if (!clause.ignore) { | ||
| this.parts.push('when matched'); | ||
| if (clause.where) { | ||
| this.parts.push(`and ${clause.where.sql}`); | ||
| this.params.push(...clause.where.params); | ||
| } | ||
| this.parts.push('then update set'); | ||
| if (!clause.merge || Array.isArray(clause.merge)) { | ||
| const parts = (clause.merge || keys) | ||
| .filter(field => !Array.isArray(clause.fields) || !clause.fields.includes(field)) | ||
| .map((column) => `${this.quote(column)} = tsource.${this.quote(column)}`); | ||
| this.parts.push(parts.join(', ')); | ||
| } | ||
| else if (typeof clause.merge === 'object') { | ||
| const parts = Object.entries(clause.merge).map(([key, value]) => { | ||
| this.params.push(value); | ||
| return `${this.getTableName()}.${this.quote(key)} = ?`; | ||
| }); | ||
| this.parts.push(parts.join(', ')); | ||
| } | ||
| } | ||
| this.addOutputClause('inserted'); | ||
| this.parts[this.parts.length - 1] += ';'; | ||
| } | ||
| compileSelect() { | ||
| this.parts.push('select'); | ||
| if (this.options.limit != null && this.options.offset == null) { | ||
| this.parts.push(`top (?)`); | ||
| this.params.push(this.options.limit); | ||
| } | ||
| this.addHintComment(); | ||
| this.parts.push(`${this.getFields()} from ${this.getTableName()}`); | ||
| this.addLockClause(); | ||
| if (this.options.joins) { | ||
| for (const join of this.options.joins) { | ||
| this.parts.push(join.sql); | ||
| this.params.push(...join.params); | ||
| } | ||
| } | ||
| if (this.options.where?.sql.trim()) { | ||
| this.parts.push(`where ${this.options.where.sql}`); | ||
| this.params.push(...this.options.where.params); | ||
| } | ||
| if (this.options.groupBy) { | ||
| const fields = this.options.groupBy.map(field => this.quote(field)); | ||
| this.parts.push(`group by ${fields.join(', ')}`); | ||
| } | ||
| if (this.options.having) { | ||
| this.parts.push(`having ${this.options.having.sql}`); | ||
| this.params.push(...this.options.having.params); | ||
| } | ||
| if (this.options.orderBy) { | ||
| this.parts.push(`order by ${this.options.orderBy}`); | ||
| } | ||
| if (this.options.offset != null) { | ||
| /* v8 ignore next */ | ||
| if (!this.options.orderBy) { | ||
| throw new Error('Order by clause is required for pagination'); | ||
| } | ||
| this.parts.push(`offset ? rows`); | ||
| this.params.push(this.options.offset); | ||
| if (this.options.limit != null) { | ||
| this.parts.push(`fetch next ? rows only`); | ||
| this.params.push(this.options.limit); | ||
| } | ||
| } | ||
| } | ||
| addLockClause() { | ||
| if (!this.options.lockMode || ![LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_WRITE].includes(this.options.lockMode)) { | ||
| return; | ||
| } | ||
| const map = { | ||
| [LockMode.PESSIMISTIC_READ]: 'with (holdlock)', | ||
| [LockMode.PESSIMISTIC_WRITE]: 'with (updlock)', | ||
| }; | ||
| if (this.options.lockMode !== LockMode.OPTIMISTIC) { | ||
| this.parts.push(map[this.options.lockMode]); | ||
| } | ||
| } | ||
| compileTruncate() { | ||
| const tableName = this.getTableName(); | ||
| const sql = `delete from ${tableName}; declare @count int = case @@rowcount when 0 then 1 else 0 end; dbcc checkident ('${tableName.replace(/[[\]]/g, '')}', reseed, @count)`; | ||
| this.parts.push(sql); | ||
| } | ||
| } |
| export * from './MySqlExceptionConverter.js'; | ||
| export * from './MySqlSchemaHelper.js'; | ||
| export * from './MySqlPlatform.js'; | ||
| export * from './MySqlNativeQueryBuilder.js'; |
| export * from './MySqlExceptionConverter.js'; | ||
| export * from './MySqlSchemaHelper.js'; | ||
| export * from './MySqlPlatform.js'; | ||
| export * from './MySqlNativeQueryBuilder.js'; |
| import { ExceptionConverter, type Dictionary, type DriverException } from '@mikro-orm/core'; | ||
| export declare class MySqlExceptionConverter extends ExceptionConverter { | ||
| /** | ||
| * @see http://dev.mysql.com/doc/refman/5.7/en/error-messages-client.html | ||
| * @see http://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html | ||
| * @see https://github.com/doctrine/dbal/blob/master/src/Driver/AbstractMySQLDriver.php | ||
| */ | ||
| convertException(exception: Error & Dictionary): DriverException; | ||
| } |
| import { DeadlockException, LockWaitTimeoutException, TableExistsException, TableNotFoundException, ForeignKeyConstraintViolationException, UniqueConstraintViolationException, InvalidFieldNameException, NonUniqueFieldNameException, SyntaxErrorException, ConnectionException, NotNullConstraintViolationException, ExceptionConverter, CheckConstraintViolationException, } from '@mikro-orm/core'; | ||
| export class MySqlExceptionConverter extends ExceptionConverter { | ||
| /** | ||
| * @see http://dev.mysql.com/doc/refman/5.7/en/error-messages-client.html | ||
| * @see http://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html | ||
| * @see https://github.com/doctrine/dbal/blob/master/src/Driver/AbstractMySQLDriver.php | ||
| */ | ||
| convertException(exception) { | ||
| /* v8 ignore next */ | ||
| switch (exception.errno) { | ||
| case 1213: | ||
| return new DeadlockException(exception); | ||
| case 1205: | ||
| return new LockWaitTimeoutException(exception); | ||
| case 1050: | ||
| return new TableExistsException(exception); | ||
| case 1051: | ||
| case 1146: | ||
| return new TableNotFoundException(exception); | ||
| case 1216: | ||
| case 1217: | ||
| case 1451: | ||
| case 1452: | ||
| case 1701: | ||
| return new ForeignKeyConstraintViolationException(exception); | ||
| case 3819: | ||
| case 4025: | ||
| return new CheckConstraintViolationException(exception); | ||
| case 1062: | ||
| case 1557: | ||
| case 1569: | ||
| case 1586: | ||
| return new UniqueConstraintViolationException(exception); | ||
| case 1054: | ||
| case 1166: | ||
| case 1611: | ||
| return new InvalidFieldNameException(exception); | ||
| case 1052: | ||
| case 1060: | ||
| case 1110: | ||
| return new NonUniqueFieldNameException(exception); | ||
| case 1064: | ||
| case 1149: | ||
| case 1287: | ||
| case 1341: | ||
| case 1342: | ||
| case 1343: | ||
| case 1344: | ||
| case 1382: | ||
| case 1479: | ||
| case 1541: | ||
| case 1554: | ||
| case 1626: | ||
| return new SyntaxErrorException(exception); | ||
| case 1044: | ||
| case 1045: | ||
| case 1046: | ||
| case 1049: | ||
| case 1095: | ||
| case 1142: | ||
| case 1143: | ||
| case 1227: | ||
| case 1370: | ||
| case 1429: | ||
| case 2002: | ||
| case 2005: | ||
| return new ConnectionException(exception); | ||
| case 1048: | ||
| case 1121: | ||
| case 1138: | ||
| case 1171: | ||
| case 1252: | ||
| case 1263: | ||
| case 1364: | ||
| case 1566: | ||
| return new NotNullConstraintViolationException(exception); | ||
| } | ||
| return super.convertException(exception); | ||
| } | ||
| } |
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| /** @internal */ | ||
| export declare class MySqlNativeQueryBuilder extends NativeQueryBuilder { | ||
| protected compileInsert(): void; | ||
| protected addLockClause(): void; | ||
| protected addOnConflictClause(): void; | ||
| } |
| import { LockMode, RawQueryFragment, Utils } from '@mikro-orm/core'; | ||
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| /** @internal */ | ||
| export class MySqlNativeQueryBuilder extends NativeQueryBuilder { | ||
| compileInsert() { | ||
| if (!this.options.data) { | ||
| throw new Error('No data provided'); | ||
| } | ||
| this.parts.push('insert'); | ||
| if (this.options.onConflict?.ignore) { | ||
| this.parts.push('ignore'); | ||
| } | ||
| this.addHintComment(); | ||
| this.parts.push(`into ${this.getTableName()}`); | ||
| if (Object.keys(this.options.data).length === 0) { | ||
| this.parts.push('default values'); | ||
| return; | ||
| } | ||
| const parts = this.processInsertData(); | ||
| this.parts.push(parts.join(', ')); | ||
| } | ||
| addLockClause() { | ||
| if (!this.options.lockMode) { | ||
| return; | ||
| } | ||
| const map = { | ||
| [LockMode.PESSIMISTIC_READ]: 'lock in share mode', | ||
| [LockMode.PESSIMISTIC_WRITE]: 'for update', | ||
| [LockMode.PESSIMISTIC_PARTIAL_WRITE]: 'for update skip locked', | ||
| [LockMode.PESSIMISTIC_WRITE_OR_FAIL]: 'for update nowait', | ||
| [LockMode.PESSIMISTIC_PARTIAL_READ]: 'lock in share mode skip locked', | ||
| [LockMode.PESSIMISTIC_READ_OR_FAIL]: 'lock in share mode nowait', | ||
| }; | ||
| if (this.options.lockMode !== LockMode.OPTIMISTIC) { | ||
| this.parts.push(map[this.options.lockMode]); | ||
| } | ||
| } | ||
| addOnConflictClause() { | ||
| const clause = this.options.onConflict; | ||
| if (!clause || clause.ignore) { | ||
| return; | ||
| } | ||
| if (clause.merge) { | ||
| this.parts.push('on duplicate key update'); | ||
| if (Utils.isObject(clause.merge)) { | ||
| const fields = Object.keys(clause.merge).map(field => { | ||
| this.params.push(clause.merge[field]); | ||
| return `${this.quote(field)} = ?`; | ||
| }); | ||
| this.parts.push(fields.join(', ')); | ||
| } | ||
| else if (clause.merge.length === 0) { | ||
| const dataAsArray = Utils.asArray(this.options.data); | ||
| const keys = Object.keys(dataAsArray[0]); | ||
| this.parts.push(keys.map(key => `${this.quote(key)} = values(${this.quote(key)})`).join(', ')); | ||
| } | ||
| else { | ||
| const fields = clause.merge.map(key => `${this.quote(key)} = values(${this.quote(key)})`); | ||
| this.parts.push(fields.join(', ')); | ||
| } | ||
| if (clause.where) { | ||
| this.parts.push(`where ${clause.where.sql}`); | ||
| this.params.push(...clause.where.params); | ||
| } | ||
| return; | ||
| } | ||
| this.parts.push('on conflict'); | ||
| if (clause.fields instanceof RawQueryFragment) { | ||
| this.parts.push(clause.fields.sql); | ||
| this.params.push(...clause.fields.params); | ||
| } | ||
| else if (clause.fields.length > 0) { | ||
| const fields = clause.fields.map(field => this.quote(field)); | ||
| this.parts.push(`(${fields.join(', ')})`); | ||
| } | ||
| } | ||
| } |
| import { type SimpleColumnMeta, type Type, type TransformContext, type IsolationLevel } from '@mikro-orm/core'; | ||
| import { MySqlSchemaHelper } from './MySqlSchemaHelper.js'; | ||
| import { MySqlExceptionConverter } from './MySqlExceptionConverter.js'; | ||
| import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js'; | ||
| import type { IndexDef } from '../../typings.js'; | ||
| import { MySqlNativeQueryBuilder } from './MySqlNativeQueryBuilder.js'; | ||
| export declare class MySqlPlatform extends AbstractSqlPlatform { | ||
| protected readonly schemaHelper: MySqlSchemaHelper; | ||
| protected readonly exceptionConverter: MySqlExceptionConverter; | ||
| protected readonly ORDER_BY_NULLS_TRANSLATE: { | ||
| readonly "asc nulls first": "is not null"; | ||
| readonly "asc nulls last": "is null"; | ||
| readonly "desc nulls first": "is not null"; | ||
| readonly "desc nulls last": "is null"; | ||
| }; | ||
| /** @internal */ | ||
| createNativeQueryBuilder(): MySqlNativeQueryBuilder; | ||
| getDefaultCharset(): string; | ||
| getBeginTransactionSQL(options?: { | ||
| isolationLevel?: IsolationLevel; | ||
| readOnly?: boolean; | ||
| }): string[]; | ||
| convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown; | ||
| getJsonIndexDefinition(index: IndexDef): string[]; | ||
| getBooleanTypeDeclarationSQL(): string; | ||
| normalizeColumnType(type: string, options: { | ||
| length?: number; | ||
| precision?: number; | ||
| scale?: number; | ||
| }): string; | ||
| getDefaultMappedType(type: string): Type<unknown>; | ||
| isNumericColumn(mappedType: Type<unknown>): boolean; | ||
| supportsUnsigned(): boolean; | ||
| /** | ||
| * Returns the default name of index for the given columns | ||
| * cannot go past 64 character length for identifiers in MySQL | ||
| */ | ||
| getIndexName(tableName: string, columns: string[], type: 'index' | 'unique' | 'foreign' | 'primary' | 'sequence'): string; | ||
| getDefaultPrimaryName(tableName: string, columns: string[]): string; | ||
| supportsCreatingFullTextIndex(): boolean; | ||
| getFullTextWhereClause(): string; | ||
| getFullTextIndexExpression(indexName: string, schemaName: string | undefined, tableName: string, columns: SimpleColumnMeta[]): string; | ||
| getOrderByExpression(column: string, direction: string): string[]; | ||
| getDefaultClientUrl(): string; | ||
| } |
| import { Utils, QueryOrder, DecimalType, DoubleType, } from '@mikro-orm/core'; | ||
| import { MySqlSchemaHelper } from './MySqlSchemaHelper.js'; | ||
| import { MySqlExceptionConverter } from './MySqlExceptionConverter.js'; | ||
| import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js'; | ||
| import { MySqlNativeQueryBuilder } from './MySqlNativeQueryBuilder.js'; | ||
| export class MySqlPlatform extends AbstractSqlPlatform { | ||
| schemaHelper = new MySqlSchemaHelper(this); | ||
| exceptionConverter = new MySqlExceptionConverter(); | ||
| ORDER_BY_NULLS_TRANSLATE = { | ||
| [QueryOrder.asc_nulls_first]: 'is not null', | ||
| [QueryOrder.asc_nulls_last]: 'is null', | ||
| [QueryOrder.desc_nulls_first]: 'is not null', | ||
| [QueryOrder.desc_nulls_last]: 'is null', | ||
| }; | ||
| /** @internal */ | ||
| createNativeQueryBuilder() { | ||
| return new MySqlNativeQueryBuilder(this); | ||
| } | ||
| getDefaultCharset() { | ||
| return 'utf8mb4'; | ||
| } | ||
| getBeginTransactionSQL(options) { | ||
| if (options?.isolationLevel || options?.readOnly) { | ||
| const parts = []; | ||
| if (options.isolationLevel) { | ||
| parts.push(`isolation level ${options.isolationLevel}`); | ||
| } | ||
| if (options.readOnly) { | ||
| parts.push('read only'); | ||
| } | ||
| const sql = `set transaction ${parts.join(', ')}`; | ||
| return [sql, 'begin']; | ||
| } | ||
| return ['begin']; | ||
| } | ||
| convertJsonToDatabaseValue(value, context) { | ||
| if (context?.mode === 'query') { | ||
| return value; | ||
| } | ||
| return JSON.stringify(value); | ||
| } | ||
| getJsonIndexDefinition(index) { | ||
| return index.columnNames | ||
| .map(column => { | ||
| if (!column.includes('.')) { | ||
| return column; | ||
| } | ||
| const [root, ...path] = column.split('.'); | ||
| return `(json_value(${this.quoteIdentifier(root)}, '$.${path.join('.')}' returning ${index.options?.returning ?? 'char(255)'}))`; | ||
| }); | ||
| } | ||
| getBooleanTypeDeclarationSQL() { | ||
| return 'tinyint(1)'; | ||
| } | ||
| normalizeColumnType(type, options) { | ||
| const simpleType = this.extractSimpleType(type); | ||
| if (['decimal', 'numeric'].includes(simpleType)) { | ||
| return this.getDecimalTypeDeclarationSQL(options); | ||
| } | ||
| return type; | ||
| } | ||
| getDefaultMappedType(type) { | ||
| if (type === 'tinyint(1)') { | ||
| return super.getDefaultMappedType('boolean'); | ||
| } | ||
| return super.getDefaultMappedType(type); | ||
| } | ||
| isNumericColumn(mappedType) { | ||
| return super.isNumericColumn(mappedType) || [DecimalType, DoubleType].some(t => mappedType instanceof t); | ||
| } | ||
| supportsUnsigned() { | ||
| return true; | ||
| } | ||
| /** | ||
| * Returns the default name of index for the given columns | ||
| * cannot go past 64 character length for identifiers in MySQL | ||
| */ | ||
| getIndexName(tableName, columns, type) { | ||
| if (type === 'primary') { | ||
| return this.getDefaultPrimaryName(tableName, columns); | ||
| } | ||
| const indexName = super.getIndexName(tableName, columns, type); | ||
| if (indexName.length > 64) { | ||
| return `${indexName.substring(0, 56 - type.length)}_${Utils.hash(indexName, 5)}_${type}`; | ||
| } | ||
| return indexName; | ||
| } | ||
| getDefaultPrimaryName(tableName, columns) { | ||
| return 'PRIMARY'; // https://dev.mysql.com/doc/refman/8.0/en/create-table.html#create-table-indexes-keys | ||
| } | ||
| supportsCreatingFullTextIndex() { | ||
| return true; | ||
| } | ||
| getFullTextWhereClause() { | ||
| return `match(:column:) against (:query in boolean mode)`; | ||
| } | ||
| getFullTextIndexExpression(indexName, schemaName, tableName, columns) { | ||
| /* v8 ignore next */ | ||
| const quotedTableName = this.quoteIdentifier(schemaName ? `${schemaName}.${tableName}` : tableName); | ||
| const quotedColumnNames = columns.map(c => this.quoteIdentifier(c.name)); | ||
| const quotedIndexName = this.quoteIdentifier(indexName); | ||
| return `alter table ${quotedTableName} add fulltext index ${quotedIndexName}(${quotedColumnNames.join(',')})`; | ||
| } | ||
| getOrderByExpression(column, direction) { | ||
| const ret = []; | ||
| const dir = direction.toLowerCase(); | ||
| if (dir in this.ORDER_BY_NULLS_TRANSLATE) { | ||
| ret.push(`${column} ${this.ORDER_BY_NULLS_TRANSLATE[dir]}`); | ||
| } | ||
| ret.push(`${column} ${dir.replace(/(\s|nulls|first|last)*/gi, '')}`); | ||
| return ret; | ||
| } | ||
| getDefaultClientUrl() { | ||
| return 'mysql://root@127.0.0.1:3306'; | ||
| } | ||
| } |
| import { type Dictionary, type Type } from '@mikro-orm/core'; | ||
| import type { CheckDef, Column, IndexDef, TableDifference, Table, ForeignKey } from '../../typings.js'; | ||
| import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js'; | ||
| import { SchemaHelper } from '../../schema/SchemaHelper.js'; | ||
| import type { DatabaseSchema } from '../../schema/DatabaseSchema.js'; | ||
| import type { DatabaseTable } from '../../schema/DatabaseTable.js'; | ||
| export declare class MySqlSchemaHelper extends SchemaHelper { | ||
| private readonly _cache; | ||
| static readonly DEFAULT_VALUES: { | ||
| 'now()': string[]; | ||
| 'current_timestamp(?)': string[]; | ||
| '0': string[]; | ||
| }; | ||
| getSchemaBeginning(charset: string, disableForeignKeys?: boolean): string; | ||
| disableForeignKeysSQL(): string; | ||
| enableForeignKeysSQL(): string; | ||
| finalizeTable(table: DatabaseTable, charset: string, collate?: string): string; | ||
| getListTablesSQL(): string; | ||
| loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[]): Promise<void>; | ||
| getAllIndexes(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<IndexDef[]>>; | ||
| getCreateIndexSQL(tableName: string, index: IndexDef, partialExpression?: boolean): string; | ||
| getAllColumns(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<Column[]>>; | ||
| getAllChecks(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<CheckDef[]>>; | ||
| getAllForeignKeys(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<Dictionary<ForeignKey>>>; | ||
| getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[]; | ||
| getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column): string; | ||
| getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[]; | ||
| getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string; | ||
| alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[]; | ||
| private getColumnDeclarationSQL; | ||
| getAllEnumDefinitions(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<Dictionary<string[]>>>; | ||
| private supportsCheckConstraints; | ||
| protected getChecksSQL(tables: Table[]): string; | ||
| normalizeDefaultValue(defaultValue: string, length: number): string | number; | ||
| protected wrap(val: string | null | undefined, type: Type<unknown>): string | null | undefined; | ||
| } |
| import { EnumType, StringType, TextType } from '@mikro-orm/core'; | ||
| import { SchemaHelper } from '../../schema/SchemaHelper.js'; | ||
| export class MySqlSchemaHelper extends SchemaHelper { | ||
| _cache = {}; | ||
| static DEFAULT_VALUES = { | ||
| 'now()': ['now()', 'current_timestamp'], | ||
| 'current_timestamp(?)': ['current_timestamp(?)'], | ||
| '0': ['0', 'false'], | ||
| }; | ||
| getSchemaBeginning(charset, disableForeignKeys) { | ||
| if (disableForeignKeys) { | ||
| return `set names ${charset};\n${this.disableForeignKeysSQL()}\n\n`; | ||
| } | ||
| return `set names ${charset};\n\n`; | ||
| } | ||
| disableForeignKeysSQL() { | ||
| return 'set foreign_key_checks = 0;'; | ||
| } | ||
| enableForeignKeysSQL() { | ||
| return 'set foreign_key_checks = 1;'; | ||
| } | ||
| finalizeTable(table, charset, collate) { | ||
| let sql = ` default character set ${charset}`; | ||
| if (collate) { | ||
| sql += ` collate ${collate}`; | ||
| } | ||
| sql += ' engine = InnoDB'; | ||
| if (table.comment) { | ||
| sql += ` comment = ${this.platform.quoteValue(table.comment)}`; | ||
| } | ||
| return sql; | ||
| } | ||
| getListTablesSQL() { | ||
| return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`; | ||
| } | ||
| async loadInformationSchema(schema, connection, tables) { | ||
| if (tables.length === 0) { | ||
| return; | ||
| } | ||
| const columns = await this.getAllColumns(connection, tables); | ||
| const indexes = await this.getAllIndexes(connection, tables); | ||
| const checks = await this.getAllChecks(connection, tables); | ||
| const fks = await this.getAllForeignKeys(connection, tables); | ||
| const enums = await this.getAllEnumDefinitions(connection, tables); | ||
| for (const t of tables) { | ||
| const key = this.getTableKey(t); | ||
| const table = schema.addTable(t.table_name, t.schema_name, t.table_comment); | ||
| const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema); | ||
| table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums[key]); | ||
| } | ||
| } | ||
| async getAllIndexes(connection, tables) { | ||
| const sql = `select table_name as table_name, nullif(table_schema, schema()) as schema_name, index_name as index_name, non_unique as non_unique, column_name as column_name /*!80013 , expression as expression */ | ||
| from information_schema.statistics where table_schema = database() | ||
| and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(', ')}) | ||
| order by schema_name, table_name, index_name, seq_in_index`; | ||
| const allIndexes = await connection.execute(sql); | ||
| const ret = {}; | ||
| for (const index of allIndexes) { | ||
| const key = this.getTableKey(index); | ||
| const indexDef = { | ||
| columnNames: [index.column_name], | ||
| keyName: index.index_name, | ||
| unique: !index.non_unique, | ||
| primary: index.index_name === 'PRIMARY', | ||
| constraint: !index.non_unique, | ||
| }; | ||
| if (!index.column_name || index.expression?.match(/ where /i)) { | ||
| indexDef.expression = index.expression; // required for the `getCreateIndexSQL()` call | ||
| indexDef.expression = this.getCreateIndexSQL(index.table_name, indexDef, !!index.expression); | ||
| } | ||
| ret[key] ??= []; | ||
| ret[key].push(indexDef); | ||
| } | ||
| for (const key of Object.keys(ret)) { | ||
| ret[key] = await this.mapIndexes(ret[key]); | ||
| } | ||
| return ret; | ||
| } | ||
| getCreateIndexSQL(tableName, index, partialExpression = false) { | ||
| /* v8 ignore next */ | ||
| if (index.expression && !partialExpression) { | ||
| return index.expression; | ||
| } | ||
| tableName = this.quote(tableName); | ||
| const keyName = this.quote(index.keyName); | ||
| const sql = `alter table ${tableName} add ${index.unique ? 'unique' : 'index'} ${keyName} `; | ||
| if (index.expression && partialExpression) { | ||
| return `${sql}(${index.expression})`; | ||
| } | ||
| // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them | ||
| if (index.columnNames.some(column => column.includes('.'))) { | ||
| const columns = this.platform.getJsonIndexDefinition(index); | ||
| const sql = `alter table ${tableName} add ${index.unique ? 'unique ' : ''}index ${keyName} `; | ||
| return `${sql}(${columns.join(', ')})`; | ||
| } | ||
| return `${sql}(${index.columnNames.map(c => this.quote(c)).join(', ')})`; | ||
| } | ||
| async getAllColumns(connection, tables) { | ||
| const sql = `select table_name as table_name, | ||
| nullif(table_schema, schema()) as schema_name, | ||
| column_name as column_name, | ||
| column_default as column_default, | ||
| nullif(column_comment, '') as column_comment, | ||
| is_nullable as is_nullable, | ||
| data_type as data_type, | ||
| column_type as column_type, | ||
| column_key as column_key, | ||
| extra as extra, | ||
| generation_expression as generation_expression, | ||
| numeric_precision as numeric_precision, | ||
| numeric_scale as numeric_scale, | ||
| ifnull(datetime_precision, character_maximum_length) length | ||
| from information_schema.columns where table_schema = database() and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name))}) | ||
| order by ordinal_position`; | ||
| const allColumns = await connection.execute(sql); | ||
| const str = (val) => val != null ? '' + val : val; | ||
| const extra = (val) => val.replace(/auto_increment|default_generated|(stored|virtual) generated/i, '').trim() || undefined; | ||
| const ret = {}; | ||
| for (const col of allColumns) { | ||
| const mappedType = this.platform.getMappedType(col.column_type); | ||
| const defaultValue = str(this.normalizeDefaultValue((mappedType.compareAsType() === 'boolean' && ['0', '1'].includes(col.column_default)) | ||
| ? ['false', 'true'][+col.column_default] | ||
| : col.column_default, col.length)); | ||
| const key = this.getTableKey(col); | ||
| const generated = col.generation_expression ? `(${col.generation_expression.replaceAll(`\\'`, `'`)}) ${col.extra.match(/stored generated/i) ? 'stored' : 'virtual'}` : undefined; | ||
| ret[key] ??= []; | ||
| ret[key].push({ | ||
| name: col.column_name, | ||
| type: this.platform.isNumericColumn(mappedType) ? col.column_type.replace(/ unsigned$/, '').replace(/\(\d+\)$/, '') : col.column_type, | ||
| mappedType, | ||
| unsigned: col.column_type.endsWith(' unsigned'), | ||
| length: col.length, | ||
| default: this.wrap(defaultValue, mappedType), | ||
| nullable: col.is_nullable === 'YES', | ||
| primary: col.column_key === 'PRI', | ||
| unique: col.column_key === 'UNI', | ||
| autoincrement: col.extra === 'auto_increment', | ||
| precision: col.numeric_precision, | ||
| scale: col.numeric_scale, | ||
| comment: col.column_comment, | ||
| extra: extra(col.extra), | ||
| generated, | ||
| }); | ||
| } | ||
| return ret; | ||
| } | ||
| async getAllChecks(connection, tables) { | ||
| /* v8 ignore next */ | ||
| if (!(await this.supportsCheckConstraints(connection))) { | ||
| return {}; | ||
| } | ||
| const sql = this.getChecksSQL(tables); | ||
| const allChecks = await connection.execute(sql); | ||
| const ret = {}; | ||
| for (const check of allChecks) { | ||
| const key = this.getTableKey(check); | ||
| ret[key] ??= []; | ||
| ret[key].push({ | ||
| name: check.name, | ||
| columnName: check.column_name, | ||
| definition: `check ${check.expression}`, | ||
| expression: check.expression.replace(/^\((.*)\)$/, '$1'), | ||
| }); | ||
| } | ||
| return ret; | ||
| } | ||
| async getAllForeignKeys(connection, tables) { | ||
| const sql = `select k.constraint_name as constraint_name, nullif(k.table_schema, schema()) as schema_name, k.table_name as table_name, k.column_name as column_name, k.referenced_table_name as referenced_table_name, k.referenced_column_name as referenced_column_name, c.update_rule as update_rule, c.delete_rule as delete_rule | ||
| from information_schema.key_column_usage k | ||
| inner join information_schema.referential_constraints c on c.constraint_name = k.constraint_name and c.table_name = k.table_name | ||
| where k.table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(', ')}) | ||
| and k.table_schema = database() and c.constraint_schema = database() and k.referenced_column_name is not null | ||
| order by constraint_name, k.ordinal_position`; | ||
| const allFks = await connection.execute(sql); | ||
| const ret = {}; | ||
| for (const fk of allFks) { | ||
| const key = this.getTableKey(fk); | ||
| ret[key] ??= []; | ||
| ret[key].push(fk); | ||
| } | ||
| Object.keys(ret).forEach(key => { | ||
| const parts = key.split('.'); | ||
| /* v8 ignore next */ | ||
| const schemaName = parts.length > 1 ? parts[0] : undefined; | ||
| ret[key] = this.mapForeignKeys(ret[key], key, schemaName); | ||
| }); | ||
| return ret; | ||
| } | ||
| getPreAlterTable(tableDiff, safe) { | ||
| // Dropping primary keys requires to unset autoincrement attribute on the particular column first. | ||
| const pk = Object.values(tableDiff.removedIndexes).find(idx => idx.primary); | ||
| if (!pk || safe) { | ||
| return []; | ||
| } | ||
| return pk.columnNames | ||
| .filter(col => tableDiff.fromTable.hasColumn(col)) | ||
| .map(col => tableDiff.fromTable.getColumn(col)) | ||
| .filter(col => col.autoincrement) | ||
| .map(col => `alter table \`${tableDiff.name}\` modify \`${col.name}\` ${this.getColumnDeclarationSQL({ ...col, autoincrement: false })}`); | ||
| } | ||
| getRenameColumnSQL(tableName, oldColumnName, to) { | ||
| tableName = this.quote(tableName); | ||
| oldColumnName = this.quote(oldColumnName); | ||
| const columnName = this.quote(to.name); | ||
| return `alter table ${tableName} change ${oldColumnName} ${columnName} ${this.getColumnDeclarationSQL(to)}`; | ||
| } | ||
| getRenameIndexSQL(tableName, index, oldIndexName) { | ||
| tableName = this.quote(tableName); | ||
| oldIndexName = this.quote(oldIndexName); | ||
| const keyName = this.quote(index.keyName); | ||
| return [`alter table ${tableName} rename index ${oldIndexName} to ${keyName}`]; | ||
| } | ||
| getChangeColumnCommentSQL(tableName, to, schemaName) { | ||
| tableName = this.quote(tableName); | ||
| const columnName = this.quote(to.name); | ||
| return `alter table ${tableName} modify ${columnName} ${this.getColumnDeclarationSQL(to)}`; | ||
| } | ||
| alterTableColumn(column, table, changedProperties) { | ||
| const col = this.createTableColumn(column, table, changedProperties); | ||
| return [`alter table ${table.getQuotedName()} modify ${col}`]; | ||
| } | ||
| getColumnDeclarationSQL(col) { | ||
| let ret = col.type; | ||
| ret += col.unsigned ? ' unsigned' : ''; | ||
| ret += col.autoincrement ? ' auto_increment' : ''; | ||
| ret += ' '; | ||
| ret += col.nullable ? 'null' : 'not null'; | ||
| ret += col.default ? ' default ' + col.default : ''; | ||
| ret += col.comment ? ` comment ${this.platform.quoteValue(col.comment)}` : ''; | ||
| return ret; | ||
| } | ||
| async getAllEnumDefinitions(connection, tables) { | ||
| const sql = `select column_name as column_name, column_type as column_type, table_name as table_name | ||
| from information_schema.columns | ||
| where data_type = 'enum' and table_name in (${tables.map(t => `'${t.table_name}'`).join(', ')}) and table_schema = database()`; | ||
| const enums = await connection.execute(sql); | ||
| return enums.reduce((o, item) => { | ||
| o[item.table_name] ??= {}; | ||
| o[item.table_name][item.column_name] = item.column_type.match(/enum\((.*)\)/)[1].split(',').map((item) => item.match(/'(.*)'/)[1]); | ||
| return o; | ||
| }, {}); | ||
| } | ||
| async supportsCheckConstraints(connection) { | ||
| if (this._cache.supportsCheckConstraints != null) { | ||
| return this._cache.supportsCheckConstraints; | ||
| } | ||
| const sql = `select 1 from information_schema.tables where table_name = 'CHECK_CONSTRAINTS' and table_schema = 'information_schema'`; | ||
| const res = await connection.execute(sql); | ||
| return this._cache.supportsCheckConstraints = res.length > 0; | ||
| } | ||
| getChecksSQL(tables) { | ||
| return `select cc.constraint_schema as table_schema, tc.table_name as table_name, cc.constraint_name as name, cc.check_clause as expression | ||
| from information_schema.check_constraints cc | ||
| join information_schema.table_constraints tc | ||
| on tc.constraint_schema = cc.constraint_schema | ||
| and tc.constraint_name = cc.constraint_name | ||
| and constraint_type = 'CHECK' | ||
| where tc.table_name in (${tables.map(t => this.platform.quoteValue(t.table_name))}) and tc.constraint_schema = database() | ||
| order by tc.constraint_name`; | ||
| } | ||
| normalizeDefaultValue(defaultValue, length) { | ||
| return super.normalizeDefaultValue(defaultValue, length, MySqlSchemaHelper.DEFAULT_VALUES); | ||
| } | ||
| wrap(val, type) { | ||
| const stringType = type instanceof StringType || type instanceof TextType || type instanceof EnumType; | ||
| return typeof val === 'string' && val.length > 0 && stringType ? this.platform.quoteValue(val) : val; | ||
| } | ||
| } |
| export * from './PostgreSqlNativeQueryBuilder.js'; |
| export * from './PostgreSqlNativeQueryBuilder.js'; |
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| /** @internal */ | ||
| export declare class PostgreSqlNativeQueryBuilder extends NativeQueryBuilder { | ||
| protected compileTruncate(): void; | ||
| } |
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| /** @internal */ | ||
| export class PostgreSqlNativeQueryBuilder extends NativeQueryBuilder { | ||
| compileTruncate() { | ||
| super.compileTruncate(); | ||
| this.parts.push('restart identity cascade'); | ||
| } | ||
| } |
| export {}; |
| export {}; |
| import { AbstractSqlConnection } from '../../AbstractSqlConnection.js'; | ||
| export declare abstract class BaseSqliteConnection extends AbstractSqlConnection { | ||
| connect(options?: { | ||
| skipOnConnect?: boolean; | ||
| }): Promise<void>; | ||
| } |
| import { CompiledQuery } from 'kysely'; | ||
| import { AbstractSqlConnection } from '../../AbstractSqlConnection.js'; | ||
| export class BaseSqliteConnection extends AbstractSqlConnection { | ||
| async connect(options) { | ||
| await super.connect(options); | ||
| await this.client.executeQuery(CompiledQuery.raw('pragma foreign_keys = on')); | ||
| } | ||
| } |
| import { type EntityProperty, type IsolationLevel } from '@mikro-orm/core'; | ||
| import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js'; | ||
| import { SqliteNativeQueryBuilder } from './SqliteNativeQueryBuilder.js'; | ||
| import { SqliteSchemaHelper } from './SqliteSchemaHelper.js'; | ||
| import { SqliteExceptionConverter } from './SqliteExceptionConverter.js'; | ||
| export declare abstract class BaseSqlitePlatform extends AbstractSqlPlatform { | ||
| protected readonly schemaHelper: SqliteSchemaHelper; | ||
| protected readonly exceptionConverter: SqliteExceptionConverter; | ||
| /** @internal */ | ||
| createNativeQueryBuilder(): SqliteNativeQueryBuilder; | ||
| usesDefaultKeyword(): boolean; | ||
| usesReturningStatement(): boolean; | ||
| usesEnumCheckConstraints(): boolean; | ||
| getCurrentTimestampSQL(length: number): string; | ||
| getDateTimeTypeDeclarationSQL(column: { | ||
| length: number; | ||
| }): string; | ||
| getBeginTransactionSQL(options?: { | ||
| isolationLevel?: IsolationLevel; | ||
| readOnly?: boolean; | ||
| }): string[]; | ||
| getEnumTypeDeclarationSQL(column: { | ||
| items?: unknown[]; | ||
| fieldNames: string[]; | ||
| length?: number; | ||
| unsigned?: boolean; | ||
| autoincrement?: boolean; | ||
| }): string; | ||
| getTinyIntTypeDeclarationSQL(column: { | ||
| length?: number; | ||
| unsigned?: boolean; | ||
| autoincrement?: boolean; | ||
| }): string; | ||
| getSmallIntTypeDeclarationSQL(column: { | ||
| length?: number; | ||
| unsigned?: boolean; | ||
| autoincrement?: boolean; | ||
| }): string; | ||
| getIntegerTypeDeclarationSQL(column: { | ||
| length?: number; | ||
| unsigned?: boolean; | ||
| autoincrement?: boolean; | ||
| }): string; | ||
| getFloatDeclarationSQL(): string; | ||
| getBooleanTypeDeclarationSQL(): string; | ||
| getCharTypeDeclarationSQL(column: { | ||
| length?: number; | ||
| }): string; | ||
| getVarcharTypeDeclarationSQL(column: { | ||
| length?: number; | ||
| }): string; | ||
| normalizeColumnType(type: string, options: { | ||
| length?: number; | ||
| precision?: number; | ||
| scale?: number; | ||
| }): string; | ||
| convertsJsonAutomatically(): boolean; | ||
| /** | ||
| * This is used to narrow the value of Date properties as they will be stored as timestamps in sqlite. | ||
| * We use this method to convert Dates to timestamps when computing the changeset, so we have the right | ||
| * data type in the payload as well as in original entity data. Without that, we would end up with diffs | ||
| * including all Date properties, as we would be comparing Date object with timestamp. | ||
| */ | ||
| processDateProperty(value: unknown): string | number | Date; | ||
| getIndexName(tableName: string, columns: string[], type: 'index' | 'unique' | 'foreign' | 'primary' | 'sequence'): string; | ||
| supportsDeferredUniqueConstraints(): boolean; | ||
| getFullTextWhereClause(): string; | ||
| quoteVersionValue(value: Date | number, prop: EntityProperty): Date | string | number; | ||
| quoteValue(value: any): string; | ||
| } |
| import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js'; | ||
| import { SqliteNativeQueryBuilder } from './SqliteNativeQueryBuilder.js'; | ||
| import { SqliteSchemaHelper } from './SqliteSchemaHelper.js'; | ||
| import { SqliteExceptionConverter } from './SqliteExceptionConverter.js'; | ||
| export class BaseSqlitePlatform extends AbstractSqlPlatform { | ||
| schemaHelper = new SqliteSchemaHelper(this); | ||
| exceptionConverter = new SqliteExceptionConverter(); | ||
| /** @internal */ | ||
| createNativeQueryBuilder() { | ||
| return new SqliteNativeQueryBuilder(this); | ||
| } | ||
| usesDefaultKeyword() { | ||
| return false; | ||
| } | ||
| usesReturningStatement() { | ||
| return true; | ||
| } | ||
| usesEnumCheckConstraints() { | ||
| return true; | ||
| } | ||
| getCurrentTimestampSQL(length) { | ||
| return super.getCurrentTimestampSQL(0); | ||
| } | ||
| getDateTimeTypeDeclarationSQL(column) { | ||
| return 'datetime'; | ||
| } | ||
| getBeginTransactionSQL(options) { | ||
| return ['begin']; | ||
| } | ||
| getEnumTypeDeclarationSQL(column) { | ||
| if (column.items?.every(item => typeof item === 'string')) { | ||
| return 'text'; | ||
| } | ||
| /* v8 ignore next */ | ||
| return this.getTinyIntTypeDeclarationSQL(column); | ||
| } | ||
| getTinyIntTypeDeclarationSQL(column) { | ||
| return this.getIntegerTypeDeclarationSQL(column); | ||
| } | ||
| getSmallIntTypeDeclarationSQL(column) { | ||
| return this.getIntegerTypeDeclarationSQL(column); | ||
| } | ||
| getIntegerTypeDeclarationSQL(column) { | ||
| return 'integer'; | ||
| } | ||
| getFloatDeclarationSQL() { | ||
| return 'real'; | ||
| } | ||
| getBooleanTypeDeclarationSQL() { | ||
| return 'integer'; | ||
| } | ||
| getCharTypeDeclarationSQL(column) { | ||
| return 'text'; | ||
| } | ||
| getVarcharTypeDeclarationSQL(column) { | ||
| return 'text'; | ||
| } | ||
| normalizeColumnType(type, options) { | ||
| const simpleType = this.extractSimpleType(type); | ||
| if (['varchar', 'text'].includes(simpleType)) { | ||
| return this.getVarcharTypeDeclarationSQL(options); | ||
| } | ||
| return simpleType; | ||
| } | ||
| convertsJsonAutomatically() { | ||
| return false; | ||
| } | ||
| /** | ||
| * This is used to narrow the value of Date properties as they will be stored as timestamps in sqlite. | ||
| * We use this method to convert Dates to timestamps when computing the changeset, so we have the right | ||
| * data type in the payload as well as in original entity data. Without that, we would end up with diffs | ||
| * including all Date properties, as we would be comparing Date object with timestamp. | ||
| */ | ||
| processDateProperty(value) { | ||
| if (value instanceof Date) { | ||
| return +value; | ||
| } | ||
| return value; | ||
| } | ||
| getIndexName(tableName, columns, type) { | ||
| if (type === 'primary') { | ||
| return this.getDefaultPrimaryName(tableName, columns); | ||
| } | ||
| return super.getIndexName(tableName, columns, type); | ||
| } | ||
| supportsDeferredUniqueConstraints() { | ||
| return false; | ||
| } | ||
| getFullTextWhereClause() { | ||
| return `:column: match :query`; | ||
| } | ||
| quoteVersionValue(value, prop) { | ||
| if (prop.runtimeType === 'Date') { | ||
| return this.escape(value).replace(/^'|\.\d{3}'$/g, ''); | ||
| } | ||
| return value; | ||
| } | ||
| quoteValue(value) { | ||
| if (value instanceof Date) { | ||
| return '' + +value; | ||
| } | ||
| return super.quoteValue(value); | ||
| } | ||
| } |
| export * from './BaseSqliteConnection.js'; | ||
| export * from './BaseSqlitePlatform.js'; | ||
| export * from './SqliteSchemaHelper.js'; | ||
| export * from './SqliteNativeQueryBuilder.js'; | ||
| export * from './SqliteExceptionConverter.js'; |
| export * from './BaseSqliteConnection.js'; | ||
| export * from './BaseSqlitePlatform.js'; | ||
| export * from './SqliteSchemaHelper.js'; | ||
| export * from './SqliteNativeQueryBuilder.js'; | ||
| export * from './SqliteExceptionConverter.js'; |
| import { ExceptionConverter, type Dictionary, type DriverException } from '@mikro-orm/core'; | ||
| export declare class SqliteExceptionConverter extends ExceptionConverter { | ||
| /** | ||
| * @inheritDoc | ||
| * @see http://www.sqlite.org/c3ref/c_abort.html | ||
| * @see https://github.com/doctrine/dbal/blob/master/src/Driver/AbstractSQLiteDriver.php | ||
| */ | ||
| convertException(exception: Error & Dictionary): DriverException; | ||
| } |
| import { ConnectionException, ExceptionConverter, InvalidFieldNameException, LockWaitTimeoutException, NonUniqueFieldNameException, CheckConstraintViolationException, NotNullConstraintViolationException, ReadOnlyException, SyntaxErrorException, TableExistsException, TableNotFoundException, UniqueConstraintViolationException, ForeignKeyConstraintViolationException, } from '@mikro-orm/core'; | ||
| export class SqliteExceptionConverter extends ExceptionConverter { | ||
| /** | ||
| * @inheritDoc | ||
| * @see http://www.sqlite.org/c3ref/c_abort.html | ||
| * @see https://github.com/doctrine/dbal/blob/master/src/Driver/AbstractSQLiteDriver.php | ||
| */ | ||
| convertException(exception) { | ||
| /* v8 ignore next */ | ||
| if (exception.message.includes('database is locked')) { | ||
| return new LockWaitTimeoutException(exception); | ||
| } | ||
| if (exception.message.includes('must be unique') || | ||
| exception.message.includes('is not unique') || | ||
| exception.message.includes('are not unique') || | ||
| exception.message.includes('UNIQUE constraint failed')) { | ||
| return new UniqueConstraintViolationException(exception); | ||
| } | ||
| if (exception.message.includes('may not be NULL') || exception.message.includes('NOT NULL constraint failed')) { | ||
| return new NotNullConstraintViolationException(exception); | ||
| } | ||
| /* v8 ignore next */ | ||
| if (exception.message.includes('CHECK constraint failed')) { | ||
| return new CheckConstraintViolationException(exception); | ||
| } | ||
| if (exception.message.includes('no such table:')) { | ||
| return new TableNotFoundException(exception); | ||
| } | ||
| if (exception.message.includes('already exists')) { | ||
| return new TableExistsException(exception); | ||
| } | ||
| if (exception.message.includes('no such column:')) { | ||
| return new InvalidFieldNameException(exception); | ||
| } | ||
| if (exception.message.includes('ambiguous column name')) { | ||
| return new NonUniqueFieldNameException(exception); | ||
| } | ||
| if (exception.message.includes('syntax error')) { | ||
| return new SyntaxErrorException(exception); | ||
| } | ||
| /* v8 ignore next */ | ||
| if (exception.message.includes('attempt to write a readonly database')) { | ||
| return new ReadOnlyException(exception); | ||
| } | ||
| /* v8 ignore next */ | ||
| if (exception.message.includes('unable to open database file')) { | ||
| return new ConnectionException(exception); | ||
| } | ||
| if (exception.message.includes('FOREIGN KEY constraint failed')) { | ||
| return new ForeignKeyConstraintViolationException(exception); | ||
| } | ||
| return super.convertException(exception); | ||
| } | ||
| } |
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| /** @internal */ | ||
| export declare class SqliteNativeQueryBuilder extends NativeQueryBuilder { | ||
| protected compileTruncate(): void; | ||
| protected addLockClause(): void; | ||
| } |
| import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; | ||
| /** @internal */ | ||
| export class SqliteNativeQueryBuilder extends NativeQueryBuilder { | ||
| compileTruncate() { | ||
| const sql = `delete from ${this.getTableName()}`; | ||
| this.parts.push(sql); | ||
| } | ||
| addLockClause() { | ||
| return; // not supported | ||
| } | ||
| } |
| import { type Connection } from '@mikro-orm/core'; | ||
| import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js'; | ||
| import { SchemaHelper } from '../../schema/SchemaHelper.js'; | ||
| import type { Column, IndexDef, Table, TableDifference } from '../../typings.js'; | ||
| import type { DatabaseTable } from '../../schema/DatabaseTable.js'; | ||
| import type { DatabaseSchema } from '../../schema/DatabaseSchema.js'; | ||
| export declare class SqliteSchemaHelper extends SchemaHelper { | ||
| disableForeignKeysSQL(): string; | ||
| enableForeignKeysSQL(): string; | ||
| supportsSchemaConstraints(): boolean; | ||
| getListTablesSQL(): string; | ||
| getDropDatabaseSQL(name: string): string; | ||
| loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[]): Promise<void>; | ||
| createTable(table: DatabaseTable, alter?: boolean): string[]; | ||
| createTableColumn(column: Column, table: DatabaseTable, _changedProperties?: Set<string>): string | undefined; | ||
| getAddColumnsSQL(table: DatabaseTable, columns: Column[], diff?: TableDifference): string[]; | ||
| dropForeignKey(tableName: string, constraintName: string): string; | ||
| getDropColumnsSQL(tableName: string, columns: Column[], schemaName?: string): string; | ||
| getCreateIndexSQL(tableName: string, index: IndexDef): string; | ||
| private parseTableDefinition; | ||
| private getColumns; | ||
| private getEnumDefinitions; | ||
| getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[], tableName: string, schemaName?: string): Promise<string[]>; | ||
| private getIndexes; | ||
| private getChecks; | ||
| private getColumnDefinitions; | ||
| private getForeignKeys; | ||
| getManagementDbName(): string; | ||
| getCreateDatabaseSQL(name: string): string; | ||
| databaseExists(connection: Connection, name: string): Promise<boolean>; | ||
| /** | ||
| * Implicit indexes will be ignored when diffing | ||
| */ | ||
| isImplicitIndex(name: string): boolean; | ||
| dropIndex(table: string, index: IndexDef, oldIndexName?: string): string; | ||
| alterTable(diff: TableDifference, safe?: boolean): string[]; | ||
| private getAlterTempTableSQL; | ||
| } |
| import { Utils } from '@mikro-orm/core'; | ||
| import { SchemaHelper } from '../../schema/SchemaHelper.js'; | ||
| export class SqliteSchemaHelper extends SchemaHelper { | ||
| disableForeignKeysSQL() { | ||
| return 'pragma foreign_keys = off;'; | ||
| } | ||
| enableForeignKeysSQL() { | ||
| return 'pragma foreign_keys = on;'; | ||
| } | ||
| supportsSchemaConstraints() { | ||
| return false; | ||
| } | ||
| getListTablesSQL() { | ||
| 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' ` | ||
| + `union all select name as table_name from sqlite_temp_master where type = 'table' order by name`; | ||
| } | ||
| getDropDatabaseSQL(name) { | ||
| if (name === ':memory:') { | ||
| return ''; | ||
| } | ||
| /* v8 ignore next */ | ||
| return `drop database if exists ${this.quote(name)}`; | ||
| } | ||
| async loadInformationSchema(schema, connection, tables, schemas) { | ||
| for (const t of tables) { | ||
| const table = schema.addTable(t.table_name, t.schema_name, t.table_comment); | ||
| const cols = await this.getColumns(connection, table.name, table.schema); | ||
| const indexes = await this.getIndexes(connection, table.name, table.schema); | ||
| const checks = await this.getChecks(connection, table.name, table.schema); | ||
| const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema); | ||
| const fks = await this.getForeignKeys(connection, table.name, table.schema); | ||
| const enums = await this.getEnumDefinitions(connection, table.name); | ||
| table.init(cols, indexes, checks, pks, fks, enums); | ||
| } | ||
| } | ||
| createTable(table, alter) { | ||
| let sql = `create table ${table.getQuotedName()} (`; | ||
| const columns = table.getColumns(); | ||
| const lastColumn = columns[columns.length - 1].name; | ||
| for (const column of columns) { | ||
| const col = this.createTableColumn(column, table); | ||
| if (col) { | ||
| const comma = column.name === lastColumn ? '' : ', '; | ||
| sql += col + comma; | ||
| } | ||
| } | ||
| const primaryKey = table.getPrimaryKey(); | ||
| const createPrimary = primaryKey?.composite; | ||
| if (createPrimary && primaryKey) { | ||
| sql += `, primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`; | ||
| } | ||
| const parts = []; | ||
| for (const fk of Object.values(table.getForeignKeys())) { | ||
| parts.push(this.createForeignKey(table, fk, false)); | ||
| } | ||
| for (const check of table.getChecks()) { | ||
| const sql = `constraint ${this.quote(check.name)} check (${check.expression})`; | ||
| parts.push(sql); | ||
| } | ||
| if (parts.length > 0) { | ||
| sql += ', ' + parts.join(', '); | ||
| } | ||
| sql += ')'; | ||
| const ret = []; | ||
| this.append(ret, sql); | ||
| for (const index of table.getIndexes()) { | ||
| this.append(ret, this.createIndex(index, table)); | ||
| } | ||
| return ret; | ||
| } | ||
| createTableColumn(column, table, _changedProperties) { | ||
| const col = [this.quote(column.name)]; | ||
| const checks = table.getChecks(); | ||
| const check = checks.findIndex(check => check.columnName === column.name); | ||
| const useDefault = column.default != null && column.default !== 'null'; | ||
| let columnType = column.type; | ||
| if (column.autoincrement) { | ||
| columnType = 'integer'; | ||
| } | ||
| if (column.generated) { | ||
| columnType += ` generated always as ${column.generated}`; | ||
| } | ||
| col.push(columnType); | ||
| if (check !== -1) { | ||
| col.push(`check (${checks[check].expression})`); | ||
| checks.splice(check, 1); | ||
| } | ||
| Utils.runIfNotEmpty(() => col.push('null'), column.nullable); | ||
| Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated); | ||
| Utils.runIfNotEmpty(() => col.push('primary key'), column.primary); | ||
| Utils.runIfNotEmpty(() => col.push('autoincrement'), column.autoincrement); | ||
| Utils.runIfNotEmpty(() => col.push(`default ${column.default}`), useDefault); | ||
| return col.join(' '); | ||
| } | ||
| getAddColumnsSQL(table, columns, diff) { | ||
| return columns.map(column => { | ||
| let sql = `alter table ${table.getQuotedName()} add column ${this.createTableColumn(column, table)}`; | ||
| const foreignKey = Object.values(diff.addedForeignKeys).find(fk => fk.columnNames.length === 1 && fk.columnNames[0] === column.name); | ||
| if (foreignKey && this.options.createForeignKeyConstraints) { | ||
| delete diff.addedForeignKeys[foreignKey.constraintName]; | ||
| sql += ' ' + this.createForeignKey(diff.toTable, foreignKey, false, true); | ||
| } | ||
| return sql; | ||
| }); | ||
| } | ||
| dropForeignKey(tableName, constraintName) { | ||
| return ''; | ||
| } | ||
| getDropColumnsSQL(tableName, columns, schemaName) { | ||
| /* v8 ignore next */ | ||
| const name = this.quote((schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName); | ||
| return columns.map(column => { | ||
| return `alter table ${name} drop column ${this.quote(column.name)}`; | ||
| }).join(';\n'); | ||
| } | ||
| getCreateIndexSQL(tableName, index) { | ||
| /* v8 ignore next */ | ||
| if (index.expression) { | ||
| return index.expression; | ||
| } | ||
| tableName = this.quote(tableName); | ||
| const keyName = this.quote(index.keyName); | ||
| const sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName} `; | ||
| if (index.columnNames.some(column => column.includes('.'))) { | ||
| // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them | ||
| const sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName} `; | ||
| const columns = this.platform.getJsonIndexDefinition(index); | ||
| return `${sql}(${columns.join(', ')})`; | ||
| } | ||
| return `${sql}(${index.columnNames.map(c => this.quote(c)).join(', ')})`; | ||
| } | ||
| parseTableDefinition(sql, cols) { | ||
| const columns = {}; | ||
| const constraints = []; | ||
| // extract all columns definitions | ||
| let columnsDef = sql.replaceAll('\n', '').match(new RegExp(`create table [\`"']?.*?[\`"']? \\((.*)\\)`, 'i'))?.[1]; | ||
| /* v8 ignore next */ | ||
| if (columnsDef) { | ||
| if (columnsDef.includes(', constraint ')) { | ||
| constraints.push(...columnsDef.substring(columnsDef.indexOf(', constraint') + 2).split(', ')); | ||
| columnsDef = columnsDef.substring(0, columnsDef.indexOf(', constraint')); | ||
| } | ||
| for (let i = cols.length - 1; i >= 0; i--) { | ||
| const col = cols[i]; | ||
| const re = ` *, *[\`"']?${col.name}[\`"']? (.*)`; | ||
| const columnDef = columnsDef.match(new RegExp(re, 'i')); | ||
| if (columnDef) { | ||
| columns[col.name] = { name: col.name, definition: columnDef[1] }; | ||
| columnsDef = columnsDef.substring(0, columnDef.index); | ||
| } | ||
| } | ||
| } | ||
| return { columns, constraints }; | ||
| } | ||
| async getColumns(connection, tableName, schemaName) { | ||
| const columns = await connection.execute(`pragma table_xinfo('${tableName}')`); | ||
| const sql = `select sql from sqlite_master where type = ? and name = ?`; | ||
| const tableDefinition = await connection.execute(sql, ['table', tableName], 'get'); | ||
| const composite = columns.reduce((count, col) => count + (col.pk ? 1 : 0), 0) > 1; | ||
| // there can be only one, so naive check like this should be enough | ||
| const hasAutoincrement = tableDefinition.sql.toLowerCase().includes('autoincrement'); | ||
| const { columns: columnDefinitions } = this.parseTableDefinition(tableDefinition.sql, columns); | ||
| return columns.map(col => { | ||
| const mappedType = connection.getPlatform().getMappedType(col.type); | ||
| let generated; | ||
| if (col.hidden > 1) { | ||
| /* v8 ignore next */ | ||
| const storage = col.hidden === 2 ? 'virtual' : 'stored'; | ||
| const re = new RegExp(`(generated always)? as \\((.*)\\)( ${storage})?$`, 'i'); | ||
| const match = columnDefinitions[col.name].definition.match(re); | ||
| if (match) { | ||
| generated = `${match[2]} ${storage}`; | ||
| } | ||
| } | ||
| return { | ||
| name: col.name, | ||
| type: col.type, | ||
| default: col.dflt_value, | ||
| nullable: !col.notnull, | ||
| primary: !!col.pk, | ||
| mappedType, | ||
| unsigned: false, | ||
| autoincrement: !composite && col.pk && this.platform.isNumericColumn(mappedType) && hasAutoincrement, | ||
| generated, | ||
| }; | ||
| }); | ||
| } | ||
| async getEnumDefinitions(connection, tableName) { | ||
| const sql = `select sql from sqlite_master where type = ? and name = ?`; | ||
| const tableDefinition = await connection.execute(sql, ['table', tableName], 'get'); | ||
| const checkConstraints = [...(tableDefinition.sql.match(/[`["'][^`\]"']+[`\]"'] text check \(.*?\)/gi) ?? [])]; | ||
| return checkConstraints.reduce((o, item) => { | ||
| // check constraints are defined as (note that last closing paren is missing): | ||
| // `type` text check (`type` in ('local', 'global') | ||
| const match = item.match(/[`["']([^`\]"']+)[`\]"'] text check \(.* \((.*)\)/i); | ||
| /* v8 ignore next */ | ||
| if (match) { | ||
| o[match[1]] = match[2].split(',').map((item) => item.trim().match(/^\(?'(.*)'/)[1]); | ||
| } | ||
| return o; | ||
| }, {}); | ||
| } | ||
| async getPrimaryKeys(connection, indexes, tableName, schemaName) { | ||
| const sql = `pragma table_info(\`${tableName}\`)`; | ||
| const cols = await connection.execute(sql); | ||
| return cols.filter(col => !!col.pk).map(col => col.name); | ||
| } | ||
| async getIndexes(connection, tableName, schemaName) { | ||
| const sql = `pragma table_info(\`${tableName}\`)`; | ||
| const cols = await connection.execute(sql); | ||
| const indexes = await connection.execute(`pragma index_list(\`${tableName}\`)`); | ||
| const ret = []; | ||
| for (const col of cols.filter(c => c.pk)) { | ||
| ret.push({ | ||
| columnNames: [col.name], | ||
| keyName: 'primary', | ||
| constraint: true, | ||
| unique: true, | ||
| primary: true, | ||
| }); | ||
| } | ||
| for (const index of indexes.filter(index => !this.isImplicitIndex(index.name))) { | ||
| const res = await connection.execute(`pragma index_info(\`${index.name}\`)`); | ||
| ret.push(...res.map(row => ({ | ||
| columnNames: [row.name], | ||
| keyName: index.name, | ||
| unique: !!index.unique, | ||
| constraint: !!index.unique, | ||
| primary: false, | ||
| }))); | ||
| } | ||
| return this.mapIndexes(ret); | ||
| } | ||
| async getChecks(connection, tableName, schemaName) { | ||
| const { columns, constraints } = await this.getColumnDefinitions(connection, tableName, schemaName); | ||
| const checks = []; | ||
| for (const key of Object.keys(columns)) { | ||
| const column = columns[key]; | ||
| const expression = column.definition.match(/ (check \((.*)\))/i); | ||
| if (expression) { | ||
| checks.push({ | ||
| name: this.platform.getConfig().getNamingStrategy().indexName(tableName, [column.name], 'check'), | ||
| definition: expression[1], | ||
| expression: expression[2], | ||
| columnName: column.name, | ||
| }); | ||
| } | ||
| } | ||
| for (const constraint of constraints) { | ||
| const expression = constraint.match(/constraint *[`"']?(.*?)[`"']? * (check \((.*)\))/i); | ||
| if (expression) { | ||
| checks.push({ | ||
| name: expression[1], | ||
| definition: expression[2], | ||
| expression: expression[3], | ||
| }); | ||
| } | ||
| } | ||
| return checks; | ||
| } | ||
| async getColumnDefinitions(connection, tableName, schemaName) { | ||
| const columns = await connection.execute(`pragma table_xinfo('${tableName}')`); | ||
| const sql = `select sql from sqlite_master where type = ? and name = ?`; | ||
| const tableDefinition = await connection.execute(sql, ['table', tableName], 'get'); | ||
| return this.parseTableDefinition(tableDefinition.sql, columns); | ||
| } | ||
| async getForeignKeys(connection, tableName, schemaName) { | ||
| const { constraints } = await this.getColumnDefinitions(connection, tableName, schemaName); | ||
| const fks = await connection.execute(`pragma foreign_key_list(\`${tableName}\`)`); | ||
| return fks.reduce((ret, fk) => { | ||
| const constraintName = this.platform.getIndexName(tableName, [fk.from], 'foreign'); | ||
| const constraint = constraints?.find(c => c.includes(constraintName)); | ||
| ret[constraintName] = { | ||
| constraintName, | ||
| columnName: fk.from, | ||
| columnNames: [fk.from], | ||
| localTableName: tableName, | ||
| referencedTableName: fk.table, | ||
| referencedColumnName: fk.to, | ||
| referencedColumnNames: [fk.to], | ||
| updateRule: fk.on_update.toLowerCase(), | ||
| deleteRule: fk.on_delete.toLowerCase(), | ||
| deferMode: constraint?.match(/ deferrable initially (deferred|immediate)/i)?.[1].toLowerCase(), | ||
| }; | ||
| return ret; | ||
| }, {}); | ||
| } | ||
| getManagementDbName() { | ||
| return ''; | ||
| } | ||
| getCreateDatabaseSQL(name) { | ||
| return ''; | ||
| } | ||
| async databaseExists(connection, name) { | ||
| const tables = await connection.execute(this.getListTablesSQL()); | ||
| return tables.length > 0; | ||
| } | ||
| /** | ||
| * Implicit indexes will be ignored when diffing | ||
| */ | ||
| isImplicitIndex(name) { | ||
| // Ignore indexes with reserved names, e.g. autoindexes | ||
| return name.startsWith('sqlite_'); | ||
| } | ||
| dropIndex(table, index, oldIndexName = index.keyName) { | ||
| return `drop index ${this.quote(oldIndexName)}`; | ||
| } | ||
| alterTable(diff, safe) { | ||
| const ret = []; | ||
| const [schemaName, tableName] = this.splitTableName(diff.name); | ||
| if (Utils.hasObjectKeys(diff.removedChecks) | ||
| || Utils.hasObjectKeys(diff.changedChecks) | ||
| || Utils.hasObjectKeys(diff.changedForeignKeys) | ||
| || Utils.hasObjectKeys(diff.changedColumns)) { | ||
| return this.getAlterTempTableSQL(diff); | ||
| } | ||
| for (const index of Object.values(diff.removedIndexes)) { | ||
| this.append(ret, this.dropIndex(diff.name, index)); | ||
| } | ||
| for (const index of Object.values(diff.changedIndexes)) { | ||
| this.append(ret, this.dropIndex(diff.name, index)); | ||
| } | ||
| /* v8 ignore next */ | ||
| if (!safe && Object.values(diff.removedColumns).length > 0) { | ||
| this.append(ret, this.getDropColumnsSQL(tableName, Object.values(diff.removedColumns), schemaName)); | ||
| } | ||
| if (Object.values(diff.addedColumns).length > 0) { | ||
| this.append(ret, this.getAddColumnsSQL(diff.toTable, Object.values(diff.addedColumns), diff)); | ||
| } | ||
| if (Utils.hasObjectKeys(diff.addedForeignKeys) || Utils.hasObjectKeys(diff.addedChecks)) { | ||
| return this.getAlterTempTableSQL(diff); | ||
| } | ||
| for (const [oldColumnName, column] of Object.entries(diff.renamedColumns)) { | ||
| this.append(ret, this.getRenameColumnSQL(tableName, oldColumnName, column, schemaName)); | ||
| } | ||
| for (const index of Object.values(diff.addedIndexes)) { | ||
| ret.push(this.createIndex(index, diff.toTable)); | ||
| } | ||
| for (const index of Object.values(diff.changedIndexes)) { | ||
| ret.push(this.createIndex(index, diff.toTable, true)); | ||
| } | ||
| for (const [oldIndexName, index] of Object.entries(diff.renamedIndexes)) { | ||
| if (index.unique) { | ||
| this.append(ret, this.dropIndex(diff.name, index, oldIndexName)); | ||
| this.append(ret, this.createIndex(index, diff.toTable)); | ||
| } | ||
| else { | ||
| this.append(ret, this.getRenameIndexSQL(diff.name, index, oldIndexName)); | ||
| } | ||
| } | ||
| return ret; | ||
| } | ||
| getAlterTempTableSQL(changedTable) { | ||
| const tempName = `${(changedTable.toTable.name)}__temp_alter`; | ||
| const quotedName = this.quote(changedTable.toTable.name); | ||
| const quotedTempName = this.quote(tempName); | ||
| const [first, ...rest] = this.createTable(changedTable.toTable); | ||
| const sql = [ | ||
| 'pragma foreign_keys = off;', | ||
| first.replace(`create table ${quotedName}`, `create table ${quotedTempName}`), | ||
| ]; | ||
| const columns = []; | ||
| for (const column of changedTable.toTable.getColumns()) { | ||
| const fromColumn = changedTable.fromTable.getColumn(column.name); | ||
| if (fromColumn) { | ||
| columns.push(this.quote(column.name)); | ||
| } | ||
| else { | ||
| columns.push(`null as ${this.quote(column.name)}`); | ||
| } | ||
| } | ||
| sql.push(`insert into ${quotedTempName} select ${columns.join(', ')} from ${quotedName};`); | ||
| sql.push(`drop table ${quotedName};`); | ||
| sql.push(`alter table ${quotedTempName} rename to ${quotedName};`); | ||
| sql.push(...rest); | ||
| sql.push('pragma foreign_keys = on;'); | ||
| return sql; | ||
| } | ||
| } |
+19
| /** | ||
| * @packageDocumentation | ||
| * @module sql | ||
| */ | ||
| export { Kysely } from 'kysely'; | ||
| export * from '@mikro-orm/core'; | ||
| export * from './AbstractSqlConnection.js'; | ||
| export * from './AbstractSqlDriver.js'; | ||
| export * from './AbstractSqlPlatform.js'; | ||
| export * from './SqlEntityManager.js'; | ||
| export * from './SqlEntityRepository.js'; | ||
| export * from './query/index.js'; | ||
| export { raw } from './query/index.js'; | ||
| export * from './schema/index.js'; | ||
| export * from './dialects/index.js'; | ||
| export * from './typings.js'; | ||
| export * from './plugin/index.js'; | ||
| export { SqlEntityManager as EntityManager } from './SqlEntityManager.js'; | ||
| export { SqlEntityRepository as EntityRepository } from './SqlEntityRepository.js'; |
+19
| /** | ||
| * @packageDocumentation | ||
| * @module sql | ||
| */ | ||
| export { Kysely } from 'kysely'; | ||
| export * from '@mikro-orm/core'; | ||
| export * from './AbstractSqlConnection.js'; | ||
| export * from './AbstractSqlDriver.js'; | ||
| export * from './AbstractSqlPlatform.js'; | ||
| export * from './SqlEntityManager.js'; | ||
| export * from './SqlEntityRepository.js'; | ||
| export * from './query/index.js'; | ||
| export { raw } from './query/index.js'; | ||
| export * from './schema/index.js'; | ||
| export * from './dialects/index.js'; | ||
| export * from './typings.js'; | ||
| export * from './plugin/index.js'; | ||
| export { SqlEntityManager as EntityManager } from './SqlEntityManager.js'; | ||
| export { SqlEntityRepository as EntityRepository } from './SqlEntityRepository.js'; |
+21
| MIT License | ||
| Copyright (c) 2018 Martin Adámek | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
| import { type Dictionary, type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core'; | ||
| import { type AbstractSqlDriver } from './AbstractSqlDriver.js'; | ||
| export declare class PivotCollectionPersister<Entity extends object> { | ||
| private readonly meta; | ||
| private readonly driver; | ||
| private readonly ctx?; | ||
| private readonly schema?; | ||
| private readonly loggerContext?; | ||
| private readonly inserts; | ||
| private readonly upserts; | ||
| private readonly deletes; | ||
| private readonly batchSize; | ||
| private order; | ||
| constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction | undefined, schema?: string | undefined, loggerContext?: Dictionary | undefined); | ||
| enqueueUpdate(prop: EntityProperty<Entity>, insertDiff: Primary<Entity>[][], deleteDiff: Primary<Entity>[][] | boolean, pks: Primary<Entity>[], isInitialized?: boolean): void; | ||
| private enqueueInsert; | ||
| private enqueueUpsert; | ||
| private createInsertStatement; | ||
| private enqueueDelete; | ||
| private collectStatements; | ||
| execute(): Promise<void>; | ||
| } |
| class InsertStatement { | ||
| keys; | ||
| data; | ||
| order; | ||
| constructor(keys, data, order) { | ||
| this.keys = keys; | ||
| this.data = data; | ||
| this.order = order; | ||
| } | ||
| getHash() { | ||
| return JSON.stringify(this.data); | ||
| } | ||
| getData() { | ||
| const data = {}; | ||
| this.keys.forEach((key, idx) => data[key] = this.data[idx]); | ||
| return data; | ||
| } | ||
| } | ||
| class DeleteStatement { | ||
| keys; | ||
| cond; | ||
| constructor(keys, cond) { | ||
| this.keys = keys; | ||
| this.cond = cond; | ||
| } | ||
| getHash() { | ||
| return JSON.stringify(this.cond); | ||
| } | ||
| getCondition() { | ||
| const cond = {}; | ||
| this.keys.forEach((key, idx) => cond[key] = this.cond[idx]); | ||
| return cond; | ||
| } | ||
| } | ||
| export class PivotCollectionPersister { | ||
| meta; | ||
| driver; | ||
| ctx; | ||
| schema; | ||
| loggerContext; | ||
| inserts = new Map(); | ||
| upserts = new Map(); | ||
| deletes = new Map(); | ||
| batchSize; | ||
| order = 0; | ||
| constructor(meta, driver, ctx, schema, loggerContext) { | ||
| this.meta = meta; | ||
| this.driver = driver; | ||
| this.ctx = ctx; | ||
| this.schema = schema; | ||
| this.loggerContext = loggerContext; | ||
| this.batchSize = this.driver.config.get('batchSize'); | ||
| } | ||
| enqueueUpdate(prop, insertDiff, deleteDiff, pks, isInitialized = true) { | ||
| if (insertDiff.length) { | ||
| if (isInitialized) { | ||
| this.enqueueInsert(prop, insertDiff, pks); | ||
| } | ||
| else { | ||
| this.enqueueUpsert(prop, insertDiff, pks); | ||
| } | ||
| } | ||
| if (deleteDiff === true || (Array.isArray(deleteDiff) && deleteDiff.length)) { | ||
| this.enqueueDelete(prop, deleteDiff, pks); | ||
| } | ||
| } | ||
| enqueueInsert(prop, insertDiff, pks) { | ||
| for (const fks of insertDiff) { | ||
| const statement = this.createInsertStatement(prop, fks, pks); | ||
| const hash = statement.getHash(); | ||
| if (prop.owner || !this.inserts.has(hash)) { | ||
| this.inserts.set(hash, statement); | ||
| } | ||
| } | ||
| } | ||
| enqueueUpsert(prop, insertDiff, pks) { | ||
| for (const fks of insertDiff) { | ||
| const statement = this.createInsertStatement(prop, fks, pks); | ||
| const hash = statement.getHash(); | ||
| if (prop.owner || !this.upserts.has(hash)) { | ||
| this.upserts.set(hash, statement); | ||
| } | ||
| } | ||
| } | ||
| createInsertStatement(prop, fks, pks) { | ||
| const data = prop.owner ? [...fks, ...pks] : [...pks, ...fks]; | ||
| const keys = prop.owner | ||
| ? [...prop.inverseJoinColumns, ...prop.joinColumns] | ||
| : [...prop.joinColumns, ...prop.inverseJoinColumns]; | ||
| return new InsertStatement(keys, data, this.order++); | ||
| } | ||
| enqueueDelete(prop, deleteDiff, pks) { | ||
| if (deleteDiff === true) { | ||
| const statement = new DeleteStatement(prop.joinColumns, pks); | ||
| this.deletes.set(statement.getHash(), statement); | ||
| return; | ||
| } | ||
| for (const fks of deleteDiff) { | ||
| const data = prop.owner ? [...fks, ...pks] : [...pks, ...fks]; | ||
| const keys = prop.owner | ||
| ? [...prop.inverseJoinColumns, ...prop.joinColumns] | ||
| : [...prop.joinColumns, ...prop.inverseJoinColumns]; | ||
| const statement = new DeleteStatement(keys, data); | ||
| this.deletes.set(statement.getHash(), statement); | ||
| } | ||
| } | ||
| collectStatements(statements) { | ||
| const items = []; | ||
| for (const statement of statements.values()) { | ||
| items[statement.order] = statement.getData(); | ||
| } | ||
| return items.filter(Boolean); | ||
| } | ||
| async execute() { | ||
| if (this.deletes.size > 0) { | ||
| const deletes = [...this.deletes.values()]; | ||
| for (let i = 0; i < deletes.length; i += this.batchSize) { | ||
| const chunk = deletes.slice(i, i + this.batchSize); | ||
| const cond = { $or: [] }; | ||
| for (const item of chunk) { | ||
| cond.$or.push(item.getCondition()); | ||
| } | ||
| await this.driver.nativeDelete(this.meta.className, cond, { | ||
| ctx: this.ctx, | ||
| schema: this.schema, | ||
| loggerContext: this.loggerContext, | ||
| }); | ||
| } | ||
| } | ||
| if (this.inserts.size > 0) { | ||
| const filtered = this.collectStatements(this.inserts); | ||
| for (let i = 0; i < filtered.length; i += this.batchSize) { | ||
| const chunk = filtered.slice(i, i + this.batchSize); | ||
| await this.driver.nativeInsertMany(this.meta.className, chunk, { | ||
| ctx: this.ctx, | ||
| schema: this.schema, | ||
| convertCustomTypes: false, | ||
| processCollections: false, | ||
| loggerContext: this.loggerContext, | ||
| }); | ||
| } | ||
| } | ||
| if (this.upserts.size > 0) { | ||
| const filtered = this.collectStatements(this.upserts); | ||
| for (let i = 0; i < filtered.length; i += this.batchSize) { | ||
| const chunk = filtered.slice(i, i + this.batchSize); | ||
| await this.driver.nativeUpdateMany(this.meta.className, [], chunk, { | ||
| ctx: this.ctx, | ||
| schema: this.schema, | ||
| convertCustomTypes: false, | ||
| processCollections: false, | ||
| upsert: true, | ||
| onConflictAction: 'ignore', | ||
| loggerContext: this.loggerContext, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } |
| import { type KyselyPlugin, type PluginTransformQueryArgs, type PluginTransformResultArgs, type QueryResult, type RootOperationNode, type UnknownRow } from 'kysely'; | ||
| import { MikroTransformer } from './transformer.js'; | ||
| import type { SqlEntityManager } from '../SqlEntityManager.js'; | ||
| import type { EntityMetadata } from '@mikro-orm/core'; | ||
| /** | ||
| * Cache for query transformation data | ||
| * Stores the query node and metadata about tables/aliases | ||
| */ | ||
| interface QueryTransformCache { | ||
| entityMap: Map<string, EntityMetadata>; | ||
| } | ||
| export interface MikroKyselyPluginOptions { | ||
| /** | ||
| * Use database table names ('table') or entity names ('entity') in queries. | ||
| * | ||
| * @default 'table' | ||
| */ | ||
| tableNamingStrategy?: 'table' | 'entity'; | ||
| /** | ||
| * Use database column names ('column') or property names ('property') in queries. | ||
| * | ||
| * @default 'column' | ||
| */ | ||
| columnNamingStrategy?: 'column' | 'property'; | ||
| /** | ||
| * Automatically process entity `onCreate` hooks in INSERT queries. | ||
| * | ||
| * @default false | ||
| */ | ||
| processOnCreateHooks?: boolean; | ||
| /** | ||
| * Automatically process entity `onUpdate` hooks in UPDATE queries. | ||
| * | ||
| * @default false | ||
| */ | ||
| processOnUpdateHooks?: boolean; | ||
| /** | ||
| * Convert JavaScript values to database-compatible values (e.g., Date to timestamp, custom types). | ||
| * | ||
| * @default false | ||
| */ | ||
| convertValues?: boolean; | ||
| } | ||
| export declare class MikroKyselyPlugin implements KyselyPlugin { | ||
| protected readonly em: SqlEntityManager; | ||
| protected readonly options: MikroKyselyPluginOptions; | ||
| protected static queryNodeCache: WeakMap<any, QueryTransformCache>; | ||
| protected readonly transformer: MikroTransformer; | ||
| constructor(em: SqlEntityManager, options?: MikroKyselyPluginOptions); | ||
| transformQuery(args: PluginTransformQueryArgs): RootOperationNode; | ||
| transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>; | ||
| } | ||
| export {}; |
| import { SelectQueryNode as SelectQueryNodeClass, InsertQueryNode as InsertQueryNodeClass, UpdateQueryNode as UpdateQueryNodeClass, DeleteQueryNode as DeleteQueryNodeClass, } from 'kysely'; | ||
| import { MikroTransformer } from './transformer.js'; | ||
| export class MikroKyselyPlugin { | ||
| em; | ||
| options; | ||
| static queryNodeCache = new WeakMap(); | ||
| transformer; | ||
| constructor(em, options = {}) { | ||
| this.em = em; | ||
| this.options = options; | ||
| this.transformer = new MikroTransformer(em, options); | ||
| } | ||
| transformQuery(args) { | ||
| this.transformer.reset(); | ||
| const result = this.transformer.transformNode(args.node, args.queryId); | ||
| // Cache the entity map if it is one we can process (for use in transformResult) | ||
| if (SelectQueryNodeClass.is(args.node) || | ||
| InsertQueryNodeClass.is(args.node) || | ||
| UpdateQueryNodeClass.is(args.node) || | ||
| DeleteQueryNodeClass.is(args.node)) { | ||
| MikroKyselyPlugin.queryNodeCache.set(args.queryId, { entityMap: this.transformer.getOutputEntityMap() }); | ||
| } | ||
| return result; | ||
| } | ||
| async transformResult(args) { | ||
| // Only transform results if columnNamingStrategy is 'property' or convertValues is true | ||
| if (this.options.columnNamingStrategy !== 'property' && !this.options.convertValues) { | ||
| return args.result; | ||
| } | ||
| // Retrieve the cached query node and metadata | ||
| const cache = MikroKyselyPlugin.queryNodeCache.get(args.queryId); | ||
| if (!cache) { | ||
| return args.result; | ||
| } | ||
| // Transform the result rows using the transformer | ||
| const transformedRows = this.transformer.transformResult(args.result.rows ?? [], cache.entityMap); | ||
| return { | ||
| ...args.result, | ||
| rows: transformedRows ?? [], | ||
| }; | ||
| } | ||
| } |
| import { type EntityMetadata, type EntityProperty, type MetadataStorage } from '@mikro-orm/core'; | ||
| import { type CommonTableExpressionNameNode, type DeleteQueryNode, type IdentifierNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, OperationNodeTransformer, TableNode } from 'kysely'; | ||
| import type { MikroKyselyPluginOptions } from './index.js'; | ||
| import type { SqlEntityManager } from '../SqlEntityManager.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
| export declare class MikroTransformer extends OperationNodeTransformer { | ||
| protected readonly em: SqlEntityManager; | ||
| protected readonly options: MikroKyselyPluginOptions; | ||
| /** | ||
| * Context stack to support nested queries (subqueries, CTEs) | ||
| * Each level of query scope has its own Map of table aliases/names to EntityMetadata | ||
| * Top of stack (highest index) is the current scope | ||
| */ | ||
| protected readonly contextStack: Map<string, EntityMetadata | undefined>[]; | ||
| /** | ||
| * Subquery alias map: maps subquery/CTE alias to its source table metadata | ||
| * Used to resolve columns from subqueries/CTEs to their original table definitions | ||
| */ | ||
| protected readonly subqueryAliasMap: Map<string, EntityMetadata | undefined>; | ||
| protected readonly metadata: MetadataStorage; | ||
| protected readonly platform: AbstractSqlPlatform; | ||
| /** | ||
| * Global map of all entities involved in the query. | ||
| * Populated during AST transformation and used for result transformation. | ||
| */ | ||
| protected readonly entityMap: Map<string, EntityMetadata<any>>; | ||
| constructor(em: SqlEntityManager, options?: MikroKyselyPluginOptions); | ||
| reset(): void; | ||
| getOutputEntityMap(): Map<string, EntityMetadata>; | ||
| transformSelectQuery(node: SelectQueryNode, queryId: QueryId): SelectQueryNode; | ||
| transformInsertQuery(node: InsertQueryNode, queryId?: QueryId): InsertQueryNode; | ||
| transformUpdateQuery(node: UpdateQueryNode, queryId?: QueryId): UpdateQueryNode; | ||
| transformDeleteQuery(node: DeleteQueryNode, queryId?: QueryId): DeleteQueryNode; | ||
| transformMergeQuery(node: MergeQueryNode, queryId?: QueryId): MergeQueryNode; | ||
| transformIdentifier(node: IdentifierNode, queryId: QueryId): IdentifierNode; | ||
| /** | ||
| * Find owner entity metadata for the current identifier in the context stack. | ||
| * Supports both aliased and non-aliased table references. | ||
| * Searches up the context stack to support correlated subqueries. | ||
| * Also checks subquery/CTE aliases to resolve to their source tables. | ||
| */ | ||
| findOwnerEntityInContext(): EntityMetadata | undefined; | ||
| processOnCreateHooks(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode; | ||
| processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode; | ||
| processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode; | ||
| processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode; | ||
| mapColumnsToProperties(columns: readonly ColumnNode[], meta: EntityMetadata): (EntityProperty | undefined)[]; | ||
| normalizeColumnName(identifier: IdentifierNode): string; | ||
| findProperty(meta: EntityMetadata | undefined, columnName?: string): EntityProperty | undefined; | ||
| shouldConvertValues(): boolean; | ||
| prepareInputValue(prop: EntityProperty | undefined, value: unknown, enabled: boolean): unknown; | ||
| /** | ||
| * Look up a table name/alias in the context stack. | ||
| * Searches from current scope (top of stack) to parent scopes (bottom). | ||
| * This supports correlated subqueries and references to outer query tables. | ||
| */ | ||
| lookupInContextStack(tableNameOrAlias: string): EntityMetadata | undefined; | ||
| /** | ||
| * Process WITH node (CTE definitions) | ||
| */ | ||
| processWithNode(withNode: WithNode, context: Map<string, EntityMetadata | undefined>): void; | ||
| /** | ||
| * Extract CTE name from CommonTableExpressionNameNode | ||
| */ | ||
| getCTEName(nameNode: CommonTableExpressionNameNode): string | undefined; | ||
| /** | ||
| * Process a FROM item (can be TableNode or AliasNode) | ||
| */ | ||
| processFromItem(from: any, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode | ||
| context: Map<string, EntityMetadata | undefined>): void; | ||
| /** | ||
| * Process a JOIN node | ||
| */ | ||
| processJoinNode(join: JoinNode, context: Map<string, EntityMetadata | undefined>): void; | ||
| /** | ||
| * Extract the primary source table from a SELECT query | ||
| * This helps resolve columns from subqueries to their original entity tables | ||
| */ | ||
| extractSourceTableFromSelectQuery(selectQuery: SelectQueryNode): EntityMetadata | undefined; | ||
| /** | ||
| * Extract alias name from an alias node | ||
| */ | ||
| extractAliasName(alias: any): string | undefined; | ||
| /** | ||
| * Extract table name from a TableNode | ||
| */ | ||
| getTableName(node: TableNode | undefined): string | undefined; | ||
| /** | ||
| * Find entity metadata by table name or entity name | ||
| */ | ||
| findEntityMetadata(name: string): EntityMetadata | undefined; | ||
| /** | ||
| * Transform result rows by mapping database column names to property names | ||
| * This is called for SELECT queries when columnNamingStrategy is 'property' | ||
| */ | ||
| transformResult(rows: Record<string, any>[] | undefined, entityMap: Map<string, EntityMetadata>): Record<string, any>[] | undefined; | ||
| buildGlobalFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, EntityProperty>; | ||
| buildGlobalRelationFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, string>; | ||
| /** | ||
| * Build a mapping from database field names to property objects | ||
| * Format: { 'field_name': EntityProperty } | ||
| */ | ||
| buildFieldToPropertyMap(meta: EntityMetadata, alias?: string): Record<string, EntityProperty>; | ||
| /** | ||
| * Build a mapping for relation fields | ||
| * For ManyToOne relations, we need to map from the foreign key field to the relation property | ||
| * Format: { 'foreign_key_field': 'relationPropertyName' } | ||
| */ | ||
| buildRelationFieldMap(meta: EntityMetadata, alias?: string): Record<string, string>; | ||
| /** | ||
| * Transform a single row by mapping column names to property names | ||
| */ | ||
| transformRow(row: Record<string, any>, fieldToPropertyMap: Record<string, EntityProperty>, relationFieldMap: Record<string, string>): Record<string, any>; | ||
| prepareOutputValue(prop: EntityProperty | undefined, value: unknown): unknown; | ||
| } |
| import { ReferenceKind, isRaw, } from '@mikro-orm/core'; | ||
| import { AliasNode, ColumnNode, ColumnUpdateNode, OperationNodeTransformer, PrimitiveValueListNode, ReferenceNode, SchemableIdentifierNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely'; | ||
| export class MikroTransformer extends OperationNodeTransformer { | ||
| em; | ||
| options; | ||
| /** | ||
| * Context stack to support nested queries (subqueries, CTEs) | ||
| * Each level of query scope has its own Map of table aliases/names to EntityMetadata | ||
| * Top of stack (highest index) is the current scope | ||
| */ | ||
| contextStack = []; | ||
| /** | ||
| * Subquery alias map: maps subquery/CTE alias to its source table metadata | ||
| * Used to resolve columns from subqueries/CTEs to their original table definitions | ||
| */ | ||
| subqueryAliasMap = new Map(); | ||
| metadata; | ||
| platform; | ||
| /** | ||
| * Global map of all entities involved in the query. | ||
| * Populated during AST transformation and used for result transformation. | ||
| */ | ||
| entityMap = new Map(); | ||
| constructor(em, options = {}) { | ||
| super(); | ||
| this.em = em; | ||
| this.options = options; | ||
| this.metadata = em.getMetadata(); | ||
| this.platform = em.getDriver().getPlatform(); | ||
| } | ||
| reset() { | ||
| this.subqueryAliasMap.clear(); | ||
| this.entityMap.clear(); | ||
| } | ||
| getOutputEntityMap() { | ||
| return this.entityMap; | ||
| } | ||
| transformSelectQuery(node, queryId) { | ||
| // Push a new context for this query scope (starts with inherited parent context) | ||
| const currentContext = new Map(); | ||
| this.contextStack.push(currentContext); | ||
| try { | ||
| // Process WITH clause (CTEs) first - they define names available in this scope | ||
| if (node.with) { | ||
| this.processWithNode(node.with, currentContext); | ||
| } | ||
| // Process FROM clause - main tables in this scope | ||
| if (node.from?.froms) { | ||
| for (const from of node.from.froms) { | ||
| this.processFromItem(from, currentContext); | ||
| } | ||
| } | ||
| // Process JOINs - additional tables joined into this scope | ||
| if (node.joins) { | ||
| for (const join of node.joins) { | ||
| this.processJoinNode(join, currentContext); | ||
| } | ||
| } | ||
| return super.transformSelectQuery(node, queryId); | ||
| } | ||
| finally { | ||
| // Pop the context when exiting this query scope | ||
| this.contextStack.pop(); | ||
| } | ||
| } | ||
| transformInsertQuery(node, queryId) { | ||
| const currentContext = new Map(); | ||
| this.contextStack.push(currentContext); | ||
| try { | ||
| let entityMeta; | ||
| if (node.into) { | ||
| const tableName = this.getTableName(node.into); | ||
| if (tableName) { | ||
| const meta = this.findEntityMetadata(tableName); | ||
| if (meta) { | ||
| entityMeta = meta; | ||
| currentContext.set(meta.tableName, meta); | ||
| this.entityMap.set(meta.tableName, meta); | ||
| } | ||
| } | ||
| } | ||
| const nodeWithHooks = this.options.processOnCreateHooks && entityMeta | ||
| ? this.processOnCreateHooks(node, entityMeta) | ||
| : node; | ||
| const nodeWithConvertedValues = this.options.convertValues && entityMeta | ||
| ? this.processInsertValues(nodeWithHooks, entityMeta) | ||
| : nodeWithHooks; | ||
| // Handle ON CONFLICT clause | ||
| let finalNode = nodeWithConvertedValues; | ||
| if (node.onConflict?.updates && entityMeta) { | ||
| // Create a temporary UpdateQueryNode to reuse processOnUpdateHooks and processUpdateValues | ||
| // We only care about the updates part | ||
| const tempUpdateNode = { | ||
| kind: 'UpdateQueryNode', | ||
| table: node.into, // Dummy table | ||
| updates: node.onConflict.updates, | ||
| }; | ||
| const updatesWithHooks = this.options.processOnUpdateHooks | ||
| ? this.processOnUpdateHooks(tempUpdateNode, entityMeta).updates | ||
| : node.onConflict.updates; | ||
| const tempUpdateNodeWithHooks = { | ||
| ...tempUpdateNode, | ||
| updates: updatesWithHooks, | ||
| }; | ||
| const updatesWithConvertedValues = this.options.convertValues | ||
| ? this.processUpdateValues(tempUpdateNodeWithHooks, entityMeta).updates | ||
| : updatesWithHooks; | ||
| if (updatesWithConvertedValues && updatesWithConvertedValues !== node.onConflict.updates) { | ||
| // Construct the new OnConflictNode with updated values | ||
| finalNode = { | ||
| ...finalNode, | ||
| onConflict: { | ||
| ...node.onConflict, | ||
| updates: updatesWithConvertedValues, | ||
| }, | ||
| }; | ||
| } | ||
| } | ||
| return super.transformInsertQuery(finalNode, queryId); | ||
| } | ||
| finally { | ||
| this.contextStack.pop(); | ||
| } | ||
| } | ||
| transformUpdateQuery(node, queryId) { | ||
| const currentContext = new Map(); | ||
| this.contextStack.push(currentContext); | ||
| try { | ||
| let entityMeta; | ||
| if (node.table && TableNode.is(node.table)) { | ||
| const tableName = this.getTableName(node.table); | ||
| if (tableName) { | ||
| const meta = this.findEntityMetadata(tableName); | ||
| if (meta) { | ||
| entityMeta = meta; | ||
| currentContext.set(meta.tableName, meta); | ||
| this.entityMap.set(meta.tableName, meta); | ||
| } | ||
| } | ||
| } | ||
| // Process FROM clause in UPDATE queries (for UPDATE with JOIN) | ||
| if (node.from) { | ||
| for (const fromItem of node.from.froms) { | ||
| this.processFromItem(fromItem, currentContext); | ||
| } | ||
| } | ||
| // Also process JOINs in UPDATE queries | ||
| if (node.joins) { | ||
| for (const join of node.joins) { | ||
| this.processJoinNode(join, currentContext); | ||
| } | ||
| } | ||
| const nodeWithHooks = this.options.processOnUpdateHooks && entityMeta | ||
| ? this.processOnUpdateHooks(node, entityMeta) | ||
| : node; | ||
| const nodeWithConvertedValues = this.options.convertValues && entityMeta | ||
| ? this.processUpdateValues(nodeWithHooks, entityMeta) | ||
| : nodeWithHooks; | ||
| return super.transformUpdateQuery(nodeWithConvertedValues, queryId); | ||
| } | ||
| finally { | ||
| this.contextStack.pop(); | ||
| } | ||
| } | ||
| transformDeleteQuery(node, queryId) { | ||
| const currentContext = new Map(); | ||
| this.contextStack.push(currentContext); | ||
| try { | ||
| const froms = node.from?.froms; | ||
| if (froms && froms.length > 0) { | ||
| const firstFrom = froms[0]; | ||
| if (TableNode.is(firstFrom)) { | ||
| const tableName = this.getTableName(firstFrom); | ||
| if (tableName) { | ||
| const meta = this.findEntityMetadata(tableName); | ||
| if (meta) { | ||
| currentContext.set(meta.tableName, meta); | ||
| this.entityMap.set(meta.tableName, meta); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Also process JOINs in DELETE queries | ||
| if (node.joins) { | ||
| for (const join of node.joins) { | ||
| this.processJoinNode(join, currentContext); | ||
| } | ||
| } | ||
| return super.transformDeleteQuery(node, queryId); | ||
| } | ||
| finally { | ||
| this.contextStack.pop(); | ||
| } | ||
| } | ||
| transformMergeQuery(node, queryId) { | ||
| const currentContext = new Map(); | ||
| this.contextStack.push(currentContext); | ||
| try { | ||
| return super.transformMergeQuery(node, queryId); | ||
| } | ||
| finally { | ||
| this.contextStack.pop(); | ||
| } | ||
| } | ||
| transformIdentifier(node, queryId) { | ||
| node = super.transformIdentifier(node, queryId); | ||
| const parent = this.nodeStack[this.nodeStack.length - 2]; | ||
| // Transform table names when tableNamingStrategy is 'entity' | ||
| if (this.options.tableNamingStrategy === 'entity' && parent && SchemableIdentifierNode.is(parent)) { | ||
| const meta = this.findEntityMetadata(node.name); | ||
| if (meta) { | ||
| return { | ||
| ...node, | ||
| name: meta.tableName, | ||
| }; | ||
| } | ||
| } | ||
| // Transform column names when columnNamingStrategy is 'property' | ||
| // Support ColumnNode, ColumnUpdateNode, and ReferenceNode (for JOIN conditions) | ||
| if (this.options.columnNamingStrategy === 'property' && parent && (ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))) { | ||
| const ownerMeta = this.findOwnerEntityInContext(); | ||
| if (ownerMeta) { | ||
| const prop = ownerMeta.properties[node.name]; | ||
| const fieldName = prop?.fieldNames?.[0]; | ||
| if (fieldName) { | ||
| return { | ||
| ...node, | ||
| name: fieldName, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| return node; | ||
| } | ||
| /** | ||
| * Find owner entity metadata for the current identifier in the context stack. | ||
| * Supports both aliased and non-aliased table references. | ||
| * Searches up the context stack to support correlated subqueries. | ||
| * Also checks subquery/CTE aliases to resolve to their source tables. | ||
| */ | ||
| findOwnerEntityInContext() { | ||
| // Check if current column has a table reference (e.g., u.firstName) | ||
| const reference = this.nodeStack.find(it => ReferenceNode.is(it)); | ||
| if (reference?.table && TableNode.is(reference.table)) { | ||
| const tableName = this.getTableName(reference.table); | ||
| if (tableName) { | ||
| // First, check in subquery alias map (for CTE/subquery columns) | ||
| if (this.subqueryAliasMap.has(tableName)) { | ||
| return this.subqueryAliasMap.get(tableName); | ||
| } | ||
| // Find entity metadata to get the actual table name | ||
| // Context uses table names (meta.tableName) as keys, not entity names | ||
| const entityMeta = this.findEntityMetadata(tableName); | ||
| if (entityMeta) { | ||
| // Search in context stack using the actual table name | ||
| const meta = this.lookupInContextStack(entityMeta.tableName); | ||
| if (meta) { | ||
| return meta; | ||
| } | ||
| // Also try with the entity name (for cases where context uses entity name) | ||
| const metaByEntityName = this.lookupInContextStack(tableName); | ||
| if (metaByEntityName) { | ||
| return metaByEntityName; | ||
| } | ||
| } | ||
| else { | ||
| // If entity metadata not found, try direct lookup (for CTE/subquery cases) | ||
| const meta = this.lookupInContextStack(tableName); | ||
| if (meta) { | ||
| return meta; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // If no explicit table reference, use the first entity in current context | ||
| if (this.contextStack.length > 0) { | ||
| const currentContext = this.contextStack[this.contextStack.length - 1]; | ||
| for (const [alias, meta] of currentContext.entries()) { | ||
| if (meta) { | ||
| return meta; | ||
| } | ||
| // If the context value is undefined but the alias is in subqueryAliasMap, | ||
| // use the mapped metadata (for CTE/subquery cases) | ||
| if (!meta && this.subqueryAliasMap.has(alias)) { | ||
| const mappedMeta = this.subqueryAliasMap.get(alias); | ||
| if (mappedMeta) { | ||
| return mappedMeta; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| processOnCreateHooks(node, meta) { | ||
| if (!node.columns || !node.values || !ValuesNode.is(node.values)) { | ||
| return node; | ||
| } | ||
| const existingProps = new Set(); | ||
| for (const col of node.columns) { | ||
| const prop = this.findProperty(meta, this.normalizeColumnName(col.column)); | ||
| if (prop) { | ||
| existingProps.add(prop.name); | ||
| } | ||
| } | ||
| const missingProps = meta.props.filter(prop => prop.onCreate && !existingProps.has(prop.name)); | ||
| if (missingProps.length === 0) { | ||
| return node; | ||
| } | ||
| const newColumns = [...node.columns]; | ||
| for (const prop of missingProps) { | ||
| newColumns.push(ColumnNode.create(prop.name)); | ||
| } | ||
| const newRows = node.values.values.map(row => { | ||
| const valuesToAdd = missingProps.map(prop => { | ||
| const val = prop.onCreate(undefined, this.em); | ||
| return val; | ||
| }); | ||
| if (ValueListNode.is(row)) { | ||
| const newValues = [...row.values, ...valuesToAdd.map(v => ValueNode.create(v))]; | ||
| return ValueListNode.create(newValues); | ||
| } | ||
| if (PrimitiveValueListNode.is(row)) { | ||
| const newValues = [...row.values, ...valuesToAdd]; | ||
| return PrimitiveValueListNode.create(newValues); | ||
| } | ||
| return row; | ||
| }); | ||
| return { | ||
| ...node, | ||
| columns: Object.freeze(newColumns), | ||
| values: ValuesNode.create(newRows), | ||
| }; | ||
| } | ||
| processOnUpdateHooks(node, meta) { | ||
| if (!node.updates) { | ||
| return node; | ||
| } | ||
| const existingProps = new Set(); | ||
| for (const update of node.updates) { | ||
| if (ColumnNode.is(update.column)) { | ||
| const prop = this.findProperty(meta, this.normalizeColumnName(update.column.column)); | ||
| if (prop) { | ||
| existingProps.add(prop.name); | ||
| } | ||
| } | ||
| } | ||
| const missingProps = meta.props.filter(prop => prop.onUpdate && !existingProps.has(prop.name)); | ||
| if (missingProps.length === 0) { | ||
| return node; | ||
| } | ||
| const newUpdates = [...node.updates]; | ||
| for (const prop of missingProps) { | ||
| const val = prop.onUpdate(undefined, this.em); | ||
| newUpdates.push(ColumnUpdateNode.create(ColumnNode.create(prop.name), ValueNode.create(val))); | ||
| } | ||
| return { | ||
| ...node, | ||
| updates: Object.freeze(newUpdates), | ||
| }; | ||
| } | ||
| processInsertValues(node, meta) { | ||
| if (!node.columns?.length || !node.values || !ValuesNode.is(node.values)) { | ||
| return node; | ||
| } | ||
| const columnProps = this.mapColumnsToProperties(node.columns, meta); | ||
| const shouldConvert = this.shouldConvertValues(); | ||
| let changed = false; | ||
| const convertedRows = node.values.values.map(row => { | ||
| if (ValueListNode.is(row)) { | ||
| if (row.values.length !== columnProps.length) { | ||
| return row; | ||
| } | ||
| const values = row.values.map((valueNode, idx) => { | ||
| if (!ValueNode.is(valueNode)) { | ||
| return valueNode; | ||
| } | ||
| const converted = this.prepareInputValue(columnProps[idx], valueNode.value, shouldConvert); | ||
| if (converted === valueNode.value) { | ||
| return valueNode; | ||
| } | ||
| changed = true; | ||
| return valueNode.immediate ? ValueNode.createImmediate(converted) : ValueNode.create(converted); | ||
| }); | ||
| return ValueListNode.create(values); | ||
| } | ||
| if (PrimitiveValueListNode.is(row)) { | ||
| if (row.values.length !== columnProps.length) { | ||
| return row; | ||
| } | ||
| const values = row.values.map((value, idx) => { | ||
| const converted = this.prepareInputValue(columnProps[idx], value, shouldConvert); | ||
| if (converted !== value) { | ||
| changed = true; | ||
| } | ||
| return converted; | ||
| }); | ||
| return PrimitiveValueListNode.create(values); | ||
| } | ||
| return row; | ||
| }); | ||
| if (!changed) { | ||
| return node; | ||
| } | ||
| return { | ||
| ...node, | ||
| values: ValuesNode.create(convertedRows), | ||
| }; | ||
| } | ||
| processUpdateValues(node, meta) { | ||
| if (!node.updates?.length) { | ||
| return node; | ||
| } | ||
| const shouldConvert = this.shouldConvertValues(); | ||
| let changed = false; | ||
| const updates = node.updates.map(updateNode => { | ||
| if (!ValueNode.is(updateNode.value)) { | ||
| return updateNode; | ||
| } | ||
| const columnName = ColumnNode.is(updateNode.column) | ||
| ? this.normalizeColumnName(updateNode.column.column) | ||
| : undefined; | ||
| const property = this.findProperty(meta, columnName); | ||
| const converted = this.prepareInputValue(property, updateNode.value.value, shouldConvert); | ||
| if (converted === updateNode.value.value) { | ||
| return updateNode; | ||
| } | ||
| changed = true; | ||
| const newValueNode = updateNode.value.immediate | ||
| ? ValueNode.createImmediate(converted) | ||
| : ValueNode.create(converted); | ||
| return { | ||
| ...updateNode, | ||
| value: newValueNode, | ||
| }; | ||
| }); | ||
| if (!changed) { | ||
| return node; | ||
| } | ||
| return { | ||
| ...node, | ||
| updates, | ||
| }; | ||
| } | ||
| mapColumnsToProperties(columns, meta) { | ||
| return columns.map(column => { | ||
| const columnName = this.normalizeColumnName(column.column); | ||
| return this.findProperty(meta, columnName); | ||
| }); | ||
| } | ||
| normalizeColumnName(identifier) { | ||
| const name = identifier.name; | ||
| if (!name.includes('.')) { | ||
| return name; | ||
| } | ||
| const parts = name.split('.'); | ||
| return parts[parts.length - 1] ?? name; | ||
| } | ||
| findProperty(meta, columnName) { | ||
| if (!meta || !columnName) { | ||
| return undefined; | ||
| } | ||
| if (meta.properties[columnName]) { | ||
| return meta.properties[columnName]; | ||
| } | ||
| return meta.props.find(prop => prop.fieldNames?.includes(columnName)); | ||
| } | ||
| shouldConvertValues() { | ||
| return !!this.options.convertValues; | ||
| } | ||
| prepareInputValue(prop, value, enabled) { | ||
| if (!enabled || !prop || value == null) { | ||
| return value; | ||
| } | ||
| if (typeof value === 'object' && value !== null) { | ||
| if (isRaw(value)) { | ||
| return value; | ||
| } | ||
| if ('kind' in value) { | ||
| return value; | ||
| } | ||
| } | ||
| if (prop.customType && !isRaw(value)) { | ||
| return prop.customType.convertToDatabaseValue(value, this.platform, { fromQuery: true, key: prop.name, mode: 'query-data' }); | ||
| } | ||
| if (value instanceof Date) { | ||
| return this.platform.processDateProperty(value); | ||
| } | ||
| return value; | ||
| } | ||
| /** | ||
| * Look up a table name/alias in the context stack. | ||
| * Searches from current scope (top of stack) to parent scopes (bottom). | ||
| * This supports correlated subqueries and references to outer query tables. | ||
| */ | ||
| lookupInContextStack(tableNameOrAlias) { | ||
| // Search from top of stack (current scope) to bottom (parent scopes) | ||
| for (let i = this.contextStack.length - 1; i >= 0; i--) { | ||
| const context = this.contextStack[i]; | ||
| if (context.has(tableNameOrAlias)) { | ||
| return context.get(tableNameOrAlias); | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Process WITH node (CTE definitions) | ||
| */ | ||
| processWithNode(withNode, context) { | ||
| for (const cte of withNode.expressions) { | ||
| const cteName = this.getCTEName(cte.name); | ||
| if (cteName) { | ||
| // CTEs are not entities, so map to undefined | ||
| // They will be transformed recursively by transformSelectQuery | ||
| context.set(cteName, undefined); | ||
| // Also try to extract the source table from the CTE's expression | ||
| // This helps resolve columns in subsequent queries that use the CTE | ||
| if (cte.expression?.kind === 'SelectQueryNode') { | ||
| const sourceMeta = this.extractSourceTableFromSelectQuery(cte.expression); | ||
| if (sourceMeta) { | ||
| this.subqueryAliasMap.set(cteName, sourceMeta); | ||
| // Add CTE to entityMap so it can be used for result transformation if needed | ||
| // (though CTEs usually don't appear in result rows directly, but their columns might) | ||
| this.entityMap.set(cteName, sourceMeta); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Extract CTE name from CommonTableExpressionNameNode | ||
| */ | ||
| getCTEName(nameNode) { | ||
| if (TableNode.is(nameNode.table)) { | ||
| return this.getTableName(nameNode.table); | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Process a FROM item (can be TableNode or AliasNode) | ||
| */ | ||
| processFromItem(from, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode | ||
| context) { | ||
| if (AliasNode.is(from)) { | ||
| if (TableNode.is(from.node)) { | ||
| // Regular table with alias | ||
| const tableName = this.getTableName(from.node); | ||
| if (tableName && from.alias) { | ||
| const meta = this.findEntityMetadata(tableName); | ||
| const aliasName = this.extractAliasName(from.alias); | ||
| if (aliasName) { | ||
| context.set(aliasName, meta); | ||
| if (meta) { | ||
| this.entityMap.set(aliasName, meta); | ||
| } | ||
| // Also map the alias in subqueryAliasMap if the table name is a CTE | ||
| if (this.subqueryAliasMap.has(tableName)) { | ||
| this.subqueryAliasMap.set(aliasName, this.subqueryAliasMap.get(tableName)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| else if (from.node?.kind === 'SelectQueryNode') { | ||
| // Subquery with alias | ||
| const aliasName = this.extractAliasName(from.alias); | ||
| if (aliasName) { | ||
| context.set(aliasName, undefined); | ||
| // Try to extract the source table from the subquery | ||
| const sourceMeta = this.extractSourceTableFromSelectQuery(from.node); | ||
| if (sourceMeta) { | ||
| this.subqueryAliasMap.set(aliasName, sourceMeta); | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| // Other types with alias | ||
| const aliasName = this.extractAliasName(from.alias); | ||
| if (aliasName) { | ||
| context.set(aliasName, undefined); | ||
| } | ||
| } | ||
| } | ||
| else if (TableNode.is(from)) { | ||
| // Table without alias | ||
| const tableName = this.getTableName(from); | ||
| if (tableName) { | ||
| const meta = this.findEntityMetadata(tableName); | ||
| context.set(tableName, meta); | ||
| if (meta) { | ||
| this.entityMap.set(tableName, meta); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Process a JOIN node | ||
| */ | ||
| processJoinNode(join, context) { | ||
| const joinTable = join.table; | ||
| if (AliasNode.is(joinTable)) { | ||
| if (TableNode.is(joinTable.node)) { | ||
| // Regular table with alias in JOIN | ||
| const tableName = this.getTableName(joinTable.node); | ||
| if (tableName && joinTable.alias) { | ||
| const meta = this.findEntityMetadata(tableName); | ||
| const aliasName = this.extractAliasName(joinTable.alias); | ||
| if (aliasName) { | ||
| context.set(aliasName, meta); | ||
| if (meta) { | ||
| this.entityMap.set(aliasName, meta); | ||
| } | ||
| // Also map the alias in subqueryAliasMap if the table name is a CTE | ||
| if (this.subqueryAliasMap.has(tableName)) { | ||
| this.subqueryAliasMap.set(aliasName, this.subqueryAliasMap.get(tableName)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| else if (joinTable.node?.kind === 'SelectQueryNode') { | ||
| // Subquery with alias in JOIN | ||
| const aliasName = this.extractAliasName(joinTable.alias); | ||
| if (aliasName) { | ||
| context.set(aliasName, undefined); | ||
| // Try to extract the source table from the subquery | ||
| const sourceMeta = this.extractSourceTableFromSelectQuery(joinTable.node); | ||
| if (sourceMeta) { | ||
| this.subqueryAliasMap.set(aliasName, sourceMeta); | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| // Other types with alias | ||
| const aliasName = this.extractAliasName(joinTable.alias); | ||
| if (aliasName) { | ||
| context.set(aliasName, undefined); | ||
| } | ||
| } | ||
| } | ||
| else if (TableNode.is(joinTable)) { | ||
| // Table without alias in JOIN | ||
| const tableName = this.getTableName(joinTable); | ||
| if (tableName) { | ||
| const meta = this.findEntityMetadata(tableName); | ||
| // Use table name (meta.tableName) as key to match transformUpdateQuery behavior | ||
| if (meta) { | ||
| context.set(meta.tableName, meta); | ||
| this.entityMap.set(meta.tableName, meta); | ||
| // Also set with entity name for backward compatibility | ||
| context.set(tableName, meta); | ||
| } | ||
| else { | ||
| context.set(tableName, undefined); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Extract the primary source table from a SELECT query | ||
| * This helps resolve columns from subqueries to their original entity tables | ||
| */ | ||
| extractSourceTableFromSelectQuery(selectQuery) { | ||
| if (!selectQuery.from?.froms || selectQuery.from.froms.length === 0) { | ||
| return undefined; | ||
| } | ||
| // Get the first FROM table | ||
| const firstFrom = selectQuery.from.froms[0]; | ||
| let sourceTable; | ||
| if (AliasNode.is(firstFrom) && TableNode.is(firstFrom.node)) { | ||
| sourceTable = firstFrom.node; | ||
| } | ||
| else if (TableNode.is(firstFrom)) { | ||
| sourceTable = firstFrom; | ||
| } | ||
| if (sourceTable) { | ||
| const tableName = this.getTableName(sourceTable); | ||
| if (tableName) { | ||
| return this.findEntityMetadata(tableName); | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Extract alias name from an alias node | ||
| */ | ||
| extractAliasName(alias) { | ||
| if (typeof alias === 'object' && 'name' in alias) { | ||
| return alias.name; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Extract table name from a TableNode | ||
| */ | ||
| getTableName(node) { | ||
| if (!node) { | ||
| return undefined; | ||
| } | ||
| if (TableNode.is(node) && SchemableIdentifierNode.is(node.table)) { | ||
| const identifier = node.table.identifier; | ||
| if (typeof identifier === 'object' && 'name' in identifier) { | ||
| return identifier.name; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Find entity metadata by table name or entity name | ||
| */ | ||
| findEntityMetadata(name) { | ||
| const byEntity = this.metadata.find(name); | ||
| if (byEntity) { | ||
| return byEntity; | ||
| } | ||
| const allMetadata = Array.from(this.metadata); | ||
| const byTable = allMetadata.find(m => m.tableName === name); | ||
| if (byTable) { | ||
| return byTable; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Transform result rows by mapping database column names to property names | ||
| * This is called for SELECT queries when columnNamingStrategy is 'property' | ||
| */ | ||
| transformResult(rows, entityMap) { | ||
| // Only transform if columnNamingStrategy is 'property' or convertValues is true, and we have data | ||
| if ((this.options.columnNamingStrategy !== 'property' && !this.options.convertValues) || !rows || rows.length === 0) { | ||
| return rows; | ||
| } | ||
| // If no entities found (e.g. raw query without known tables), return rows as is | ||
| if (entityMap.size === 0) { | ||
| return rows; | ||
| } | ||
| // Build a global mapping from database field names to property objects | ||
| const fieldToPropertyMap = this.buildGlobalFieldMap(entityMap); | ||
| const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap); | ||
| // Transform each row | ||
| return rows.map(row => this.transformRow(row, fieldToPropertyMap, relationFieldMap)); | ||
| } | ||
| buildGlobalFieldMap(entityMap) { | ||
| const map = {}; | ||
| for (const [alias, meta] of entityMap.entries()) { | ||
| Object.assign(map, this.buildFieldToPropertyMap(meta, alias)); | ||
| } | ||
| return map; | ||
| } | ||
| buildGlobalRelationFieldMap(entityMap) { | ||
| const map = {}; | ||
| for (const [alias, meta] of entityMap.entries()) { | ||
| Object.assign(map, this.buildRelationFieldMap(meta, alias)); | ||
| } | ||
| return map; | ||
| } | ||
| /** | ||
| * Build a mapping from database field names to property objects | ||
| * Format: { 'field_name': EntityProperty } | ||
| */ | ||
| buildFieldToPropertyMap(meta, alias) { | ||
| const map = {}; | ||
| for (const prop of meta.props) { | ||
| if (prop.fieldNames && prop.fieldNames.length > 0) { | ||
| for (const fieldName of prop.fieldNames) { | ||
| if (!(fieldName in map)) { | ||
| map[fieldName] = prop; | ||
| } | ||
| if (alias) { | ||
| const dotted = `${alias}.${fieldName}`; | ||
| if (!(dotted in map)) { | ||
| map[dotted] = prop; | ||
| } | ||
| const underscored = `${alias}_${fieldName}`; | ||
| if (!(underscored in map)) { | ||
| map[underscored] = prop; | ||
| } | ||
| const doubleUnderscored = `${alias}__${fieldName}`; | ||
| if (!(doubleUnderscored in map)) { | ||
| map[doubleUnderscored] = prop; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (!(prop.name in map)) { | ||
| map[prop.name] = prop; | ||
| } | ||
| } | ||
| return map; | ||
| } | ||
| /** | ||
| * Build a mapping for relation fields | ||
| * For ManyToOne relations, we need to map from the foreign key field to the relation property | ||
| * Format: { 'foreign_key_field': 'relationPropertyName' } | ||
| */ | ||
| buildRelationFieldMap(meta, alias) { | ||
| const map = {}; | ||
| for (const prop of meta.props) { | ||
| // For ManyToOne/OneToOne relations, find the foreign key field | ||
| if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE) { | ||
| if (prop.fieldNames && prop.fieldNames.length > 0) { | ||
| const fieldName = prop.fieldNames[0]; | ||
| map[fieldName] = prop.name; | ||
| if (alias) { | ||
| map[`${alias}.${fieldName}`] = prop.name; | ||
| map[`${alias}_${fieldName}`] = prop.name; | ||
| map[`${alias}__${fieldName}`] = prop.name; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return map; | ||
| } | ||
| /** | ||
| * Transform a single row by mapping column names to property names | ||
| */ | ||
| transformRow(row, fieldToPropertyMap, relationFieldMap) { | ||
| const transformed = { ...row }; | ||
| // First pass: map regular fields from fieldName to propertyName and convert values | ||
| for (const [fieldName, prop] of Object.entries(fieldToPropertyMap)) { | ||
| if (!(fieldName in transformed)) { | ||
| continue; | ||
| } | ||
| const converted = this.prepareOutputValue(prop, transformed[fieldName]); | ||
| if (this.options.columnNamingStrategy === 'property' && prop.name !== fieldName) { | ||
| if (!(prop.name in transformed)) { | ||
| transformed[prop.name] = converted; | ||
| } | ||
| else { | ||
| transformed[prop.name] = converted; | ||
| } | ||
| delete transformed[fieldName]; | ||
| continue; | ||
| } | ||
| if (this.options.convertValues) { | ||
| transformed[fieldName] = converted; | ||
| } | ||
| } | ||
| // Second pass: handle relation fields | ||
| // Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise | ||
| if (this.options.columnNamingStrategy === 'property') { | ||
| for (const [fieldName, relationPropertyName] of Object.entries(relationFieldMap)) { | ||
| if (fieldName in transformed && !(relationPropertyName in transformed)) { | ||
| // Move the foreign key value to the relation property name | ||
| transformed[relationPropertyName] = transformed[fieldName]; | ||
| delete transformed[fieldName]; | ||
| } | ||
| } | ||
| } | ||
| return transformed; | ||
| } | ||
| prepareOutputValue(prop, value) { | ||
| if (!this.options.convertValues || !prop || value == null) { | ||
| return value; | ||
| } | ||
| if (prop.customType) { | ||
| return prop.customType.convertToJSValue(value, this.platform); | ||
| } | ||
| // Aligned with EntityComparator.getResultMapper logic | ||
| if (prop.runtimeType === 'boolean') { | ||
| // Use !! conversion like EntityComparator: value == null ? value : !!value | ||
| return value == null ? value : !!value; | ||
| } | ||
| if (prop.runtimeType === 'Date' && !this.platform.isNumericProperty(prop)) { | ||
| // Aligned with EntityComparator: exclude numeric timestamp properties | ||
| // If already Date instance or null, return as is | ||
| if (value == null || value instanceof Date) { | ||
| return value; | ||
| } | ||
| // Handle timezone like EntityComparator.parseDate | ||
| const tz = this.platform.getTimezone(); | ||
| if (!tz || tz === 'local') { | ||
| return this.platform.parseDate(value); | ||
| } | ||
| // For non-local timezone, check if value already has timezone info | ||
| // Number (timestamp) doesn't need timezone handling, string needs check | ||
| if (typeof value === 'number' || (typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))) { | ||
| return this.platform.parseDate(value); | ||
| } | ||
| // Append timezone if not present (only for string values) | ||
| return this.platform.parseDate(value + tz); | ||
| } | ||
| // For all other runtimeTypes (number, string, bigint, Buffer, object, any, etc.) | ||
| // EntityComparator just assigns directly without conversion | ||
| return value; | ||
| } | ||
| } |
| import { CriteriaNode } from './CriteriaNode.js'; | ||
| import type { IQueryBuilder, ICriteriaNodeProcessOptions } from '../typings.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export declare class ArrayCriteriaNode<T extends object> extends CriteriaNode<T> { | ||
| process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any; | ||
| unwrap(): any; | ||
| willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): any; | ||
| isStrict(): boolean; | ||
| } |
| import { CriteriaNode } from './CriteriaNode.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export class ArrayCriteriaNode extends CriteriaNode { | ||
| process(qb, options) { | ||
| return this.payload.map((node) => { | ||
| return node.process(qb, options); | ||
| }); | ||
| } | ||
| unwrap() { | ||
| return this.payload.map((node) => { | ||
| return node.unwrap(); | ||
| }); | ||
| } | ||
| willAutoJoin(qb, alias, options) { | ||
| return this.payload.some((node) => { | ||
| return node.willAutoJoin(qb, alias, options); | ||
| }); | ||
| } | ||
| isStrict() { | ||
| return this.strict || this.payload.some((node) => node.isStrict()); | ||
| } | ||
| } |
| import { type EntityKey, type EntityProperty, type MetadataStorage } from '@mikro-orm/core'; | ||
| import type { ICriteriaNode, ICriteriaNodeProcessOptions, IQueryBuilder } from '../typings.js'; | ||
| /** | ||
| * Helper for working with deeply nested where/orderBy/having criteria. Uses composite pattern to build tree from the payload. | ||
| * Auto-joins relations and converts payload from { books: { publisher: { name: '...' } } } to { 'publisher_alias.name': '...' } | ||
| * @internal | ||
| */ | ||
| export declare class CriteriaNode<T extends object> implements ICriteriaNode<T> { | ||
| protected readonly metadata: MetadataStorage; | ||
| readonly entityName: string; | ||
| readonly parent?: ICriteriaNode<T> | undefined; | ||
| readonly key?: EntityKey<T> | undefined; | ||
| readonly strict: boolean; | ||
| payload: any; | ||
| prop?: EntityProperty<T>; | ||
| index?: number; | ||
| constructor(metadata: MetadataStorage, entityName: string, parent?: ICriteriaNode<T> | undefined, key?: EntityKey<T> | undefined, validate?: boolean, strict?: boolean); | ||
| process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any; | ||
| unwrap(): any; | ||
| shouldInline(payload: any): boolean; | ||
| willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean; | ||
| shouldRename(payload: any): boolean; | ||
| renameFieldToPK<T>(qb: IQueryBuilder<T>, ownerAlias?: string): string; | ||
| getPath(addIndex?: boolean): string; | ||
| private isPivotJoin; | ||
| getPivotPath(path: string): string; | ||
| aliased(field: string, alias?: string): string; | ||
| isStrict(): boolean; | ||
| } |
| import { RawQueryFragment, ReferenceKind, Utils, inspect, } from '@mikro-orm/core'; | ||
| /** | ||
| * Helper for working with deeply nested where/orderBy/having criteria. Uses composite pattern to build tree from the payload. | ||
| * Auto-joins relations and converts payload from { books: { publisher: { name: '...' } } } to { 'publisher_alias.name': '...' } | ||
| * @internal | ||
| */ | ||
| export class CriteriaNode { | ||
| metadata; | ||
| entityName; | ||
| parent; | ||
| key; | ||
| strict; | ||
| payload; | ||
| prop; | ||
| index; | ||
| constructor(metadata, entityName, parent, key, validate = true, strict = false) { | ||
| this.metadata = metadata; | ||
| this.entityName = entityName; | ||
| this.parent = parent; | ||
| this.key = key; | ||
| this.strict = strict; | ||
| const meta = parent && metadata.find(parent.entityName); | ||
| if (meta && key) { | ||
| const pks = Utils.splitPrimaryKeys(key); | ||
| if (pks.length > 1) { | ||
| return; | ||
| } | ||
| for (const k of pks) { | ||
| this.prop = meta.props.find(prop => prop.name === k || (prop.fieldNames?.length === 1 && prop.fieldNames[0] === k && prop.persist !== false)); | ||
| const isProp = this.prop || meta.props.find(prop => (prop.fieldNames || []).includes(k)); | ||
| // do not validate if the key is prefixed or type casted (e.g. `k::text`) | ||
| if (validate && !isProp && !k.includes('.') && !k.includes('::') && !Utils.isOperator(k) && !RawQueryFragment.isKnownFragment(k)) { | ||
| throw new Error(`Trying to query by not existing property ${entityName}.${k}`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| process(qb, options) { | ||
| return this.payload; | ||
| } | ||
| unwrap() { | ||
| return this.payload; | ||
| } | ||
| shouldInline(payload) { | ||
| return false; | ||
| } | ||
| willAutoJoin(qb, alias, options) { | ||
| return false; | ||
| } | ||
| shouldRename(payload) { | ||
| const type = this.prop ? this.prop.kind : null; | ||
| const composite = this.prop?.joinColumns ? this.prop.joinColumns.length > 1 : false; | ||
| const customExpression = RawQueryFragment.isKnownFragment(this.key); | ||
| const scalar = payload === null || Utils.isPrimaryKey(payload) || payload instanceof RegExp || payload instanceof Date || customExpression; | ||
| const plainObject = Utils.isPlainObject(payload); | ||
| const keys = plainObject ? Object.keys(payload) : []; | ||
| const operator = plainObject && keys.every(k => Utils.isOperator(k, false)); | ||
| if (composite) { | ||
| return true; | ||
| } | ||
| switch (type) { | ||
| case ReferenceKind.MANY_TO_ONE: return false; | ||
| case ReferenceKind.ONE_TO_ONE: return !this.prop.owner; | ||
| case ReferenceKind.ONE_TO_MANY: return scalar || operator; | ||
| case ReferenceKind.MANY_TO_MANY: return scalar || operator; | ||
| default: return false; | ||
| } | ||
| } | ||
| renameFieldToPK(qb, ownerAlias) { | ||
| const joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true }); | ||
| if (!joinAlias && this.parent && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) && this.prop.owner) { | ||
| const alias = qb.getAliasForJoinPath(this.parent.getPath()) ?? ownerAlias ?? qb.alias; | ||
| return Utils.getPrimaryKeyHash(this.prop.joinColumns.map(col => `${alias}.${col}`)); | ||
| } | ||
| const alias = joinAlias ?? ownerAlias ?? qb.alias; | ||
| if (this.prop.kind === ReferenceKind.MANY_TO_MANY) { | ||
| return Utils.getPrimaryKeyHash(this.prop.inverseJoinColumns.map(col => `${alias}.${col}`)); | ||
| } | ||
| return Utils.getPrimaryKeyHash(this.prop.referencedColumnNames.map(col => `${alias}.${col}`)); | ||
| } | ||
| getPath(addIndex = false) { | ||
| // use index on parent only if we are processing to-many relation | ||
| const addParentIndex = this.prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(this.prop.kind); | ||
| const parentPath = this.parent?.getPath(addParentIndex) ?? this.entityName; | ||
| const index = addIndex && this.index != null ? `[${this.index}]` : ''; | ||
| // ignore group operators to allow easier mapping (e.g. for orderBy) | ||
| const key = this.key && !['$and', '$or', '$not'].includes(this.key) ? '.' + this.key : ''; | ||
| const ret = parentPath + index + key; | ||
| if (this.isPivotJoin()) { | ||
| // distinguish pivot table join from target entity join | ||
| return this.getPivotPath(ret); | ||
| } | ||
| return ret; | ||
| } | ||
| isPivotJoin() { | ||
| if (!this.key || !this.prop) { | ||
| return false; | ||
| } | ||
| const customExpression = RawQueryFragment.isKnownFragment(this.key); | ||
| const scalar = this.payload === null || Utils.isPrimaryKey(this.payload) || this.payload instanceof RegExp || this.payload instanceof Date || customExpression; | ||
| const operator = Utils.isObject(this.payload) && Object.keys(this.payload).every(k => Utils.isOperator(k, false)); | ||
| return this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator); | ||
| } | ||
| getPivotPath(path) { | ||
| return `${path}[pivot]`; | ||
| } | ||
| aliased(field, alias) { | ||
| return alias ? `${alias}.${field}` : field; | ||
| } | ||
| isStrict() { | ||
| return this.strict; | ||
| } | ||
| /** @ignore */ | ||
| [Symbol.for('nodejs.util.inspect.custom')]() { | ||
| const o = {}; | ||
| ['entityName', 'key', 'index', 'payload'] | ||
| .filter(k => this[k] !== undefined) | ||
| .forEach(k => o[k] = this[k]); | ||
| return `${this.constructor.name} ${inspect(o)}`; | ||
| } | ||
| } |
| import { type Dictionary, type EntityKey, type EntityMetadata, type MetadataStorage } from '@mikro-orm/core'; | ||
| import type { ICriteriaNode } from '../typings.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export declare class CriteriaNodeFactory { | ||
| static createNode<T extends object>(metadata: MetadataStorage, entityName: string, payload: any, parent?: ICriteriaNode<T>, key?: EntityKey<T>): ICriteriaNode<T>; | ||
| static createScalarNode<T extends object>(metadata: MetadataStorage, entityName: string, payload: any, parent?: ICriteriaNode<T>, key?: EntityKey<T>): ICriteriaNode<T>; | ||
| static createArrayNode<T extends object>(metadata: MetadataStorage, entityName: string, payload: any[], parent?: ICriteriaNode<T>, key?: EntityKey<T>): ICriteriaNode<T>; | ||
| static createObjectNode<T extends object>(metadata: MetadataStorage, entityName: string, payload: Dictionary, parent?: ICriteriaNode<T>, key?: EntityKey<T>): ICriteriaNode<T>; | ||
| static createObjectItemNode<T extends object>(metadata: MetadataStorage, entityName: string, node: ICriteriaNode<T>, payload: Dictionary, key: EntityKey<T>, meta?: EntityMetadata<T>): ICriteriaNode<T>; | ||
| } |
| import { GroupOperator, isRaw, JsonType, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core'; | ||
| import { ObjectCriteriaNode } from './ObjectCriteriaNode.js'; | ||
| import { ArrayCriteriaNode } from './ArrayCriteriaNode.js'; | ||
| import { ScalarCriteriaNode } from './ScalarCriteriaNode.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export class CriteriaNodeFactory { | ||
| static createNode(metadata, entityName, payload, parent, key) { | ||
| const customExpression = RawQueryFragment.isKnownFragment(key || ''); | ||
| const scalar = Utils.isPrimaryKey(payload) || isRaw(payload) || payload instanceof RegExp || payload instanceof Date || customExpression; | ||
| if (Array.isArray(payload) && !scalar) { | ||
| return this.createArrayNode(metadata, entityName, payload, parent, key); | ||
| } | ||
| if (Utils.isPlainObject(payload) && !scalar) { | ||
| return this.createObjectNode(metadata, entityName, payload, parent, key); | ||
| } | ||
| return this.createScalarNode(metadata, entityName, payload, parent, key); | ||
| } | ||
| static createScalarNode(metadata, entityName, payload, parent, key) { | ||
| const node = new ScalarCriteriaNode(metadata, entityName, parent, key); | ||
| node.payload = payload; | ||
| return node; | ||
| } | ||
| static createArrayNode(metadata, entityName, payload, parent, key) { | ||
| const node = new ArrayCriteriaNode(metadata, entityName, parent, key); | ||
| node.payload = payload.map((item, index) => { | ||
| const n = this.createNode(metadata, entityName, item, node); | ||
| // we care about branching only for $and | ||
| if (key === '$and' && payload.length > 1) { | ||
| n.index = index; | ||
| } | ||
| return n; | ||
| }); | ||
| return node; | ||
| } | ||
| static createObjectNode(metadata, entityName, payload, parent, key) { | ||
| const meta = metadata.find(entityName); | ||
| const node = new ObjectCriteriaNode(metadata, entityName, parent, key, true, payload.__strict); | ||
| node.payload = {}; | ||
| for (const key of Object.keys(payload)) { | ||
| node.payload[key] = this.createObjectItemNode(metadata, entityName, node, payload, key, meta); | ||
| } | ||
| return node; | ||
| } | ||
| static createObjectItemNode(metadata, entityName, node, payload, key, meta) { | ||
| const prop = meta?.properties[key]; | ||
| const childEntity = prop && prop.kind !== ReferenceKind.SCALAR ? prop.type : entityName; | ||
| const isNotEmbedded = prop?.kind !== ReferenceKind.EMBEDDED; | ||
| if (isNotEmbedded && prop?.customType instanceof JsonType) { | ||
| return this.createScalarNode(metadata, childEntity, payload[key], node, key); | ||
| } | ||
| if (prop?.kind === ReferenceKind.SCALAR && payload[key] != null && Object.keys(payload[key]).some(f => f in GroupOperator)) { | ||
| throw ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload); | ||
| } | ||
| if (isNotEmbedded) { | ||
| return this.createNode(metadata, childEntity, payload[key], node, key); | ||
| } | ||
| if (payload[key] == null) { | ||
| const map = Object.keys(prop.embeddedProps).reduce((oo, k) => { | ||
| oo[prop.embeddedProps[k].name] = null; | ||
| return oo; | ||
| }, {}); | ||
| return this.createNode(metadata, entityName, map, node, key); | ||
| } | ||
| // array operators can be used on embedded properties | ||
| const allowedOperators = ['$contains', '$contained', '$overlap']; | ||
| const operator = Object.keys(payload[key]).some(f => Utils.isOperator(f) && !allowedOperators.includes(f)); | ||
| if (operator) { | ||
| throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload); | ||
| } | ||
| const map = Object.keys(payload[key]).reduce((oo, k) => { | ||
| const embeddedProp = prop.embeddedProps[k] ?? Object.values(prop.embeddedProps).find(p => p.name === k); | ||
| if (!embeddedProp && !allowedOperators.includes(k)) { | ||
| throw ValidationError.invalidEmbeddableQuery(entityName, k, prop.type); | ||
| } | ||
| if (embeddedProp) { | ||
| oo[embeddedProp.name] = payload[key][k]; | ||
| } | ||
| else if (typeof payload[key][k] === 'object') { | ||
| oo[k] = JSON.stringify(payload[key][k]); | ||
| } | ||
| else { | ||
| oo[k] = payload[key][k]; | ||
| } | ||
| return oo; | ||
| }, {}); | ||
| return this.createNode(metadata, entityName, map, node, key); | ||
| } | ||
| } |
| export declare enum QueryType { | ||
| TRUNCATE = "TRUNCATE", | ||
| SELECT = "SELECT", | ||
| COUNT = "COUNT", | ||
| INSERT = "INSERT", | ||
| UPDATE = "UPDATE", | ||
| DELETE = "DELETE", | ||
| UPSERT = "UPSERT" | ||
| } | ||
| export declare enum JoinType { | ||
| leftJoin = "left join", | ||
| innerJoin = "inner join", | ||
| nestedLeftJoin = "nested left join", | ||
| nestedInnerJoin = "nested inner join", | ||
| pivotJoin = "pivot join", | ||
| innerJoinLateral = "inner join lateral", | ||
| leftJoinLateral = "left join lateral" | ||
| } |
| export var QueryType; | ||
| (function (QueryType) { | ||
| QueryType["TRUNCATE"] = "TRUNCATE"; | ||
| QueryType["SELECT"] = "SELECT"; | ||
| QueryType["COUNT"] = "COUNT"; | ||
| QueryType["INSERT"] = "INSERT"; | ||
| QueryType["UPDATE"] = "UPDATE"; | ||
| QueryType["DELETE"] = "DELETE"; | ||
| QueryType["UPSERT"] = "UPSERT"; | ||
| })(QueryType || (QueryType = {})); | ||
| export var JoinType; | ||
| (function (JoinType) { | ||
| JoinType["leftJoin"] = "left join"; | ||
| JoinType["innerJoin"] = "inner join"; | ||
| JoinType["nestedLeftJoin"] = "nested left join"; | ||
| JoinType["nestedInnerJoin"] = "nested inner join"; | ||
| JoinType["pivotJoin"] = "pivot join"; | ||
| JoinType["innerJoinLateral"] = "inner join lateral"; | ||
| JoinType["leftJoinLateral"] = "left join lateral"; | ||
| })(JoinType || (JoinType = {})); |
| export * from './enums.js'; | ||
| export * from './QueryBuilderHelper.js'; | ||
| export * from './QueryBuilder.js'; | ||
| export * from './CriteriaNode.js'; | ||
| export * from './ArrayCriteriaNode.js'; | ||
| export * from './ObjectCriteriaNode.js'; | ||
| export * from './ScalarCriteriaNode.js'; | ||
| export * from './CriteriaNodeFactory.js'; | ||
| export * from './NativeQueryBuilder.js'; | ||
| export * from './raw.js'; |
| export * from './enums.js'; | ||
| export * from './QueryBuilderHelper.js'; | ||
| export * from './QueryBuilder.js'; | ||
| export * from './CriteriaNode.js'; | ||
| export * from './ArrayCriteriaNode.js'; | ||
| export * from './ObjectCriteriaNode.js'; | ||
| export * from './ScalarCriteriaNode.js'; | ||
| export * from './CriteriaNodeFactory.js'; | ||
| export * from './NativeQueryBuilder.js'; | ||
| export * from './raw.js'; |
| import { type Dictionary, LockMode, type QueryFlag, RawQueryFragment } from '@mikro-orm/core'; | ||
| import { QueryType } from './enums.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
| interface Options { | ||
| tableName?: string | RawQueryFragment; | ||
| indexHint?: string; | ||
| select?: (string | RawQueryFragment)[]; | ||
| distinct?: boolean; | ||
| distinctOn?: string[]; | ||
| joins?: { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }[]; | ||
| groupBy?: (string | RawQueryFragment)[]; | ||
| where?: { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| having?: { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| orderBy?: string; | ||
| limit?: number; | ||
| offset?: number; | ||
| data?: Dictionary; | ||
| onConflict?: OnConflictClause; | ||
| lockMode?: LockMode; | ||
| lockTables?: string[]; | ||
| returning?: (string | RawQueryFragment)[]; | ||
| comment?: string[]; | ||
| hintComment?: string[]; | ||
| flags?: Set<QueryFlag>; | ||
| wrap?: [prefix: string, suffix: string]; | ||
| } | ||
| interface TableOptions { | ||
| schema?: string; | ||
| indexHint?: string; | ||
| alias?: string; | ||
| } | ||
| interface OnConflictClause { | ||
| fields: string[] | RawQueryFragment; | ||
| ignore?: boolean; | ||
| merge?: Dictionary | (string | RawQueryFragment)[]; | ||
| where?: { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| } | ||
| /** @internal */ | ||
| export declare class NativeQueryBuilder { | ||
| protected readonly platform: AbstractSqlPlatform; | ||
| protected type?: QueryType; | ||
| protected parts: string[]; | ||
| protected params: unknown[]; | ||
| protected options: Options; | ||
| constructor(platform: AbstractSqlPlatform); | ||
| select(fields: string | RawQueryFragment | (string | RawQueryFragment)[]): this; | ||
| count(fields?: string | RawQueryFragment | (string | RawQueryFragment)[], distinct?: boolean): this; | ||
| into(tableName: string | RawQueryFragment | NativeQueryBuilder, options?: TableOptions): this; | ||
| from(tableName: string | RawQueryFragment | NativeQueryBuilder, options?: TableOptions): this; | ||
| where(sql: string, params: unknown[]): this; | ||
| having(sql: string, params: unknown[]): this; | ||
| groupBy(groupBy: (string | RawQueryFragment)[]): this; | ||
| join(sql: string, params: unknown[]): this; | ||
| orderBy(orderBy: string): this; | ||
| toString(): string; | ||
| compile(): { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| protected addLockClause(): void; | ||
| protected addOnConflictClause(): void; | ||
| protected combineParts(): { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| limit(limit: number): this; | ||
| offset(offset: number): this; | ||
| insert(data: Dictionary): this; | ||
| update(data: Dictionary): this; | ||
| delete(): this; | ||
| truncate(): this; | ||
| distinct(): this; | ||
| distinctOn(fields: string[]): this; | ||
| onConflict(options: OnConflictClause): OnConflictClause; | ||
| returning(fields: (string | RawQueryFragment)[]): this; | ||
| lockMode(lockMode: LockMode, lockTables?: string[]): this; | ||
| comment(comment: string | string[]): this; | ||
| hintComment(comment: string | string[]): this; | ||
| setFlags(flags: Set<QueryFlag>): this; | ||
| clear(clause: keyof Options): this; | ||
| wrap(prefix: string, suffix: string): this; | ||
| as(alias: string): this; | ||
| toRaw(): RawQueryFragment; | ||
| protected compileSelect(): void; | ||
| protected getFields(): string; | ||
| protected compileInsert(): void; | ||
| protected addOutputClause(type: 'inserted' | 'deleted'): void; | ||
| protected processInsertData(): string[]; | ||
| protected compileUpdate(): void; | ||
| protected compileDelete(): void; | ||
| protected compileTruncate(): void; | ||
| protected addHintComment(): void; | ||
| protected getTableName(): string; | ||
| protected quote(id: string | RawQueryFragment | NativeQueryBuilder): string; | ||
| } | ||
| export {}; |
| import { LockMode, raw, RawQueryFragment, Utils } from '@mikro-orm/core'; | ||
| import { QueryType } from './enums.js'; | ||
| /** @internal */ | ||
| export class NativeQueryBuilder { | ||
| platform; | ||
| type; | ||
| parts = []; | ||
| params = []; | ||
| options = {}; | ||
| constructor(platform) { | ||
| this.platform = platform; | ||
| } | ||
| select(fields) { | ||
| this.type = QueryType.SELECT; | ||
| this.options.select ??= []; | ||
| this.options.select.push(...Utils.asArray(fields)); | ||
| return this; | ||
| } | ||
| count(fields = '*', distinct) { | ||
| this.type = QueryType.COUNT; | ||
| this.options.select = Utils.asArray(fields); | ||
| this.options.distinct = distinct; | ||
| return this; | ||
| } | ||
| into(tableName, options) { | ||
| return this.from(tableName, options); | ||
| } | ||
| from(tableName, options) { | ||
| if (tableName instanceof NativeQueryBuilder) { | ||
| const { sql, params } = tableName.compile(); | ||
| tableName = raw(sql, params); | ||
| } | ||
| if (typeof tableName === 'string') { | ||
| const alias = options?.alias ? ` as ${this.platform.quoteIdentifier(options.alias)}` : ''; | ||
| const schema = options?.schema && options.schema !== this.platform.getDefaultSchemaName() ? `${options.schema}.` : ''; | ||
| tableName = this.quote(schema + tableName) + alias; | ||
| } | ||
| this.options.tableName = tableName; | ||
| this.options.indexHint = options?.indexHint; | ||
| return this; | ||
| } | ||
| where(sql, params) { | ||
| this.options.where = { sql, params }; | ||
| return this; | ||
| } | ||
| having(sql, params) { | ||
| this.options.having = { sql, params }; | ||
| return this; | ||
| } | ||
| groupBy(groupBy) { | ||
| this.options.groupBy = groupBy; | ||
| return this; | ||
| } | ||
| join(sql, params) { | ||
| this.options.joins ??= []; | ||
| this.options.joins.push({ sql, params }); | ||
| return this; | ||
| } | ||
| orderBy(orderBy) { | ||
| this.options.orderBy = orderBy; | ||
| return this; | ||
| } | ||
| toString() { | ||
| const { sql, params } = this.compile(); | ||
| return this.platform.formatQuery(sql, params); | ||
| } | ||
| compile() { | ||
| if (!this.type) { | ||
| throw new Error('No query type provided'); | ||
| } | ||
| this.parts.length = 0; | ||
| this.params.length = 0; | ||
| if (this.options.comment) { | ||
| this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`)); | ||
| } | ||
| switch (this.type) { | ||
| case QueryType.SELECT: | ||
| case QueryType.COUNT: | ||
| this.compileSelect(); | ||
| break; | ||
| case QueryType.INSERT: | ||
| this.compileInsert(); | ||
| break; | ||
| case QueryType.UPDATE: | ||
| this.compileUpdate(); | ||
| break; | ||
| case QueryType.DELETE: | ||
| this.compileDelete(); | ||
| break; | ||
| case QueryType.TRUNCATE: | ||
| this.compileTruncate(); | ||
| break; | ||
| } | ||
| this.addOnConflictClause(); | ||
| if (this.options.returning && this.platform.usesReturningStatement()) { | ||
| const fields = this.options.returning.map(field => this.quote(field)); | ||
| this.parts.push(`returning ${fields.join(', ')}`); | ||
| } | ||
| this.addLockClause(); | ||
| return this.combineParts(); | ||
| } | ||
| addLockClause() { | ||
| if (!this.options.lockMode) { | ||
| return; | ||
| } | ||
| if ([LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_PARTIAL_READ, LockMode.PESSIMISTIC_READ_OR_FAIL].includes(this.options.lockMode)) { | ||
| this.parts.push('for share'); | ||
| } | ||
| if ([LockMode.PESSIMISTIC_WRITE, LockMode.PESSIMISTIC_PARTIAL_WRITE, LockMode.PESSIMISTIC_WRITE_OR_FAIL].includes(this.options.lockMode)) { | ||
| this.parts.push('for update'); | ||
| } | ||
| if (this.options.lockTables?.length) { | ||
| const fields = this.options.lockTables.map(field => this.quote(field)); | ||
| this.parts.push(`of ${fields.join(', ')}`); | ||
| } | ||
| if ([LockMode.PESSIMISTIC_PARTIAL_READ, LockMode.PESSIMISTIC_PARTIAL_WRITE].includes(this.options.lockMode)) { | ||
| this.parts.push('skip locked'); | ||
| } | ||
| if ([LockMode.PESSIMISTIC_READ_OR_FAIL, LockMode.PESSIMISTIC_WRITE_OR_FAIL].includes(this.options.lockMode)) { | ||
| this.parts.push('nowait'); | ||
| } | ||
| } | ||
| addOnConflictClause() { | ||
| const clause = this.options.onConflict; | ||
| if (!clause) { | ||
| return; | ||
| } | ||
| this.parts.push('on conflict'); | ||
| if (clause.fields instanceof RawQueryFragment) { | ||
| this.parts.push(clause.fields.sql); | ||
| this.params.push(...clause.fields.params); | ||
| } | ||
| else if (clause.fields.length > 0) { | ||
| const fields = clause.fields.map(field => this.quote(field)); | ||
| this.parts.push(`(${fields.join(', ')})`); | ||
| } | ||
| if (clause.ignore) { | ||
| this.parts.push('do nothing'); | ||
| } | ||
| if (Utils.isObject(clause.merge)) { | ||
| this.parts.push('do update set'); | ||
| const fields = Object.keys(clause.merge).map(field => { | ||
| this.params.push(clause.merge[field]); | ||
| return `${this.quote(field)} = ?`; | ||
| }); | ||
| this.parts.push(fields.join(', ')); | ||
| } | ||
| else if (clause.merge) { | ||
| this.parts.push('do update set'); | ||
| if (clause.merge.length) { | ||
| const fields = clause.merge.map(field => `${this.quote(field)} = excluded.${this.quote(field)}`); | ||
| this.parts.push(fields.join(', ')); | ||
| } | ||
| else { | ||
| const dataAsArray = Utils.asArray(this.options.data); | ||
| const keys = Object.keys(dataAsArray[0]); | ||
| const fields = keys.map(field => `${this.quote(field)} = excluded.${this.quote(field)}`); | ||
| this.parts.push(fields.join(', ')); | ||
| } | ||
| } | ||
| if (clause.where) { | ||
| this.parts.push(`where ${clause.where.sql}`); | ||
| this.params.push(...clause.where.params); | ||
| } | ||
| } | ||
| combineParts() { | ||
| let sql = this.parts.join(' '); | ||
| if (this.options.wrap) { | ||
| const [a, b] = this.options.wrap; | ||
| sql = `${a}${sql}${b}`; | ||
| } | ||
| return { sql, params: this.params }; | ||
| } | ||
| limit(limit) { | ||
| this.options.limit = limit; | ||
| return this; | ||
| } | ||
| offset(offset) { | ||
| this.options.offset = offset; | ||
| return this; | ||
| } | ||
| insert(data) { | ||
| this.type = QueryType.INSERT; | ||
| this.options.data = data; | ||
| return this; | ||
| } | ||
| update(data) { | ||
| this.type = QueryType.UPDATE; | ||
| this.options.data ??= {}; | ||
| Object.assign(this.options.data, data); | ||
| return this; | ||
| } | ||
| delete() { | ||
| this.type = QueryType.DELETE; | ||
| return this; | ||
| } | ||
| truncate() { | ||
| this.type = QueryType.TRUNCATE; | ||
| return this; | ||
| } | ||
| distinct() { | ||
| this.options.distinct = true; | ||
| return this; | ||
| } | ||
| distinctOn(fields) { | ||
| this.options.distinctOn = fields; | ||
| return this; | ||
| } | ||
| onConflict(options) { | ||
| this.options.onConflict = options; | ||
| return options; | ||
| } | ||
| returning(fields) { | ||
| this.options.returning = fields; | ||
| return this; | ||
| } | ||
| lockMode(lockMode, lockTables) { | ||
| this.options.lockMode = lockMode; | ||
| this.options.lockTables = lockTables; | ||
| return this; | ||
| } | ||
| comment(comment) { | ||
| this.options.comment ??= []; | ||
| this.options.comment.push(...Utils.asArray(comment)); | ||
| return this; | ||
| } | ||
| hintComment(comment) { | ||
| this.options.hintComment ??= []; | ||
| this.options.hintComment.push(...Utils.asArray(comment)); | ||
| return this; | ||
| } | ||
| setFlags(flags) { | ||
| this.options.flags = flags; | ||
| return this; | ||
| } | ||
| clear(clause) { | ||
| delete this.options[clause]; | ||
| return this; | ||
| } | ||
| wrap(prefix, suffix) { | ||
| this.options.wrap = [prefix, suffix]; | ||
| return this; | ||
| } | ||
| as(alias) { | ||
| this.wrap('(', `) as ${this.platform.quoteIdentifier(alias)}`); | ||
| return this; | ||
| } | ||
| toRaw() { | ||
| const { sql, params } = this.compile(); | ||
| return raw(sql, params); | ||
| } | ||
| compileSelect() { | ||
| this.parts.push('select'); | ||
| this.addHintComment(); | ||
| this.parts.push(`${this.getFields()} from ${this.getTableName()}`); | ||
| if (this.options.joins) { | ||
| for (const join of this.options.joins) { | ||
| this.parts.push(join.sql); | ||
| this.params.push(...join.params); | ||
| } | ||
| } | ||
| if (this.options.where?.sql.trim()) { | ||
| this.parts.push(`where ${this.options.where.sql}`); | ||
| this.options.where.params.forEach(p => this.params.push(p)); | ||
| } | ||
| if (this.options.groupBy) { | ||
| const fields = this.options.groupBy.map(field => this.quote(field)); | ||
| this.parts.push(`group by ${fields.join(', ')}`); | ||
| } | ||
| if (this.options.having) { | ||
| this.parts.push(`having ${this.options.having.sql}`); | ||
| this.params.push(...this.options.having.params); | ||
| } | ||
| if (this.options.orderBy) { | ||
| this.parts.push(`order by ${this.options.orderBy}`); | ||
| } | ||
| if (this.options.limit != null) { | ||
| this.parts.push(`limit ?`); | ||
| this.params.push(this.options.limit); | ||
| } | ||
| if (this.options.offset != null) { | ||
| this.parts.push(`offset ?`); | ||
| this.params.push(this.options.offset); | ||
| } | ||
| } | ||
| getFields() { | ||
| if (!this.options.select || this.options.select.length === 0) { | ||
| throw new Error('No fields selected'); | ||
| } | ||
| let fields = this.options.select.map(field => this.quote(field)).join(', '); | ||
| if (this.options.distinct) { | ||
| fields = `distinct ${fields}`; | ||
| } | ||
| else if (this.options.distinctOn) { | ||
| fields = `distinct on (${this.options.distinctOn.map(field => this.quote(field)).join(', ')}) ${fields}`; | ||
| } | ||
| if (this.type === QueryType.COUNT) { | ||
| fields = `count(${fields}) as ${this.quote('count')}`; | ||
| } | ||
| return fields; | ||
| } | ||
| compileInsert() { | ||
| if (!this.options.data) { | ||
| throw new Error('No data provided'); | ||
| } | ||
| this.parts.push('insert'); | ||
| this.addHintComment(); | ||
| this.parts.push(`into ${this.getTableName()}`); | ||
| if (Object.keys(this.options.data).length === 0) { | ||
| this.addOutputClause('inserted'); | ||
| this.parts.push('default values'); | ||
| return; | ||
| } | ||
| const parts = this.processInsertData(); | ||
| this.parts.push(parts.join(', ')); | ||
| } | ||
| addOutputClause(type) { | ||
| if (this.options.returning && this.platform.usesOutputStatement()) { | ||
| const fields = this.options.returning.map(field => `${type}.${this.quote(field)}`); | ||
| this.parts.push(`output ${fields.join(', ')}`); | ||
| } | ||
| } | ||
| processInsertData() { | ||
| const dataAsArray = Utils.asArray(this.options.data); | ||
| const keys = Object.keys(dataAsArray[0]); | ||
| const values = keys.map(() => '?'); | ||
| const parts = []; | ||
| this.parts.push(`(${keys.map(key => this.quote(key)).join(', ')})`); | ||
| this.addOutputClause('inserted'); | ||
| this.parts.push('values'); | ||
| for (const data of dataAsArray) { | ||
| for (const key of keys) { | ||
| if (typeof data[key] === 'undefined') { | ||
| this.params.push(this.platform.usesDefaultKeyword() ? raw('default') : null); | ||
| } | ||
| else { | ||
| this.params.push(data[key]); | ||
| } | ||
| } | ||
| parts.push(`(${values.join(', ')})`); | ||
| } | ||
| return parts; | ||
| } | ||
| compileUpdate() { | ||
| if (!this.options.data || Object.keys(this.options.data).length === 0) { | ||
| throw new Error('No data provided'); | ||
| } | ||
| this.parts.push('update'); | ||
| this.addHintComment(); | ||
| this.parts.push(this.getTableName()); | ||
| if (this.options.joins) { | ||
| for (const join of this.options.joins) { | ||
| this.parts.push(join.sql); | ||
| this.params.push(...join.params); | ||
| } | ||
| } | ||
| this.parts.push('set'); | ||
| if (this.options.data) { | ||
| const parts = []; | ||
| for (const key of Object.keys(this.options.data)) { | ||
| parts.push(`${this.quote(key)} = ?`); | ||
| this.params.push(this.options.data[key]); | ||
| } | ||
| this.parts.push(parts.join(', ')); | ||
| } | ||
| this.addOutputClause('inserted'); | ||
| if (this.options.where?.sql.trim()) { | ||
| this.parts.push(`where ${this.options.where.sql}`); | ||
| this.params.push(...this.options.where.params); | ||
| } | ||
| } | ||
| compileDelete() { | ||
| this.parts.push('delete'); | ||
| this.addHintComment(); | ||
| this.parts.push(`from ${this.getTableName()}`); | ||
| this.addOutputClause('deleted'); | ||
| if (this.options.where?.sql.trim()) { | ||
| this.parts.push(`where ${this.options.where.sql}`); | ||
| this.params.push(...this.options.where.params); | ||
| } | ||
| } | ||
| compileTruncate() { | ||
| const sql = `truncate table ${this.getTableName()}`; | ||
| this.parts.push(sql); | ||
| } | ||
| addHintComment() { | ||
| if (this.options.hintComment) { | ||
| this.parts.push(`/*+ ${this.options.hintComment.join(' ')} */`); | ||
| } | ||
| } | ||
| getTableName() { | ||
| if (!this.options.tableName) { | ||
| throw new Error('No table name provided'); | ||
| } | ||
| const indexHint = this.options.indexHint ? ' ' + this.options.indexHint : ''; | ||
| if (this.options.tableName instanceof RawQueryFragment) { | ||
| this.params.push(...this.options.tableName.params); | ||
| return this.options.tableName.sql + indexHint; | ||
| } | ||
| return this.options.tableName + indexHint; | ||
| } | ||
| quote(id) { | ||
| if (id instanceof RawQueryFragment) { | ||
| return this.platform.formatQuery(id.sql, id.params); | ||
| } | ||
| if (id instanceof NativeQueryBuilder) { | ||
| const { sql, params } = id.compile(); | ||
| return this.platform.formatQuery(sql, params); | ||
| } | ||
| if (id.endsWith('.*')) { | ||
| const schema = this.platform.quoteIdentifier(id.substring(0, id.indexOf('.'))); | ||
| return schema + '.*'; | ||
| } | ||
| if (id.toLowerCase().includes(' as ')) { | ||
| const parts = id.split(/ as /i); | ||
| const a = this.platform.quoteIdentifier(parts[0]); | ||
| const b = this.platform.quoteIdentifier(parts[1]); | ||
| return `${a} as ${b}`; | ||
| } | ||
| if (id === '*') { | ||
| return id; | ||
| } | ||
| return this.platform.quoteIdentifier(id); | ||
| } | ||
| } |
| import { CriteriaNode } from './CriteriaNode.js'; | ||
| import type { ICriteriaNodeProcessOptions, IQueryBuilder } from '../typings.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export declare class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> { | ||
| process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any; | ||
| isStrict(): boolean; | ||
| unwrap(): any; | ||
| willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean; | ||
| shouldInline(payload: any): boolean; | ||
| private getChildKey; | ||
| private inlineArrayChildPayload; | ||
| private inlineChildPayload; | ||
| private inlineCondition; | ||
| private shouldAutoJoin; | ||
| private autoJoin; | ||
| private isPrefixed; | ||
| } |
| import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core'; | ||
| import { CriteriaNode } from './CriteriaNode.js'; | ||
| import { JoinType, QueryType } from './enums.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export class ObjectCriteriaNode extends CriteriaNode { | ||
| process(qb, options) { | ||
| const matchPopulateJoins = options?.matchPopulateJoins || (this.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind)); | ||
| const nestedAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins }); | ||
| const ownerAlias = options?.alias || qb.alias; | ||
| const keys = Object.keys(this.payload); | ||
| let alias = options?.alias; | ||
| if (nestedAlias) { | ||
| alias = nestedAlias; | ||
| } | ||
| if (this.shouldAutoJoin(qb, nestedAlias)) { | ||
| if (keys.some(k => ['$some', '$none', '$every'].includes(k))) { | ||
| if (![ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(this.prop.kind)) { | ||
| // ignore collection operators when used on a non-relational property - this can happen when they get into | ||
| // populateWhere via `infer` on m:n properties with select-in strategy | ||
| if (this.parent?.parent) { // we validate only usage on top level | ||
| return {}; | ||
| } | ||
| throw new Error(`Collection operators can be used only inside a collection property context, but it was used for ${this.getPath()}.`); | ||
| } | ||
| const $and = []; | ||
| const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) || (this.prop.kind === ReferenceKind.ONE_TO_ONE && this.prop.owner); | ||
| const parentMeta = this.metadata.find(this.parent.entityName); | ||
| const primaryKeys = parentMeta.primaryKeys.map(pk => { | ||
| return [QueryType.SELECT, QueryType.COUNT].includes(qb.type) ? `${knownKey ? alias : ownerAlias}.${pk}` : pk; | ||
| }); | ||
| for (const key of keys) { | ||
| if (!['$some', '$none', '$every'].includes(key)) { | ||
| throw new Error('Mixing collection operators with other filters is not allowed.'); | ||
| } | ||
| const payload = this.payload[key].unwrap(); | ||
| const qb2 = qb.clone(true); | ||
| const sub = qb2 | ||
| .from(parentMeta.className) | ||
| .innerJoin(this.key, qb2.getNextAlias(this.prop.type)) | ||
| .select(parentMeta.primaryKeys); | ||
| if (key === '$every') { | ||
| sub.where({ $not: { [this.key]: payload } }); | ||
| } | ||
| else { | ||
| sub.where({ [this.key]: payload }); | ||
| } | ||
| const op = key === '$some' ? '$in' : '$nin'; | ||
| $and.push({ | ||
| [Utils.getPrimaryKeyHash(primaryKeys)]: { [op]: sub.getNativeQuery().toRaw() }, | ||
| }); | ||
| } | ||
| if ($and.length === 1) { | ||
| return $and[0]; | ||
| } | ||
| return { $and }; | ||
| } | ||
| alias = this.autoJoin(qb, ownerAlias, options); | ||
| } | ||
| if (this.prop && nestedAlias) { | ||
| const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind); | ||
| // if the property is nullable and the filter is strict, we need to use left join, so we mimic the inner join behaviour | ||
| // with an exclusive condition on the join columns: | ||
| // - if the owning column is null, the row is missing, we don't apply the filter | ||
| // - if the target column is not null, the row is matched, we apply the filter | ||
| if (toOneProperty && this.prop.nullable && this.isStrict()) { | ||
| const key = this.prop.owner ? this.prop.name : this.prop.referencedPKs; | ||
| qb.andWhere({ | ||
| $or: [ | ||
| { [ownerAlias + '.' + key]: null }, | ||
| { [nestedAlias + '.' + Utils.getPrimaryKeyHash(this.prop.referencedPKs)]: { $ne: null } }, | ||
| ], | ||
| }); | ||
| } | ||
| } | ||
| return keys.reduce((o, field) => { | ||
| const childNode = this.payload[field]; | ||
| const payload = childNode.process(qb, { ...options, alias: this.prop ? alias : ownerAlias }); | ||
| const operator = Utils.isOperator(field); | ||
| const isRawField = RawQueryFragment.isKnownFragment(field); | ||
| // we need to keep the prefixing for formulas otherwise we would lose aliasing context when nesting inside group operators | ||
| const virtual = childNode.prop?.persist === false && !childNode.prop?.formula; | ||
| // if key is missing, we are inside group operator and we need to prefix with alias | ||
| const primaryKey = this.key && this.metadata.find(this.entityName)?.primaryKeys.includes(field); | ||
| const isToOne = childNode.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(childNode.prop.kind); | ||
| if (childNode.shouldInline(payload)) { | ||
| const childAlias = qb.getAliasForJoinPath(childNode.getPath(), { preferNoBranch: isToOne, ...options }); | ||
| const a = qb.helper.isTableNameAliasRequired(qb.type) ? alias : undefined; | ||
| this.inlineChildPayload(o, payload, field, a, childAlias); | ||
| } | ||
| else if (childNode.shouldRename(payload)) { | ||
| this.inlineCondition(childNode.renameFieldToPK(qb, alias), o, payload); | ||
| } | ||
| else if (isRawField) { | ||
| const rawField = RawQueryFragment.getKnownFragment(field); | ||
| o[raw(rawField.sql.replaceAll(ALIAS_REPLACEMENT, alias), rawField.params)] = payload; | ||
| } | ||
| else if (primaryKey || virtual || operator || field.includes('.') || ![QueryType.SELECT, QueryType.COUNT].includes(qb.type)) { | ||
| this.inlineCondition(field.replaceAll(ALIAS_REPLACEMENT, alias), o, payload); | ||
| } | ||
| else { | ||
| this.inlineCondition(`${alias}.${field}`, o, payload); | ||
| } | ||
| return o; | ||
| }, {}); | ||
| } | ||
| isStrict() { | ||
| return this.strict || Object.keys(this.payload).some(key => { | ||
| return this.payload[key].isStrict(); | ||
| }); | ||
| } | ||
| unwrap() { | ||
| return Object.keys(this.payload).reduce((o, field) => { | ||
| o[field] = this.payload[field].unwrap(); | ||
| return o; | ||
| }, {}); | ||
| } | ||
| willAutoJoin(qb, alias, options) { | ||
| const nestedAlias = qb.getAliasForJoinPath(this.getPath(), options); | ||
| const ownerAlias = alias || qb.alias; | ||
| const keys = Object.keys(this.payload); | ||
| if (nestedAlias) { | ||
| alias = nestedAlias; | ||
| } | ||
| if (this.shouldAutoJoin(qb, nestedAlias)) { | ||
| return !keys.some(k => ['$some', '$none', '$every'].includes(k)); | ||
| } | ||
| return keys.some(field => { | ||
| const childNode = this.payload[field]; | ||
| return childNode.willAutoJoin(qb, this.prop ? alias : ownerAlias, options); | ||
| }); | ||
| } | ||
| shouldInline(payload) { | ||
| const customExpression = RawQueryFragment.isKnownFragment(this.key); | ||
| const scalar = Utils.isPrimaryKey(payload) || payload instanceof RegExp || payload instanceof Date || customExpression; | ||
| const operator = Utils.isObject(payload) && Object.keys(payload).every(k => Utils.isOperator(k, false)); | ||
| return !!this.prop && this.prop.kind !== ReferenceKind.SCALAR && !scalar && !operator; | ||
| } | ||
| getChildKey(k, prop, childAlias, alias) { | ||
| const idx = prop.referencedPKs.indexOf(k); | ||
| return idx !== -1 && !childAlias && ![ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) | ||
| ? this.aliased(prop.joinColumns[idx], alias) | ||
| : k; | ||
| } | ||
| inlineArrayChildPayload(obj, payload, k, prop, childAlias, alias) { | ||
| const key = this.getChildKey(k, prop, childAlias); | ||
| const value = payload.map((child) => Object.keys(child).reduce((inner, childKey) => { | ||
| const key = (this.isPrefixed(childKey) || Utils.isOperator(childKey)) ? childKey : this.aliased(childKey, childAlias); | ||
| inner[key] = child[childKey]; | ||
| return inner; | ||
| }, {})); | ||
| this.inlineCondition(key, obj, value); | ||
| } | ||
| inlineChildPayload(o, payload, field, alias, childAlias) { | ||
| const prop = this.metadata.find(this.entityName).properties[field]; | ||
| for (const k of Object.keys(payload)) { | ||
| if (Utils.isOperator(k, false)) { | ||
| const tmp = payload[k]; | ||
| delete payload[k]; | ||
| o[this.aliased(field, alias)] = { [k]: tmp, ...o[this.aliased(field, alias)] }; | ||
| } | ||
| else if (k in GroupOperator && Array.isArray(payload[k])) { | ||
| this.inlineArrayChildPayload(o, payload[k], k, prop, childAlias, alias); | ||
| } | ||
| else if (this.isPrefixed(k) || Utils.isOperator(k) || !childAlias) { | ||
| const key = this.getChildKey(k, prop, childAlias, alias); | ||
| this.inlineCondition(key, o, payload[k]); | ||
| } | ||
| else if (RawQueryFragment.isKnownFragment(k)) { | ||
| o[k] = payload[k]; | ||
| } | ||
| else { | ||
| o[this.aliased(k, childAlias)] = payload[k]; | ||
| } | ||
| } | ||
| } | ||
| inlineCondition(key, o, value) { | ||
| if (!(key in o)) { | ||
| o[key] = value; | ||
| return; | ||
| } | ||
| /* v8 ignore next */ | ||
| if (key === '$and') { | ||
| o.$and.push({ [key]: value }); | ||
| return; | ||
| } | ||
| const $and = o.$and ?? []; | ||
| $and.push({ [key]: o[key] }, { [key]: value }); | ||
| delete o[key]; | ||
| o.$and = $and; | ||
| } | ||
| shouldAutoJoin(qb, nestedAlias) { | ||
| if (!this.prop || !this.parent) { | ||
| return false; | ||
| } | ||
| const keys = Object.keys(this.payload); | ||
| if (keys.every(k => k.includes('.') && k.startsWith(`${qb.alias}.`))) { | ||
| return false; | ||
| } | ||
| if (keys.some(k => ['$some', '$none', '$every'].includes(k))) { | ||
| return true; | ||
| } | ||
| const meta = this.metadata.find(this.entityName); | ||
| const embeddable = this.prop.kind === ReferenceKind.EMBEDDED; | ||
| const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) || (this.prop.kind === ReferenceKind.ONE_TO_ONE && this.prop.owner); | ||
| const operatorKeys = knownKey && keys.every(key => Utils.isOperator(key, false)); | ||
| const primaryKeys = knownKey && keys.every(key => { | ||
| if (!meta.primaryKeys.includes(key)) { | ||
| return false; | ||
| } | ||
| if (!Utils.isPlainObject(this.payload[key].payload) || ![ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(meta.properties[key].kind)) { | ||
| return true; | ||
| } | ||
| return Object.keys(this.payload[key].payload).every(k => meta.properties[key].targetMeta.primaryKeys.includes(k)); | ||
| }); | ||
| return !primaryKeys && !nestedAlias && !operatorKeys && !embeddable; | ||
| } | ||
| autoJoin(qb, alias, options) { | ||
| const nestedAlias = qb.getNextAlias(this.prop?.pivotTable ?? this.entityName); | ||
| const customExpression = RawQueryFragment.isKnownFragment(this.key); | ||
| const scalar = Utils.isPrimaryKey(this.payload) || this.payload instanceof RegExp || this.payload instanceof Date || customExpression; | ||
| const operator = Utils.isPlainObject(this.payload) && Object.keys(this.payload).every(k => Utils.isOperator(k, false)); | ||
| const field = `${alias}.${this.prop.name}`; | ||
| const method = qb.hasFlag(QueryFlag.INFER_POPULATE) ? 'joinAndSelect' : 'join'; | ||
| const path = this.getPath(); | ||
| if (this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator)) { | ||
| qb.join(field, nestedAlias, undefined, JoinType.pivotJoin, path); | ||
| } | ||
| else { | ||
| const prev = qb._fields?.slice(); | ||
| const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind); | ||
| const joinType = toOneProperty && !this.prop.nullable | ||
| ? JoinType.innerJoin | ||
| : JoinType.leftJoin; | ||
| qb[method](field, nestedAlias, undefined, joinType, path); | ||
| if (!qb.hasFlag(QueryFlag.INFER_POPULATE)) { | ||
| qb._fields = prev; | ||
| } | ||
| } | ||
| if (options?.type !== 'orderBy') { | ||
| qb.scheduleFilterCheck(path); | ||
| } | ||
| return nestedAlias; | ||
| } | ||
| isPrefixed(field) { | ||
| return !!field.match(/\w+\./); | ||
| } | ||
| } |
| import { type AnyEntity, type ConnectionType, type Dictionary, type EntityData, type EntityKey, type EntityManager, type EntityMetadata, type EntityName, type EntityProperty, type ExpandProperty, type FilterOptions, type FlushMode, type GroupOperator, type Loaded, LockMode, type LoggingOptions, type MetadataStorage, type ObjectQuery, PopulateHint, type PopulateOptions, type QBFilterQuery, type QBQueryOrderMap, QueryFlag, type QueryOrderMap, type QueryResult, RawQueryFragment, type RequiredEntityData, type Transaction } from '@mikro-orm/core'; | ||
| import { JoinType, QueryType } from './enums.js'; | ||
| import type { AbstractSqlDriver } from '../AbstractSqlDriver.js'; | ||
| import { type Alias, type OnConflictClause, QueryBuilderHelper } from './QueryBuilderHelper.js'; | ||
| import type { SqlEntityManager } from '../SqlEntityManager.js'; | ||
| import type { Field, ICriteriaNodeProcessOptions, JoinOptions } from '../typings.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
| import { NativeQueryBuilder } from './NativeQueryBuilder.js'; | ||
| export interface ExecuteOptions { | ||
| mapResults?: boolean; | ||
| mergeResults?: boolean; | ||
| } | ||
| export interface QBStreamOptions { | ||
| /** | ||
| * Results are mapped to entities, if you set `mapResults: false` you will get POJOs instead. | ||
| * | ||
| * @default true | ||
| */ | ||
| mapResults?: boolean; | ||
| /** | ||
| * When populating to-many relations, the ORM streams fully merged entities instead of yielding every row. | ||
| * You can opt out of this behavior by specifying `mergeResults: false`. This will yield every row from | ||
| * the SQL result, but still mapped to entities, meaning that to-many collections will contain at most | ||
| * one item, and you will get duplicate root entities when they have multiple items in the populated | ||
| * collection. | ||
| * | ||
| * @default true | ||
| */ | ||
| mergeResults?: boolean; | ||
| /** | ||
| * When enabled, the driver will return the raw database results without renaming the fields to match the entity property names. | ||
| * | ||
| * @default false | ||
| */ | ||
| rawResults?: boolean; | ||
| } | ||
| type AnyString = string & {}; | ||
| type Compute<T> = { | ||
| [K in keyof T]: T[K]; | ||
| } & {}; | ||
| type IsNever<T, True = true, False = false> = [T] extends [never] ? True : False; | ||
| type GetAlias<T extends string> = T extends `${infer A}.${string}` ? A : never; | ||
| type GetPropName<T extends string> = T extends `${string}.${infer P}` ? P : T; | ||
| type AppendToHint<Parent extends string, Child extends string> = `${Parent}.${Child}`; | ||
| type AddToContext<Type extends object, Context, Field extends string, Alias extends string, Select extends boolean> = { | ||
| [K in Alias]: [GetPath<Context, Field>, K, ExpandProperty<Type[GetPropName<Field> & keyof Type]>, Select]; | ||
| }; | ||
| type GetPath<Context, Field extends string> = GetAlias<Field> extends infer Alias ? IsNever<Alias> extends true ? GetPropName<Field> : Alias extends keyof Context ? Context[Alias] extends [infer Path, ...any[]] ? AppendToHint<Path & string, GetPropName<Field>> : GetPropName<Field> : GetPropName<Field> : GetPropName<Field>; | ||
| type GetType<Type extends object, Context, Field extends string> = GetAlias<Field> extends infer Alias ? IsNever<Alias> extends true ? Type : Alias extends keyof Context ? Context[Alias] extends [string, string, infer PropType] ? PropType & object : Type : Type : Type; | ||
| type AddToHint<RootAlias, Context, Field extends string, Select extends boolean = false> = Select extends true ? GetAlias<Field> extends infer Alias ? IsNever<Alias> extends true ? GetPropName<Field> : Alias extends RootAlias ? GetPropName<Field> : Alias extends keyof Context ? Context[Alias] extends [infer Path, ...any[]] ? AppendToHint<Path & string, GetPropName<Field>> : GetPropName<Field> : GetPropName<Field> : GetPropName<Field> : never; | ||
| export type ModifyHint<RootAlias, Context, Hint extends string, Field extends string, Select extends boolean = false> = Hint | AddToHint<RootAlias, Context, Field, Select>; | ||
| export type ModifyContext<Entity extends object, Context, Field extends string, Alias extends string, Select extends boolean = false> = Compute<IsNever<Context> extends true ? AddToContext<GetType<Entity, object, Field>, object, Field, Alias, Select> : Context & AddToContext<GetType<Entity, Context, Field>, Context, Field, Alias, Select>>; | ||
| type EntityRelations<T> = EntityKey<T, true>; | ||
| type AddAliasesFromContext<Context> = Context[keyof Context] extends infer Join ? Join extends any ? Join extends [string, infer Alias, infer Type, any] ? `${Alias & string}.${EntityRelations<Type & {}>}` : never : never : never; | ||
| export type QBField<Entity, RootAlias extends string, Context> = (EntityRelations<Entity> | `${RootAlias}.${EntityRelations<Entity>}` | AddAliasesFromContext<Context>) & {} | AnyString; | ||
| type EntityKeyOrString<Entity extends object = AnyEntity> = AnyString | keyof Entity; | ||
| /** | ||
| * SQL query builder with fluent interface. | ||
| * | ||
| * ```ts | ||
| * const qb = orm.em.createQueryBuilder(Publisher); | ||
| * qb.select('*') | ||
| * .where({ | ||
| * name: 'test 123', | ||
| * type: PublisherType.GLOBAL, | ||
| * }) | ||
| * .orderBy({ | ||
| * name: QueryOrder.DESC, | ||
| * type: QueryOrder.ASC, | ||
| * }) | ||
| * .limit(2, 1); | ||
| * | ||
| * const publisher = await qb.getSingleResult(); | ||
| * ``` | ||
| */ | ||
| export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias extends string = never, Hint extends string = never, Context extends object = never> { | ||
| protected readonly metadata: MetadataStorage; | ||
| protected readonly driver: AbstractSqlDriver; | ||
| protected readonly context?: Transaction | undefined; | ||
| protected connectionType?: ConnectionType | undefined; | ||
| protected em?: SqlEntityManager | undefined; | ||
| protected loggerContext?: (LoggingOptions & Dictionary) | undefined; | ||
| get mainAlias(): Alias<Entity>; | ||
| get alias(): string; | ||
| get helper(): QueryBuilderHelper; | ||
| get type(): QueryType; | ||
| /** @internal */ | ||
| _type?: QueryType; | ||
| /** @internal */ | ||
| _fields?: Field<Entity>[]; | ||
| /** @internal */ | ||
| _populate: PopulateOptions<Entity>[]; | ||
| /** @internal */ | ||
| _populateWhere?: ObjectQuery<Entity> | PopulateHint | `${PopulateHint}`; | ||
| /** @internal */ | ||
| _populateFilter?: ObjectQuery<Entity> | PopulateHint | `${PopulateHint}`; | ||
| /** @internal */ | ||
| __populateWhere?: ObjectQuery<Entity> | PopulateHint | `${PopulateHint}`; | ||
| /** @internal */ | ||
| _populateMap: Dictionary<string>; | ||
| /** @internal */ | ||
| readonly rawFragments: Set<string>; | ||
| protected aliasCounter: number; | ||
| protected flags: Set<QueryFlag>; | ||
| protected finalized: boolean; | ||
| protected populateHintFinalized: boolean; | ||
| protected _joins: Dictionary<JoinOptions>; | ||
| protected _explicitAlias: boolean; | ||
| protected _schema?: string; | ||
| protected _cond: Dictionary; | ||
| protected _data: Dictionary; | ||
| protected _orderBy: QueryOrderMap<Entity>[]; | ||
| protected _groupBy: Field<Entity>[]; | ||
| protected _having: Dictionary; | ||
| protected _returning?: Field<Entity>[]; | ||
| protected _onConflict?: OnConflictClause<Entity>[]; | ||
| protected _limit?: number; | ||
| protected _offset?: number; | ||
| protected _distinctOn?: string[]; | ||
| protected _joinedProps: Map<string, PopulateOptions<any>>; | ||
| protected _cache?: boolean | number | [string, number]; | ||
| protected _indexHint?: string; | ||
| protected _comments: string[]; | ||
| protected _hintComments: string[]; | ||
| protected flushMode?: FlushMode; | ||
| protected lockMode?: LockMode; | ||
| protected lockTables?: string[]; | ||
| protected subQueries: Dictionary<string>; | ||
| protected _mainAlias?: Alias<Entity>; | ||
| protected _aliases: Dictionary<Alias<any>>; | ||
| protected _helper?: QueryBuilderHelper; | ||
| protected _query?: { | ||
| sql?: string; | ||
| params?: readonly unknown[]; | ||
| qb: NativeQueryBuilder; | ||
| }; | ||
| protected readonly platform: AbstractSqlPlatform; | ||
| /** | ||
| * @internal | ||
| */ | ||
| constructor(entityName: EntityName<Entity> | QueryBuilder<Entity, any, any>, metadata: MetadataStorage, driver: AbstractSqlDriver, context?: Transaction | undefined, alias?: string, connectionType?: ConnectionType | undefined, em?: SqlEntityManager | undefined, loggerContext?: (LoggingOptions & Dictionary) | undefined); | ||
| select(fields: Field<Entity> | Field<Entity>[], distinct?: boolean): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| addSelect(fields: Field<Entity> | Field<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| distinct(): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| /** postgres only */ | ||
| distinctOn(fields: EntityKeyOrString<Entity> | EntityKeyOrString<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| insert(data: RequiredEntityData<Entity> | RequiredEntityData<Entity>[]): InsertQueryBuilder<Entity>; | ||
| update(data: EntityData<Entity>): UpdateQueryBuilder<Entity>; | ||
| delete(cond?: QBFilterQuery): DeleteQueryBuilder<Entity>; | ||
| truncate(): TruncateQueryBuilder<Entity>; | ||
| count(field?: EntityKeyOrString<Entity> | EntityKeyOrString<Entity>[], distinct?: boolean): CountQueryBuilder<Entity>; | ||
| join<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field | RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: QBFilterQuery, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>>; | ||
| innerJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field | RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: QBFilterQuery, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>>; | ||
| innerJoinLateral(field: RawQueryFragment | QueryBuilder<any>, alias: string, cond: QBFilterQuery, schema?: string): this; | ||
| leftJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field | RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: QBFilterQuery, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>>; | ||
| leftJoinLateral(field: RawQueryFragment | QueryBuilder<any>, alias: string, cond: QBFilterQuery, schema?: string): this; | ||
| joinAndSelect<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field | [field: Field, qb: RawQueryFragment | QueryBuilder<any>], alias: Alias, cond?: QBFilterQuery, type?: JoinType, path?: string, fields?: string[], schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field, true> & {}, ModifyContext<Entity, Context, Field, Alias, true>>; | ||
| leftJoinAndSelect<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field | [field: Field, qb: RawQueryFragment | QueryBuilder<any>], alias: Alias, cond?: QBFilterQuery, fields?: string[], schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field, true> & {}, ModifyContext<Entity, Context, Field, Alias, true>>; | ||
| leftJoinLateralAndSelect<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: [field: Field, qb: RawQueryFragment | QueryBuilder<any>], alias: Alias, cond?: QBFilterQuery, fields?: string[], schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field, true> & {}, ModifyContext<Entity, Context, Field, Alias, true>>; | ||
| innerJoinAndSelect<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field | [field: Field, qb: RawQueryFragment | QueryBuilder<any>], alias: Alias, cond?: QBFilterQuery, fields?: string[], schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field, true> & {}, ModifyContext<Entity, Context, Field, Alias, true>>; | ||
| innerJoinLateralAndSelect<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: [field: Field, qb: RawQueryFragment | QueryBuilder<any>], alias: Alias, cond?: QBFilterQuery, fields?: string[], schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field, true> & {}, ModifyContext<Entity, Context, Field, Alias, true>>; | ||
| protected getFieldsForJoinedLoad(prop: EntityProperty<Entity>, alias: string, explicitFields?: string[]): Field<Entity>[]; | ||
| /** | ||
| * Apply filters to the QB where condition. | ||
| */ | ||
| applyFilters(filterOptions?: FilterOptions): Promise<void>; | ||
| private readonly autoJoinedPaths; | ||
| /** | ||
| * @internal | ||
| */ | ||
| scheduleFilterCheck(path: string): void; | ||
| /** | ||
| * @internal | ||
| */ | ||
| applyJoinedFilters(em: EntityManager, filterOptions: FilterOptions | undefined): Promise<void>; | ||
| withSubQuery(subQuery: RawQueryFragment | NativeQueryBuilder, alias: string): this; | ||
| where(cond: QBFilterQuery<Entity>, operator?: keyof typeof GroupOperator): this; | ||
| where(cond: string, params?: any[], operator?: keyof typeof GroupOperator): this; | ||
| andWhere(cond: QBFilterQuery<Entity>): this; | ||
| andWhere(cond: string, params?: any[]): this; | ||
| orWhere(cond: QBFilterQuery<Entity>): this; | ||
| orWhere(cond: string, params?: any[]): this; | ||
| orderBy(orderBy: QBQueryOrderMap<Entity> | QBQueryOrderMap<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| andOrderBy(orderBy: QBQueryOrderMap<Entity> | QBQueryOrderMap<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| private processOrderBy; | ||
| groupBy(fields: EntityKeyOrString<Entity> | readonly EntityKeyOrString<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| having(cond?: QBFilterQuery | string, params?: any[], operator?: keyof typeof GroupOperator): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| andHaving(cond?: QBFilterQuery | string, params?: any[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| orHaving(cond?: QBFilterQuery | string, params?: any[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| onConflict(fields?: Field<Entity> | Field<Entity>[]): InsertQueryBuilder<Entity>; | ||
| ignore(): this; | ||
| merge(data?: EntityData<Entity> | Field<Entity>[]): this; | ||
| returning(fields?: Field<Entity> | Field<Entity>[]): this; | ||
| /** | ||
| * @internal | ||
| */ | ||
| populate(populate: PopulateOptions<Entity>[], populateWhere?: ObjectQuery<Entity> | PopulateHint | `${PopulateHint}`, populateFilter?: ObjectQuery<Entity> | PopulateHint | `${PopulateHint}`): this; | ||
| limit(limit?: number, offset?: number): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| offset(offset?: number): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| withSchema(schema?: string): this; | ||
| setLockMode(mode?: LockMode, tables?: string[]): this; | ||
| setFlushMode(flushMode?: FlushMode): this; | ||
| setFlag(flag: QueryFlag): this; | ||
| unsetFlag(flag: QueryFlag): this; | ||
| hasFlag(flag: QueryFlag): boolean; | ||
| cache(config?: boolean | number | [string, number]): this; | ||
| /** | ||
| * Adds index hint to the FROM clause. | ||
| */ | ||
| indexHint(sql: string | undefined): this; | ||
| /** | ||
| * Prepend comment to the sql query using the syntax `/* ... *‍/`. Some characters are forbidden such as `/*, *‍/` and `?`. | ||
| */ | ||
| comment(comment: string | string[] | undefined): this; | ||
| /** | ||
| * Add hints to the query using comment-like syntax `/*+ ... *‍/`. MySQL and Oracle use this syntax for optimizer hints. | ||
| * Also various DB proxies and routers use this syntax to pass hints to alter their behavior. In other dialects the hints | ||
| * are ignored as simple comments. | ||
| */ | ||
| hintComment(comment: string | string[] | undefined): this; | ||
| /** | ||
| * Specifies FROM which entity's table select/update/delete will be executed, removing all previously set FROM-s. | ||
| * Allows setting a main string alias of the selection data. | ||
| */ | ||
| from<Entity extends AnyEntity<Entity> = AnyEntity>(target: QueryBuilder<Entity>, aliasName?: string): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| from<Entity extends AnyEntity<Entity> = AnyEntity>(target: EntityName<Entity>): SelectQueryBuilder<Entity, RootAlias, Hint, Context>; | ||
| getNativeQuery(processVirtualEntity?: boolean): NativeQueryBuilder; | ||
| /** | ||
| * @internal | ||
| */ | ||
| clearRawFragmentsCache(): void; | ||
| /** | ||
| * Returns the query with parameters as wildcards. | ||
| */ | ||
| getQuery(): string; | ||
| /** | ||
| * Returns raw fragment representation of this QueryBuilder. | ||
| */ | ||
| toRaw(): RawQueryFragment; | ||
| toQuery(): { | ||
| sql: string; | ||
| params: readonly unknown[]; | ||
| }; | ||
| /** | ||
| * Returns the list of all parameters for this query. | ||
| */ | ||
| getParams(): readonly unknown[]; | ||
| /** | ||
| * Returns raw interpolated query string with all the parameters inlined. | ||
| */ | ||
| getFormattedQuery(): string; | ||
| /** | ||
| * @internal | ||
| */ | ||
| getAliasForJoinPath(path?: string | JoinOptions, options?: ICriteriaNodeProcessOptions): string | undefined; | ||
| /** | ||
| * @internal | ||
| */ | ||
| getJoinForPath(path: string, options?: ICriteriaNodeProcessOptions): JoinOptions | undefined; | ||
| /** | ||
| * @internal | ||
| */ | ||
| getNextAlias(entityName?: string): string; | ||
| /** | ||
| * @internal | ||
| */ | ||
| getAliasMap(): Dictionary<string>; | ||
| /** | ||
| * Executes this QB and returns the raw results, mapped to the property names (unless disabled via last parameter). | ||
| * Use `method` to specify what kind of result you want to get (array/single/meta). | ||
| */ | ||
| execute<U = any>(method?: 'all' | 'get' | 'run', options?: ExecuteOptions | boolean): Promise<U>; | ||
| private getConnection; | ||
| /** | ||
| * Executes the query and returns an async iterable (async generator) that yields results one by one. | ||
| * By default, the results are merged and mapped to entity instances, without adding them to the identity map. | ||
| * You can disable merging and mapping by passing the options `{ mergeResults: false, mapResults: false }`. | ||
| * This is useful for processing large datasets without loading everything into memory at once. | ||
| * | ||
| * ```ts | ||
| * const qb = em.createQueryBuilder(Book, 'b'); | ||
| * qb.select('*').where({ title: '1984' }).leftJoinAndSelect('b.author', 'a'); | ||
| * | ||
| * for await (const book of qb.stream()) { | ||
| * // book is an instance of Book entity | ||
| * console.log(book.title, book.author.name); | ||
| * } | ||
| * ``` | ||
| */ | ||
| stream(options?: QBStreamOptions): AsyncIterableIterator<Loaded<Entity, Hint>>; | ||
| /** | ||
| * Alias for `qb.getResultList()` | ||
| */ | ||
| getResult(): Promise<Loaded<Entity, Hint>[]>; | ||
| /** | ||
| * Executes the query, returning array of results mapped to entity instances. | ||
| */ | ||
| getResultList(limit?: number): Promise<Loaded<Entity, Hint>[]>; | ||
| private propagatePopulateHint; | ||
| private mapResult; | ||
| private mapResults; | ||
| /** | ||
| * Executes the query, returning the first result or null | ||
| */ | ||
| getSingleResult(): Promise<Entity | null>; | ||
| /** | ||
| * Executes count query (without offset and limit), returning total count of results | ||
| */ | ||
| getCount(field?: EntityKeyOrString<Entity> | EntityKeyOrString<Entity>[], distinct?: boolean): Promise<number>; | ||
| /** | ||
| * Executes the query, returning both array of results and total count query (without offset and limit). | ||
| */ | ||
| getResultAndCount(): Promise<[Entity[], number]>; | ||
| /** | ||
| * Returns native query builder instance with sub-query aliased with given alias. | ||
| * You can provide `EntityName.propName` as alias, then the field name will be used based on the metadata | ||
| */ | ||
| as(alias: string): NativeQueryBuilder; | ||
| clone(reset?: boolean | string[]): QueryBuilder<Entity>; | ||
| /** | ||
| * Sets logger context for this query builder. | ||
| */ | ||
| setLoggerContext(context: LoggingOptions & Dictionary): void; | ||
| /** | ||
| * Gets logger context for this query builder. | ||
| */ | ||
| getLoggerContext<T extends Dictionary & LoggingOptions = Dictionary>(): T; | ||
| private fromVirtual; | ||
| private joinReference; | ||
| protected prepareFields<T>(fields: Field<T>[], type?: 'where' | 'groupBy' | 'sub-query'): (string | RawQueryFragment)[]; | ||
| private init; | ||
| private getQueryBase; | ||
| private applyDiscriminatorCondition; | ||
| private finalize; | ||
| /** @internal */ | ||
| processPopulateHint(): void; | ||
| private processPopulateWhere; | ||
| private mergeOnConditions; | ||
| /** | ||
| * When adding an inner join on a left joined relation, we need to nest them, | ||
| * otherwise the inner join could discard rows of the root table. | ||
| */ | ||
| private processNestedJoins; | ||
| private hasToManyJoins; | ||
| protected wrapPaginateSubQuery(meta: EntityMetadata): void; | ||
| private pruneExtraJoins; | ||
| private wrapModifySubQuery; | ||
| private getSchema; | ||
| private createAlias; | ||
| private createMainAlias; | ||
| private fromSubQuery; | ||
| private fromEntityName; | ||
| private createQueryBuilderHelper; | ||
| private ensureFromClause; | ||
| private ensureNotFinalized; | ||
| } | ||
| export interface RunQueryBuilder<Entity extends object> extends Omit<QueryBuilder<Entity, any, any>, 'getResult' | 'getSingleResult' | 'getResultList' | 'where'> { | ||
| where(cond: QBFilterQuery<Entity> | string, params?: keyof typeof GroupOperator | any[], operator?: keyof typeof GroupOperator): this; | ||
| execute<Result = QueryResult<Entity>>(method?: 'all' | 'get' | 'run', mapResults?: boolean): Promise<Result>; | ||
| } | ||
| export interface SelectQueryBuilder<Entity extends object = AnyEntity, RootAlias extends string = never, Hint extends string = never, Context extends object = never> extends QueryBuilder<Entity, RootAlias, Hint, Context> { | ||
| execute<Result = Entity[]>(method?: 'all' | 'get' | 'run', mapResults?: boolean): Promise<Result>; | ||
| execute<Result = Entity[]>(method: 'all', mapResults?: boolean): Promise<Result>; | ||
| execute<Result = Entity>(method: 'get', mapResults?: boolean): Promise<Result>; | ||
| execute<Result = QueryResult<Entity>>(method: 'run', mapResults?: boolean): Promise<Result>; | ||
| } | ||
| export interface CountQueryBuilder<Entity extends object> extends QueryBuilder<Entity, any, any> { | ||
| execute<Result = { | ||
| count: number; | ||
| }[]>(method?: 'all' | 'get' | 'run', mapResults?: boolean): Promise<Result>; | ||
| execute<Result = { | ||
| count: number; | ||
| }[]>(method: 'all', mapResults?: boolean): Promise<Result>; | ||
| execute<Result = { | ||
| count: number; | ||
| }>(method: 'get', mapResults?: boolean): Promise<Result>; | ||
| execute<Result = QueryResult<{ | ||
| count: number; | ||
| }>>(method: 'run', mapResults?: boolean): Promise<Result>; | ||
| } | ||
| export interface InsertQueryBuilder<T extends object> extends RunQueryBuilder<T> { | ||
| } | ||
| export interface UpdateQueryBuilder<T extends object> extends RunQueryBuilder<T> { | ||
| } | ||
| export interface DeleteQueryBuilder<T extends object> extends RunQueryBuilder<T> { | ||
| } | ||
| export interface TruncateQueryBuilder<T extends object> extends RunQueryBuilder<T> { | ||
| } | ||
| export {}; |
Sorry, the diff of this file is too big to display
| import { type Dictionary, type EntityData, type EntityKey, type EntityMetadata, type EntityProperty, type FlatQueryOrderMap, LockMode, type QBFilterQuery, RawQueryFragment } from '@mikro-orm/core'; | ||
| import { JoinType, QueryType } from './enums.js'; | ||
| import type { Field, JoinOptions } from '../typings.js'; | ||
| import type { AbstractSqlDriver } from '../AbstractSqlDriver.js'; | ||
| import { NativeQueryBuilder } from './NativeQueryBuilder.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export declare class QueryBuilderHelper { | ||
| private readonly entityName; | ||
| private alias; | ||
| private readonly aliasMap; | ||
| private readonly subQueries; | ||
| private readonly driver; | ||
| private readonly platform; | ||
| private readonly metadata; | ||
| constructor(entityName: string, alias: string, aliasMap: Dictionary<Alias<any>>, subQueries: Dictionary<string>, driver: AbstractSqlDriver); | ||
| mapper(field: string | RawQueryFragment, type?: QueryType): string; | ||
| mapper(field: string | RawQueryFragment, type?: QueryType, value?: any, alias?: string | null): string; | ||
| processData(data: Dictionary, convertCustomTypes: boolean, multi?: boolean): any; | ||
| joinOneToReference(prop: EntityProperty, ownerAlias: string, alias: string, type: JoinType, cond?: Dictionary, schema?: string): JoinOptions; | ||
| joinManyToOneReference(prop: EntityProperty, ownerAlias: string, alias: string, type: JoinType, cond?: Dictionary, schema?: string): JoinOptions; | ||
| joinManyToManyReference(prop: EntityProperty, ownerAlias: string, alias: string, pivotAlias: string, type: JoinType, cond: Dictionary, path: string, schema?: string): Dictionary<JoinOptions>; | ||
| processJoins(qb: NativeQueryBuilder, joins: Dictionary<JoinOptions>, schema?: string): void; | ||
| createJoinExpression(join: JoinOptions, joins: Dictionary<JoinOptions>, schema?: string): { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| mapJoinColumns(type: QueryType, join: JoinOptions): (string | RawQueryFragment)[]; | ||
| isOneToOneInverse(field: string, meta?: EntityMetadata): boolean; | ||
| getTableName(entityName: string): string; | ||
| /** | ||
| * Checks whether the RE can be rewritten to simple LIKE query | ||
| */ | ||
| isSimpleRegExp(re: any): re is RegExp; | ||
| getRegExpParam(re: RegExp): string; | ||
| appendOnConflictClause<T>(type: QueryType, onConflict: OnConflictClause<T>[], qb: NativeQueryBuilder): void; | ||
| appendQueryCondition(type: QueryType, cond: any, qb: NativeQueryBuilder, operator?: '$and' | '$or', method?: 'where' | 'having'): void; | ||
| _appendQueryCondition(type: QueryType, cond: any, operator?: '$and' | '$or'): { | ||
| sql: string; | ||
| params: unknown[]; | ||
| }; | ||
| private append; | ||
| private appendQuerySubCondition; | ||
| private processObjectSubCondition; | ||
| private getValueReplacement; | ||
| private getOperatorReplacement; | ||
| getQueryOrder(type: QueryType, orderBy: FlatQueryOrderMap | FlatQueryOrderMap[], populate: Dictionary<string>): string[]; | ||
| getQueryOrderFromObject(type: QueryType, orderBy: FlatQueryOrderMap, populate: Dictionary<string>): string[]; | ||
| finalize(type: QueryType, qb: NativeQueryBuilder, meta?: EntityMetadata, data?: Dictionary, returning?: Field<any>[]): void; | ||
| splitField<T>(field: EntityKey<T>, greedyAlias?: boolean): [string, EntityKey<T>, string | undefined]; | ||
| getLockSQL(qb: NativeQueryBuilder, lockMode: LockMode, lockTables?: string[], joinsMap?: Dictionary<JoinOptions>): void; | ||
| updateVersionProperty(qb: NativeQueryBuilder, data: Dictionary): void; | ||
| private prefix; | ||
| private appendGroupCondition; | ||
| private isPrefixed; | ||
| private fieldName; | ||
| getProperty(field: string, alias?: string): EntityProperty | undefined; | ||
| isTableNameAliasRequired(type: QueryType): boolean; | ||
| processOnConflictCondition(cond: QBFilterQuery, schema?: string): QBFilterQuery; | ||
| } | ||
| export interface Alias<T> { | ||
| aliasName: string; | ||
| entityName: string; | ||
| metadata?: EntityMetadata<T>; | ||
| subQuery?: NativeQueryBuilder; | ||
| } | ||
| export interface OnConflictClause<T> { | ||
| fields: string[] | RawQueryFragment; | ||
| ignore?: boolean; | ||
| merge?: EntityData<T> | Field<T>[]; | ||
| where?: QBFilterQuery<T>; | ||
| } |
| import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core'; | ||
| import { JoinType, QueryType } from './enums.js'; | ||
| import { NativeQueryBuilder } from './NativeQueryBuilder.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export class QueryBuilderHelper { | ||
| entityName; | ||
| alias; | ||
| aliasMap; | ||
| subQueries; | ||
| driver; | ||
| platform; | ||
| metadata; | ||
| constructor(entityName, alias, aliasMap, subQueries, driver) { | ||
| this.entityName = entityName; | ||
| this.alias = alias; | ||
| this.aliasMap = aliasMap; | ||
| this.subQueries = subQueries; | ||
| this.driver = driver; | ||
| this.platform = this.driver.getPlatform(); | ||
| this.metadata = this.driver.getMetadata(); | ||
| } | ||
| mapper(field, type = QueryType.SELECT, value, alias) { | ||
| if (isRaw(field)) { | ||
| return raw(field.sql, field.params); | ||
| } | ||
| /* v8 ignore next */ | ||
| if (typeof field !== 'string') { | ||
| return field; | ||
| } | ||
| const isTableNameAliasRequired = this.isTableNameAliasRequired(type); | ||
| const fields = Utils.splitPrimaryKeys(field); | ||
| if (fields.length > 1) { | ||
| const parts = []; | ||
| for (const p of fields) { | ||
| const [a, f] = this.splitField(p); | ||
| const prop = this.getProperty(f, a); | ||
| const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1; | ||
| if (fkIdx2 !== -1) { | ||
| parts.push(this.mapper(a !== this.alias ? `${a}.${prop.fieldNames[fkIdx2]}` : prop.fieldNames[fkIdx2], type, value, alias)); | ||
| } | ||
| else if (prop) { | ||
| parts.push(...prop.fieldNames.map(f => this.mapper(a !== this.alias ? `${a}.${f}` : f, type, value, alias))); | ||
| } | ||
| else { | ||
| parts.push(this.mapper(a !== this.alias ? `${a}.${f}` : f, type, value, alias)); | ||
| } | ||
| } | ||
| // flatten the value if we see we are expanding nested composite key | ||
| // hackish, but cleaner solution would require quite a lot of refactoring | ||
| if (fields.length !== parts.length && Array.isArray(value)) { | ||
| value.forEach(row => { | ||
| if (Array.isArray(row)) { | ||
| const tmp = Utils.flatten(row); | ||
| row.length = 0; | ||
| row.push(...tmp); | ||
| } | ||
| }); | ||
| } | ||
| return raw('(' + parts.map(part => this.platform.quoteIdentifier(part)).join(', ') + ')'); | ||
| } | ||
| const rawField = RawQueryFragment.getKnownFragment(field); | ||
| if (rawField) { | ||
| return rawField; | ||
| } | ||
| const aliasPrefix = isTableNameAliasRequired ? this.alias + '.' : ''; | ||
| const [a, f] = this.splitField(field); | ||
| const prop = this.getProperty(f, a); | ||
| const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1; | ||
| const fkIdx = fkIdx2 === -1 ? 0 : fkIdx2; | ||
| let ret = field; | ||
| // embeddable nested path instead of a regular property with table alias, reset alias | ||
| if (prop?.name === a && prop.embeddedProps[f]) { | ||
| return aliasPrefix + prop.fieldNames[fkIdx]; | ||
| } | ||
| if (a === prop?.embedded?.[0]) { | ||
| return aliasPrefix + prop.fieldNames[fkIdx]; | ||
| } | ||
| const noPrefix = prop?.persist === false; | ||
| if (prop?.fieldNameRaw) { | ||
| return raw(this.prefix(field, isTableNameAliasRequired)); | ||
| } | ||
| if (prop?.formula) { | ||
| const alias2 = this.platform.quoteIdentifier(a).toString(); | ||
| const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]).toString(); | ||
| const as = alias === null ? '' : ` as ${aliased}`; | ||
| let value = prop.formula(alias2); | ||
| if (!this.isTableNameAliasRequired(type)) { | ||
| value = value.replaceAll(alias2 + '.', ''); | ||
| } | ||
| return raw(`${value}${as}`); | ||
| } | ||
| if (prop?.hasConvertToJSValueSQL && type !== QueryType.UPSERT) { | ||
| let valueSQL; | ||
| if (prop.fieldNames.length > 1 && fkIdx !== -1) { | ||
| const fk = prop.targetMeta.getPrimaryProps()[fkIdx]; | ||
| const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx); | ||
| valueSQL = fk.customType.convertToJSValueSQL(prefixed, this.platform); | ||
| } | ||
| else { | ||
| const prefixed = this.prefix(field, isTableNameAliasRequired, true); | ||
| valueSQL = prop.customType.convertToJSValueSQL(prefixed, this.platform); | ||
| } | ||
| if (alias === null) { | ||
| return raw(valueSQL); | ||
| } | ||
| return raw(`${valueSQL} as ${this.platform.quoteIdentifier(alias ?? prop.fieldNames[fkIdx])}`); | ||
| } | ||
| // do not wrap custom expressions | ||
| if (!rawField) { | ||
| ret = this.prefix(field, false, false, fkIdx); | ||
| } | ||
| if (alias) { | ||
| ret += ' as ' + alias; | ||
| } | ||
| if (!isTableNameAliasRequired || this.isPrefixed(ret) || noPrefix) { | ||
| return ret; | ||
| } | ||
| return this.alias + '.' + ret; | ||
| } | ||
| processData(data, convertCustomTypes, multi = false) { | ||
| if (Array.isArray(data)) { | ||
| return data.map(d => this.processData(d, convertCustomTypes, true)); | ||
| } | ||
| const meta = this.metadata.find(this.entityName); | ||
| data = this.driver.mapDataToFieldNames(data, true, meta?.properties, convertCustomTypes); | ||
| if (!Utils.hasObjectKeys(data) && meta && multi) { | ||
| /* v8 ignore next */ | ||
| data[meta.getPrimaryProps()[0].fieldNames[0]] = this.platform.usesDefaultKeyword() ? raw('default') : undefined; | ||
| } | ||
| return data; | ||
| } | ||
| joinOneToReference(prop, ownerAlias, alias, type, cond = {}, schema) { | ||
| const prop2 = prop.targetMeta.properties[prop.mappedBy || prop.inversedBy]; | ||
| const table = this.getTableName(prop.type); | ||
| const joinColumns = prop.owner ? prop.referencedColumnNames : prop2.joinColumns; | ||
| const inverseJoinColumns = prop.referencedColumnNames; | ||
| const primaryKeys = prop.owner ? prop.joinColumns : prop2.referencedColumnNames; | ||
| schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta); | ||
| cond = Utils.merge(cond, prop.where); | ||
| return { | ||
| prop, type, cond, ownerAlias, alias, table, schema, | ||
| joinColumns, inverseJoinColumns, primaryKeys, | ||
| }; | ||
| } | ||
| joinManyToOneReference(prop, ownerAlias, alias, type, cond = {}, schema) { | ||
| return { | ||
| prop, type, cond, ownerAlias, alias, | ||
| table: this.getTableName(prop.type), | ||
| schema: prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta, { schema }), | ||
| joinColumns: prop.referencedColumnNames, | ||
| primaryKeys: prop.fieldNames, | ||
| }; | ||
| } | ||
| joinManyToManyReference(prop, ownerAlias, alias, pivotAlias, type, cond, path, schema) { | ||
| const pivotMeta = this.metadata.find(prop.pivotEntity); | ||
| const ret = { | ||
| [`${ownerAlias}.${prop.name}#${pivotAlias}`]: { | ||
| prop, type, ownerAlias, | ||
| alias: pivotAlias, | ||
| inverseAlias: alias, | ||
| joinColumns: prop.joinColumns, | ||
| inverseJoinColumns: prop.inverseJoinColumns, | ||
| primaryKeys: prop.referencedColumnNames, | ||
| cond: {}, | ||
| table: pivotMeta.tableName, | ||
| schema: prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(pivotMeta, { schema }), | ||
| path: path.endsWith('[pivot]') ? path : `${path}[pivot]`, | ||
| }, | ||
| }; | ||
| if (type === JoinType.pivotJoin) { | ||
| return ret; | ||
| } | ||
| const prop2 = prop.owner ? pivotMeta.relations[1] : pivotMeta.relations[0]; | ||
| ret[`${pivotAlias}.${prop2.name}#${alias}`] = this.joinManyToOneReference(prop2, pivotAlias, alias, type, cond, schema); | ||
| ret[`${pivotAlias}.${prop2.name}#${alias}`].path = path; | ||
| const tmp = prop2.referencedTableName.split('.'); | ||
| ret[`${pivotAlias}.${prop2.name}#${alias}`].schema ??= tmp.length > 1 ? tmp[0] : undefined; | ||
| return ret; | ||
| } | ||
| processJoins(qb, joins, schema) { | ||
| Object.values(joins).forEach(join => { | ||
| if ([JoinType.nestedInnerJoin, JoinType.nestedLeftJoin].includes(join.type)) { | ||
| return; | ||
| } | ||
| const { sql, params } = this.createJoinExpression(join, joins, schema); | ||
| qb.join(sql, params); | ||
| }); | ||
| } | ||
| createJoinExpression(join, joins, schema) { | ||
| let table = join.table; | ||
| const method = { | ||
| [JoinType.nestedInnerJoin]: 'inner join', | ||
| [JoinType.nestedLeftJoin]: 'left join', | ||
| [JoinType.pivotJoin]: 'left join', | ||
| }[join.type] ?? join.type; | ||
| const conditions = []; | ||
| const params = []; | ||
| schema = join.schema && join.schema !== '*' ? join.schema : schema; | ||
| if (schema && schema !== this.platform.getDefaultSchemaName()) { | ||
| table = `${schema}.${table}`; | ||
| } | ||
| if (join.prop.name !== '__subquery__') { | ||
| join.primaryKeys.forEach((primaryKey, idx) => { | ||
| const right = `${join.alias}.${join.joinColumns[idx]}`; | ||
| if (join.prop.formula) { | ||
| const alias = this.platform.quoteIdentifier(join.ownerAlias); | ||
| const left = join.prop.formula(alias); | ||
| conditions.push(`${left} = ${this.platform.quoteIdentifier(right)}`); | ||
| return; | ||
| } | ||
| const left = join.prop.object && join.prop.fieldNameRaw | ||
| ? join.prop.fieldNameRaw.replaceAll(ALIAS_REPLACEMENT, join.ownerAlias) | ||
| : this.platform.quoteIdentifier(`${join.ownerAlias}.${primaryKey}`); | ||
| conditions.push(`${left} = ${this.platform.quoteIdentifier(right)}`); | ||
| }); | ||
| } | ||
| if (join.prop.targetMeta?.discriminatorValue && !join.path?.endsWith('[pivot]')) { | ||
| const typeProperty = join.prop.targetMeta.root.discriminatorColumn; | ||
| const alias = join.inverseAlias ?? join.alias; | ||
| join.cond[`${alias}.${typeProperty}`] = join.prop.targetMeta.discriminatorValue; | ||
| } | ||
| let sql = method + ' '; | ||
| if (join.nested) { | ||
| sql += `(${this.platform.quoteIdentifier(table)} as ${this.platform.quoteIdentifier(join.alias)}`; | ||
| for (const nested of join.nested) { | ||
| const { sql: nestedSql, params: nestedParams } = this.createJoinExpression(nested, joins, schema); | ||
| sql += ' ' + nestedSql; | ||
| params.push(...nestedParams); | ||
| } | ||
| sql += `)`; | ||
| } | ||
| else if (join.subquery) { | ||
| sql += `(${join.subquery}) as ${this.platform.quoteIdentifier(join.alias)}`; | ||
| } | ||
| else { | ||
| sql += `${this.platform.quoteIdentifier(table)} as ${this.platform.quoteIdentifier(join.alias)}`; | ||
| } | ||
| const oldAlias = this.alias; | ||
| this.alias = join.alias; | ||
| const subquery = this._appendQueryCondition(QueryType.SELECT, join.cond); | ||
| this.alias = oldAlias; | ||
| if (subquery.sql) { | ||
| conditions.push(subquery.sql); | ||
| params.push(...subquery.params); | ||
| } | ||
| if (conditions.length > 0) { | ||
| sql += ` on ${conditions.join(' and ')}`; | ||
| } | ||
| return { sql, params }; | ||
| } | ||
| mapJoinColumns(type, join) { | ||
| if (join.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(join.prop.kind)) { | ||
| return join.prop.fieldNames.map((_fieldName, idx) => { | ||
| const columns = join.prop.owner ? join.joinColumns : join.inverseJoinColumns; | ||
| return this.mapper(`${join.alias}.${columns[idx]}`, type, undefined, `${join.alias}__${columns[idx]}`); | ||
| }); | ||
| } | ||
| return [ | ||
| ...join.joinColumns.map(col => this.mapper(`${join.alias}.${col}`, type, undefined, `fk__${col}`)), | ||
| ...join.inverseJoinColumns.map(col => this.mapper(`${join.alias}.${col}`, type, undefined, `fk__${col}`)), | ||
| ]; | ||
| } | ||
| isOneToOneInverse(field, meta) { | ||
| meta ??= this.metadata.find(this.entityName); | ||
| const prop = meta.properties[field.replace(/:ref$/, '')]; | ||
| return prop && prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner; | ||
| } | ||
| getTableName(entityName) { | ||
| const meta = this.metadata.find(entityName); | ||
| return meta ? meta.collection : entityName; | ||
| } | ||
| /** | ||
| * Checks whether the RE can be rewritten to simple LIKE query | ||
| */ | ||
| isSimpleRegExp(re) { | ||
| if (!(re instanceof RegExp)) { | ||
| return false; | ||
| } | ||
| if (re.flags.includes('i')) { | ||
| return false; | ||
| } | ||
| // when including the opening bracket/paren we consider it complex | ||
| return !re.source.match(/[{[(]/); | ||
| } | ||
| getRegExpParam(re) { | ||
| const value = re.source | ||
| .replace(/\.\*/g, '%') // .* -> % | ||
| .replace(/\./g, '_') // . -> _ | ||
| .replace(/\\_/g, '.') // \. -> . | ||
| .replace(/^\^/g, '') // remove ^ from start | ||
| .replace(/\$$/g, ''); // remove $ from end | ||
| if (re.source.startsWith('^') && re.source.endsWith('$')) { | ||
| return value; | ||
| } | ||
| if (re.source.startsWith('^')) { | ||
| return value + '%'; | ||
| } | ||
| if (re.source.endsWith('$')) { | ||
| return '%' + value; | ||
| } | ||
| return `%${value}%`; | ||
| } | ||
| appendOnConflictClause(type, onConflict, qb) { | ||
| onConflict.forEach(item => { | ||
| const { fields, ignore } = item; | ||
| const sub = qb.onConflict({ fields, ignore }); | ||
| Utils.runIfNotEmpty(() => { | ||
| let mergeParam = item.merge; | ||
| if (Utils.isObject(item.merge)) { | ||
| mergeParam = {}; | ||
| Utils.keys(item.merge).forEach(key => { | ||
| const k = this.mapper(key, type); | ||
| mergeParam[k] = item.merge[key]; | ||
| }); | ||
| } | ||
| if (Array.isArray(item.merge)) { | ||
| mergeParam = item.merge.map(key => this.mapper(key, type)); | ||
| } | ||
| sub.merge = mergeParam ?? []; | ||
| if (item.where) { | ||
| sub.where = this._appendQueryCondition(type, item.where); | ||
| } | ||
| }, 'merge' in item); | ||
| }); | ||
| } | ||
| appendQueryCondition(type, cond, qb, operator, method = 'where') { | ||
| const { sql, params } = this._appendQueryCondition(type, cond, operator); | ||
| qb[method](sql, params); | ||
| } | ||
| _appendQueryCondition(type, cond, operator) { | ||
| const parts = []; | ||
| const params = []; | ||
| for (const k of Object.keys(cond)) { | ||
| if (k === '$and' || k === '$or') { | ||
| if (operator) { | ||
| this.append(() => this.appendGroupCondition(type, k, cond[k]), parts, params, operator); | ||
| continue; | ||
| } | ||
| this.append(() => this.appendGroupCondition(type, k, cond[k]), parts, params); | ||
| continue; | ||
| } | ||
| if (k === '$not') { | ||
| const res = this._appendQueryCondition(type, cond[k]); | ||
| parts.push(`not (${res.sql})`); | ||
| params.push(...res.params); | ||
| continue; | ||
| } | ||
| this.append(() => this.appendQuerySubCondition(type, cond, k), parts, params); | ||
| } | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| append(cb, parts, params, operator) { | ||
| const res = cb(); | ||
| if (['', '()'].includes(res.sql)) { | ||
| return; | ||
| } | ||
| parts.push(operator === '$or' ? `(${res.sql})` : res.sql); | ||
| res.params.forEach(p => params.push(p)); | ||
| } | ||
| appendQuerySubCondition(type, cond, key) { | ||
| const parts = []; | ||
| const params = []; | ||
| const fields = Utils.splitPrimaryKeys(key); | ||
| if (this.isSimpleRegExp(cond[key])) { | ||
| parts.push(`${this.platform.quoteIdentifier(this.mapper(key, type))} like ?`); | ||
| params.push(this.getRegExpParam(cond[key])); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| if (Utils.isPlainObject(cond[key]) || cond[key] instanceof RegExp) { | ||
| return this.processObjectSubCondition(cond, key, type); | ||
| } | ||
| const op = cond[key] === null ? 'is' : '='; | ||
| const raw = RawQueryFragment.getKnownFragment(key); | ||
| if (raw) { | ||
| const sql = raw.sql.replaceAll(ALIAS_REPLACEMENT, this.alias); | ||
| const value = Utils.asArray(cond[key]); | ||
| params.push(...raw.params); | ||
| if (value.length > 0) { | ||
| const val = this.getValueReplacement(fields, value[0], params, key); | ||
| parts.push(`${sql} ${op} ${val}`); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| parts.push(sql); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| if (this.subQueries[key]) { | ||
| const val = this.getValueReplacement(fields, cond[key], params, key); | ||
| parts.push(`(${this.subQueries[key]}) ${op} ${val}`); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| const val = this.getValueReplacement(fields, cond[key], params, key); | ||
| parts.push(`${this.platform.quoteIdentifier(this.mapper(key, type, cond[key], null))} ${op} ${val}`); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| processObjectSubCondition(cond, key, type) { | ||
| const parts = []; | ||
| const params = []; | ||
| let value = cond[key]; | ||
| const size = Utils.getObjectKeysSize(value); | ||
| if (Utils.isPlainObject(value) && size === 0) { | ||
| return { sql: '', params }; | ||
| } | ||
| // grouped condition for one field, e.g. `{ age: { $gte: 10, $lt: 50 } }` | ||
| if (size > 1) { | ||
| const rawField = RawQueryFragment.getKnownFragment(key); | ||
| const subCondition = Object.entries(value).map(([subKey, subValue]) => { | ||
| key = rawField?.clone().toString() ?? key; | ||
| return ({ [key]: { [subKey]: subValue } }); | ||
| }); | ||
| for (const sub of subCondition) { | ||
| this.append(() => this._appendQueryCondition(type, sub, '$and'), parts, params); | ||
| } | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| if (value instanceof RegExp) { | ||
| value = this.platform.getRegExpValue(value); | ||
| } | ||
| // operators | ||
| const op = Object.keys(QueryOperator).find(op => op in value); | ||
| /* v8 ignore next */ | ||
| if (!op) { | ||
| throw ValidationError.invalidQueryCondition(cond); | ||
| } | ||
| const replacement = this.getOperatorReplacement(op, value); | ||
| const fields = Utils.splitPrimaryKeys(key); | ||
| if (fields.length > 1 && Array.isArray(value[op])) { | ||
| const singleTuple = !value[op].every((v) => Array.isArray(v)); | ||
| if (!this.platform.allowsComparingTuples()) { | ||
| const mapped = fields.map(f => this.mapper(f, type)); | ||
| if (op === '$in') { | ||
| const conds = value[op].map(() => { | ||
| return `(${mapped.map(field => `${this.platform.quoteIdentifier(field)} = ?`).join(' and ')})`; | ||
| }); | ||
| parts.push(`(${conds.join(' or ')})`); | ||
| params.push(...Utils.flatten(value[op])); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| parts.push(...mapped.map(field => `${this.platform.quoteIdentifier(field)} = ?`)); | ||
| params.push(...Utils.flatten(value[op])); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| if (singleTuple) { | ||
| const tmp = value[op].length === 1 && Utils.isPlainObject(value[op][0]) ? fields.map(f => value[op][0][f]) : value[op]; | ||
| const sql = `(${fields.map(() => '?').join(', ')})`; | ||
| value[op] = raw(sql, tmp); | ||
| } | ||
| } | ||
| if (this.subQueries[key]) { | ||
| const val = this.getValueReplacement(fields, value[op], params, op); | ||
| parts.push(`(${this.subQueries[key]}) ${replacement} ${val}`); | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| const [a, f] = this.splitField(key); | ||
| const prop = this.getProperty(f, a); | ||
| if (op === '$fulltext') { | ||
| /* v8 ignore next */ | ||
| if (!prop) { | ||
| throw new Error(`Cannot use $fulltext operator on ${key}, property not found`); | ||
| } | ||
| const { sql, params: params2 } = raw(this.platform.getFullTextWhereClause(prop), { | ||
| column: this.mapper(key, type, undefined, null), | ||
| query: value[op], | ||
| }); | ||
| parts.push(sql); | ||
| params.push(...params2); | ||
| } | ||
| else if (['$in', '$nin'].includes(op) && Array.isArray(value[op]) && value[op].length === 0) { | ||
| parts.push(`1 = ${op === '$in' ? 0 : 1}`); | ||
| } | ||
| else if (value[op] instanceof RawQueryFragment || value[op] instanceof NativeQueryBuilder) { | ||
| const query = value[op] instanceof NativeQueryBuilder ? value[op].toRaw() : value[op]; | ||
| const mappedKey = this.mapper(key, type, query, null); | ||
| let sql = query.sql; | ||
| if (['$in', '$nin'].includes(op)) { | ||
| sql = `(${sql})`; | ||
| } | ||
| parts.push(`${this.platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`); | ||
| params.push(...query.params); | ||
| } | ||
| else { | ||
| const mappedKey = this.mapper(key, type, value[op], null); | ||
| const val = this.getValueReplacement(fields, value[op], params, op, prop); | ||
| parts.push(`${this.platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`); | ||
| } | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| getValueReplacement(fields, value, params, key, prop) { | ||
| if (Array.isArray(value)) { | ||
| if (fields.length > 1) { | ||
| const tmp = []; | ||
| for (const field of value) { | ||
| tmp.push(`(${field.map(() => '?').join(', ')})`); | ||
| params.push(...field); | ||
| } | ||
| return `(${tmp.join(', ')})`; | ||
| } | ||
| if (prop?.customType instanceof ArrayType) { | ||
| const item = prop.customType.convertToDatabaseValue(value, this.platform, { fromQuery: true, key, mode: 'query' }); | ||
| params.push(item); | ||
| } | ||
| else { | ||
| value.forEach(v => params.push(v)); | ||
| } | ||
| return `(${value.map(() => '?').join(', ')})`; | ||
| } | ||
| if (value === null) { | ||
| return 'null'; | ||
| } | ||
| params.push(value); | ||
| return '?'; | ||
| } | ||
| getOperatorReplacement(op, value) { | ||
| let replacement = QueryOperator[op]; | ||
| if (op === '$exists') { | ||
| replacement = value[op] ? 'is not' : 'is'; | ||
| value[op] = null; | ||
| } | ||
| if (value[op] === null && ['$eq', '$ne'].includes(op)) { | ||
| replacement = op === '$eq' ? 'is' : 'is not'; | ||
| } | ||
| if (op === '$re') { | ||
| replacement = this.platform.getRegExpOperator(value[op], value.$flags); | ||
| } | ||
| if (replacement.includes('?')) { | ||
| replacement = replacement.replaceAll('?', '\\?'); | ||
| } | ||
| return replacement; | ||
| } | ||
| getQueryOrder(type, orderBy, populate) { | ||
| if (Array.isArray(orderBy)) { | ||
| return orderBy.flatMap(o => this.getQueryOrder(type, o, populate)); | ||
| } | ||
| return this.getQueryOrderFromObject(type, orderBy, populate); | ||
| } | ||
| getQueryOrderFromObject(type, orderBy, populate) { | ||
| const ret = []; | ||
| for (const key of Object.keys(orderBy)) { | ||
| const direction = orderBy[key]; | ||
| const order = typeof direction === 'number' ? QueryOrderNumeric[direction] : direction; | ||
| const raw = RawQueryFragment.getKnownFragment(key); | ||
| if (raw) { | ||
| ret.push(...this.platform.getOrderByExpression(this.platform.formatQuery(raw.sql, raw.params), order)); | ||
| continue; | ||
| } | ||
| for (const f of Utils.splitPrimaryKeys(key)) { | ||
| // eslint-disable-next-line prefer-const | ||
| let [alias, field] = this.splitField(f, true); | ||
| alias = populate[alias] || alias; | ||
| const prop = this.getProperty(field, alias); | ||
| const noPrefix = (prop?.persist === false && !prop.formula && !prop.embedded) || RawQueryFragment.isKnownFragment(f); | ||
| const column = this.mapper(noPrefix ? field : `${alias}.${field}`, type, undefined, null); | ||
| /* v8 ignore next */ | ||
| const rawColumn = typeof column === 'string' ? column.split('.').map(e => this.platform.quoteIdentifier(e)).join('.') : column; | ||
| const customOrder = prop?.customOrder; | ||
| let colPart = customOrder | ||
| ? this.platform.generateCustomOrder(rawColumn, customOrder) | ||
| : rawColumn; | ||
| if (isRaw(colPart)) { | ||
| colPart = this.platform.formatQuery(colPart.sql, colPart.params); | ||
| } | ||
| if (Array.isArray(order)) { | ||
| order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate))); | ||
| } | ||
| else { | ||
| ret.push(...this.platform.getOrderByExpression(colPart, order)); | ||
| } | ||
| } | ||
| } | ||
| return ret; | ||
| } | ||
| finalize(type, qb, meta, data, returning) { | ||
| const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement(); | ||
| if (!meta || !data || !usesReturningStatement) { | ||
| return; | ||
| } | ||
| // always respect explicit returning hint | ||
| if (returning && returning.length > 0) { | ||
| qb.returning(returning.map(field => this.mapper(field, type))); | ||
| return; | ||
| } | ||
| if (type === QueryType.INSERT) { | ||
| const returningProps = meta.hydrateProps | ||
| .filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw))) | ||
| .filter(prop => !(prop.name in data)); | ||
| if (returningProps.length > 0) { | ||
| qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames))); | ||
| } | ||
| return; | ||
| } | ||
| if (type === QueryType.UPDATE) { | ||
| const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]])); | ||
| if (returningProps.length > 0) { | ||
| qb.returning(returningProps.flatMap(prop => { | ||
| if (prop.hasConvertToJSValueSQL) { | ||
| const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]); | ||
| const sql = prop.customType.convertToJSValueSQL(aliased, this.platform) + ' as ' + this.platform.quoteIdentifier(prop.fieldNames[0]); | ||
| return [raw(sql)]; | ||
| } | ||
| return prop.fieldNames; | ||
| })); | ||
| } | ||
| } | ||
| } | ||
| splitField(field, greedyAlias = false) { | ||
| const parts = field.split('.'); | ||
| const ref = parts[parts.length - 1].split(':')[1]; | ||
| if (ref) { | ||
| parts[parts.length - 1] = parts[parts.length - 1].substring(0, parts[parts.length - 1].indexOf(':')); | ||
| } | ||
| if (parts.length === 1) { | ||
| return [this.alias, parts[0], ref]; | ||
| } | ||
| if (greedyAlias) { | ||
| const fromField = parts.pop(); | ||
| const fromAlias = parts.join('.'); | ||
| return [fromAlias, fromField, ref]; | ||
| } | ||
| const fromAlias = parts.shift(); | ||
| const fromField = parts.join('.'); | ||
| return [fromAlias, fromField, ref]; | ||
| } | ||
| getLockSQL(qb, lockMode, lockTables = [], joinsMap) { | ||
| const meta = this.metadata.find(this.entityName); | ||
| if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) { | ||
| throw OptimisticLockError.lockFailed(this.entityName); | ||
| } | ||
| if (lockMode !== LockMode.OPTIMISTIC && lockTables.length === 0 && joinsMap) { | ||
| const joins = Object.values(joinsMap); | ||
| const innerJoins = joins.filter(join => [JoinType.innerJoin, JoinType.innerJoinLateral, JoinType.nestedInnerJoin].includes(join.type)); | ||
| if (joins.length > innerJoins.length) { | ||
| lockTables.push(this.alias, ...innerJoins.map(join => join.alias)); | ||
| } | ||
| } | ||
| qb.lockMode(lockMode, lockTables); | ||
| } | ||
| updateVersionProperty(qb, data) { | ||
| const meta = this.metadata.find(this.entityName); | ||
| if (!meta?.versionProperty || meta.versionProperty in data) { | ||
| return; | ||
| } | ||
| const versionProperty = meta.properties[meta.versionProperty]; | ||
| let sql = this.platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1'; | ||
| if (versionProperty.runtimeType === 'Date') { | ||
| sql = this.platform.getCurrentTimestampSQL(versionProperty.length); | ||
| } | ||
| qb.update({ [versionProperty.fieldNames[0]]: raw(sql) }); | ||
| } | ||
| prefix(field, always = false, quote = false, idx) { | ||
| let ret; | ||
| if (!this.isPrefixed(field)) { | ||
| const alias = always ? (quote ? this.alias : this.platform.quoteIdentifier(this.alias)) + '.' : ''; | ||
| const fieldName = this.fieldName(field, this.alias, always, idx); | ||
| if (fieldName instanceof RawQueryFragment) { | ||
| return fieldName.sql; | ||
| } | ||
| ret = alias + fieldName; | ||
| } | ||
| else { | ||
| const [a, ...rest] = field.split('.'); | ||
| const f = rest.join('.'); | ||
| const fieldName = this.fieldName(f, a, always, idx); | ||
| if (fieldName instanceof RawQueryFragment) { | ||
| return fieldName.sql; | ||
| } | ||
| ret = a + '.' + fieldName; | ||
| } | ||
| if (quote) { | ||
| return this.platform.quoteIdentifier(ret); | ||
| } | ||
| return ret; | ||
| } | ||
| appendGroupCondition(type, operator, subCondition) { | ||
| const parts = []; | ||
| const params = []; | ||
| // single sub-condition can be ignored to reduce nesting of parens | ||
| if (subCondition.length === 1 || operator === '$and') { | ||
| for (const sub of subCondition) { | ||
| this.append(() => this._appendQueryCondition(type, sub), parts, params); | ||
| } | ||
| return { sql: parts.join(' and '), params }; | ||
| } | ||
| for (const sub of subCondition) { | ||
| // skip nesting parens if the value is simple = scalar or object without operators or with only single key, being the operator | ||
| const keys = Object.keys(sub); | ||
| const val = sub[keys[0]]; | ||
| const simple = !Utils.isPlainObject(val) || Utils.getObjectKeysSize(val) === 1 || Object.keys(val).every(k => !Utils.isOperator(k)); | ||
| if (keys.length === 1 && simple) { | ||
| this.append(() => this._appendQueryCondition(type, sub, operator), parts, params); | ||
| continue; | ||
| } | ||
| this.append(() => this._appendQueryCondition(type, sub), parts, params, operator); | ||
| } | ||
| return { sql: `(${parts.join(' or ')})`, params }; | ||
| } | ||
| isPrefixed(field) { | ||
| return !!field.match(/[\w`"[\]]+\./); | ||
| } | ||
| fieldName(field, alias, always, idx = 0) { | ||
| const prop = this.getProperty(field, alias); | ||
| if (!prop) { | ||
| return field; | ||
| } | ||
| if (prop.fieldNameRaw) { | ||
| if (!always) { | ||
| return raw(prop.fieldNameRaw | ||
| .replace(new RegExp(ALIAS_REPLACEMENT_RE + '\\.?', 'g'), '') | ||
| .replace(this.platform.quoteIdentifier('') + '.', '')); | ||
| } | ||
| if (alias) { | ||
| return raw(prop.fieldNameRaw.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), alias)); | ||
| } | ||
| /* v8 ignore next */ | ||
| return raw(prop.fieldNameRaw); | ||
| } | ||
| /* v8 ignore next */ | ||
| return prop.fieldNames?.[idx] ?? field; | ||
| } | ||
| getProperty(field, alias) { | ||
| const entityName = this.aliasMap[alias]?.entityName || this.entityName; | ||
| const meta = this.metadata.find(entityName); | ||
| // check if `alias` is not matching an embedded property name instead of alias, e.g. `address.city` | ||
| if (alias && meta) { | ||
| const prop = meta.properties[alias]; | ||
| if (prop?.kind === ReferenceKind.EMBEDDED) { | ||
| // we want to select the full object property so hydration works as expected | ||
| if (prop.object) { | ||
| return prop; | ||
| } | ||
| const parts = field.split('.'); | ||
| const nest = (p) => parts.length > 0 ? nest(p.embeddedProps[parts.shift()]) : p; | ||
| return nest(prop); | ||
| } | ||
| } | ||
| if (meta) { | ||
| if (meta.properties[field]) { | ||
| return meta.properties[field]; | ||
| } | ||
| return meta.relations.find(prop => prop.fieldNames?.some(name => field === name)); | ||
| } | ||
| return undefined; | ||
| } | ||
| isTableNameAliasRequired(type) { | ||
| return [QueryType.SELECT, QueryType.COUNT].includes(type); | ||
| } | ||
| processOnConflictCondition(cond, schema) { | ||
| const meta = this.metadata.get(this.entityName); | ||
| const tableName = meta.tableName; | ||
| for (const key of Object.keys(cond)) { | ||
| const mapped = this.mapper(key, QueryType.UPSERT); | ||
| Utils.renameKey(cond, key, tableName + '.' + mapped); | ||
| } | ||
| return cond; | ||
| } | ||
| } |
| import { type AnyString, type Dictionary, type EntityKey, type RawQueryFragment } from '@mikro-orm/core'; | ||
| import type { SelectQueryBuilder } from 'kysely'; | ||
| import { QueryBuilder } from './QueryBuilder.js'; | ||
| /** | ||
| * Creates raw SQL query fragment that can be assigned to a property or part of a filter. This fragment is represented | ||
| * by `RawQueryFragment` class instance that can be serialized to a string, so it can be used both as an object value | ||
| * and key. When serialized, the fragment key gets cached and only such cached key will be recognized by the ORM. | ||
| * This adds a runtime safety to the raw query fragments. | ||
| * | ||
| * > **`raw()` helper is required since v6 to use a raw fragment in your query, both through EntityManager and QueryBuilder.** | ||
| * | ||
| * ```ts | ||
| * // as a value | ||
| * await em.find(User, { time: raw('now()') }); | ||
| * | ||
| * // as a key | ||
| * await em.find(User, { [raw('lower(name)')]: name.toLowerCase() }); | ||
| * | ||
| * // value can be empty array | ||
| * await em.find(User, { [raw('(select 1 = 1)')]: [] }); | ||
| * ``` | ||
| * | ||
| * The `raw` helper supports several signatures, you can pass in a callback that receives the current property alias: | ||
| * | ||
| * ```ts | ||
| * await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() }); | ||
| * ``` | ||
| * | ||
| * You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature: | ||
| * | ||
| * ```ts | ||
| * await em.find(User, { [sql`lower(name)`]: name.toLowerCase() }); | ||
| * ``` | ||
| * | ||
| * When using inside filters, you might have to use a callback signature to create new raw instance for every filter usage. | ||
| * | ||
| * ```ts | ||
| * @Filter({ name: 'long', cond: () => ({ [raw('length(perex)')]: { $gt: 10000 } }) }) | ||
| * ``` | ||
| * | ||
| * The `raw` helper can be used within indexes and uniques to write database-agnostic SQL expressions. In that case, you can use `'??'` to tag your database identifiers (table name, column names, index name, ...) inside your expression, and pass those identifiers as a second parameter to the `raw` helper. Internally, those will automatically be quoted according to the database in use: | ||
| * | ||
| * ```ts | ||
| * // On postgres, will produce: create index "index custom_idx_on_name" on "library.author" ("country") | ||
| * // On mysql, will produce: create index `index custom_idx_on_name` on `library.author` (`country`) | ||
| * @Index({ name: 'custom_idx_on_name', expression: (table, columns) => raw(`create index ?? on ?? (??)`, ['custom_idx_on_name', table, columns.name]) }) | ||
| * @Entity({ schema: 'library' }) | ||
| * export class Author { ... } | ||
| * ``` | ||
| * | ||
| * You can also use the `quote` tag function to write database-agnostic SQL expressions. The end-result is the same as using the `raw` function regarding database identifiers quoting, only to have a more elegant expression syntax: | ||
| * | ||
| * ```ts | ||
| * @Index({ name: 'custom_idx_on_name', expression: (table, columns) => quote`create index ${'custom_idx_on_name'} on ${table} (${columns.name})` }) | ||
| * @Entity({ schema: 'library' }) | ||
| * export class Author { ... } | ||
| * ``` | ||
| */ | ||
| export declare function raw<T extends object = any, R = any>(sql: SelectQueryBuilder<any, any, any> | QueryBuilder<T> | EntityKey<T> | EntityKey<T>[] | AnyString | ((alias: string) => string) | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>): NoInfer<R>; |
+68
| import { raw as raw_, Utils } from '@mikro-orm/core'; | ||
| import { QueryBuilder } from './QueryBuilder.js'; | ||
| /** | ||
| * Creates raw SQL query fragment that can be assigned to a property or part of a filter. This fragment is represented | ||
| * by `RawQueryFragment` class instance that can be serialized to a string, so it can be used both as an object value | ||
| * and key. When serialized, the fragment key gets cached and only such cached key will be recognized by the ORM. | ||
| * This adds a runtime safety to the raw query fragments. | ||
| * | ||
| * > **`raw()` helper is required since v6 to use a raw fragment in your query, both through EntityManager and QueryBuilder.** | ||
| * | ||
| * ```ts | ||
| * // as a value | ||
| * await em.find(User, { time: raw('now()') }); | ||
| * | ||
| * // as a key | ||
| * await em.find(User, { [raw('lower(name)')]: name.toLowerCase() }); | ||
| * | ||
| * // value can be empty array | ||
| * await em.find(User, { [raw('(select 1 = 1)')]: [] }); | ||
| * ``` | ||
| * | ||
| * The `raw` helper supports several signatures, you can pass in a callback that receives the current property alias: | ||
| * | ||
| * ```ts | ||
| * await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() }); | ||
| * ``` | ||
| * | ||
| * You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature: | ||
| * | ||
| * ```ts | ||
| * await em.find(User, { [sql`lower(name)`]: name.toLowerCase() }); | ||
| * ``` | ||
| * | ||
| * When using inside filters, you might have to use a callback signature to create new raw instance for every filter usage. | ||
| * | ||
| * ```ts | ||
| * @Filter({ name: 'long', cond: () => ({ [raw('length(perex)')]: { $gt: 10000 } }) }) | ||
| * ``` | ||
| * | ||
| * The `raw` helper can be used within indexes and uniques to write database-agnostic SQL expressions. In that case, you can use `'??'` to tag your database identifiers (table name, column names, index name, ...) inside your expression, and pass those identifiers as a second parameter to the `raw` helper. Internally, those will automatically be quoted according to the database in use: | ||
| * | ||
| * ```ts | ||
| * // On postgres, will produce: create index "index custom_idx_on_name" on "library.author" ("country") | ||
| * // On mysql, will produce: create index `index custom_idx_on_name` on `library.author` (`country`) | ||
| * @Index({ name: 'custom_idx_on_name', expression: (table, columns) => raw(`create index ?? on ?? (??)`, ['custom_idx_on_name', table, columns.name]) }) | ||
| * @Entity({ schema: 'library' }) | ||
| * export class Author { ... } | ||
| * ``` | ||
| * | ||
| * You can also use the `quote` tag function to write database-agnostic SQL expressions. The end-result is the same as using the `raw` function regarding database identifiers quoting, only to have a more elegant expression syntax: | ||
| * | ||
| * ```ts | ||
| * @Index({ name: 'custom_idx_on_name', expression: (table, columns) => quote`create index ${'custom_idx_on_name'} on ${table} (${columns.name})` }) | ||
| * @Entity({ schema: 'library' }) | ||
| * export class Author { ... } | ||
| * ``` | ||
| */ | ||
| export function raw(sql, params) { | ||
| if (Utils.isObject(sql) && 'compile' in sql) { | ||
| const query = sql.compile(); | ||
| return raw_(query.sql, query.parameters); | ||
| } | ||
| if (sql instanceof QueryBuilder) { | ||
| const query = sql.toQuery(); | ||
| return raw_(query.sql, query.params); | ||
| } | ||
| return raw_(sql, params); | ||
| } |
| import { CriteriaNode } from './CriteriaNode.js'; | ||
| import type { ICriteriaNodeProcessOptions, IQueryBuilder } from '../typings.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export declare class ScalarCriteriaNode<T extends object> extends CriteriaNode<T> { | ||
| process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any; | ||
| willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean; | ||
| private shouldJoin; | ||
| } |
| import { ARRAY_OPERATORS, ReferenceKind } from '@mikro-orm/core'; | ||
| import { CriteriaNode } from './CriteriaNode.js'; | ||
| import { JoinType, QueryType } from './enums.js'; | ||
| import { QueryBuilder } from './QueryBuilder.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export class ScalarCriteriaNode extends CriteriaNode { | ||
| process(qb, options) { | ||
| const matchPopulateJoins = options?.matchPopulateJoins || (this.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind)); | ||
| const nestedAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins }); | ||
| if (this.shouldJoin(qb, nestedAlias)) { | ||
| const path = this.getPath(); | ||
| const parentPath = this.parent.getPath(); // the parent is always there, otherwise `shouldJoin` would return `false` | ||
| const nestedAlias = qb.getAliasForJoinPath(path) || qb.getNextAlias(this.prop?.pivotTable ?? this.entityName); | ||
| const field = this.aliased(this.prop.name, options?.alias); | ||
| const type = this.prop.kind === ReferenceKind.MANY_TO_MANY ? JoinType.pivotJoin : JoinType.leftJoin; | ||
| qb.join(field, nestedAlias, undefined, type, path); | ||
| // select the owner as virtual property when joining from 1:1 inverse side, but only if the parent is root entity | ||
| if (this.prop.kind === ReferenceKind.ONE_TO_ONE && !parentPath.includes('.') && !qb._fields?.includes(field)) { | ||
| qb.addSelect(field); | ||
| } | ||
| } | ||
| if (this.payload instanceof QueryBuilder) { | ||
| return this.payload.getNativeQuery().toRaw(); | ||
| } | ||
| if (this.payload && typeof this.payload === 'object') { | ||
| const keys = Object.keys(this.payload).filter(key => ARRAY_OPERATORS.includes(key) && Array.isArray(this.payload[key])); | ||
| for (const key of keys) { | ||
| this.payload[key] = JSON.stringify(this.payload[key]); | ||
| } | ||
| } | ||
| return this.payload; | ||
| } | ||
| willAutoJoin(qb, alias, options) { | ||
| return this.shouldJoin(qb, alias); | ||
| } | ||
| shouldJoin(qb, nestedAlias) { | ||
| if (!this.parent || !this.prop || (nestedAlias && [QueryType.SELECT, QueryType.COUNT].includes(qb.type ?? QueryType.SELECT))) { | ||
| return false; | ||
| } | ||
| switch (this.prop.kind) { | ||
| case ReferenceKind.ONE_TO_MANY: return true; | ||
| case ReferenceKind.MANY_TO_MANY: return true; | ||
| case ReferenceKind.ONE_TO_ONE: return !this.prop.owner; | ||
| default: return false; // SCALAR, MANY_TO_ONE | ||
| } | ||
| } | ||
| } |
+390
| <h1 align="center"> | ||
| <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a> | ||
| </h1> | ||
| TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL and SQLite (including libSQL) databases. | ||
| > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/). | ||
| [](https://www.npmjs.com/package/@mikro-orm/core) | ||
| [](https://www.npmjs.com/package/@mikro-orm/core) | ||
| [](https://discord.gg/w8bjxFHS7X) | ||
| [](https://www.npmjs.com/package/@mikro-orm/core) | ||
| [](https://coveralls.io/r/mikro-orm/mikro-orm?branch=master) | ||
| [](https://github.com/mikro-orm/mikro-orm/actions?workflow=tests) | ||
| ## 🤔 Unit of What? | ||
| You might be asking: _What the hell is Unit of Work and why should I care about it?_ | ||
| > Unit of Work maintains a list of objects (_entities_) affected by a business transaction | ||
| > and coordinates the writing out of changes. [(Martin Fowler)](https://www.martinfowler.com/eaaCatalog/unitOfWork.html) | ||
| > Identity Map ensures that each object (_entity_) gets loaded only once by keeping every | ||
| > loaded object in a map. Looks up objects using the map when referring to them. | ||
| > [(Martin Fowler)](https://www.martinfowler.com/eaaCatalog/identityMap.html) | ||
| So what benefits does it bring to us? | ||
| ### Implicit Transactions | ||
| First and most important implication of having Unit of Work is that it allows handling transactions automatically. | ||
| When you call `em.flush()`, all computed changes are queried inside a database transaction (if supported by given driver). This means that you can control the boundaries of transactions simply by calling `em.persistLater()` and once all your changes are ready, calling `flush()` will run them inside a transaction. | ||
| > You can also control the transaction boundaries manually via `em.transactional(cb)`. | ||
| ```typescript | ||
| const user = await em.findOneOrFail(User, 1); | ||
| user.email = 'foo@bar.com'; | ||
| const car = new Car(); | ||
| user.cars.add(car); | ||
| // thanks to bi-directional cascading we only need to persist user entity | ||
| // flushing will create a transaction, insert new car and update user with new email | ||
| // as user entity is managed, calling flush() is enough | ||
| await em.flush(); | ||
| ``` | ||
| ### ChangeSet based persistence | ||
| MikroORM allows you to implement your domain/business logic directly in the entities. To maintain always valid entities, you can use constructors to mark required properties. Let's define the `User` entity used in previous example: | ||
| ```typescript | ||
| @Entity() | ||
| export class User { | ||
| @PrimaryKey() | ||
| id!: number; | ||
| @Property() | ||
| name!: string; | ||
| @OneToOne(() => Address) | ||
| address?: Address; | ||
| @ManyToMany(() => Car) | ||
| cars = new Collection<Car>(this); | ||
| constructor(name: string) { | ||
| this.name = name; | ||
| } | ||
| } | ||
| ``` | ||
| Now to create new instance of the `User` entity, we are forced to provide the `name`: | ||
| ```typescript | ||
| const user = new User('John Doe'); // name is required to create new user instance | ||
| user.address = new Address('10 Downing Street'); // address is optional | ||
| ``` | ||
| Once your entities are loaded, make a number of synchronous actions on your entities, | ||
| then call `em.flush()`. This will trigger computing of change sets. Only entities | ||
| (and properties) that were changed will generate database queries, if there are no changes, | ||
| no transaction will be started. | ||
| ```typescript | ||
| const user = await em.findOneOrFail(User, 1, { | ||
| populate: ['cars', 'address.city'], | ||
| }); | ||
| user.title = 'Mr.'; | ||
| user.address.street = '10 Downing Street'; // address is 1:1 relation of Address entity | ||
| user.cars.getItems().forEach(car => car.forSale = true); // cars is 1:m collection of Car entities | ||
| const car = new Car('VW'); | ||
| user.cars.add(car); | ||
| // now we can flush all changes done to managed entities | ||
| await em.flush(); | ||
| ``` | ||
| `em.flush()` will then execute these queries from the example above: | ||
| ```sql | ||
| begin; | ||
| update "user" set "title" = 'Mr.' where "id" = 1; | ||
| update "user_address" set "street" = '10 Downing Street' where "id" = 123; | ||
| update "car" | ||
| set "for_sale" = case | ||
| when ("id" = 1) then true | ||
| when ("id" = 2) then true | ||
| when ("id" = 3) then true | ||
| else "for_sale" end | ||
| where "id" in (1, 2, 3) | ||
| insert into "car" ("brand", "owner") values ('VW', 1); | ||
| commit; | ||
| ``` | ||
| ### Identity Map | ||
| Thanks to Identity Map, you will always have only one instance of given entity in one context. This allows for some optimizations (skipping loading of already loaded entities), as well as comparison by identity (`ent1 === ent2`). | ||
| ## 📖 Documentation | ||
| MikroORM documentation, included in this repo in the root directory, is built with [Docusaurus](https://docusaurus.io) and publicly hosted on GitHub Pages at https://mikro-orm.io. | ||
| There is also auto-generated [CHANGELOG.md](CHANGELOG.md) file based on commit messages (via `semantic-release`). | ||
| ## ✨ Core Features | ||
| - [Clean and Simple Entity Definition](https://mikro-orm.io/docs/defining-entities) | ||
| - [Identity Map](https://mikro-orm.io/docs/identity-map) | ||
| - [Entity References](https://mikro-orm.io/docs/entity-references) | ||
| - [Using Entity Constructors](https://mikro-orm.io/docs/entity-constructors) | ||
| - [Modelling Relationships](https://mikro-orm.io/docs/relationships) | ||
| - [Collections](https://mikro-orm.io/docs/collections) | ||
| - [Unit of Work](https://mikro-orm.io/docs/unit-of-work) | ||
| - [Transactions](https://mikro-orm.io/docs/transactions) | ||
| - [Cascading persist and remove](https://mikro-orm.io/docs/cascading) | ||
| - [Composite and Foreign Keys as Primary Key](https://mikro-orm.io/docs/composite-keys) | ||
| - [Filters](https://mikro-orm.io/docs/filters) | ||
| - [Using `QueryBuilder`](https://mikro-orm.io/docs/query-builder) | ||
| - [Populating relations](https://mikro-orm.io/docs/populating-relations) | ||
| - [Property Validation](https://mikro-orm.io/docs/property-validation) | ||
| - [Lifecycle Hooks](https://mikro-orm.io/docs/events#hooks) | ||
| - [Vanilla JS Support](https://mikro-orm.io/docs/usage-with-js) | ||
| - [Schema Generator](https://mikro-orm.io/docs/schema-generator) | ||
| - [Entity Generator](https://mikro-orm.io/docs/entity-generator) | ||
| ## 📦 Example Integrations | ||
| You can find example integrations for some popular frameworks in the [`mikro-orm-examples` repository](https://github.com/mikro-orm/mikro-orm-examples): | ||
| ### TypeScript Examples | ||
| - [Express + MongoDB](https://github.com/mikro-orm/express-ts-example-app) | ||
| - [Nest + MySQL](https://github.com/mikro-orm/nestjs-example-app) | ||
| - [RealWorld example app (Nest + MySQL)](https://github.com/mikro-orm/nestjs-realworld-example-app) | ||
| - [Koa + SQLite](https://github.com/mikro-orm/koa-ts-example-app) | ||
| - [GraphQL + PostgreSQL](https://github.com/driescroons/mikro-orm-graphql-example) | ||
| - [Inversify + PostgreSQL](https://github.com/PodaruDragos/inversify-example-app) | ||
| - [NextJS + MySQL](https://github.com/jonahallibone/mikro-orm-nextjs) | ||
| - [Accounts.js REST and GraphQL authentication + SQLite](https://github.com/darkbasic/mikro-orm-accounts-example) | ||
| - [Nest + Shopify + PostgreSQL + GraphQL](https://github.com/Cloudshelf/Shopify_CSConnector) | ||
| - [Elysia.js + libSQL + Bun](https://github.com/mikro-orm/elysia-bun-example-app) | ||
| - [Electron.js + PostgreSQL](https://github.com/adnanlah/electron-mikro-orm-example-app) | ||
| ### JavaScript Examples | ||
| - [Express + SQLite](https://github.com/mikro-orm/express-js-example-app) | ||
| ## 🚀 Quick Start | ||
| First install the module via `yarn` or `npm` and do not forget to install the database driver as well: | ||
| > Since v4, you should install the driver package, but not the db connector itself, e.g. install `@mikro-orm/sqlite`, but not `sqlite3` as that is already included in the driver package. | ||
| ```sh | ||
| yarn add @mikro-orm/core @mikro-orm/mongodb # for mongo | ||
| yarn add @mikro-orm/core @mikro-orm/mysql # for mysql/mariadb | ||
| yarn add @mikro-orm/core @mikro-orm/mariadb # for mysql/mariadb | ||
| yarn add @mikro-orm/core @mikro-orm/postgresql # for postgresql | ||
| yarn add @mikro-orm/core @mikro-orm/mssql # for mssql | ||
| yarn add @mikro-orm/core @mikro-orm/sqlite # for sqlite | ||
| yarn add @mikro-orm/core @mikro-orm/libsql # for libsql | ||
| ``` | ||
| or | ||
| ```sh | ||
| npm i -s @mikro-orm/core @mikro-orm/mongodb # for mongo | ||
| npm i -s @mikro-orm/core @mikro-orm/mysql # for mysql/mariadb | ||
| npm i -s @mikro-orm/core @mikro-orm/mariadb # for mysql/mariadb | ||
| npm i -s @mikro-orm/core @mikro-orm/postgresql # for postgresql | ||
| npm i -s @mikro-orm/core @mikro-orm/mssql # for mssql | ||
| npm i -s @mikro-orm/core @mikro-orm/sqlite # for sqlite | ||
| npm i -s @mikro-orm/core @mikro-orm/libsql # for libsql | ||
| ``` | ||
| Next, if you want to use decorators for your entity definition, you will need to enable support for [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) as well as `esModuleInterop` in `tsconfig.json` via: | ||
| ```json | ||
| "experimentalDecorators": true, | ||
| "emitDecoratorMetadata": true, | ||
| "esModuleInterop": true, | ||
| ``` | ||
| Alternatively, you can use [`EntitySchema`](https://mikro-orm.io/docs/entity-schema). | ||
| Then call `MikroORM.init` as part of bootstrapping your app: | ||
| > To access driver specific methods like `em.createQueryBuilder()` we need to specify the driver type when calling `MikroORM.init()`. Alternatively we can cast the `orm.em` to `EntityManager` exported from the driver package: | ||
| > | ||
| > ```ts | ||
| > import { EntityManager } from '@mikro-orm/postgresql'; | ||
| > const em = orm.em as EntityManager; | ||
| > const qb = em.createQueryBuilder(...); | ||
| > ``` | ||
| ```typescript | ||
| import type { PostgreSqlDriver } from '@mikro-orm/postgresql'; // or any other SQL driver package | ||
| const orm = await MikroORM.init<PostgreSqlDriver>({ | ||
| entities: ['./dist/entities'], // path to your JS entities (dist), relative to `baseDir` | ||
| dbName: 'my-db-name', | ||
| type: 'postgresql', | ||
| }); | ||
| console.log(orm.em); // access EntityManager via `em` property | ||
| ``` | ||
| There are more ways to configure your entities, take a look at [installation page](https://mikro-orm.io/docs/installation/). | ||
| > Read more about all the possible configuration options in [Advanced Configuration](https://mikro-orm.io/docs/configuration) section. | ||
| Then you will need to fork entity manager for each request so their [identity maps](https://mikro-orm.io/docs/identity-map/) will not collide. To do so, use the `RequestContext` helper: | ||
| ```typescript | ||
| const app = express(); | ||
| app.use((req, res, next) => { | ||
| RequestContext.create(orm.em, next); | ||
| }); | ||
| ``` | ||
| > You should register this middleware as the last one just before request handlers and before any of your custom middleware that is using the ORM. There might be issues when you register it before request processing middleware like `queryParser` or `bodyParser`, so definitely register the context after them. | ||
| More info about `RequestContext` is described [here](https://mikro-orm.io/docs/identity-map/#request-context). | ||
| Now you can start defining your entities (in one of the `entities` folders). This is how simple entity can look like in mongo driver: | ||
| **`./entities/MongoBook.ts`** | ||
| ```typescript | ||
| @Entity() | ||
| export class MongoBook { | ||
| @PrimaryKey() | ||
| _id: ObjectID; | ||
| @SerializedPrimaryKey() | ||
| id: string; | ||
| @Property() | ||
| title: string; | ||
| @ManyToOne(() => Author) | ||
| author: Author; | ||
| @ManyToMany(() => BookTag) | ||
| tags = new Collection<BookTag>(this); | ||
| constructor(title: string, author: Author) { | ||
| this.title = title; | ||
| this.author = author; | ||
| } | ||
| } | ||
| ``` | ||
| For SQL drivers, you can use `id: number` PK: | ||
| **`./entities/SqlBook.ts`** | ||
| ```typescript | ||
| @Entity() | ||
| export class SqlBook { | ||
| @PrimaryKey() | ||
| id: number; | ||
| } | ||
| ``` | ||
| Or if you want to use UUID primary keys: | ||
| **`./entities/UuidBook.ts`** | ||
| ```typescript | ||
| import { randomUUID } from 'node:crypto'; | ||
| @Entity() | ||
| export class UuidBook { | ||
| @PrimaryKey() | ||
| uuid = randomUUID(); | ||
| } | ||
| ``` | ||
| More information can be found in [defining entities section](https://mikro-orm.io/docs/defining-entities/) in docs. | ||
| When you have your entities defined, you can start using ORM either via `EntityManager` or via `EntityRepository`s. | ||
| To save entity state to database, you need to persist it. Persist takes care or deciding whether to use `insert` or `update` and computes appropriate change-set. Entity references that are not persisted yet (does not have identifier) will be cascade persisted automatically. | ||
| ```typescript | ||
| // use constructors in your entities for required parameters | ||
| const author = new Author('Jon Snow', 'snow@wall.st'); | ||
| author.born = new Date(); | ||
| const publisher = new Publisher('7K publisher'); | ||
| const book1 = new Book('My Life on The Wall, part 1', author); | ||
| book1.publisher = publisher; | ||
| const book2 = new Book('My Life on The Wall, part 2', author); | ||
| book2.publisher = publisher; | ||
| const book3 = new Book('My Life on The Wall, part 3', author); | ||
| book3.publisher = publisher; | ||
| // just persist books, author and publisher will be automatically cascade persisted | ||
| await em.persistAndFlush([book1, book2, book3]); | ||
| ``` | ||
| To fetch entities from database you can use `find()` and `findOne()` of `EntityManager`: | ||
| ```typescript | ||
| const authors = em.find(Author, {}, { populate: ['books'] }); | ||
| for (const author of authors) { | ||
| console.log(author); // instance of Author entity | ||
| console.log(author.name); // Jon Snow | ||
| for (const book of author.books) { // iterating books collection | ||
| console.log(book); // instance of Book entity | ||
| console.log(book.title); // My Life on The Wall, part 1/2/3 | ||
| } | ||
| } | ||
| ``` | ||
| More convenient way of fetching entities from database is by using `EntityRepository`, that carries the entity name, so you do not have to pass it to every `find` and `findOne` calls: | ||
| ```typescript | ||
| const booksRepository = em.getRepository(Book); | ||
| const books = await booksRepository.find({ author: '...' }, { | ||
| populate: ['author'], | ||
| limit: 1, | ||
| offset: 2, | ||
| orderBy: { title: QueryOrder.DESC }, | ||
| }); | ||
| console.log(books); // Loaded<Book, 'author'>[] | ||
| ``` | ||
| Take a look at docs about [working with `EntityManager`](https://mikro-orm.io/docs/entity-manager/) or [using `EntityRepository` instead](https://mikro-orm.io/docs/repositories/). | ||
| ## 🤝 Contributing | ||
| Contributions, issues and feature requests are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the process for submitting pull requests to us. | ||
| ## Authors | ||
| 👤 **Martin Adámek** | ||
| - Twitter: [@B4nan](https://twitter.com/B4nan) | ||
| - Github: [@b4nan](https://github.com/b4nan) | ||
| See also the list of contributors who [participated](https://github.com/mikro-orm/mikro-orm/contributors) in this project. | ||
| ## Show Your Support | ||
| Please ⭐️ this repository if this project helped you! | ||
| > If you'd like to support my open-source work, consider sponsoring me directly at [github.com/sponsors/b4nan](https://github.com/sponsors/b4nan). | ||
| ## 📝 License | ||
| Copyright © 2018 [Martin Adámek](https://github.com/b4nan). | ||
| This project is licensed under the MIT License - see the [LICENSE file](LICENSE) for details. |
| import { type Configuration, type Dictionary, type EntityMetadata } from '@mikro-orm/core'; | ||
| import { DatabaseTable } from './DatabaseTable.js'; | ||
| import type { AbstractSqlConnection } from '../AbstractSqlConnection.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export declare class DatabaseSchema { | ||
| private readonly platform; | ||
| readonly name: string; | ||
| private tables; | ||
| private namespaces; | ||
| private nativeEnums; | ||
| constructor(platform: AbstractSqlPlatform, name: string); | ||
| addTable(name: string, schema: string | undefined | null, comment?: string): DatabaseTable; | ||
| getTables(): DatabaseTable[]; | ||
| getTable(name: string): DatabaseTable | undefined; | ||
| hasTable(name: string): boolean; | ||
| setNativeEnums(nativeEnums: Dictionary<{ | ||
| name: string; | ||
| schema?: string; | ||
| items: string[]; | ||
| }>): void; | ||
| getNativeEnums(): Dictionary<{ | ||
| name: string; | ||
| schema?: string; | ||
| items: string[]; | ||
| }>; | ||
| getNativeEnum(name: string): { | ||
| name: string; | ||
| schema?: string; | ||
| items: string[]; | ||
| }; | ||
| hasNamespace(namespace: string): boolean; | ||
| hasNativeEnum(name: string): boolean; | ||
| getNamespaces(): string[]; | ||
| static create(connection: AbstractSqlConnection, platform: AbstractSqlPlatform, config: Configuration, schemaName?: string, schemas?: string[], takeTables?: (string | RegExp)[], skipTables?: (string | RegExp)[]): Promise<DatabaseSchema>; | ||
| static fromMetadata(metadata: EntityMetadata[], platform: AbstractSqlPlatform, config: Configuration, schemaName?: string): DatabaseSchema; | ||
| private static getSchemaName; | ||
| private static matchName; | ||
| private static isTableNameAllowed; | ||
| private static shouldHaveColumn; | ||
| toJSON(): Dictionary; | ||
| prune(schema: string | undefined, wildcardSchemaTables: string[]): void; | ||
| } |
| import { ReferenceKind, RawQueryFragment, } from '@mikro-orm/core'; | ||
| import { DatabaseTable } from './DatabaseTable.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export class DatabaseSchema { | ||
| platform; | ||
| name; | ||
| tables = []; | ||
| namespaces = new Set(); | ||
| nativeEnums = {}; // for postgres | ||
| constructor(platform, name) { | ||
| this.platform = platform; | ||
| this.name = name; | ||
| } | ||
| addTable(name, schema, comment) { | ||
| const namespaceName = schema ?? this.name; | ||
| const table = new DatabaseTable(this.platform, name, namespaceName); | ||
| table.nativeEnums = this.nativeEnums; | ||
| table.comment = comment; | ||
| this.tables.push(table); | ||
| if (namespaceName != null) { | ||
| this.namespaces.add(namespaceName); | ||
| } | ||
| return table; | ||
| } | ||
| getTables() { | ||
| return this.tables; | ||
| } | ||
| getTable(name) { | ||
| return this.tables.find(t => t.name === name || `${t.schema}.${t.name}` === name); | ||
| } | ||
| hasTable(name) { | ||
| return !!this.getTable(name); | ||
| } | ||
| setNativeEnums(nativeEnums) { | ||
| this.nativeEnums = nativeEnums; | ||
| for (const nativeEnum of Object.values(nativeEnums)) { | ||
| if (nativeEnum.schema && nativeEnum.schema !== '*') { | ||
| this.namespaces.add(nativeEnum.schema); | ||
| } | ||
| } | ||
| } | ||
| getNativeEnums() { | ||
| return this.nativeEnums; | ||
| } | ||
| getNativeEnum(name) { | ||
| return this.nativeEnums[name]; | ||
| } | ||
| hasNamespace(namespace) { | ||
| return this.namespaces.has(namespace); | ||
| } | ||
| hasNativeEnum(name) { | ||
| return name in this.nativeEnums; | ||
| } | ||
| getNamespaces() { | ||
| return [...this.namespaces]; | ||
| } | ||
| static async create(connection, platform, config, schemaName, schemas, takeTables, skipTables) { | ||
| const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema') ?? platform.getDefaultSchemaName()); | ||
| const allTables = await connection.execute(platform.getSchemaHelper().getListTablesSQL()); | ||
| const parts = config.get('migrations').tableName.split('.'); | ||
| const migrationsTableName = parts[1] ?? parts[0]; | ||
| const migrationsSchemaName = parts.length > 1 ? parts[0] : config.get('schema', platform.getDefaultSchemaName()); | ||
| const tables = allTables.filter(t => this.isTableNameAllowed(t.table_name, takeTables, skipTables) && (t.table_name !== migrationsTableName || (t.schema_name && t.schema_name !== migrationsSchemaName))); | ||
| await platform.getSchemaHelper().loadInformationSchema(schema, connection, tables, schemas && schemas.length > 0 ? schemas : undefined); | ||
| return schema; | ||
| } | ||
| static fromMetadata(metadata, platform, config, schemaName) { | ||
| const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema')); | ||
| const nativeEnums = {}; | ||
| const skipColumns = config.get('schemaGenerator').skipColumns || {}; | ||
| for (const meta of metadata) { | ||
| for (const prop of meta.props) { | ||
| if (prop.nativeEnumName) { | ||
| let key = prop.nativeEnumName; | ||
| let enumName = prop.nativeEnumName; | ||
| let enumSchema = meta.schema ?? schema.name; | ||
| if (key.includes('.')) { | ||
| const [explicitSchema, ...parts] = prop.nativeEnumName.split('.'); | ||
| enumName = parts.join('.'); | ||
| key = enumName; | ||
| enumSchema = explicitSchema; | ||
| } | ||
| if (enumSchema && enumSchema !== '*' && enumSchema !== platform.getDefaultSchemaName()) { | ||
| key = enumSchema + '.' + key; | ||
| } | ||
| nativeEnums[key] = { | ||
| name: enumName, | ||
| schema: enumSchema, | ||
| items: prop.items?.map(val => '' + val) ?? [], | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| schema.setNativeEnums(nativeEnums); | ||
| for (const meta of metadata) { | ||
| const table = schema.addTable(meta.collection, this.getSchemaName(meta, config, schemaName)); | ||
| table.comment = meta.comment; | ||
| for (const prop of meta.props) { | ||
| if (!this.shouldHaveColumn(meta, prop, skipColumns)) { | ||
| continue; | ||
| } | ||
| table.addColumnFromProperty(prop, meta, config); | ||
| } | ||
| meta.indexes.forEach(index => table.addIndex(meta, index, 'index')); | ||
| meta.uniques.forEach(index => table.addIndex(meta, index, 'unique')); | ||
| table.addIndex(meta, { properties: meta.props.filter(prop => prop.primary).map(prop => prop.name) }, 'primary'); | ||
| for (const check of meta.checks) { | ||
| const columnName = check.property ? meta.properties[check.property].fieldNames[0] : undefined; | ||
| let expression = check.expression; | ||
| const raw = RawQueryFragment.getKnownFragment(expression); | ||
| if (raw) { | ||
| expression = platform.formatQuery(raw.sql, raw.params); | ||
| } | ||
| table.addCheck({ | ||
| name: check.name, | ||
| expression, | ||
| definition: `check (${check.expression})`, | ||
| columnName, | ||
| }); | ||
| } | ||
| } | ||
| return schema; | ||
| } | ||
| static getSchemaName(meta, config, schema) { | ||
| return (meta.schema === '*' ? schema : meta.schema) ?? config.get('schema'); | ||
| } | ||
| static matchName(name, nameToMatch) { | ||
| return typeof nameToMatch === 'string' | ||
| ? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase() | ||
| : nameToMatch.test(name); | ||
| } | ||
| static isTableNameAllowed(tableName, takeTables, skipTables) { | ||
| return ((takeTables?.some(tableNameToMatch => this.matchName(tableName, tableNameToMatch)) ?? true) && | ||
| !(skipTables?.some(tableNameToMatch => this.matchName(tableName, tableNameToMatch)) ?? false)); | ||
| } | ||
| static shouldHaveColumn(meta, prop, skipColumns) { | ||
| if (prop.persist === false || (prop.columnTypes?.length ?? 0) === 0) { | ||
| return false; | ||
| } | ||
| // Check if column should be skipped | ||
| if (skipColumns) { | ||
| const tableName = meta.tableName; | ||
| const tableSchema = meta.schema; | ||
| const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName; | ||
| // Check for skipColumns by table name or fully qualified table name | ||
| const columnsToSkip = skipColumns[tableName] || skipColumns[fullTableName]; | ||
| if (columnsToSkip) { | ||
| for (const fieldName of prop.fieldNames) { | ||
| if (columnsToSkip.some(pattern => this.matchName(fieldName, pattern))) { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (prop.kind === ReferenceKind.EMBEDDED && prop.object) { | ||
| return true; | ||
| } | ||
| const getRootProperty = (prop) => prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop; | ||
| const rootProp = getRootProperty(prop); | ||
| if (rootProp.kind === ReferenceKind.EMBEDDED) { | ||
| return prop === rootProp || !rootProp.object; | ||
| } | ||
| return [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind) || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner); | ||
| } | ||
| toJSON() { | ||
| const { platform, namespaces, ...rest } = this; | ||
| return { namespaces: [...namespaces], ...rest }; | ||
| } | ||
| prune(schema, wildcardSchemaTables) { | ||
| const hasWildcardSchema = wildcardSchemaTables.length > 0; | ||
| this.tables = this.tables.filter(table => { | ||
| return (!schema && !hasWildcardSchema) // no schema specified and we don't have any multi-schema entity | ||
| || table.schema === schema // specified schema matches the table's one | ||
| || (!schema && !wildcardSchemaTables.includes(table.name)); // no schema specified and the table has fixed one provided | ||
| }); | ||
| // remove namespaces of ignored tables | ||
| for (const ns of this.namespaces) { | ||
| if (!this.tables.some(t => t.schema === ns) && !Object.values(this.nativeEnums).some(e => e.schema === ns)) { | ||
| this.namespaces.delete(ns); | ||
| } | ||
| } | ||
| } | ||
| } |
| import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type IndexCallback, type NamingStrategy } from '@mikro-orm/core'; | ||
| import type { SchemaHelper } from './SchemaHelper.js'; | ||
| import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export declare class DatabaseTable { | ||
| private readonly platform; | ||
| readonly name: string; | ||
| readonly schema?: string | undefined; | ||
| private columns; | ||
| private indexes; | ||
| private checks; | ||
| private foreignKeys; | ||
| nativeEnums: Dictionary<{ | ||
| name: string; | ||
| schema?: string; | ||
| items: string[]; | ||
| }>; | ||
| comment?: string; | ||
| constructor(platform: AbstractSqlPlatform, name: string, schema?: string | undefined); | ||
| getQuotedName(): string; | ||
| getColumns(): Column[]; | ||
| getColumn(name: string): Column | undefined; | ||
| removeColumn(name: string): void; | ||
| getIndexes(): IndexDef[]; | ||
| getChecks(): CheckDef[]; | ||
| init(cols: Column[], indexes: IndexDef[] | undefined, checks: CheckDef[] | undefined, pks: string[], fks?: Dictionary<ForeignKey>, enums?: Dictionary<string[]>): void; | ||
| addColumn(column: Column): void; | ||
| addColumnFromProperty(prop: EntityProperty, meta: EntityMetadata, config: Configuration): void; | ||
| private getIndexName; | ||
| getEntityDeclaration(namingStrategy: NamingStrategy, schemaHelper: SchemaHelper, scalarPropertiesForRelations: 'always' | 'never' | 'smart'): EntityMetadata; | ||
| private foreignKeysToProps; | ||
| private findFkIndex; | ||
| private getIndexProperties; | ||
| private getSafeBaseNameForFkProp; | ||
| /** | ||
| * The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names. | ||
| */ | ||
| getShortestName(skipDefaultSchema?: boolean): string; | ||
| getForeignKeys(): Dictionary<ForeignKey>; | ||
| hasColumn(columnName: string): boolean; | ||
| getIndex(indexName: string): IndexDef | undefined; | ||
| hasIndex(indexName: string): boolean; | ||
| getCheck(checkName: string): CheckDef<unknown> | undefined; | ||
| hasCheck(checkName: string): boolean; | ||
| getPrimaryKey(): IndexDef | undefined; | ||
| hasPrimaryKey(): boolean; | ||
| private getForeignKeyDeclaration; | ||
| private getPropertyDeclaration; | ||
| private getReferenceKind; | ||
| private getPropertyName; | ||
| private getPropertyTypeForForeignKey; | ||
| private getPropertyTypeForColumn; | ||
| private getPropertyDefaultValue; | ||
| private processIndexExpression; | ||
| addIndex(meta: EntityMetadata, index: { | ||
| properties?: string | string[]; | ||
| name?: string; | ||
| type?: string; | ||
| expression?: string | IndexCallback<any>; | ||
| deferMode?: DeferMode | `${DeferMode}`; | ||
| options?: Dictionary; | ||
| }, type: 'index' | 'unique' | 'primary'): void; | ||
| addCheck(check: CheckDef): void; | ||
| toJSON(): Dictionary; | ||
| } |
| import { Cascade, DecimalType, EntitySchema, RawQueryFragment, ReferenceKind, t, Type, UnknownType, Utils, } from '@mikro-orm/core'; | ||
| /** | ||
| * @internal | ||
| */ | ||
| export class DatabaseTable { | ||
| platform; | ||
| name; | ||
| schema; | ||
| columns = {}; | ||
| indexes = []; | ||
| checks = []; | ||
| foreignKeys = {}; | ||
| nativeEnums = {}; // for postgres | ||
| comment; | ||
| constructor(platform, name, schema) { | ||
| this.platform = platform; | ||
| this.name = name; | ||
| this.schema = schema; | ||
| Object.defineProperties(this, { | ||
| platform: { enumerable: false, writable: true }, | ||
| }); | ||
| } | ||
| getQuotedName() { | ||
| return this.platform.quoteIdentifier(this.getShortestName()); | ||
| } | ||
| getColumns() { | ||
| return Object.values(this.columns); | ||
| } | ||
| getColumn(name) { | ||
| return this.columns[name]; | ||
| } | ||
| removeColumn(name) { | ||
| delete this.columns[name]; | ||
| } | ||
| getIndexes() { | ||
| return Utils.removeDuplicates(this.indexes); | ||
| } | ||
| getChecks() { | ||
| return this.checks; | ||
| } | ||
| init(cols, indexes = [], checks = [], pks, fks = {}, enums = {}) { | ||
| this.indexes = indexes; | ||
| this.checks = checks; | ||
| this.foreignKeys = fks; | ||
| this.columns = cols.reduce((o, v) => { | ||
| const index = indexes.filter(i => i.columnNames[0] === v.name); | ||
| v.primary = v.primary || pks.includes(v.name); | ||
| v.unique = index.some(i => i.unique && !i.primary); | ||
| const type = v.name in enums ? 'enum' : v.type; | ||
| v.mappedType = this.platform.getMappedType(type); | ||
| v.default = v.default?.toString().startsWith('nextval(') ? null : v.default; | ||
| v.enumItems ??= enums[v.name] || []; | ||
| o[v.name] = v; | ||
| return o; | ||
| }, {}); | ||
| } | ||
| addColumn(column) { | ||
| this.columns[column.name] = column; | ||
| } | ||
| addColumnFromProperty(prop, meta, config) { | ||
| prop.fieldNames?.forEach((field, idx) => { | ||
| const type = prop.enum ? 'enum' : prop.columnTypes[idx]; | ||
| const mappedType = this.platform.getMappedType(type); | ||
| if (mappedType instanceof DecimalType) { | ||
| const match = prop.columnTypes[idx].match(/\w+\((\d+), ?(\d+)\)/); | ||
| /* v8 ignore next */ | ||
| if (match) { | ||
| prop.precision ??= +match[1]; | ||
| prop.scale ??= +match[2]; | ||
| prop.length = undefined; | ||
| } | ||
| } | ||
| if (prop.length == null && prop.columnTypes[idx]) { | ||
| prop.length = this.platform.getSchemaHelper().inferLengthFromColumnType(prop.columnTypes[idx]); | ||
| if (typeof mappedType.getDefaultLength !== 'undefined') { | ||
| prop.length ??= mappedType.getDefaultLength(this.platform); | ||
| } | ||
| } | ||
| const primary = !meta.compositePK && prop.fieldNames.length === 1 && !!prop.primary; | ||
| this.columns[field] = { | ||
| name: prop.fieldNames[idx], | ||
| type: prop.columnTypes[idx], | ||
| generated: prop.generated, | ||
| mappedType, | ||
| unsigned: prop.unsigned && this.platform.isNumericColumn(mappedType), | ||
| autoincrement: prop.autoincrement ?? (primary && prop.kind === ReferenceKind.SCALAR && this.platform.isNumericColumn(mappedType)), | ||
| primary, | ||
| nullable: this.columns[field]?.nullable ?? !!prop.nullable, | ||
| nativeEnumName: prop.nativeEnumName, | ||
| length: prop.length, | ||
| precision: prop.precision, | ||
| scale: prop.scale, | ||
| default: prop.defaultRaw, | ||
| enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined, | ||
| comment: prop.comment, | ||
| extra: prop.extra, | ||
| ignoreSchemaChanges: prop.ignoreSchemaChanges, | ||
| }; | ||
| this.columns[field].unsigned ??= this.columns[field].autoincrement; | ||
| if (this.nativeEnums[type]) { | ||
| this.columns[field].enumItems ??= this.nativeEnums[type].items; | ||
| } | ||
| const defaultValue = this.platform.getSchemaHelper().normalizeDefaultValue(prop.defaultRaw, prop.length); | ||
| this.columns[field].default = defaultValue; | ||
| }); | ||
| if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) { | ||
| const constraintName = this.getIndexName(prop.foreignKeyName ?? true, prop.fieldNames, 'foreign'); | ||
| let schema = prop.targetMeta.root.schema === '*' ? this.schema : (prop.targetMeta.root.schema ?? config.get('schema', this.platform.getDefaultSchemaName())); | ||
| if (prop.referencedTableName.includes('.')) { | ||
| schema = undefined; | ||
| } | ||
| if (prop.createForeignKeyConstraint) { | ||
| this.foreignKeys[constraintName] = { | ||
| constraintName, | ||
| columnNames: prop.fieldNames, | ||
| localTableName: this.getShortestName(false), | ||
| referencedColumnNames: prop.referencedColumnNames, | ||
| referencedTableName: schema ? `${schema}.${prop.referencedTableName}` : prop.referencedTableName, | ||
| }; | ||
| const cascade = prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL); | ||
| if (prop.deleteRule || cascade || prop.nullable) { | ||
| this.foreignKeys[constraintName].deleteRule = prop.deleteRule || (cascade ? 'cascade' : 'set null'); | ||
| } | ||
| if (prop.updateRule) { | ||
| this.foreignKeys[constraintName].updateRule = prop.updateRule || 'cascade'; | ||
| } | ||
| if ((prop.cascade.includes(Cascade.PERSIST) || prop.cascade.includes(Cascade.ALL))) { | ||
| const hasCascadePath = Object.values(this.foreignKeys).some(fk => { | ||
| return fk.constraintName !== constraintName | ||
| && ((fk.updateRule && fk.updateRule !== 'no action') || (fk.deleteRule && fk.deleteRule !== 'no action')) | ||
| && fk.referencedTableName === this.foreignKeys[constraintName].referencedTableName; | ||
| }); | ||
| if (!hasCascadePath || this.platform.supportsMultipleCascadePaths()) { | ||
| this.foreignKeys[constraintName].updateRule ??= 'cascade'; | ||
| } | ||
| } | ||
| if (prop.deferMode) { | ||
| this.foreignKeys[constraintName].deferMode = prop.deferMode; | ||
| } | ||
| } | ||
| } | ||
| if (prop.index) { | ||
| this.indexes.push({ | ||
| columnNames: prop.fieldNames, | ||
| composite: prop.fieldNames.length > 1, | ||
| keyName: this.getIndexName(prop.index, prop.fieldNames, 'index'), | ||
| constraint: false, | ||
| primary: false, | ||
| unique: false, | ||
| }); | ||
| } | ||
| if (prop.unique && !(prop.primary && !meta.compositePK)) { | ||
| this.indexes.push({ | ||
| columnNames: prop.fieldNames, | ||
| composite: prop.fieldNames.length > 1, | ||
| keyName: this.getIndexName(prop.unique, prop.fieldNames, 'unique'), | ||
| constraint: !prop.fieldNames.some((d) => d.includes('.')), | ||
| primary: false, | ||
| unique: true, | ||
| deferMode: prop.deferMode, | ||
| }); | ||
| } | ||
| } | ||
| getIndexName(value, columnNames, type) { | ||
| if (typeof value === 'string') { | ||
| return value; | ||
| } | ||
| return this.platform.getIndexName(this.name, columnNames, type); | ||
| } | ||
| getEntityDeclaration(namingStrategy, schemaHelper, scalarPropertiesForRelations) { | ||
| const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames, } = this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations); | ||
| const name = namingStrategy.getEntityName(this.name, this.schema); | ||
| const schema = new EntitySchema({ name, collection: this.name, schema: this.schema, comment: this.comment }); | ||
| const compositeFkIndexes = {}; | ||
| const compositeFkUniques = {}; | ||
| const potentiallyUnmappedIndexes = this.indexes.filter(index => !index.primary // Skip primary index. Whether it's in use by scalar column or FK, it's already mapped. | ||
| && (( // Non-trivial non-composite indexes will be declared at the entity's metadata, though later outputted in the property | ||
| index.columnNames.length > 1 // All composite indexes are to be mapped to entity decorators or FK props. | ||
| || skippedColumnNames.includes(index.columnNames[0]) // Non-composite indexes for skipped columns are to be mapped as entity decorators. | ||
| || index.deferMode || index.expression || !(index.columnNames[0] in columnFks)) // Trivial non-composite indexes for scalar props are to be mapped to the column. | ||
| ) | ||
| // ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions) | ||
| && !(index.columnNames.some(col => !col) && !index.expression)); | ||
| for (const index of potentiallyUnmappedIndexes) { | ||
| const ret = { | ||
| name: index.keyName, | ||
| deferMode: index.deferMode, | ||
| expression: index.expression, | ||
| }; | ||
| const isTrivial = !index.deferMode && !index.expression; | ||
| if (isTrivial) { | ||
| // Index is for FK. Map to the FK prop and move on. | ||
| const fkForIndex = fkIndexes.get(index); | ||
| if (fkForIndex && !fkForIndex.fk.columnNames.some(col => !index.columnNames.includes(col))) { | ||
| ret.properties = [this.getPropertyName(namingStrategy, fkForIndex.baseName, fkForIndex.fk)]; | ||
| const map = index.unique ? compositeFkUniques : compositeFkIndexes; | ||
| if (typeof map[ret.properties[0]] === 'undefined') { | ||
| map[ret.properties[0]] = index; | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
| const properties = ret.properties ?? this.getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy); | ||
| // If there is a column that cannot be unambiguously mapped to a prop, render an expression. | ||
| if (typeof properties === 'undefined') { | ||
| ret.expression ??= schemaHelper.getCreateIndexSQL(this.name, index); | ||
| } | ||
| else { | ||
| ret.properties ??= properties; | ||
| // If the index is for one property that is not a FK prop, map to the column prop and move on. | ||
| if (properties.length === 1 && isTrivial && !fksOnStandaloneProps.has(properties[0])) { | ||
| const map = index.unique ? compositeFkUniques : compositeFkIndexes; | ||
| // Only map one trivial index. If the same column is indexed many times over, output | ||
| if (typeof map[properties[0]] === 'undefined') { | ||
| map[properties[0]] = index; | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
| // Composite indexes that aren't exclusively mapped to FK props get an entity decorator. | ||
| if (index.unique) { | ||
| schema.addUnique(ret); | ||
| continue; | ||
| } | ||
| schema.addIndex(ret); | ||
| } | ||
| const addedStandaloneFkPropsBasedOnColumn = new Set; | ||
| const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name)); | ||
| for (const column of nonSkippedColumns) { | ||
| const columnName = column.name; | ||
| const standaloneFkPropBasedOnColumn = fksOnStandaloneProps.get(columnName); | ||
| if (standaloneFkPropBasedOnColumn && !fksOnColumnProps.get(columnName)) { | ||
| addedStandaloneFkPropsBasedOnColumn.add(columnName); | ||
| const { fkIndex, currentFk } = standaloneFkPropBasedOnColumn; | ||
| const prop = this.getForeignKeyDeclaration(currentFk, namingStrategy, schemaHelper, fkIndex, nullableForeignKeys.has(currentFk), columnName, fksOnColumnProps); | ||
| schema.addProperty(prop.name, prop.type, prop); | ||
| } | ||
| const prop = this.getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fksOnColumnProps.get(columnName)); | ||
| schema.addProperty(prop.name, prop.type, prop); | ||
| } | ||
| for (const [propBaseName, { fkIndex, currentFk }] of fksOnStandaloneProps.entries()) { | ||
| if (addedStandaloneFkPropsBasedOnColumn.has(propBaseName)) { | ||
| continue; | ||
| } | ||
| const prop = this.getForeignKeyDeclaration(currentFk, namingStrategy, schemaHelper, fkIndex, nullableForeignKeys.has(currentFk), propBaseName, fksOnColumnProps); | ||
| schema.addProperty(prop.name, prop.type, prop); | ||
| } | ||
| const meta = schema.init().meta; | ||
| const oneToOneCandidateProperties = meta.relations | ||
| .filter(prop => prop.primary && prop.kind === ReferenceKind.MANY_TO_ONE); | ||
| if (oneToOneCandidateProperties.length === 1 | ||
| && oneToOneCandidateProperties[0].fieldNames.length === (new Set(meta.getPrimaryProps().flatMap(prop => prop.fieldNames))).size) { | ||
| oneToOneCandidateProperties[0].kind = ReferenceKind.ONE_TO_ONE; | ||
| } | ||
| return meta; | ||
| } | ||
| foreignKeysToProps(namingStrategy, scalarPropertiesForRelations) { | ||
| const fks = Object.values(this.getForeignKeys()); | ||
| const fksOnColumnProps = new Map(); | ||
| const fksOnStandaloneProps = new Map(); | ||
| const columnFks = {}; | ||
| const fkIndexes = new Map(); | ||
| const nullableForeignKeys = new Set(); | ||
| const standaloneFksBasedOnColumnNames = new Map(); | ||
| for (const currentFk of fks) { | ||
| const fkIndex = this.findFkIndex(currentFk); | ||
| if (currentFk.columnNames.length === 1 && !fks.some(fk => fk !== currentFk && fk.columnNames.length === 1 && currentFk.columnNames[0] === fk.columnNames[0])) { | ||
| // Non-composite FK is the only possible one for a column. Render the column with it. | ||
| const columnName = currentFk.columnNames[0]; | ||
| columnFks[columnName] ??= []; | ||
| columnFks[columnName].push(currentFk); | ||
| if (this.getColumn(columnName)?.nullable) { | ||
| nullableForeignKeys.add(currentFk); | ||
| } | ||
| if (scalarPropertiesForRelations === 'always') { | ||
| const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName); | ||
| standaloneFksBasedOnColumnNames.set(baseName, currentFk); | ||
| fksOnStandaloneProps.set(baseName, { fkIndex, currentFk }); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName }); | ||
| } | ||
| } | ||
| else { | ||
| fksOnColumnProps.set(columnName, currentFk); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName }); | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| const specificColumnNames = []; | ||
| const nullableColumnsInFk = []; | ||
| for (const columnName of currentFk.columnNames) { | ||
| columnFks[columnName] ??= []; | ||
| columnFks[columnName].push(currentFk); | ||
| if (!fks.some(fk => fk !== currentFk && fk.columnNames.includes(columnName))) { | ||
| specificColumnNames.push(columnName); | ||
| } | ||
| if (this.getColumn(columnName)?.nullable) { | ||
| nullableColumnsInFk.push(columnName); | ||
| } | ||
| } | ||
| if (nullableColumnsInFk.length > 0) { | ||
| nullableForeignKeys.add(currentFk); | ||
| } | ||
| if (specificColumnNames.length === 1 && ((nullableColumnsInFk.length === currentFk.columnNames.length || nullableColumnsInFk.length === 0) || (nullableColumnsInFk.length === 1 && nullableColumnsInFk[0] === specificColumnNames[0]))) { | ||
| // Composite FK has exactly one column which is not used in any other FK. | ||
| // The FK also doesn't have a mix of nullable and non-nullable columns, | ||
| // or its only nullable column is this very one. | ||
| // It is safe to just render this FK attached to the specific column. | ||
| const columnName = specificColumnNames[0]; | ||
| if (scalarPropertiesForRelations === 'always') { | ||
| const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName); | ||
| standaloneFksBasedOnColumnNames.set(baseName, currentFk); | ||
| fksOnStandaloneProps.set(baseName, { fkIndex, currentFk }); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName }); | ||
| } | ||
| } | ||
| else { | ||
| fksOnColumnProps.set(columnName, currentFk); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName }); | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| if (specificColumnNames.length === currentFk.columnNames.length) { | ||
| // All columns involved with this FK are only covered by this one FK. | ||
| if (nullableColumnsInFk.length <= 1) { | ||
| // Also, this FK is either not nullable, or has only one nullable column. | ||
| // It is safe to name the FK after the nullable column, or any non-nullable one (the first one is picked). | ||
| const columnName = nullableColumnsInFk.at(0) ?? currentFk.columnNames[0]; | ||
| if (scalarPropertiesForRelations === 'always') { | ||
| const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName); | ||
| standaloneFksBasedOnColumnNames.set(baseName, currentFk); | ||
| fksOnStandaloneProps.set(baseName, { fkIndex, currentFk }); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName }); | ||
| } | ||
| } | ||
| else { | ||
| fksOnColumnProps.set(columnName, currentFk); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName }); | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| // If the first nullable column's name with FK is different from the name without FK, | ||
| // name a standalone prop after the column, but treat the column prop itself as not having FK. | ||
| const columnName = nullableColumnsInFk[0]; | ||
| const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName); | ||
| standaloneFksBasedOnColumnNames.set(baseName, currentFk); | ||
| fksOnStandaloneProps.set(baseName, { fkIndex, currentFk }); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName }); | ||
| } | ||
| continue; | ||
| } | ||
| // FK is not unambiguously mappable to a column. Pick another name for a standalone FK prop. | ||
| const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks); | ||
| fksOnStandaloneProps.set(baseName, { fkIndex, currentFk }); | ||
| if (fkIndex) { | ||
| fkIndexes.set(fkIndex, { fk: currentFk, baseName }); | ||
| } | ||
| } | ||
| const columnsInFks = Object.keys(columnFks); | ||
| const skippingHandlers = { | ||
| // Never generate scalar props for composite keys, | ||
| // i.e. always skip columns if they are covered by foreign keys. | ||
| never: (column) => columnsInFks.includes(column.name) && !fksOnColumnProps.has(column.name), | ||
| // Always generate scalar props for composite keys, | ||
| // i.e. do not skip columns, even if they are covered by foreign keys. | ||
| always: (column) => false, | ||
| // Smart scalar props generation. | ||
| // Skips columns if they are covered by foreign keys. | ||
| // But also does not skip if the column is not nullable, and yet all involved FKs are nullable, | ||
| // or if one or more FKs involved has multiple nullable columns. | ||
| smart: (column) => { | ||
| return columnsInFks.includes(column.name) | ||
| && !fksOnColumnProps.has(column.name) | ||
| && (column.nullable | ||
| ? columnFks[column.name].some(fk => !fk.columnNames.some(fkColumnName => fkColumnName !== column.name && this.getColumn(fkColumnName)?.nullable)) | ||
| : columnFks[column.name].some(fk => !nullableForeignKeys.has(fk))); | ||
| }, | ||
| }; | ||
| const skippedColumnNames = this.getColumns().filter(skippingHandlers[scalarPropertiesForRelations]).map(column => column.name); | ||
| // Check standalone FKs named after columns for potential conflicts among themselves. | ||
| // This typically happens when two standalone FKs named after a column resolve to the same prop name | ||
| // because the respective columns include the referenced table in the name. | ||
| // Depending on naming strategy and actual names, it may also originate from other scenarios. | ||
| // We do our best to de-duplicate them here. | ||
| const safePropNames = new Set(); | ||
| const unsafePropNames = new Map(); | ||
| for (const [unsafeBaseName, currentFk] of standaloneFksBasedOnColumnNames) { | ||
| const propName = this.getPropertyName(namingStrategy, unsafeBaseName, currentFk); | ||
| if (safePropNames.has(propName)) { | ||
| if (!unsafePropNames.has(propName)) { | ||
| unsafePropNames.set(propName, []); | ||
| } | ||
| unsafePropNames.get(propName).push({ unsafeBaseName, currentFk }); | ||
| continue; | ||
| } | ||
| safePropNames.add(propName); | ||
| } | ||
| for (const [unsafePropName, affectedBaseNames] of unsafePropNames) { | ||
| safePropNames.delete(unsafePropName); | ||
| for (const { unsafeBaseName, currentFk } of affectedBaseNames) { | ||
| const newBaseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks); | ||
| fksOnStandaloneProps.delete(unsafeBaseName); | ||
| let fkIndex; | ||
| for (const [indexDef, fkIndexDesc] of fkIndexes) { | ||
| if (fkIndexDesc.fk !== currentFk) { | ||
| continue; | ||
| } | ||
| fkIndexDesc.baseName = newBaseName; | ||
| fkIndex = indexDef; | ||
| break; | ||
| } | ||
| fksOnStandaloneProps.set(newBaseName, { fkIndex, currentFk }); | ||
| } | ||
| } | ||
| return { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames }; | ||
| } | ||
| findFkIndex(currentFk) { | ||
| const fkColumnsLength = currentFk.columnNames.length; | ||
| const possibleIndexes = this.indexes.filter(index => { | ||
| return index.columnNames.length === fkColumnsLength && !currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName); | ||
| }); | ||
| possibleIndexes.sort((a, b) => { | ||
| if (a.primary !== b.primary) { | ||
| return a.primary ? -1 : 1; | ||
| } | ||
| if (a.unique !== b.unique) { | ||
| return a.unique ? -1 : 1; | ||
| } | ||
| return a.keyName.localeCompare(b.keyName); | ||
| }); | ||
| return possibleIndexes.at(0); | ||
| } | ||
| getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) { | ||
| const propBaseNames = new Set(); | ||
| const columnNames = index.columnNames; | ||
| const l = columnNames.length; | ||
| if (columnNames.some(col => !col)) { | ||
| return; | ||
| } | ||
| for (let i = 0; i < l; ++i) { | ||
| const columnName = columnNames[i]; | ||
| // The column is not involved with FKs. | ||
| if (!(columnName in columnFks)) { | ||
| // If there is no such column, the "name" is actually an expression. | ||
| if (!this.hasColumn(columnName)) { | ||
| return; | ||
| } | ||
| // It has a prop named after it. | ||
| // Add it and move on. | ||
| propBaseNames.add(columnName); | ||
| continue; | ||
| } | ||
| // If the prop named after the column has a FK and the FK's columns are a subset of this index, | ||
| // include this prop and move on. | ||
| const columnPropFk = fksOnColumnProps.get(columnName); | ||
| if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) { | ||
| propBaseNames.add(columnName); | ||
| continue; | ||
| } | ||
| // If there is at least one standalone FK featuring this column, | ||
| // and all of its columns are a subset of this index, | ||
| // include that FK, and consider mapping of this column to a prop a success. | ||
| let propAdded = false; | ||
| for (const [propName, { currentFk: fk }] of fksOnStandaloneProps) { | ||
| if (!columnFks[columnName].includes(fk)) { | ||
| continue; | ||
| } | ||
| if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) { | ||
| propBaseNames.add(propName); | ||
| propAdded = true; | ||
| } | ||
| } | ||
| if (propAdded) { | ||
| continue; | ||
| } | ||
| // If we have reached this point, it means the column is not mappable to a prop name. | ||
| // Break the whole prop creation. | ||
| return; | ||
| } | ||
| return Array.from(propBaseNames).map(baseName => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName))); | ||
| } | ||
| getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) { | ||
| if (columnName && this.getPropertyName(namingStrategy, columnName, currentFk) !== this.getPropertyName(namingStrategy, columnName)) { | ||
| // The eligible scalar column name is different from the name of the FK prop of the same column. | ||
| // Both can be safely rendered. | ||
| // Use the column name as a base for the FK prop. | ||
| return columnName; | ||
| } | ||
| if (!fks.some(fk => fk !== currentFk && fk.referencedTableName === currentFk.referencedTableName) && !this.getColumn(currentFk.referencedTableName)) { | ||
| // FK is the only one in this table that references this other table. | ||
| // The name of the referenced table is not shared with a column in this table, | ||
| // so it is safe to output prop name based on the referenced entity. | ||
| return currentFk.referencedTableName; | ||
| } | ||
| // Any ambiguous FK is rendered with a name based on the FK constraint name | ||
| let finalPropBaseName = currentFk.constraintName; | ||
| while (this.getColumn(finalPropBaseName)) { | ||
| // In the unlikely event that the FK constraint name is shared by a column name, generate a name by | ||
| // continuously prefixing with "fk_", until a non-existent column is hit. | ||
| // The worst case scenario is a very long name with several repeated "fk_" | ||
| // that is not really a valid DB identifier but a valid JS variable name. | ||
| finalPropBaseName = `fk_${finalPropBaseName}`; | ||
| } | ||
| return finalPropBaseName; | ||
| } | ||
| /** | ||
| * The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names. | ||
| */ | ||
| getShortestName(skipDefaultSchema = true) { | ||
| const defaultSchema = this.platform.getDefaultSchemaName(); | ||
| if (!this.schema || this.name.startsWith(defaultSchema + '.') || (this.schema === defaultSchema && skipDefaultSchema)) { | ||
| return this.name; | ||
| } | ||
| return `${this.schema}.${this.name}`; | ||
| } | ||
| getForeignKeys() { | ||
| return this.foreignKeys; | ||
| } | ||
| hasColumn(columnName) { | ||
| return columnName in this.columns; | ||
| } | ||
| getIndex(indexName) { | ||
| return this.indexes.find(i => i.keyName === indexName); | ||
| } | ||
| hasIndex(indexName) { | ||
| return !!this.getIndex(indexName); | ||
| } | ||
| getCheck(checkName) { | ||
| return this.checks.find(i => i.name === checkName); | ||
| } | ||
| hasCheck(checkName) { | ||
| return !!this.getCheck(checkName); | ||
| } | ||
| getPrimaryKey() { | ||
| return this.indexes.find(i => i.primary); | ||
| } | ||
| hasPrimaryKey() { | ||
| return !!this.getPrimaryKey(); | ||
| } | ||
| getForeignKeyDeclaration(fk, namingStrategy, schemaHelper, fkIndex, nullable, propNameBase, fksOnColumnProps) { | ||
| const prop = this.getPropertyName(namingStrategy, propNameBase, fk); | ||
| const kind = (fkIndex?.unique && !fkIndex.primary) ? this.getReferenceKind(fk, fkIndex) : this.getReferenceKind(fk); | ||
| const runtimeType = this.getPropertyTypeForForeignKey(namingStrategy, fk); | ||
| const fkOptions = {}; | ||
| fkOptions.fieldNames = fk.columnNames; | ||
| fkOptions.referencedTableName = fk.referencedTableName; | ||
| fkOptions.referencedColumnNames = fk.referencedColumnNames; | ||
| fkOptions.updateRule = fk.updateRule?.toLowerCase(); | ||
| fkOptions.deleteRule = fk.deleteRule?.toLowerCase(); | ||
| fkOptions.deferMode = fk.deferMode; | ||
| fkOptions.columnTypes = fk.columnNames.map(c => this.getColumn(c).type); | ||
| const columnOptions = {}; | ||
| if (fk.columnNames.length === 1) { | ||
| const column = this.getColumn(fk.columnNames[0]); | ||
| const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, column.type, true); | ||
| const defaultTs = this.getPropertyDefaultValue(schemaHelper, column, column.type); | ||
| columnOptions.default = (defaultRaw !== defaultTs || defaultRaw === '') ? defaultTs : undefined; | ||
| columnOptions.defaultRaw = (column.nullable && defaultRaw === 'null') ? undefined : defaultRaw; | ||
| columnOptions.optional = typeof column.generated !== 'undefined' || defaultRaw !== 'null'; | ||
| columnOptions.generated = column.generated; | ||
| columnOptions.nullable = column.nullable; | ||
| columnOptions.primary = column.primary; | ||
| columnOptions.length = column.length; | ||
| columnOptions.precision = column.precision; | ||
| columnOptions.scale = column.scale; | ||
| columnOptions.extra = column.extra; | ||
| columnOptions.comment = column.comment; | ||
| columnOptions.enum = !!column.enumItems?.length; | ||
| columnOptions.items = column.enumItems; | ||
| } | ||
| return { | ||
| name: prop, | ||
| type: runtimeType, | ||
| runtimeType, | ||
| kind, | ||
| ...columnOptions, | ||
| nullable, | ||
| primary: fkIndex?.primary || !fk.columnNames.some(columnName => !this.getPrimaryKey()?.columnNames.includes(columnName)), | ||
| index: !fkIndex?.unique ? fkIndex?.keyName : undefined, | ||
| unique: (fkIndex?.unique && !fkIndex.primary) ? fkIndex.keyName : undefined, | ||
| ...fkOptions, | ||
| }; | ||
| } | ||
| getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fk) { | ||
| const prop = this.getPropertyName(namingStrategy, column.name, fk); | ||
| const persist = !(column.name in columnFks && typeof fk === 'undefined'); | ||
| const index = compositeFkIndexes[prop] || this.indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && !idx.unique && !idx.primary); | ||
| const unique = compositeFkUniques[prop] || this.indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && idx.unique && !idx.primary); | ||
| const kind = this.getReferenceKind(fk, unique); | ||
| const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk); | ||
| const type = fk ? runtimeType : (Utils.keys(t).find(k => { | ||
| const typeInCoreMap = this.platform.getMappedType(k); | ||
| return (typeInCoreMap !== Type.getType(UnknownType) || k === 'unknown') && typeInCoreMap === column.mappedType; | ||
| }) ?? runtimeType); | ||
| const ignoreSchemaChanges = (type === 'unknown' && column.length) ? (column.extra ? ['type', 'extra'] : ['type']) : undefined; | ||
| const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, runtimeType, true); | ||
| const defaultParsed = this.getPropertyDefaultValue(schemaHelper, column, runtimeType); | ||
| const defaultTs = (defaultRaw !== defaultParsed || defaultParsed === '') ? defaultParsed : undefined; | ||
| const fkOptions = {}; | ||
| if (fk) { | ||
| fkOptions.fieldNames = fk.columnNames; | ||
| fkOptions.referencedTableName = fk.referencedTableName; | ||
| fkOptions.referencedColumnNames = fk.referencedColumnNames; | ||
| fkOptions.updateRule = fk.updateRule?.toLowerCase(); | ||
| fkOptions.deleteRule = fk.deleteRule?.toLowerCase(); | ||
| fkOptions.deferMode = fk.deferMode; | ||
| fkOptions.columnTypes = fk.columnNames.map(col => this.getColumn(col).type); | ||
| } | ||
| const ret = { | ||
| name: prop, | ||
| type, | ||
| runtimeType, | ||
| kind, | ||
| ignoreSchemaChanges, | ||
| generated: column.generated, | ||
| optional: defaultRaw !== 'null' || defaultTs != null || typeof column.generated !== 'undefined', | ||
| columnType: column.type, | ||
| default: defaultTs, | ||
| defaultRaw: (column.nullable && defaultRaw === 'null') ? undefined : defaultRaw, | ||
| nullable: column.nullable, | ||
| primary: column.primary && persist, | ||
| autoincrement: column.autoincrement, | ||
| fieldName: column.name, | ||
| unsigned: column.unsigned, | ||
| length: column.length, | ||
| precision: column.precision, | ||
| scale: column.scale, | ||
| extra: column.extra, | ||
| comment: column.comment, | ||
| index: index ? index.keyName : undefined, | ||
| unique: unique ? unique.keyName : undefined, | ||
| enum: !!column.enumItems?.length, | ||
| items: column.enumItems, | ||
| persist, | ||
| ...fkOptions, | ||
| }; | ||
| const nativeEnumName = Object.keys(this.nativeEnums).find(name => name === column.type); | ||
| if (nativeEnumName) { | ||
| ret.nativeEnumName = nativeEnumName; | ||
| } | ||
| return ret; | ||
| } | ||
| getReferenceKind(fk, unique) { | ||
| if (fk && unique) { | ||
| return ReferenceKind.ONE_TO_ONE; | ||
| } | ||
| if (fk) { | ||
| return ReferenceKind.MANY_TO_ONE; | ||
| } | ||
| return ReferenceKind.SCALAR; | ||
| } | ||
| getPropertyName(namingStrategy, baseName, fk) { | ||
| let field = baseName; | ||
| if (fk) { | ||
| const idx = fk.columnNames.indexOf(baseName); | ||
| let replacedFieldName = field.replace(new RegExp(`_${fk.referencedColumnNames[idx]}$`), ''); | ||
| if (replacedFieldName === field) { | ||
| replacedFieldName = field.replace(new RegExp(`_${namingStrategy.referenceColumnName()}$`), ''); | ||
| } | ||
| field = replacedFieldName; | ||
| } | ||
| if (field.startsWith('_')) { | ||
| return field; | ||
| } | ||
| return namingStrategy.columnNameToProperty(field); | ||
| } | ||
| getPropertyTypeForForeignKey(namingStrategy, fk) { | ||
| const parts = fk.referencedTableName.split('.', 2); | ||
| return namingStrategy.getEntityName(...parts.reverse()); | ||
| } | ||
| getPropertyTypeForColumn(namingStrategy, column, fk) { | ||
| if (fk) { | ||
| return this.getPropertyTypeForForeignKey(namingStrategy, fk); | ||
| } | ||
| const enumMode = this.platform.getConfig().get('entityGenerator').enumMode; | ||
| // If this column is using an enum. | ||
| if (column.enumItems?.length) { | ||
| const name = column.nativeEnumName ?? column.name; | ||
| const tableName = column.nativeEnumName ? undefined : this.name; | ||
| if (enumMode === 'ts-enum') { | ||
| // We will create a new enum name for this type and set it as the property type as well. | ||
| return namingStrategy.getEnumClassName(name, tableName, this.schema); | ||
| } | ||
| // With other enum strategies, we need to use the type name. | ||
| return namingStrategy.getEnumTypeName(name, tableName, this.schema); | ||
| } | ||
| return column.mappedType?.runtimeType ?? 'unknown'; | ||
| } | ||
| getPropertyDefaultValue(schemaHelper, column, propType, raw = false) { | ||
| const defaultValue = column.default ?? 'null'; | ||
| const val = schemaHelper.normalizeDefaultValue(defaultValue, column.length); | ||
| if (val === 'null') { | ||
| return raw ? 'null' : (column.nullable ? null : undefined); | ||
| } | ||
| if (propType === 'boolean' && !raw) { | ||
| return !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + column.default); | ||
| } | ||
| if (propType === 'number' && !raw) { | ||
| return +defaultValue; | ||
| } | ||
| // unquote string defaults if `raw = false` | ||
| const match = ('' + val).match(/^'(.*)'$/); | ||
| if (!raw && match) { | ||
| return match[1]; | ||
| } | ||
| return '' + val; | ||
| } | ||
| processIndexExpression(indexName, expression, meta) { | ||
| if (expression instanceof Function) { | ||
| const table = { | ||
| name: this.name, | ||
| schema: this.schema, | ||
| toString() { | ||
| if (this.schema) { | ||
| return `${this.schema}.${this.name}`; | ||
| } | ||
| return this.name; | ||
| }, | ||
| }; | ||
| const exp = expression(table, meta.createColumnMappingObject(), indexName); | ||
| return exp instanceof RawQueryFragment ? this.platform.formatQuery(exp.sql, exp.params) : exp; | ||
| } | ||
| return expression; | ||
| } | ||
| addIndex(meta, index, type) { | ||
| const properties = Utils.unique(Utils.flatten(Utils.asArray(index.properties).map(prop => { | ||
| const parts = prop.split('.'); | ||
| const root = parts[0]; | ||
| if (meta.properties[prop]) { | ||
| if (meta.properties[prop].embeddedPath) { | ||
| return [meta.properties[prop].embeddedPath.join('.')]; | ||
| } | ||
| return meta.properties[prop].fieldNames; | ||
| } | ||
| const rootProp = meta.properties[root]; | ||
| // inline embedded property index, we need to find the field name of the child property | ||
| if (rootProp?.embeddable && !rootProp.object && parts.length > 1) { | ||
| const expand = (p, i) => { | ||
| if (parts.length === i) { | ||
| return p.fieldNames[0]; | ||
| } | ||
| return expand(p.embeddedProps[parts[i]], i + 1); | ||
| }; | ||
| return [expand(rootProp, 1)]; | ||
| } | ||
| // json index, we need to rename the column only | ||
| if (rootProp) { | ||
| return [prop.replace(root, rootProp.fieldNames[0])]; | ||
| } | ||
| /* v8 ignore next */ | ||
| return [prop]; | ||
| }))); | ||
| if (properties.length === 0 && !index.expression) { | ||
| return; | ||
| } | ||
| const name = this.getIndexName(index.name, properties, type); | ||
| this.indexes.push({ | ||
| keyName: name, | ||
| columnNames: properties, | ||
| composite: properties.length > 1, | ||
| // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them | ||
| constraint: type !== 'index' && !properties.some((d) => d.includes('.')), | ||
| primary: type === 'primary', | ||
| unique: type !== 'index', | ||
| type: index.type, | ||
| expression: this.processIndexExpression(name, index.expression, meta), | ||
| options: index.options, | ||
| deferMode: index.deferMode, | ||
| }); | ||
| } | ||
| addCheck(check) { | ||
| this.checks.push(check); | ||
| } | ||
| toJSON() { | ||
| const { platform, columns, ...rest } = this; | ||
| const columnsMapped = Utils.keys(columns).reduce((o, col) => { | ||
| const { mappedType, ...restCol } = columns[col]; | ||
| o[col] = restCol; | ||
| o[col].mappedType = Utils.keys(t).find(k => t[k] === mappedType.constructor); | ||
| return o; | ||
| }, {}); | ||
| return { columns: columnsMapped, ...rest }; | ||
| } | ||
| } |
| export * from './DatabaseSchema.js'; | ||
| export * from './DatabaseTable.js'; | ||
| export * from './SqlSchemaGenerator.js'; | ||
| export * from './SchemaHelper.js'; | ||
| export * from './SchemaComparator.js'; |
| export * from './DatabaseSchema.js'; | ||
| export * from './DatabaseTable.js'; | ||
| export * from './SqlSchemaGenerator.js'; | ||
| export * from './SchemaHelper.js'; | ||
| export * from './SchemaComparator.js'; |
| import { type Dictionary } from '@mikro-orm/core'; | ||
| import type { Column, ForeignKey, IndexDef, SchemaDifference, TableDifference } from '../typings.js'; | ||
| import type { DatabaseSchema } from './DatabaseSchema.js'; | ||
| import type { DatabaseTable } from './DatabaseTable.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
| /** | ||
| * Compares two Schemas and return an instance of SchemaDifference. | ||
| */ | ||
| export declare class SchemaComparator { | ||
| private readonly platform; | ||
| private readonly helper; | ||
| private readonly logger; | ||
| constructor(platform: AbstractSqlPlatform); | ||
| /** | ||
| * Returns a SchemaDifference object containing the differences between the schemas fromSchema and toSchema. | ||
| * | ||
| * The returned differences are returned in such a way that they contain the | ||
| * operations to change the schema stored in fromSchema to the schema that is | ||
| * stored in toSchema. | ||
| */ | ||
| compare(fromSchema: DatabaseSchema, toSchema: DatabaseSchema, inverseDiff?: SchemaDifference): SchemaDifference; | ||
| /** | ||
| * Returns the difference between the tables fromTable and toTable. | ||
| * If there are no differences this method returns the boolean false. | ||
| */ | ||
| diffTable(fromTable: DatabaseTable, toTable: DatabaseTable, inverseTableDiff?: TableDifference): TableDifference | false; | ||
| /** | ||
| * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop | ||
| * however ambiguities between different possibilities should not lead to renaming at all. | ||
| */ | ||
| private detectColumnRenamings; | ||
| /** | ||
| * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop | ||
| * however ambiguities between different possibilities should not lead to renaming at all. | ||
| */ | ||
| private detectIndexRenamings; | ||
| diffForeignKey(key1: ForeignKey, key2: ForeignKey, tableDifferences: TableDifference): boolean; | ||
| /** | ||
| * Returns the difference between the columns | ||
| */ | ||
| diffColumn(fromColumn: Column, toColumn: Column, fromTable: DatabaseTable, logging?: boolean): Set<string>; | ||
| diffEnumItems(items1?: string[], items2?: string[]): boolean; | ||
| diffComment(comment1?: string, comment2?: string): boolean; | ||
| /** | ||
| * Finds the difference between the indexes index1 and index2. | ||
| * Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences. | ||
| */ | ||
| diffIndex(index1: IndexDef, index2: IndexDef): boolean; | ||
| /** | ||
| * Checks if the other index already fulfills all the indexing and constraint needs of the current one. | ||
| */ | ||
| isIndexFulfilledBy(index1: IndexDef, index2: IndexDef): boolean; | ||
| diffExpression(expr1: string, expr2: string): boolean; | ||
| parseJsonDefault(defaultValue?: string | null): Dictionary | string | null; | ||
| hasSameDefaultValue(from: Column, to: Column): boolean; | ||
| private mapColumnToProperty; | ||
| private log; | ||
| } |
| import { ArrayType, BooleanType, DateTimeType, JsonType, parseJsonSafe, Utils, inspect, } from '@mikro-orm/core'; | ||
| /** | ||
| * Compares two Schemas and return an instance of SchemaDifference. | ||
| */ | ||
| export class SchemaComparator { | ||
| platform; | ||
| helper; | ||
| logger; | ||
| constructor(platform) { | ||
| this.platform = platform; | ||
| this.helper = this.platform.getSchemaHelper(); | ||
| this.logger = this.platform.getConfig().getLogger(); | ||
| } | ||
| /** | ||
| * Returns a SchemaDifference object containing the differences between the schemas fromSchema and toSchema. | ||
| * | ||
| * The returned differences are returned in such a way that they contain the | ||
| * operations to change the schema stored in fromSchema to the schema that is | ||
| * stored in toSchema. | ||
| */ | ||
| compare(fromSchema, toSchema, inverseDiff) { | ||
| const diff = { | ||
| newTables: {}, | ||
| removedTables: {}, | ||
| changedTables: {}, | ||
| orphanedForeignKeys: [], | ||
| newNativeEnums: [], | ||
| removedNativeEnums: [], | ||
| newNamespaces: new Set(), | ||
| removedNamespaces: new Set(), | ||
| fromSchema, | ||
| }; | ||
| const foreignKeysToTable = {}; | ||
| for (const namespace of toSchema.getNamespaces()) { | ||
| if (fromSchema.hasNamespace(namespace) || namespace === this.platform.getDefaultSchemaName()) { | ||
| continue; | ||
| } | ||
| diff.newNamespaces.add(namespace); | ||
| } | ||
| for (const namespace of fromSchema.getNamespaces()) { | ||
| if (toSchema.hasNamespace(namespace) || namespace === this.platform.getDefaultSchemaName()) { | ||
| continue; | ||
| } | ||
| diff.removedNamespaces.add(namespace); | ||
| } | ||
| for (const [key, nativeEnum] of Object.entries(toSchema.getNativeEnums())) { | ||
| if (fromSchema.hasNativeEnum(key)) { | ||
| continue; | ||
| } | ||
| if (nativeEnum.schema === '*' && fromSchema.hasNativeEnum(`${toSchema.name}.${key}`)) { | ||
| continue; | ||
| } | ||
| diff.newNativeEnums.push(nativeEnum); | ||
| } | ||
| for (const [key, nativeEnum] of Object.entries(fromSchema.getNativeEnums())) { | ||
| if (toSchema.hasNativeEnum(key)) { | ||
| continue; | ||
| } | ||
| if (key.startsWith(`${fromSchema.name}.`) && (fromSchema.name !== toSchema.name || toSchema.getNativeEnum(key.substring(fromSchema.name.length + 1))?.schema === '*')) { | ||
| continue; | ||
| } | ||
| diff.removedNativeEnums.push(nativeEnum); | ||
| } | ||
| for (const table of toSchema.getTables()) { | ||
| const tableName = table.getShortestName(false); | ||
| if (!fromSchema.hasTable(tableName)) { | ||
| diff.newTables[tableName] = toSchema.getTable(tableName); | ||
| } | ||
| else { | ||
| const tableDifferences = this.diffTable(fromSchema.getTable(tableName), toSchema.getTable(tableName), inverseDiff?.changedTables[tableName]); | ||
| if (tableDifferences !== false) { | ||
| diff.changedTables[tableName] = tableDifferences; | ||
| } | ||
| } | ||
| } | ||
| // Check if there are tables removed | ||
| for (let table of fromSchema.getTables()) { | ||
| const tableName = table.getShortestName(); | ||
| table = fromSchema.getTable(tableName); | ||
| if (!toSchema.hasTable(tableName)) { | ||
| diff.removedTables[tableName] = table; | ||
| } | ||
| // also remember all foreign keys that point to a specific table | ||
| for (const foreignKey of Object.values(table.getForeignKeys())) { | ||
| if (!foreignKeysToTable[foreignKey.referencedTableName]) { | ||
| foreignKeysToTable[foreignKey.referencedTableName] = []; | ||
| } | ||
| foreignKeysToTable[foreignKey.referencedTableName].push(foreignKey); | ||
| } | ||
| } | ||
| for (const table of Object.values(diff.removedTables)) { | ||
| const tableName = (table.schema ? table.schema + '.' : '') + table.name; | ||
| if (!foreignKeysToTable[tableName]) { | ||
| continue; | ||
| } | ||
| diff.orphanedForeignKeys.push(...foreignKeysToTable[tableName]); | ||
| // Deleting duplicated foreign keys present both on the orphanedForeignKey and the removedForeignKeys from changedTables. | ||
| for (const foreignKey of foreignKeysToTable[tableName]) { | ||
| const localTableName = foreignKey.localTableName; | ||
| if (!diff.changedTables[localTableName]) { | ||
| continue; | ||
| } | ||
| for (const [key, fk] of Object.entries(diff.changedTables[localTableName].removedForeignKeys)) { | ||
| // We check if the key is from the removed table, if not we skip. | ||
| if (tableName !== fk.referencedTableName) { | ||
| continue; | ||
| } | ||
| delete diff.changedTables[localTableName].removedForeignKeys[key]; | ||
| } | ||
| } | ||
| } | ||
| return diff; | ||
| } | ||
| /** | ||
| * Returns the difference between the tables fromTable and toTable. | ||
| * If there are no differences this method returns the boolean false. | ||
| */ | ||
| diffTable(fromTable, toTable, inverseTableDiff) { | ||
| let changes = 0; | ||
| const tableDifferences = { | ||
| name: fromTable.getShortestName(), | ||
| addedColumns: {}, | ||
| addedForeignKeys: {}, | ||
| addedIndexes: {}, | ||
| addedChecks: {}, | ||
| changedColumns: {}, | ||
| changedForeignKeys: {}, | ||
| changedIndexes: {}, | ||
| changedChecks: {}, | ||
| removedColumns: {}, | ||
| removedForeignKeys: {}, | ||
| removedIndexes: {}, | ||
| removedChecks: {}, | ||
| renamedColumns: {}, | ||
| renamedIndexes: {}, | ||
| fromTable, | ||
| toTable, | ||
| }; | ||
| if (this.diffComment(fromTable.comment, toTable.comment)) { | ||
| tableDifferences.changedComment = toTable.comment; | ||
| this.log(`table comment changed for ${tableDifferences.name}`, { fromTableComment: fromTable.comment, toTableComment: toTable.comment }); | ||
| } | ||
| const fromTableColumns = fromTable.getColumns(); | ||
| const toTableColumns = toTable.getColumns(); | ||
| // See if all the columns in "from" table exist in "to" table | ||
| for (const column of toTableColumns) { | ||
| if (fromTable.hasColumn(column.name)) { | ||
| continue; | ||
| } | ||
| tableDifferences.addedColumns[column.name] = column; | ||
| this.log(`column ${tableDifferences.name}.${column.name} of type ${column.type} added`); | ||
| changes++; | ||
| } | ||
| /* See if there are any removed columns in "to" table */ | ||
| for (const column of fromTableColumns) { | ||
| // See if column is removed in "to" table. | ||
| if (!toTable.hasColumn(column.name)) { | ||
| tableDifferences.removedColumns[column.name] = column; | ||
| this.log(`column ${tableDifferences.name}.${column.name} removed`); | ||
| changes++; | ||
| continue; | ||
| } | ||
| // See if column has changed properties in "to" table. | ||
| const changedProperties = this.diffColumn(column, toTable.getColumn(column.name), fromTable, true); | ||
| if (changedProperties.size === 0) { | ||
| continue; | ||
| } | ||
| if (changedProperties.size === 1 && changedProperties.has('generated')) { | ||
| tableDifferences.addedColumns[column.name] = toTable.getColumn(column.name); | ||
| tableDifferences.removedColumns[column.name] = column; | ||
| changes++; | ||
| continue; | ||
| } | ||
| tableDifferences.changedColumns[column.name] = { | ||
| oldColumnName: column.name, | ||
| fromColumn: column, | ||
| column: toTable.getColumn(column.name), | ||
| changedProperties, | ||
| }; | ||
| this.log(`column ${tableDifferences.name}.${column.name} changed`, { changedProperties }); | ||
| changes++; | ||
| } | ||
| this.detectColumnRenamings(tableDifferences, inverseTableDiff); | ||
| const fromTableIndexes = fromTable.getIndexes(); | ||
| const toTableIndexes = toTable.getIndexes(); | ||
| // See if all the indexes in "from" table exist in "to" table | ||
| for (const index of Object.values(toTableIndexes)) { | ||
| if ((index.primary && fromTableIndexes.find(i => i.primary)) || fromTable.hasIndex(index.keyName)) { | ||
| continue; | ||
| } | ||
| tableDifferences.addedIndexes[index.keyName] = index; | ||
| this.log(`index ${index.keyName} added to table ${tableDifferences.name}`, { index }); | ||
| changes++; | ||
| } | ||
| // See if there are any removed indexes in "to" table | ||
| for (const index of fromTableIndexes) { | ||
| // See if index is removed in "to" table. | ||
| if ((index.primary && !toTable.hasPrimaryKey()) || !index.primary && !toTable.hasIndex(index.keyName)) { | ||
| tableDifferences.removedIndexes[index.keyName] = index; | ||
| this.log(`index ${index.keyName} removed from table ${tableDifferences.name}`); | ||
| changes++; | ||
| continue; | ||
| } | ||
| // See if index has changed in "to" table. | ||
| const toTableIndex = index.primary ? toTable.getPrimaryKey() : toTable.getIndex(index.keyName); | ||
| if (!this.diffIndex(index, toTableIndex)) { | ||
| continue; | ||
| } | ||
| tableDifferences.changedIndexes[index.keyName] = toTableIndex; | ||
| this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, { fromTableIndex: index, toTableIndex }); | ||
| changes++; | ||
| } | ||
| this.detectIndexRenamings(tableDifferences); | ||
| const fromTableChecks = fromTable.getChecks(); | ||
| const toTableChecks = toTable.getChecks(); | ||
| // See if all the checks in "from" table exist in "to" table | ||
| for (const check of toTableChecks) { | ||
| if (fromTable.hasCheck(check.name)) { | ||
| continue; | ||
| } | ||
| tableDifferences.addedChecks[check.name] = check; | ||
| this.log(`check constraint ${check.name} added to table ${tableDifferences.name}`, { check }); | ||
| changes++; | ||
| } | ||
| // See if there are any removed checks in "to" table | ||
| for (const check of fromTableChecks) { | ||
| if (!toTable.hasCheck(check.name)) { | ||
| tableDifferences.removedChecks[check.name] = check; | ||
| this.log(`check constraint ${check.name} removed from table ${tableDifferences.name}`); | ||
| changes++; | ||
| continue; | ||
| } | ||
| // See if check has changed in "to" table | ||
| const toTableCheck = toTable.getCheck(check.name); | ||
| const toColumn = toTable.getColumn(check.columnName); | ||
| const fromColumn = fromTable.getColumn(check.columnName); | ||
| if (!this.diffExpression(check.expression, toTableCheck.expression)) { | ||
| continue; | ||
| } | ||
| if (fromColumn?.enumItems && toColumn?.enumItems && !this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) { | ||
| continue; | ||
| } | ||
| this.log(`check constraint ${check.name} changed in table ${tableDifferences.name}`, { fromTableCheck: check, toTableCheck }); | ||
| tableDifferences.changedChecks[check.name] = toTableCheck; | ||
| changes++; | ||
| } | ||
| const fromForeignKeys = { ...fromTable.getForeignKeys() }; | ||
| const toForeignKeys = { ...toTable.getForeignKeys() }; | ||
| for (const fromConstraint of Object.values(fromForeignKeys)) { | ||
| for (const toConstraint of Object.values(toForeignKeys)) { | ||
| if (!this.diffForeignKey(fromConstraint, toConstraint, tableDifferences)) { | ||
| delete fromForeignKeys[fromConstraint.constraintName]; | ||
| delete toForeignKeys[toConstraint.constraintName]; | ||
| } | ||
| else if (fromConstraint.constraintName.toLowerCase() === toConstraint.constraintName.toLowerCase()) { | ||
| this.log(`FK constraint ${fromConstraint.constraintName} changed in table ${tableDifferences.name}`, { fromConstraint, toConstraint }); | ||
| tableDifferences.changedForeignKeys[toConstraint.constraintName] = toConstraint; | ||
| changes++; | ||
| delete fromForeignKeys[fromConstraint.constraintName]; | ||
| delete toForeignKeys[toConstraint.constraintName]; | ||
| } | ||
| } | ||
| } | ||
| for (const fromConstraint of Object.values(fromForeignKeys)) { | ||
| tableDifferences.removedForeignKeys[fromConstraint.constraintName] = fromConstraint; | ||
| this.log(`FK constraint ${fromConstraint.constraintName} removed from table ${tableDifferences.name}`); | ||
| changes++; | ||
| } | ||
| for (const toConstraint of Object.values(toForeignKeys)) { | ||
| tableDifferences.addedForeignKeys[toConstraint.constraintName] = toConstraint; | ||
| this.log(`FK constraint ${toConstraint.constraintName} added to table ${tableDifferences.name}`, { constraint: toConstraint }); | ||
| changes++; | ||
| } | ||
| return changes ? tableDifferences : false; | ||
| } | ||
| /** | ||
| * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop | ||
| * however ambiguities between different possibilities should not lead to renaming at all. | ||
| */ | ||
| detectColumnRenamings(tableDifferences, inverseTableDiff) { | ||
| const renameCandidates = {}; | ||
| const oldFKs = Object.values(tableDifferences.fromTable.getForeignKeys()); | ||
| const newFKs = Object.values(tableDifferences.toTable.getForeignKeys()); | ||
| for (const addedColumn of Object.values(tableDifferences.addedColumns)) { | ||
| for (const removedColumn of Object.values(tableDifferences.removedColumns)) { | ||
| const diff = this.diffColumn(addedColumn, removedColumn, tableDifferences.fromTable); | ||
| if (diff.size !== 0) { | ||
| continue; | ||
| } | ||
| const wasFK = oldFKs.some(fk => fk.columnNames.includes(removedColumn.name)); | ||
| const isFK = newFKs.some(fk => fk.columnNames.includes(addedColumn.name)); | ||
| if (wasFK !== isFK) { | ||
| continue; | ||
| } | ||
| const renamedColumn = inverseTableDiff?.renamedColumns[addedColumn.name]; | ||
| if (renamedColumn && renamedColumn?.name !== removedColumn.name) { | ||
| continue; | ||
| } | ||
| renameCandidates[addedColumn.name] = renameCandidates[addedColumn.name] ?? []; | ||
| renameCandidates[addedColumn.name].push([removedColumn, addedColumn]); | ||
| } | ||
| } | ||
| for (const candidateColumns of Object.values(renameCandidates)) { | ||
| if (candidateColumns.length !== 1) { | ||
| continue; | ||
| } | ||
| const [removedColumn, addedColumn] = candidateColumns[0]; | ||
| const removedColumnName = removedColumn.name; | ||
| const addedColumnName = addedColumn.name; | ||
| /* v8 ignore next */ | ||
| if (tableDifferences.renamedColumns[removedColumnName]) { | ||
| continue; | ||
| } | ||
| tableDifferences.renamedColumns[removedColumnName] = addedColumn; | ||
| delete tableDifferences.addedColumns[addedColumnName]; | ||
| delete tableDifferences.removedColumns[removedColumnName]; | ||
| this.log(`renamed column detected in table ${tableDifferences.name}`, { old: removedColumnName, new: addedColumnName }); | ||
| } | ||
| } | ||
| /** | ||
| * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop | ||
| * however ambiguities between different possibilities should not lead to renaming at all. | ||
| */ | ||
| detectIndexRenamings(tableDifferences) { | ||
| const renameCandidates = {}; | ||
| // Gather possible rename candidates by comparing each added and removed index based on semantics. | ||
| for (const addedIndex of Object.values(tableDifferences.addedIndexes)) { | ||
| for (const removedIndex of Object.values(tableDifferences.removedIndexes)) { | ||
| if (this.diffIndex(addedIndex, removedIndex)) { | ||
| continue; | ||
| } | ||
| renameCandidates[addedIndex.keyName] = renameCandidates[addedIndex.keyName] ?? []; | ||
| renameCandidates[addedIndex.keyName].push([removedIndex, addedIndex]); | ||
| } | ||
| } | ||
| for (const candidateIndexes of Object.values(renameCandidates)) { | ||
| // If the current rename candidate contains exactly one semantically equal index, we can safely rename it. | ||
| // Otherwise it is unclear if a rename action is really intended, therefore we let those ambiguous indexes be added/dropped. | ||
| if (candidateIndexes.length !== 1) { | ||
| continue; | ||
| } | ||
| const [removedIndex, addedIndex] = candidateIndexes[0]; | ||
| const removedIndexName = removedIndex.keyName; | ||
| const addedIndexName = addedIndex.keyName; | ||
| if (tableDifferences.renamedIndexes[removedIndexName]) { | ||
| continue; | ||
| } | ||
| tableDifferences.renamedIndexes[removedIndexName] = addedIndex; | ||
| delete tableDifferences.addedIndexes[addedIndexName]; | ||
| delete tableDifferences.removedIndexes[removedIndexName]; | ||
| this.log(`renamed index detected in table ${tableDifferences.name}`, { old: removedIndexName, new: addedIndexName }); | ||
| } | ||
| } | ||
| diffForeignKey(key1, key2, tableDifferences) { | ||
| if (key1.columnNames.join('~').toLowerCase() !== key2.columnNames.join('~').toLowerCase()) { | ||
| return true; | ||
| } | ||
| if (key1.referencedColumnNames.join('~').toLowerCase() !== key2.referencedColumnNames.join('~').toLowerCase()) { | ||
| return true; | ||
| } | ||
| if (key1.constraintName !== key2.constraintName) { | ||
| return true; | ||
| } | ||
| if (key1.referencedTableName !== key2.referencedTableName) { | ||
| return true; | ||
| } | ||
| if (key1.deferMode !== key2.deferMode) { | ||
| return true; | ||
| } | ||
| if (key1.localTableName === key1.referencedTableName && !this.platform.supportsMultipleCascadePaths()) { | ||
| return false; | ||
| } | ||
| if (key1.columnNames.some(col => tableDifferences.changedColumns[col]?.changedProperties.has('type'))) { | ||
| return true; | ||
| } | ||
| const defaultRule = ['restrict', 'no action']; | ||
| const rule = (key, method) => { | ||
| return (key[method] ?? defaultRule[0]) | ||
| .toLowerCase() | ||
| .replace(defaultRule[1], defaultRule[0]) | ||
| .replace(/"/g, ''); | ||
| }; | ||
| const compare = (method) => rule(key1, method) === rule(key2, method); | ||
| return !compare('updateRule') || !compare('deleteRule'); | ||
| } | ||
| /** | ||
| * Returns the difference between the columns | ||
| */ | ||
| diffColumn(fromColumn, toColumn, fromTable, logging) { | ||
| const changedProperties = new Set(); | ||
| const fromProp = this.mapColumnToProperty({ ...fromColumn, autoincrement: false }); | ||
| const toProp = this.mapColumnToProperty({ ...toColumn, autoincrement: false }); | ||
| const fromColumnType = this.platform.normalizeColumnType(fromColumn.mappedType.getColumnType(fromProp, this.platform).toLowerCase(), fromProp); | ||
| const fromNativeEnum = fromTable.nativeEnums[fromColumnType] ?? Object.values(fromTable.nativeEnums).find(e => e.name === fromColumnType && e.schema !== '*'); | ||
| let toColumnType = this.platform.normalizeColumnType(toColumn.mappedType.getColumnType(toProp, this.platform).toLowerCase(), toProp); | ||
| const log = (msg, params) => { | ||
| if (logging) { | ||
| const copy = Utils.copy(params); | ||
| Utils.dropUndefinedProperties(copy); | ||
| this.log(msg, copy); | ||
| } | ||
| }; | ||
| if (fromColumnType !== toColumnType && | ||
| (!fromNativeEnum || `${fromNativeEnum.schema}.${fromNativeEnum.name}` !== toColumnType) && | ||
| !(fromColumn.ignoreSchemaChanges?.includes('type') || toColumn.ignoreSchemaChanges?.includes('type')) && | ||
| !fromColumn.generated && !toColumn.generated) { | ||
| if (!toColumnType.includes('.') && fromTable.schema && fromTable.schema !== this.platform.getDefaultSchemaName()) { | ||
| toColumnType = `${fromTable.schema}.${toColumnType}`; | ||
| } | ||
| if (fromColumnType !== toColumnType) { | ||
| log(`'type' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumnType, toColumnType }); | ||
| changedProperties.add('type'); | ||
| } | ||
| } | ||
| if (fromColumn.nullable !== toColumn.nullable && !fromColumn.generated && !toColumn.generated) { | ||
| log(`'nullable' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('nullable'); | ||
| } | ||
| if (this.diffExpression(fromColumn.generated, toColumn.generated)) { | ||
| log(`'generated' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('generated'); | ||
| } | ||
| if (!!fromColumn.autoincrement !== !!toColumn.autoincrement) { | ||
| log(`'autoincrement' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('autoincrement'); | ||
| } | ||
| if (fromColumn.unsigned !== toColumn.unsigned && this.platform.supportsUnsigned()) { | ||
| log(`'unsigned' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('unsigned'); | ||
| } | ||
| if (!(fromColumn.ignoreSchemaChanges?.includes('default') || | ||
| toColumn.ignoreSchemaChanges?.includes('default')) && !this.hasSameDefaultValue(fromColumn, toColumn)) { | ||
| log(`'default' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('default'); | ||
| } | ||
| if (this.diffComment(fromColumn.comment, toColumn.comment)) { | ||
| log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('comment'); | ||
| } | ||
| if (!(fromColumn.mappedType instanceof ArrayType) && | ||
| !(toColumn.mappedType instanceof ArrayType) && | ||
| this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) { | ||
| log(`'enumItems' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('enumItems'); | ||
| } | ||
| if ((fromColumn.extra || '').toLowerCase() !== (toColumn.extra || '').toLowerCase() && | ||
| !(fromColumn.ignoreSchemaChanges?.includes('extra') || | ||
| toColumn.ignoreSchemaChanges?.includes('extra'))) { | ||
| log(`'extra' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn }); | ||
| changedProperties.add('extra'); | ||
| } | ||
| return changedProperties; | ||
| } | ||
| diffEnumItems(items1 = [], items2 = []) { | ||
| return items1.length !== items2.length || items1.some((v, i) => v !== items2[i]); | ||
| } | ||
| diffComment(comment1, comment2) { | ||
| // A null value and an empty string are actually equal for a comment so they should not trigger a change. | ||
| // eslint-disable-next-line eqeqeq | ||
| return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === ''); | ||
| } | ||
| /** | ||
| * Finds the difference between the indexes index1 and index2. | ||
| * Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences. | ||
| */ | ||
| diffIndex(index1, index2) { | ||
| // if one of them is a custom expression or full text index, compare only by name | ||
| if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') { | ||
| return index1.keyName !== index2.keyName; | ||
| } | ||
| return !this.isIndexFulfilledBy(index1, index2) || !this.isIndexFulfilledBy(index2, index1); | ||
| } | ||
| /** | ||
| * Checks if the other index already fulfills all the indexing and constraint needs of the current one. | ||
| */ | ||
| isIndexFulfilledBy(index1, index2) { | ||
| // allow the other index to be equally large only. It being larger is an option but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo) | ||
| if (index1.columnNames.length !== index2.columnNames.length) { | ||
| return false; | ||
| } | ||
| function spansColumns() { | ||
| for (let i = 0; i < index1.columnNames.length; i++) { | ||
| if (index1.columnNames[i] === index2.columnNames[i]) { | ||
| continue; | ||
| } | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| // Check if columns are the same, and even in the same order | ||
| if (!spansColumns()) { | ||
| return false; | ||
| } | ||
| if (!index1.unique && !index1.primary) { | ||
| // this is a special case: If the current key is neither primary or unique, any unique or | ||
| // primary key will always have the same effect for the index and there cannot be any constraint | ||
| // overlaps. This means a primary or unique index can always fulfill the requirements of just an | ||
| // index that has no constraints. | ||
| return true; | ||
| } | ||
| if (this.platform.supportsDeferredUniqueConstraints() && index1.deferMode !== index2.deferMode) { | ||
| return false; | ||
| } | ||
| return index1.primary === index2.primary && index1.unique === index2.unique; | ||
| } | ||
| diffExpression(expr1, expr2) { | ||
| // expressions like check constraints might be normalized by the driver, | ||
| // e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827) | ||
| const simplify = (str) => { | ||
| return str | ||
| ?.replace(/_\w+'(.*?)'/g, '$1') | ||
| .replace(/in\s*\((.*?)\)/ig, '= any (array[$1])') | ||
| .replace(/['"`()\n[\]]|::\w+| +/g, '') | ||
| .replace(/anyarray\[(.*)]/ig, '$1') | ||
| .toLowerCase(); | ||
| }; | ||
| return simplify(expr1) !== simplify(expr2); | ||
| } | ||
| parseJsonDefault(defaultValue) { | ||
| /* v8 ignore next */ | ||
| if (!defaultValue) { | ||
| return null; | ||
| } | ||
| const val = defaultValue | ||
| .replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2') | ||
| .replace(/^\(?'(.*?)'\)?$/, '$1'); | ||
| return parseJsonSafe(val); | ||
| } | ||
| hasSameDefaultValue(from, to) { | ||
| if (from.default == null || from.default.toString().toLowerCase() === 'null' || from.default.toString().startsWith('nextval(')) { | ||
| return to.default == null || to.default.toLowerCase() === 'null'; | ||
| } | ||
| if (to.mappedType instanceof BooleanType) { | ||
| const defaultValueFrom = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + from.default); | ||
| const defaultValueTo = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + to.default); | ||
| return defaultValueFrom === defaultValueTo; | ||
| } | ||
| if (to.mappedType instanceof JsonType) { | ||
| const defaultValueFrom = this.parseJsonDefault(from.default); | ||
| const defaultValueTo = this.parseJsonDefault(to.default); | ||
| return Utils.equals(defaultValueFrom, defaultValueTo); | ||
| } | ||
| if (to.mappedType instanceof DateTimeType && from.default && to.default) { | ||
| // normalize now/current_timestamp defaults, also remove `()` from the end of default expression | ||
| const defaultValueFrom = from.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, ''); | ||
| const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, ''); | ||
| return defaultValueFrom === defaultValueTo; | ||
| } | ||
| if (from.default && to.default) { | ||
| return from.default.toString().toLowerCase() === to.default.toString().toLowerCase(); | ||
| } | ||
| if (['', this.helper.getDefaultEmptyString()].includes(to.default) && from.default != null) { | ||
| return ['', this.helper.getDefaultEmptyString()].includes(from.default.toString()); | ||
| } | ||
| // eslint-disable-next-line eqeqeq | ||
| return from.default == to.default; // == intentionally | ||
| } | ||
| mapColumnToProperty(column) { | ||
| const length = column.type.match(/\w+\((\d+)\)/); | ||
| const match = column.type.match(/\w+\((\d+), ?(\d+)\)/); | ||
| return { | ||
| fieldNames: [column.name], | ||
| columnTypes: [column.type], | ||
| items: column.enumItems, | ||
| ...column, | ||
| length: length ? +length[1] : column.length, | ||
| precision: match ? +match[1] : column.precision, | ||
| scale: match ? +match[2] : column.scale, | ||
| }; | ||
| } | ||
| log(message, params) { | ||
| if (params) { | ||
| message += ' ' + inspect(params); | ||
| } | ||
| this.logger.log('schema', message); | ||
| } | ||
| } |
| import { type Connection, type Dictionary } from '@mikro-orm/core'; | ||
| import type { AbstractSqlConnection } from '../AbstractSqlConnection.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
| import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference } from '../typings.js'; | ||
| import type { DatabaseSchema } from './DatabaseSchema.js'; | ||
| import type { DatabaseTable } from './DatabaseTable.js'; | ||
| export declare abstract class SchemaHelper { | ||
| protected readonly platform: AbstractSqlPlatform; | ||
| constructor(platform: AbstractSqlPlatform); | ||
| getSchemaBeginning(_charset: string, disableForeignKeys?: boolean): string; | ||
| disableForeignKeysSQL(): string; | ||
| enableForeignKeysSQL(): string; | ||
| getSchemaEnd(disableForeignKeys?: boolean): string; | ||
| finalizeTable(table: DatabaseTable, charset: string, collate?: string): string; | ||
| appendComments(table: DatabaseTable): string[]; | ||
| supportsSchemaConstraints(): boolean; | ||
| getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[] | undefined, tableName: string, schemaName?: string): Promise<string[]>; | ||
| inferLengthFromColumnType(type: string): number | undefined; | ||
| protected getTableKey(t: Table): string; | ||
| getCreateNativeEnumSQL(name: string, values: unknown[], schema?: string): string; | ||
| getDropNativeEnumSQL(name: string, schema?: string): string; | ||
| getAlterNativeEnumSQL(name: string, schema?: string, value?: string, items?: string[], oldItems?: string[]): string; | ||
| abstract loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[]): Promise<void>; | ||
| getListTablesSQL(schemaName?: string): string; | ||
| getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column, schemaName?: string): string; | ||
| getCreateIndexSQL(tableName: string, index: IndexDef): string; | ||
| getDropIndexSQL(tableName: string, index: IndexDef): string; | ||
| getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[]; | ||
| alterTable(diff: TableDifference, safe?: boolean): string[]; | ||
| getAddColumnsSQL(table: DatabaseTable, columns: Column[]): string[]; | ||
| getDropColumnsSQL(tableName: string, columns: Column[], schemaName?: string): string; | ||
| hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean; | ||
| castColumn(name: string, type: string): string; | ||
| alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[]; | ||
| createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined; | ||
| getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[]; | ||
| getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[]; | ||
| getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string; | ||
| getNamespaces(connection: AbstractSqlConnection): Promise<string[]>; | ||
| protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>; | ||
| mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary; | ||
| normalizeDefaultValue(defaultValue: string, length?: number, defaultValues?: Dictionary<string[]>): string | number; | ||
| getCreateDatabaseSQL(name: string): string; | ||
| getDropDatabaseSQL(name: string): string; | ||
| getCreateNamespaceSQL(name: string): string; | ||
| getDropNamespaceSQL(name: string): string; | ||
| getDatabaseExistsSQL(name: string): string; | ||
| getDatabaseNotExistsError(dbName: string): string; | ||
| getManagementDbName(): string; | ||
| getDefaultEmptyString(): string; | ||
| databaseExists(connection: Connection, name: string): Promise<boolean>; | ||
| append(array: string[], sql: string | string[], pad?: boolean): void; | ||
| createTable(table: DatabaseTable, alter?: boolean): string[]; | ||
| alterTableComment(table: DatabaseTable, comment?: string): string; | ||
| createForeignKey(table: DatabaseTable, foreignKey: ForeignKey, alterTable?: boolean, inline?: boolean): string; | ||
| splitTableName(name: string, skipDefaultSchema?: boolean): [string | undefined, string]; | ||
| getReferencedTableName(referencedTableName: string, schema?: string): string; | ||
| createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string; | ||
| createCheck(table: DatabaseTable, check: CheckDef): string; | ||
| protected getTableName(table: string, schema?: string): string; | ||
| getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>; | ||
| get options(): { | ||
| disableForeignKeys?: boolean; | ||
| createForeignKeyConstraints?: boolean; | ||
| ignoreSchema?: string[]; | ||
| skipTables?: (string | RegExp)[]; | ||
| skipColumns?: Dictionary<(string | RegExp)[]>; | ||
| managementDbName?: string; | ||
| }; | ||
| protected processComment(comment: string): string; | ||
| protected quote(...keys: (string | undefined)[]): string; | ||
| dropForeignKey(tableName: string, constraintName: string): string; | ||
| dropIndex(table: string, index: IndexDef, oldIndexName?: string): string; | ||
| dropConstraint(table: string, name: string): string; | ||
| dropTableIfExists(name: string, schema?: string): string; | ||
| } |
| import { RawQueryFragment, Utils } from '@mikro-orm/core'; | ||
| export class SchemaHelper { | ||
| platform; | ||
| constructor(platform) { | ||
| this.platform = platform; | ||
| } | ||
| getSchemaBeginning(_charset, disableForeignKeys) { | ||
| if (disableForeignKeys) { | ||
| return `${this.disableForeignKeysSQL()}\n`; | ||
| } | ||
| return ''; | ||
| } | ||
| disableForeignKeysSQL() { | ||
| return ''; | ||
| } | ||
| enableForeignKeysSQL() { | ||
| return ''; | ||
| } | ||
| getSchemaEnd(disableForeignKeys) { | ||
| if (disableForeignKeys) { | ||
| return `${this.enableForeignKeysSQL()}\n`; | ||
| } | ||
| return ''; | ||
| } | ||
| finalizeTable(table, charset, collate) { | ||
| return ''; | ||
| } | ||
| appendComments(table) { | ||
| return []; | ||
| } | ||
| supportsSchemaConstraints() { | ||
| return true; | ||
| } | ||
| async getPrimaryKeys(connection, indexes = [], tableName, schemaName) { | ||
| const pks = indexes.filter(i => i.primary).map(pk => pk.columnNames); | ||
| return Utils.flatten(pks); | ||
| } | ||
| inferLengthFromColumnType(type) { | ||
| const match = type.match(/^\w+\s*(?:\(\s*(\d+)\s*\)|$)/); | ||
| if (!match) { | ||
| return; | ||
| } | ||
| return +match[1]; | ||
| } | ||
| getTableKey(t) { | ||
| const unquote = (str) => str.replace(/['"`]/g, ''); | ||
| const parts = t.table_name.split('.'); | ||
| if (parts.length > 1) { | ||
| return `${unquote(parts[0])}.${unquote(parts[1])}`; | ||
| } | ||
| if (t.schema_name) { | ||
| return `${unquote(t.schema_name)}.${unquote(t.table_name)}`; | ||
| } | ||
| return unquote(t.table_name); | ||
| } | ||
| getCreateNativeEnumSQL(name, values, schema) { | ||
| throw new Error('Not supported by given driver'); | ||
| } | ||
| getDropNativeEnumSQL(name, schema) { | ||
| throw new Error('Not supported by given driver'); | ||
| } | ||
| getAlterNativeEnumSQL(name, schema, value, items, oldItems) { | ||
| throw new Error('Not supported by given driver'); | ||
| } | ||
| getListTablesSQL(schemaName) { | ||
| throw new Error('Not supported by given driver'); | ||
| } | ||
| getRenameColumnSQL(tableName, oldColumnName, to, schemaName) { | ||
| tableName = this.quote(tableName); | ||
| oldColumnName = this.quote(oldColumnName); | ||
| const columnName = this.quote(to.name); | ||
| const schemaReference = (schemaName !== undefined && schemaName !== 'public') ? ('"' + schemaName + '".') : ''; | ||
| const tableReference = schemaReference + tableName; | ||
| return `alter table ${tableReference} rename column ${oldColumnName} to ${columnName}`; | ||
| } | ||
| getCreateIndexSQL(tableName, index) { | ||
| /* v8 ignore next */ | ||
| if (index.expression) { | ||
| return index.expression; | ||
| } | ||
| tableName = this.quote(tableName); | ||
| const keyName = this.quote(index.keyName); | ||
| const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : ''; | ||
| let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName} `; | ||
| if (index.unique && index.constraint) { | ||
| sql = `alter table ${tableName} add constraint ${keyName} unique `; | ||
| } | ||
| if (index.columnNames.some(column => column.includes('.'))) { | ||
| // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them | ||
| const sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName} `; | ||
| const columns = this.platform.getJsonIndexDefinition(index); | ||
| return `${sql}(${columns.join(', ')})${defer}`; | ||
| } | ||
| return `${sql}(${index.columnNames.map(c => this.quote(c)).join(', ')})${defer}`; | ||
| } | ||
| getDropIndexSQL(tableName, index) { | ||
| return `drop index ${this.quote(index.keyName)}`; | ||
| } | ||
| getRenameIndexSQL(tableName, index, oldIndexName) { | ||
| return [ | ||
| this.getDropIndexSQL(tableName, { ...index, keyName: oldIndexName }), | ||
| this.getCreateIndexSQL(tableName, index), | ||
| ]; | ||
| } | ||
| alterTable(diff, safe) { | ||
| const ret = []; | ||
| const [schemaName, tableName] = this.splitTableName(diff.name); | ||
| if (this.platform.supportsNativeEnums()) { | ||
| const changedNativeEnums = []; | ||
| for (const { column, changedProperties } of Object.values(diff.changedColumns)) { | ||
| if (!column.nativeEnumName) { | ||
| continue; | ||
| } | ||
| const key = schemaName && schemaName !== this.platform.getDefaultSchemaName() && !column.nativeEnumName.includes('.') | ||
| ? schemaName + '.' + column.nativeEnumName | ||
| : column.nativeEnumName; | ||
| if (changedProperties.has('enumItems') && key in diff.fromTable.nativeEnums) { | ||
| changedNativeEnums.push([column.nativeEnumName, column.enumItems, diff.fromTable.nativeEnums[key].items]); | ||
| } | ||
| } | ||
| Utils.removeDuplicates(changedNativeEnums).forEach(([enumName, itemsNew, itemsOld]) => { | ||
| // postgres allows only adding new items | ||
| const newItems = itemsNew.filter(val => !itemsOld.includes(val)); | ||
| if (enumName.includes('.')) { | ||
| const [enumSchemaName, rawEnumName] = enumName.split('.'); | ||
| ret.push(...newItems.map(val => this.getAlterNativeEnumSQL(rawEnumName, enumSchemaName, val, itemsNew, itemsOld))); | ||
| return; | ||
| } | ||
| ret.push(...newItems.map(val => this.getAlterNativeEnumSQL(enumName, schemaName, val, itemsNew, itemsOld))); | ||
| }); | ||
| } | ||
| for (const index of Object.values(diff.removedIndexes)) { | ||
| ret.push(this.dropIndex(diff.name, index)); | ||
| } | ||
| for (const index of Object.values(diff.changedIndexes)) { | ||
| ret.push(this.dropIndex(diff.name, index)); | ||
| } | ||
| for (const check of Object.values(diff.removedChecks)) { | ||
| ret.push(this.dropConstraint(diff.name, check.name)); | ||
| } | ||
| for (const check of Object.values(diff.changedChecks)) { | ||
| ret.push(this.dropConstraint(diff.name, check.name)); | ||
| } | ||
| /* v8 ignore next */ | ||
| if (!safe && Object.values(diff.removedColumns).length > 0) { | ||
| ret.push(this.getDropColumnsSQL(tableName, Object.values(diff.removedColumns), schemaName)); | ||
| } | ||
| if (Object.values(diff.addedColumns).length > 0) { | ||
| this.append(ret, this.getAddColumnsSQL(diff.toTable, Object.values(diff.addedColumns))); | ||
| } | ||
| for (const column of Object.values(diff.addedColumns)) { | ||
| const foreignKey = Object.values(diff.addedForeignKeys).find(fk => fk.columnNames.length === 1 && fk.columnNames[0] === column.name); | ||
| if (foreignKey && this.options.createForeignKeyConstraints) { | ||
| delete diff.addedForeignKeys[foreignKey.constraintName]; | ||
| ret.push(this.createForeignKey(diff.toTable, foreignKey)); | ||
| } | ||
| } | ||
| for (const { column, changedProperties } of Object.values(diff.changedColumns)) { | ||
| if (changedProperties.size === 1 && changedProperties.has('comment')) { | ||
| continue; | ||
| } | ||
| if (changedProperties.size === 1 && changedProperties.has('enumItems') && column.nativeEnumName) { | ||
| continue; | ||
| } | ||
| this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties)); | ||
| } | ||
| for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) { | ||
| if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems'].some(t => changedProperties.has(t))) { | ||
| continue; // will be handled via column update | ||
| } | ||
| ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName)); | ||
| } | ||
| for (const [oldColumnName, column] of Object.entries(diff.renamedColumns)) { | ||
| ret.push(this.getRenameColumnSQL(tableName, oldColumnName, column, schemaName)); | ||
| } | ||
| for (const foreignKey of Object.values(diff.addedForeignKeys)) { | ||
| ret.push(this.createForeignKey(diff.toTable, foreignKey)); | ||
| } | ||
| for (const foreignKey of Object.values(diff.changedForeignKeys)) { | ||
| ret.push(this.createForeignKey(diff.toTable, foreignKey)); | ||
| } | ||
| for (const index of Object.values(diff.addedIndexes)) { | ||
| ret.push(this.createIndex(index, diff.toTable)); | ||
| } | ||
| for (const index of Object.values(diff.changedIndexes)) { | ||
| ret.push(this.createIndex(index, diff.toTable, true)); | ||
| } | ||
| for (const [oldIndexName, index] of Object.entries(diff.renamedIndexes)) { | ||
| if (index.unique) { | ||
| ret.push(this.dropIndex(diff.name, index, oldIndexName)); | ||
| ret.push(this.createIndex(index, diff.toTable)); | ||
| } | ||
| else { | ||
| ret.push(...this.getRenameIndexSQL(diff.name, index, oldIndexName)); | ||
| } | ||
| } | ||
| for (const check of Object.values(diff.addedChecks)) { | ||
| ret.push(this.createCheck(diff.toTable, check)); | ||
| } | ||
| for (const check of Object.values(diff.changedChecks)) { | ||
| ret.push(this.createCheck(diff.toTable, check)); | ||
| } | ||
| if ('changedComment' in diff) { | ||
| ret.push(this.alterTableComment(diff.toTable, diff.changedComment)); | ||
| } | ||
| return ret; | ||
| } | ||
| getAddColumnsSQL(table, columns) { | ||
| const adds = columns.map(column => { | ||
| return `add ${this.createTableColumn(column, table)}`; | ||
| }).join(', '); | ||
| return [`alter table ${table.getQuotedName()} ${adds}`]; | ||
| } | ||
| getDropColumnsSQL(tableName, columns, schemaName) { | ||
| const name = this.quote(this.getTableName(tableName, schemaName)); | ||
| const drops = columns.map(column => `drop column ${this.quote(column.name)}`).join(', '); | ||
| return `alter table ${name} ${drops}`; | ||
| } | ||
| hasNonDefaultPrimaryKeyName(table) { | ||
| const pkIndex = table.getPrimaryKey(); | ||
| if (!pkIndex || !this.platform.supportsCustomPrimaryKeyNames()) { | ||
| return false; | ||
| } | ||
| const defaultName = this.platform.getDefaultPrimaryName(table.name, pkIndex.columnNames); | ||
| return pkIndex?.keyName !== defaultName; | ||
| } | ||
| /* v8 ignore next */ | ||
| castColumn(name, type) { | ||
| return ''; | ||
| } | ||
| alterTableColumn(column, table, changedProperties) { | ||
| const sql = []; | ||
| if (changedProperties.has('default') && column.default == null) { | ||
| sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} drop default`); | ||
| } | ||
| if (changedProperties.has('type')) { | ||
| let type = column.type + (column.generated ? ` generated always as ${column.generated}` : ''); | ||
| if (column.nativeEnumName) { | ||
| type = this.quote(this.getTableName(type, table.schema)); | ||
| } | ||
| sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + this.castColumn(column.name, type)}`); | ||
| } | ||
| if (changedProperties.has('default') && column.default != null) { | ||
| sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} set default ${column.default}`); | ||
| } | ||
| if (changedProperties.has('nullable')) { | ||
| const action = column.nullable ? 'drop' : 'set'; | ||
| sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} ${action} not null`); | ||
| } | ||
| return sql; | ||
| } | ||
| createTableColumn(column, table, changedProperties) { | ||
| const compositePK = table.getPrimaryKey()?.composite; | ||
| const primaryKey = !changedProperties && !this.hasNonDefaultPrimaryKeyName(table); | ||
| const columnType = column.type + (column.generated ? ` generated always as ${column.generated}` : ''); | ||
| const useDefault = column.default != null && column.default !== 'null' && !column.autoincrement; | ||
| const col = [this.quote(column.name), columnType]; | ||
| Utils.runIfNotEmpty(() => col.push('unsigned'), column.unsigned && this.platform.supportsUnsigned()); | ||
| Utils.runIfNotEmpty(() => col.push('null'), column.nullable); | ||
| Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated); | ||
| Utils.runIfNotEmpty(() => col.push('auto_increment'), column.autoincrement); | ||
| Utils.runIfNotEmpty(() => col.push('unique'), column.autoincrement && !column.primary); | ||
| if (column.autoincrement && !column.generated && !compositePK && (!changedProperties || changedProperties.has('autoincrement') || changedProperties.has('type'))) { | ||
| Utils.runIfNotEmpty(() => col.push('primary key'), primaryKey && column.primary); | ||
| } | ||
| if (useDefault) { | ||
| // https://dev.mysql.com/doc/refman/9.0/en/data-type-defaults.html | ||
| const needsExpression = ['blob', 'text', 'json', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'geometrycollection'].some(type => column.type.toLowerCase().startsWith(type)); | ||
| const defaultSql = needsExpression && !column.default.startsWith('(') ? `(${column.default})` : column.default; | ||
| col.push(`default ${defaultSql}`); | ||
| } | ||
| Utils.runIfNotEmpty(() => col.push(column.extra), column.extra); | ||
| Utils.runIfNotEmpty(() => col.push(`comment ${this.platform.quoteValue(column.comment)}`), column.comment); | ||
| return col.join(' '); | ||
| } | ||
| getPreAlterTable(tableDiff, safe) { | ||
| return []; | ||
| } | ||
| getPostAlterTable(tableDiff, safe) { | ||
| return []; | ||
| } | ||
| getChangeColumnCommentSQL(tableName, to, schemaName) { | ||
| return ''; | ||
| } | ||
| async getNamespaces(connection) { | ||
| return []; | ||
| } | ||
| async mapIndexes(indexes) { | ||
| const map = {}; | ||
| indexes.forEach(index => { | ||
| if (map[index.keyName]) { | ||
| map[index.keyName].composite = true; | ||
| map[index.keyName].columnNames.push(index.columnNames[0]); | ||
| } | ||
| else { | ||
| map[index.keyName] = index; | ||
| } | ||
| }); | ||
| return Object.values(map); | ||
| } | ||
| mapForeignKeys(fks, tableName, schemaName) { | ||
| return fks.reduce((ret, fk) => { | ||
| if (ret[fk.constraint_name]) { | ||
| ret[fk.constraint_name].columnNames.push(fk.column_name); | ||
| ret[fk.constraint_name].referencedColumnNames.push(fk.referenced_column_name); | ||
| } | ||
| else { | ||
| ret[fk.constraint_name] = { | ||
| columnNames: [fk.column_name], | ||
| constraintName: fk.constraint_name, | ||
| localTableName: schemaName ? `${schemaName}.${tableName}` : tableName, | ||
| referencedTableName: fk.referenced_schema_name ? `${fk.referenced_schema_name}.${fk.referenced_table_name}` : fk.referenced_table_name, | ||
| referencedColumnNames: [fk.referenced_column_name], | ||
| updateRule: fk.update_rule.toLowerCase(), | ||
| deleteRule: fk.delete_rule.toLowerCase(), | ||
| deferMode: fk.defer_mode, | ||
| }; | ||
| } | ||
| return ret; | ||
| }, {}); | ||
| } | ||
| normalizeDefaultValue(defaultValue, length, defaultValues = {}) { | ||
| if (defaultValue == null) { | ||
| return defaultValue; | ||
| } | ||
| const raw = RawQueryFragment.getKnownFragment(defaultValue); | ||
| if (raw) { | ||
| return this.platform.formatQuery(raw.sql, raw.params); | ||
| } | ||
| const genericValue = defaultValue.replace(/\(\d+\)/, '(?)').toLowerCase(); | ||
| const norm = defaultValues[genericValue]; | ||
| if (!norm) { | ||
| return defaultValue; | ||
| } | ||
| return norm[0].replace('(?)', length != null ? `(${length})` : ''); | ||
| } | ||
| getCreateDatabaseSQL(name) { | ||
| // two line breaks to force separate execution | ||
| return `create database ${name};\n\nuse ${name}`; | ||
| } | ||
| getDropDatabaseSQL(name) { | ||
| return `drop database if exists ${this.quote(name)}`; | ||
| } | ||
| /* v8 ignore next */ | ||
| getCreateNamespaceSQL(name) { | ||
| return `create schema if not exists ${this.quote(name)}`; | ||
| } | ||
| /* v8 ignore next */ | ||
| getDropNamespaceSQL(name) { | ||
| return `drop schema if exists ${this.quote(name)}`; | ||
| } | ||
| getDatabaseExistsSQL(name) { | ||
| return `select 1 from information_schema.schemata where schema_name = '${name}'`; | ||
| } | ||
| getDatabaseNotExistsError(dbName) { | ||
| return `Unknown database '${dbName}'`; | ||
| } | ||
| getManagementDbName() { | ||
| return 'information_schema'; | ||
| } | ||
| getDefaultEmptyString() { | ||
| return "''"; | ||
| } | ||
| async databaseExists(connection, name) { | ||
| try { | ||
| const res = await connection.execute(this.getDatabaseExistsSQL(name)); | ||
| return res.length > 0; | ||
| } | ||
| catch (e) { | ||
| if (e instanceof Error && e.message.includes(this.getDatabaseNotExistsError(name))) { | ||
| return false; | ||
| } | ||
| /* v8 ignore next */ | ||
| throw e; | ||
| } | ||
| } | ||
| append(array, sql, pad = false) { | ||
| const length = array.length; | ||
| for (const row of Utils.asArray(sql)) { | ||
| if (!row) { | ||
| continue; | ||
| } | ||
| let tmp = row.trim(); | ||
| if (!tmp.endsWith(';')) { | ||
| tmp += ';'; | ||
| } | ||
| array.push(tmp); | ||
| } | ||
| if (pad && array.length > length) { | ||
| array.push(''); | ||
| } | ||
| } | ||
| createTable(table, alter) { | ||
| let sql = `create table ${table.getQuotedName()} (`; | ||
| const columns = table.getColumns(); | ||
| const lastColumn = columns[columns.length - 1].name; | ||
| for (const column of columns) { | ||
| const col = this.createTableColumn(column, table); | ||
| if (col) { | ||
| const comma = column.name === lastColumn ? '' : ', '; | ||
| sql += col + comma; | ||
| } | ||
| } | ||
| const primaryKey = table.getPrimaryKey(); | ||
| const createPrimary = !table.getColumns().some(c => c.autoincrement && c.primary) || this.hasNonDefaultPrimaryKeyName(table); | ||
| if (createPrimary && primaryKey) { | ||
| const name = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(primaryKey.keyName)} ` : ''; | ||
| sql += `, ${name}primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`; | ||
| } | ||
| sql += ')'; | ||
| sql += this.finalizeTable(table, this.platform.getConfig().get('charset'), this.platform.getConfig().get('collate')); | ||
| const ret = []; | ||
| this.append(ret, sql); | ||
| this.append(ret, this.appendComments(table)); | ||
| for (const index of table.getIndexes()) { | ||
| this.append(ret, this.createIndex(index, table)); | ||
| } | ||
| if (!alter) { | ||
| for (const check of table.getChecks()) { | ||
| this.append(ret, this.createCheck(table, check)); | ||
| } | ||
| } | ||
| return ret; | ||
| } | ||
| alterTableComment(table, comment) { | ||
| return `alter table ${table.getQuotedName()} comment = ${this.platform.quoteValue(comment ?? '')}`; | ||
| } | ||
| createForeignKey(table, foreignKey, alterTable = true, inline = false) { | ||
| if (!this.options.createForeignKeyConstraints) { | ||
| return ''; | ||
| } | ||
| const constraintName = this.quote(foreignKey.constraintName); | ||
| const columnNames = foreignKey.columnNames.map(c => this.quote(c)).join(', '); | ||
| const referencedColumnNames = foreignKey.referencedColumnNames.map(c => this.quote(c)).join(', '); | ||
| const referencedTableName = this.quote(this.getReferencedTableName(foreignKey.referencedTableName, table.schema)); | ||
| const sql = []; | ||
| if (alterTable) { | ||
| sql.push(`alter table ${table.getQuotedName()} add`); | ||
| } | ||
| sql.push(`constraint ${constraintName}`); | ||
| if (!inline) { | ||
| sql.push(`foreign key (${columnNames})`); | ||
| } | ||
| sql.push(`references ${referencedTableName} (${referencedColumnNames})`); | ||
| if (foreignKey.localTableName !== foreignKey.referencedTableName || this.platform.supportsMultipleCascadePaths()) { | ||
| if (foreignKey.updateRule) { | ||
| sql.push(`on update ${foreignKey.updateRule}`); | ||
| } | ||
| if (foreignKey.deleteRule) { | ||
| sql.push(`on delete ${foreignKey.deleteRule}`); | ||
| } | ||
| } | ||
| if (foreignKey.deferMode) { | ||
| sql.push(`deferrable initially ${foreignKey.deferMode}`); | ||
| } | ||
| return sql.join(' '); | ||
| } | ||
| splitTableName(name, skipDefaultSchema = false) { | ||
| const parts = name.split('.'); | ||
| const tableName = parts.pop(); | ||
| let schemaName = parts.pop(); | ||
| if (skipDefaultSchema && schemaName === this.platform.getDefaultSchemaName()) { | ||
| schemaName = undefined; | ||
| } | ||
| return [schemaName, tableName]; | ||
| } | ||
| getReferencedTableName(referencedTableName, schema) { | ||
| const [schemaName, tableName] = this.splitTableName(referencedTableName); | ||
| schema = schemaName ?? schema ?? this.platform.getConfig().get('schema'); | ||
| /* v8 ignore next */ | ||
| if (schema && schemaName === '*') { | ||
| return `${schema}.${referencedTableName.replace(/^\*\./, '')}`; | ||
| } | ||
| return this.getTableName(tableName, schema); | ||
| } | ||
| createIndex(index, table, createPrimary = false) { | ||
| if (index.primary && !createPrimary) { | ||
| return ''; | ||
| } | ||
| if (index.expression) { | ||
| return index.expression; | ||
| } | ||
| const columns = index.columnNames.map(c => this.quote(c)).join(', '); | ||
| const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : ''; | ||
| if (index.primary) { | ||
| const keyName = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${index.keyName} ` : ''; | ||
| return `alter table ${table.getQuotedName()} add ${keyName}primary key (${columns})${defer}`; | ||
| } | ||
| if (index.type === 'fulltext') { | ||
| const columns = index.columnNames.map(name => ({ name, type: table.getColumn(name).type })); | ||
| if (this.platform.supportsCreatingFullTextIndex()) { | ||
| return this.platform.getFullTextIndexExpression(index.keyName, table.schema, table.name, columns); | ||
| } | ||
| } | ||
| return this.getCreateIndexSQL(table.getShortestName(), index); | ||
| } | ||
| createCheck(table, check) { | ||
| return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`; | ||
| } | ||
| getTableName(table, schema) { | ||
| if (schema && schema !== this.platform.getDefaultSchemaName()) { | ||
| return `${schema}.${table}`; | ||
| } | ||
| return table; | ||
| } | ||
| getTablesGroupedBySchemas(tables) { | ||
| return tables.reduce((acc, table) => { | ||
| const schemaTables = acc.get(table.schema_name); | ||
| if (!schemaTables) { | ||
| acc.set(table.schema_name, [table]); | ||
| return acc; | ||
| } | ||
| schemaTables.push(table); | ||
| return acc; | ||
| }, new Map()); | ||
| } | ||
| get options() { | ||
| return this.platform.getConfig().get('schemaGenerator'); | ||
| } | ||
| processComment(comment) { | ||
| return comment; | ||
| } | ||
| quote(...keys) { | ||
| return this.platform.quoteIdentifier(keys.filter(Boolean).join('.')); | ||
| } | ||
| dropForeignKey(tableName, constraintName) { | ||
| return `alter table ${this.quote(tableName)} drop foreign key ${this.quote(constraintName)}`; | ||
| } | ||
| dropIndex(table, index, oldIndexName = index.keyName) { | ||
| if (index.primary) { | ||
| return `alter table ${this.quote(table)} drop primary key`; | ||
| } | ||
| return `alter table ${this.quote(table)} drop index ${this.quote(oldIndexName)}`; | ||
| } | ||
| dropConstraint(table, name) { | ||
| return `alter table ${this.quote(table)} drop constraint ${this.quote(name)}`; | ||
| } | ||
| dropTableIfExists(name, schema) { | ||
| let sql = `drop table if exists ${this.quote(this.getTableName(name, schema))}`; | ||
| if (this.platform.usesCascadeStatement()) { | ||
| sql += ' cascade'; | ||
| } | ||
| return sql; | ||
| } | ||
| } |
| import { AbstractSchemaGenerator, type ClearDatabaseOptions, type CreateSchemaOptions, type Dictionary, type DropSchemaOptions, type EnsureDatabaseOptions, type EntityMetadata, type ISchemaGenerator, type MikroORM, type Transaction, type UpdateSchemaOptions } from '@mikro-orm/core'; | ||
| import type { SchemaDifference } from '../typings.js'; | ||
| import { DatabaseSchema } from './DatabaseSchema.js'; | ||
| import type { AbstractSqlDriver } from '../AbstractSqlDriver.js'; | ||
| export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<AbstractSqlDriver> implements ISchemaGenerator { | ||
| protected readonly helper: import("./SchemaHelper.js").SchemaHelper; | ||
| protected readonly options: { | ||
| disableForeignKeys?: boolean; | ||
| createForeignKeyConstraints?: boolean; | ||
| ignoreSchema?: string[]; | ||
| skipTables?: (string | RegExp)[]; | ||
| skipColumns?: Dictionary<(string | RegExp)[]>; | ||
| managementDbName?: string; | ||
| }; | ||
| protected lastEnsuredDatabase?: string; | ||
| static register(orm: MikroORM): void; | ||
| create(options?: CreateSchemaOptions): Promise<void>; | ||
| /** | ||
| * Returns true if the database was created. | ||
| */ | ||
| ensureDatabase(options?: EnsureDatabaseOptions): Promise<boolean>; | ||
| getTargetSchema(schema?: string): DatabaseSchema; | ||
| protected getOrderedMetadata(schema?: string): EntityMetadata[]; | ||
| getCreateSchemaSQL(options?: CreateSchemaOptions): Promise<string>; | ||
| drop(options?: DropSchemaOptions): Promise<void>; | ||
| createNamespace(name: string): Promise<void>; | ||
| dropNamespace(name: string): Promise<void>; | ||
| clear(options?: ClearDatabaseOptions): Promise<void>; | ||
| getDropSchemaSQL(options?: Omit<DropSchemaOptions, 'dropDb'>): Promise<string>; | ||
| private getSchemaName; | ||
| update(options?: UpdateSchemaOptions<DatabaseSchema>): Promise<void>; | ||
| getUpdateSchemaSQL(options?: UpdateSchemaOptions<DatabaseSchema>): Promise<string>; | ||
| getUpdateSchemaMigrationSQL(options?: UpdateSchemaOptions<DatabaseSchema>): Promise<{ | ||
| up: string; | ||
| down: string; | ||
| }>; | ||
| private prepareSchemaForComparison; | ||
| diffToSQL(schemaDiff: SchemaDifference, options: { | ||
| wrap?: boolean; | ||
| safe?: boolean; | ||
| dropTables?: boolean; | ||
| schema?: string; | ||
| }): string; | ||
| /** | ||
| * We need to drop foreign keys first for all tables to allow dropping PK constraints. | ||
| */ | ||
| private preAlterTable; | ||
| /** | ||
| * creates new database and connects to it | ||
| */ | ||
| createDatabase(name?: string, options?: { | ||
| skipOnConnect?: boolean; | ||
| }): Promise<void>; | ||
| dropDatabase(name?: string): Promise<void>; | ||
| execute(sql: string, options?: { | ||
| wrap?: boolean; | ||
| ctx?: Transaction; | ||
| }): Promise<void>; | ||
| dropTableIfExists(name: string, schema?: string): Promise<void>; | ||
| private wrapSchema; | ||
| private append; | ||
| private matchName; | ||
| private isTableSkipped; | ||
| } | ||
| export { SqlSchemaGenerator as SchemaGenerator }; |
| import { AbstractSchemaGenerator, Utils, } from '@mikro-orm/core'; | ||
| import { DatabaseSchema } from './DatabaseSchema.js'; | ||
| import { SchemaComparator } from './SchemaComparator.js'; | ||
| export class SqlSchemaGenerator extends AbstractSchemaGenerator { | ||
| helper = this.platform.getSchemaHelper(); | ||
| options = this.config.get('schemaGenerator'); | ||
| lastEnsuredDatabase; | ||
| static register(orm) { | ||
| orm.config.registerExtension('@mikro-orm/schema-generator', () => new SqlSchemaGenerator(orm.em)); | ||
| } | ||
| async create(options) { | ||
| await this.ensureDatabase(); | ||
| const sql = await this.getCreateSchemaSQL(options); | ||
| await this.execute(sql); | ||
| } | ||
| /** | ||
| * Returns true if the database was created. | ||
| */ | ||
| async ensureDatabase(options) { | ||
| await this.connection.ensureConnection(); | ||
| const dbName = this.config.get('dbName'); | ||
| if (this.lastEnsuredDatabase === dbName && !options?.forceCheck) { | ||
| return true; | ||
| } | ||
| const exists = await this.helper.databaseExists(this.connection, dbName); | ||
| this.lastEnsuredDatabase = dbName; | ||
| if (!exists) { | ||
| const managementDbName = this.helper.getManagementDbName(); | ||
| if (managementDbName) { | ||
| this.config.set('dbName', managementDbName); | ||
| await this.driver.reconnect({ skipOnConnect: true }); | ||
| await this.createDatabase(dbName, { skipOnConnect: true }); | ||
| } | ||
| if (options?.create) { | ||
| await this.create(options); | ||
| } | ||
| return true; | ||
| } | ||
| /* v8 ignore next */ | ||
| if (options?.clear) { | ||
| await this.clear({ ...options, clearIdentityMap: false }); | ||
| } | ||
| return false; | ||
| } | ||
| getTargetSchema(schema) { | ||
| const metadata = this.getOrderedMetadata(schema); | ||
| const schemaName = schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName(); | ||
| return DatabaseSchema.fromMetadata(metadata, this.platform, this.config, schemaName); | ||
| } | ||
| getOrderedMetadata(schema) { | ||
| const metadata = super.getOrderedMetadata(schema); | ||
| // Filter out skipped tables | ||
| return metadata.filter(meta => { | ||
| const tableName = meta.tableName; | ||
| const tableSchema = meta.schema ?? schema ?? this.config.get('schema'); | ||
| return !this.isTableSkipped(tableName, tableSchema); | ||
| }); | ||
| } | ||
| async getCreateSchemaSQL(options = {}) { | ||
| const toSchema = this.getTargetSchema(options.schema); | ||
| const ret = []; | ||
| for (const namespace of toSchema.getNamespaces()) { | ||
| if (namespace === this.platform.getDefaultSchemaName()) { | ||
| continue; | ||
| } | ||
| const sql = this.helper.getCreateNamespaceSQL(namespace); | ||
| this.append(ret, sql); | ||
| } | ||
| if (this.platform.supportsNativeEnums()) { | ||
| const created = []; | ||
| for (const [enumName, enumOptions] of Object.entries(toSchema.getNativeEnums())) { | ||
| /* v8 ignore next */ | ||
| if (created.includes(enumName)) { | ||
| continue; | ||
| } | ||
| created.push(enumName); | ||
| const sql = this.helper.getCreateNativeEnumSQL(enumOptions.name, enumOptions.items, this.getSchemaName(enumOptions, options)); | ||
| this.append(ret, sql); | ||
| } | ||
| } | ||
| for (const table of toSchema.getTables()) { | ||
| this.append(ret, this.helper.createTable(table), true); | ||
| } | ||
| if (this.helper.supportsSchemaConstraints()) { | ||
| for (const table of toSchema.getTables()) { | ||
| const fks = Object.values(table.getForeignKeys()).map(fk => this.helper.createForeignKey(table, fk)); | ||
| this.append(ret, fks, true); | ||
| } | ||
| } | ||
| return this.wrapSchema(ret, options); | ||
| } | ||
| async drop(options = {}) { | ||
| if (options.dropDb) { | ||
| const name = this.config.get('dbName'); | ||
| return this.dropDatabase(name); | ||
| } | ||
| const sql = await this.getDropSchemaSQL(options); | ||
| await this.execute(sql); | ||
| } | ||
| async createNamespace(name) { | ||
| const sql = this.helper.getCreateNamespaceSQL(name); | ||
| await this.execute(sql); | ||
| } | ||
| async dropNamespace(name) { | ||
| const sql = this.helper.getDropNamespaceSQL(name); | ||
| await this.execute(sql); | ||
| } | ||
| async clear(options) { | ||
| // truncate by default, so no value is considered as true | ||
| /* v8 ignore next */ | ||
| if (options?.truncate === false) { | ||
| return super.clear(options); | ||
| } | ||
| await this.execute(this.helper.disableForeignKeysSQL()); | ||
| const schema = options?.schema ?? this.config.get('schema', this.platform.getDefaultSchemaName()); | ||
| for (const meta of this.getOrderedMetadata(schema).reverse()) { | ||
| await this.driver.createQueryBuilder(meta.className, this.em?.getTransactionContext(), 'write', false) | ||
| .withSchema(schema) | ||
| .truncate() | ||
| .execute(); | ||
| } | ||
| await this.execute(this.helper.enableForeignKeysSQL()); | ||
| if (options?.clearIdentityMap ?? true) { | ||
| this.clearIdentityMap(); | ||
| } | ||
| } | ||
| async getDropSchemaSQL(options = {}) { | ||
| await this.ensureDatabase(); | ||
| const metadata = this.getOrderedMetadata(options.schema).reverse(); | ||
| const schemas = this.getTargetSchema(options.schema).getNamespaces(); | ||
| const schema = await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas); | ||
| const ret = []; | ||
| // remove FKs explicitly if we can't use a cascading statement and we don't disable FK checks (we need this for circular relations) | ||
| for (const meta of metadata) { | ||
| if (!this.platform.usesCascadeStatement() && (!this.options.disableForeignKeys || options.dropForeignKeys)) { | ||
| const table = schema.getTable(meta.tableName); | ||
| if (!table) { | ||
| continue; | ||
| } | ||
| const foreignKeys = Object.values(table.getForeignKeys()); | ||
| for (const fk of foreignKeys) { | ||
| this.append(ret, this.helper.dropForeignKey(table.getShortestName(), fk.constraintName)); | ||
| } | ||
| } | ||
| } | ||
| for (const meta of metadata) { | ||
| this.append(ret, this.helper.dropTableIfExists(meta.tableName, this.getSchemaName(meta, options))); | ||
| } | ||
| if (this.platform.supportsNativeEnums()) { | ||
| for (const columnName of Object.keys(schema.getNativeEnums())) { | ||
| const sql = this.helper.getDropNativeEnumSQL(columnName, options.schema ?? this.config.get('schema')); | ||
| this.append(ret, sql); | ||
| } | ||
| } | ||
| if (options.dropMigrationsTable) { | ||
| this.append(ret, this.helper.dropTableIfExists(this.config.get('migrations').tableName, this.config.get('schema'))); | ||
| } | ||
| return this.wrapSchema(ret, options); | ||
| } | ||
| getSchemaName(meta, options) { | ||
| const schemaName = options.schema ?? this.config.get('schema'); | ||
| /* v8 ignore next */ | ||
| const resolvedName = meta.schema && meta.schema === '*' ? schemaName : (meta.schema ?? schemaName); | ||
| // skip default schema name | ||
| if (resolvedName === this.platform.getDefaultSchemaName()) { | ||
| return undefined; | ||
| } | ||
| return resolvedName; | ||
| } | ||
| async update(options = {}) { | ||
| const sql = await this.getUpdateSchemaSQL(options); | ||
| await this.execute(sql); | ||
| } | ||
| async getUpdateSchemaSQL(options = {}) { | ||
| await this.ensureDatabase(); | ||
| const { fromSchema, toSchema } = await this.prepareSchemaForComparison(options); | ||
| const comparator = new SchemaComparator(this.platform); | ||
| const diffUp = comparator.compare(fromSchema, toSchema); | ||
| return this.diffToSQL(diffUp, options); | ||
| } | ||
| async getUpdateSchemaMigrationSQL(options = {}) { | ||
| if (!options.fromSchema) { | ||
| await this.ensureDatabase(); | ||
| } | ||
| const { fromSchema, toSchema } = await this.prepareSchemaForComparison(options); | ||
| const comparator = new SchemaComparator(this.platform); | ||
| const diffUp = comparator.compare(fromSchema, toSchema); | ||
| const diffDown = comparator.compare(toSchema, fromSchema, diffUp); | ||
| return { | ||
| up: this.diffToSQL(diffUp, options), | ||
| down: this.platform.supportsDownMigrations() ? this.diffToSQL(diffDown, options) : '', | ||
| }; | ||
| } | ||
| async prepareSchemaForComparison(options) { | ||
| options.safe ??= false; | ||
| options.dropTables ??= true; | ||
| const toSchema = this.getTargetSchema(options.schema); | ||
| const schemas = toSchema.getNamespaces(); | ||
| const fromSchema = options.fromSchema ?? (await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas, undefined, this.options.skipTables)); | ||
| const wildcardSchemaTables = Object.values(this.metadata.getAll()).filter(meta => meta.schema === '*').map(meta => meta.tableName); | ||
| fromSchema.prune(options.schema, wildcardSchemaTables); | ||
| toSchema.prune(options.schema, wildcardSchemaTables); | ||
| return { fromSchema, toSchema }; | ||
| } | ||
| diffToSQL(schemaDiff, options) { | ||
| const ret = []; | ||
| globalThis.idx = 0; | ||
| if (this.platform.supportsSchemas()) { | ||
| for (const newNamespace of schemaDiff.newNamespaces) { | ||
| const sql = this.helper.getCreateNamespaceSQL(newNamespace); | ||
| this.append(ret, sql); | ||
| } | ||
| } | ||
| if (this.platform.supportsNativeEnums()) { | ||
| for (const newNativeEnum of schemaDiff.newNativeEnums) { | ||
| const sql = this.helper.getCreateNativeEnumSQL(newNativeEnum.name, newNativeEnum.items, this.getSchemaName(newNativeEnum, options)); | ||
| this.append(ret, sql); | ||
| } | ||
| } | ||
| if (!options.safe && this.options.createForeignKeyConstraints) { | ||
| for (const orphanedForeignKey of schemaDiff.orphanedForeignKeys) { | ||
| const [schemaName, tableName] = this.helper.splitTableName(orphanedForeignKey.localTableName, true); | ||
| /* v8 ignore next */ | ||
| const name = (schemaName ? schemaName + '.' : '') + tableName; | ||
| this.append(ret, this.helper.dropForeignKey(name, orphanedForeignKey.constraintName)); | ||
| } | ||
| if (schemaDiff.orphanedForeignKeys.length > 0) { | ||
| ret.push(''); | ||
| } | ||
| } | ||
| for (const newTable of Object.values(schemaDiff.newTables)) { | ||
| this.append(ret, this.helper.createTable(newTable, true), true); | ||
| } | ||
| if (this.helper.supportsSchemaConstraints()) { | ||
| for (const newTable of Object.values(schemaDiff.newTables)) { | ||
| const sql = []; | ||
| if (this.options.createForeignKeyConstraints) { | ||
| const fks = Object.values(newTable.getForeignKeys()).map(fk => this.helper.createForeignKey(newTable, fk)); | ||
| this.append(sql, fks); | ||
| } | ||
| for (const check of newTable.getChecks()) { | ||
| this.append(sql, this.helper.createCheck(newTable, check)); | ||
| } | ||
| this.append(ret, sql, true); | ||
| } | ||
| } | ||
| if (options.dropTables && !options.safe) { | ||
| for (const table of Object.values(schemaDiff.removedTables)) { | ||
| this.append(ret, this.helper.dropTableIfExists(table.name, table.schema)); | ||
| } | ||
| if (Utils.hasObjectKeys(schemaDiff.removedTables)) { | ||
| ret.push(''); | ||
| } | ||
| } | ||
| for (const changedTable of Object.values(schemaDiff.changedTables)) { | ||
| this.append(ret, this.preAlterTable(changedTable, options.safe), true); | ||
| } | ||
| for (const changedTable of Object.values(schemaDiff.changedTables)) { | ||
| this.append(ret, this.helper.alterTable(changedTable, options.safe), true); | ||
| } | ||
| for (const changedTable of Object.values(schemaDiff.changedTables)) { | ||
| this.append(ret, this.helper.getPostAlterTable(changedTable, options.safe), true); | ||
| } | ||
| if (!options.safe && this.platform.supportsNativeEnums()) { | ||
| for (const removedNativeEnum of schemaDiff.removedNativeEnums) { | ||
| this.append(ret, this.helper.getDropNativeEnumSQL(removedNativeEnum.name, removedNativeEnum.schema)); | ||
| } | ||
| } | ||
| if (options.dropTables && !options.safe) { | ||
| for (const removedNamespace of schemaDiff.removedNamespaces) { | ||
| const sql = this.helper.getDropNamespaceSQL(removedNamespace); | ||
| this.append(ret, sql); | ||
| } | ||
| } | ||
| return this.wrapSchema(ret, options); | ||
| } | ||
| /** | ||
| * We need to drop foreign keys first for all tables to allow dropping PK constraints. | ||
| */ | ||
| preAlterTable(diff, safe) { | ||
| const ret = []; | ||
| this.append(ret, this.helper.getPreAlterTable(diff, safe)); | ||
| for (const foreignKey of Object.values(diff.removedForeignKeys)) { | ||
| ret.push(this.helper.dropForeignKey(diff.toTable.getShortestName(), foreignKey.constraintName)); | ||
| } | ||
| for (const foreignKey of Object.values(diff.changedForeignKeys)) { | ||
| ret.push(this.helper.dropForeignKey(diff.toTable.getShortestName(), foreignKey.constraintName)); | ||
| } | ||
| return ret; | ||
| } | ||
| /** | ||
| * creates new database and connects to it | ||
| */ | ||
| async createDatabase(name, options) { | ||
| name ??= this.config.get('dbName'); | ||
| const sql = this.helper.getCreateDatabaseSQL('' + this.platform.quoteIdentifier(name)); | ||
| if (sql) { | ||
| await this.execute(sql); | ||
| } | ||
| this.config.set('dbName', name); | ||
| await this.driver.reconnect(options); | ||
| } | ||
| async dropDatabase(name) { | ||
| name ??= this.config.get('dbName'); | ||
| this.config.set('dbName', this.helper.getManagementDbName()); | ||
| await this.driver.reconnect(); | ||
| await this.execute(this.helper.getDropDatabaseSQL(name)); | ||
| this.config.set('dbName', name); | ||
| } | ||
| async execute(sql, options = {}) { | ||
| options.wrap ??= false; | ||
| const lines = this.wrapSchema(sql, options).split('\n'); | ||
| const groups = []; | ||
| let i = 0; | ||
| for (const line of lines) { | ||
| if (line.trim() === '') { | ||
| if (groups[i]?.length > 0) { | ||
| i++; | ||
| } | ||
| continue; | ||
| } | ||
| groups[i] ??= []; | ||
| groups[i].push(line.trim()); | ||
| } | ||
| if (groups.length === 0) { | ||
| return; | ||
| } | ||
| if (this.platform.supportsMultipleStatements()) { | ||
| for (const group of groups) { | ||
| const query = group.join('\n'); | ||
| await this.driver.execute(query); | ||
| } | ||
| return; | ||
| } | ||
| await Utils.runSerial(groups.flat(), line => this.driver.execute(line)); | ||
| } | ||
| async dropTableIfExists(name, schema) { | ||
| const sql = this.helper.dropTableIfExists(name, schema); | ||
| return this.execute(sql); | ||
| } | ||
| wrapSchema(sql, options) { | ||
| const array = Utils.asArray(sql); | ||
| if (array.length === 0) { | ||
| return ''; | ||
| } | ||
| if (array[array.length - 1] === '') { | ||
| array.pop(); | ||
| } | ||
| if (options.wrap === false) { | ||
| return array.join('\n') + '\n'; | ||
| } | ||
| let ret = this.helper.getSchemaBeginning(this.config.get('charset'), this.options.disableForeignKeys); | ||
| ret += array.join('\n') + '\n'; | ||
| ret += this.helper.getSchemaEnd(this.options.disableForeignKeys); | ||
| return ret; | ||
| } | ||
| append(array, sql, pad) { | ||
| return this.helper.append(array, sql, pad); | ||
| } | ||
| matchName(name, nameToMatch) { | ||
| return typeof nameToMatch === 'string' | ||
| ? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase() | ||
| : nameToMatch.test(name); | ||
| } | ||
| isTableSkipped(tableName, schemaName) { | ||
| const skipTables = this.options.skipTables; | ||
| if (!skipTables || skipTables.length === 0) { | ||
| return false; | ||
| } | ||
| const fullTableName = schemaName ? `${schemaName}.${tableName}` : tableName; | ||
| return skipTables.some(pattern => this.matchName(tableName, pattern) || this.matchName(fullTableName, pattern)); | ||
| } | ||
| } | ||
| // for back compatibility | ||
| export { SqlSchemaGenerator as SchemaGenerator }; |
| import { type EntitySchemaWithMeta, EntityManager, type AnyEntity, type ConnectionType, type EntityData, type EntityName, type EntityRepository, type GetRepository, type QueryResult, type FilterQuery, type LoggingOptions, type RawQueryFragment } from '@mikro-orm/core'; | ||
| import type { AbstractSqlDriver } from './AbstractSqlDriver.js'; | ||
| import type { NativeQueryBuilder } from './query/NativeQueryBuilder.js'; | ||
| import type { QueryBuilder } from './query/QueryBuilder.js'; | ||
| import type { SqlEntityRepository } from './SqlEntityRepository.js'; | ||
| import type { Kysely } from 'kysely'; | ||
| import type { InferKyselyDB } from './typings.js'; | ||
| import { type MikroKyselyPluginOptions } from './plugin/index.js'; | ||
| export interface GetKyselyOptions extends MikroKyselyPluginOptions { | ||
| type?: ConnectionType; | ||
| } | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| export declare class SqlEntityManager<Driver extends AbstractSqlDriver = AbstractSqlDriver> extends EntityManager<Driver> { | ||
| /** | ||
| * Creates a QueryBuilder instance | ||
| */ | ||
| createQueryBuilder<Entity extends object, RootAlias extends string = never>(entityName: EntityName<Entity> | QueryBuilder<Entity>, alias?: RootAlias, type?: ConnectionType, loggerContext?: LoggingOptions): QueryBuilder<Entity, RootAlias>; | ||
| /** | ||
| * Shortcut for `createQueryBuilder()` | ||
| */ | ||
| qb<Entity extends object, RootAlias extends string = never>(entityName: EntityName<Entity>, alias?: RootAlias, type?: ConnectionType, loggerContext?: LoggingOptions): QueryBuilder<Entity, RootAlias, never, never>; | ||
| /** | ||
| * Returns configured Kysely instance. | ||
| */ | ||
| getKysely<TDB = undefined, TOptions extends GetKyselyOptions = GetKyselyOptions>(options?: TOptions): Kysely<TDB extends undefined ? InferKyselyDB<EntitiesFromManager<this>, TOptions> : TDB>; | ||
| execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>; | ||
| getRepository<T extends object, U extends EntityRepository<T> = SqlEntityRepository<T>>(entityName: EntityName<T>): GetRepository<T, U>; | ||
| protected applyDiscriminatorCondition<Entity extends object>(entityName: string, where: FilterQuery<Entity>): FilterQuery<Entity>; | ||
| } | ||
| type EntitiesFromManager<TEntityManager extends EntityManager<any>> = NonNullable<TEntityManager['~entities']> extends any[] ? (Extract<NonNullable<TEntityManager['~entities']>[number], EntitySchemaWithMeta>) : never; | ||
| export {}; |
| import { EntityManager, } from '@mikro-orm/core'; | ||
| import { MikroKyselyPlugin } from './plugin/index.js'; | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| export class SqlEntityManager extends EntityManager { | ||
| /** | ||
| * Creates a QueryBuilder instance | ||
| */ | ||
| createQueryBuilder(entityName, alias, type, loggerContext) { | ||
| const context = this.getContext(false); | ||
| return this.driver.createQueryBuilder(entityName, context.getTransactionContext(), type, true, loggerContext ?? context.loggerContext, alias, this); | ||
| } | ||
| /** | ||
| * Shortcut for `createQueryBuilder()` | ||
| */ | ||
| qb(entityName, alias, type, loggerContext) { | ||
| return this.createQueryBuilder(entityName, alias, type, loggerContext); | ||
| } | ||
| /** | ||
| * Returns configured Kysely instance. | ||
| */ | ||
| getKysely(options = {}) { | ||
| let kysely = this.getConnection(options.type).getClient(); | ||
| if (options.columnNamingStrategy != null | ||
| || options.tableNamingStrategy != null | ||
| || options.processOnCreateHooks != null | ||
| || options.processOnUpdateHooks != null | ||
| || options.convertValues != null) { | ||
| kysely = kysely.withPlugin(new MikroKyselyPlugin(this, options)); | ||
| } | ||
| return kysely; | ||
| } | ||
| async execute(query, params = [], method = 'all', loggerContext) { | ||
| return this.getDriver().execute(query, params, method, this.getContext(false).getTransactionContext(), loggerContext); | ||
| } | ||
| getRepository(entityName) { | ||
| return super.getRepository(entityName); | ||
| } | ||
| applyDiscriminatorCondition(entityName, where) { | ||
| // this is handled in QueryBuilder now for SQL drivers | ||
| return where; | ||
| } | ||
| } |
| import { EntityRepository, type EntityName } from '@mikro-orm/core'; | ||
| import type { SqlEntityManager } from './SqlEntityManager.js'; | ||
| import type { QueryBuilder } from './query/QueryBuilder.js'; | ||
| export declare class SqlEntityRepository<Entity extends object> extends EntityRepository<Entity> { | ||
| protected readonly em: SqlEntityManager; | ||
| constructor(em: SqlEntityManager, entityName: EntityName<Entity>); | ||
| /** | ||
| * Creates a QueryBuilder instance | ||
| */ | ||
| createQueryBuilder<RootAlias extends string = never>(alias?: RootAlias): QueryBuilder<Entity, RootAlias>; | ||
| /** | ||
| * Shortcut for `createQueryBuilder()` | ||
| */ | ||
| qb<RootAlias extends string = never>(alias?: RootAlias): QueryBuilder<Entity, RootAlias>; | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| getEntityManager(): SqlEntityManager; | ||
| } |
| import { EntityRepository } from '@mikro-orm/core'; | ||
| export class SqlEntityRepository extends EntityRepository { | ||
| em; | ||
| constructor(em, entityName) { | ||
| super(em, entityName); | ||
| this.em = em; | ||
| } | ||
| /** | ||
| * Creates a QueryBuilder instance | ||
| */ | ||
| createQueryBuilder(alias) { | ||
| return this.getEntityManager().createQueryBuilder(this.entityName, alias); | ||
| } | ||
| /** | ||
| * Shortcut for `createQueryBuilder()` | ||
| */ | ||
| qb(alias) { | ||
| return this.createQueryBuilder(alias); | ||
| } | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| getEntityManager() { | ||
| return this.em; | ||
| } | ||
| } |
+272
| import type { Generated, Kysely } from 'kysely'; | ||
| import type { DeferMode, CheckCallback, Dictionary, EntityProperty, GroupOperator, RawQueryFragment, QBFilterQuery, QueryOrderMap, Type, QueryFlag, AnyEntity, EntityName, EntitySchemaWithMeta, Primary, PrimaryProperty, Opt } from '@mikro-orm/core'; | ||
| import type { JoinType, QueryType } from './query/enums.js'; | ||
| import type { DatabaseSchema } from './schema/DatabaseSchema.js'; | ||
| import type { DatabaseTable } from './schema/DatabaseTable.js'; | ||
| import type { QueryBuilder } from './query/QueryBuilder.js'; | ||
| import type { NativeQueryBuilder } from './query/NativeQueryBuilder.js'; | ||
| import type { MikroKyselyPluginOptions } from './plugin/index.js'; | ||
| export interface Table { | ||
| table_name: string; | ||
| schema_name?: string; | ||
| table_comment?: string; | ||
| } | ||
| type AnyString = string & {}; | ||
| export type Field<T> = AnyString | keyof T | RawQueryFragment | QueryBuilder | NativeQueryBuilder; | ||
| export interface JoinOptions { | ||
| table: string; | ||
| schema?: string; | ||
| type: JoinType; | ||
| alias: string; | ||
| ownerAlias: string; | ||
| inverseAlias?: string; | ||
| joinColumns?: string[]; | ||
| inverseJoinColumns?: string[]; | ||
| primaryKeys?: string[]; | ||
| path?: string; | ||
| prop: EntityProperty; | ||
| cond: Dictionary; | ||
| cond_?: Dictionary; | ||
| subquery?: string; | ||
| nested?: Set<JoinOptions>; | ||
| parent?: JoinOptions; | ||
| } | ||
| export interface Column { | ||
| name: string; | ||
| type: string; | ||
| mappedType: Type<unknown>; | ||
| unsigned?: boolean; | ||
| autoincrement?: boolean; | ||
| nullable?: boolean; | ||
| length?: number; | ||
| precision?: number; | ||
| scale?: number; | ||
| default?: string | null; | ||
| defaultConstraint?: string; | ||
| comment?: string; | ||
| generated?: string; | ||
| nativeEnumName?: string; | ||
| enumItems?: string[]; | ||
| primary?: boolean; | ||
| unique?: boolean; | ||
| /** mysql only */ | ||
| extra?: string; | ||
| ignoreSchemaChanges?: ('type' | 'extra' | 'default')[]; | ||
| } | ||
| export interface ForeignKey { | ||
| columnNames: string[]; | ||
| constraintName: string; | ||
| localTableName: string; | ||
| referencedTableName: string; | ||
| referencedColumnNames: string[]; | ||
| updateRule?: string; | ||
| deleteRule?: string; | ||
| deferMode?: DeferMode; | ||
| } | ||
| export interface IndexDef { | ||
| columnNames: string[]; | ||
| keyName: string; | ||
| unique: boolean; | ||
| constraint: boolean; | ||
| primary: boolean; | ||
| composite?: boolean; | ||
| expression?: string; | ||
| options?: Dictionary; | ||
| type?: string | Readonly<{ | ||
| indexType?: string; | ||
| storageEngineIndexType?: 'hash' | 'btree'; | ||
| predicate?: string; | ||
| }>; | ||
| deferMode?: DeferMode | `${DeferMode}`; | ||
| } | ||
| export interface CheckDef<T = unknown> { | ||
| name: string; | ||
| expression: string | CheckCallback<T>; | ||
| definition?: string; | ||
| columnName?: string; | ||
| } | ||
| export interface ColumnDifference { | ||
| oldColumnName: string; | ||
| column: Column; | ||
| fromColumn: Column; | ||
| changedProperties: Set<string>; | ||
| } | ||
| export interface TableDifference { | ||
| name: string; | ||
| changedComment?: string; | ||
| fromTable: DatabaseTable; | ||
| toTable: DatabaseTable; | ||
| addedColumns: Dictionary<Column>; | ||
| changedColumns: Dictionary<ColumnDifference>; | ||
| removedColumns: Dictionary<Column>; | ||
| renamedColumns: Dictionary<Column>; | ||
| addedIndexes: Dictionary<IndexDef>; | ||
| changedIndexes: Dictionary<IndexDef>; | ||
| removedIndexes: Dictionary<IndexDef>; | ||
| renamedIndexes: Dictionary<IndexDef>; | ||
| addedChecks: Dictionary<CheckDef>; | ||
| changedChecks: Dictionary<CheckDef>; | ||
| removedChecks: Dictionary<CheckDef>; | ||
| addedForeignKeys: Dictionary<ForeignKey>; | ||
| changedForeignKeys: Dictionary<ForeignKey>; | ||
| removedForeignKeys: Dictionary<ForeignKey>; | ||
| } | ||
| export interface SchemaDifference { | ||
| newNamespaces: Set<string>; | ||
| newNativeEnums: { | ||
| name: string; | ||
| schema?: string; | ||
| items: string[]; | ||
| }[]; | ||
| newTables: Dictionary<DatabaseTable>; | ||
| changedTables: Dictionary<TableDifference>; | ||
| removedTables: Dictionary<DatabaseTable>; | ||
| removedNamespaces: Set<string>; | ||
| removedNativeEnums: { | ||
| name: string; | ||
| schema?: string; | ||
| }[]; | ||
| orphanedForeignKeys: ForeignKey[]; | ||
| fromSchema: DatabaseSchema; | ||
| } | ||
| export interface IQueryBuilder<T> { | ||
| readonly alias: string; | ||
| readonly type: QueryType; | ||
| _fields?: Field<T>[]; | ||
| /** @internal */ | ||
| helper: any; | ||
| select(fields: Field<T> | Field<T>[], distinct?: boolean): this; | ||
| addSelect(fields: string | string[]): this; | ||
| from<T extends AnyEntity<T> = AnyEntity>(target: EntityName<T> | IQueryBuilder<T>, aliasName?: string): IQueryBuilder<T>; | ||
| insert(data: any): this; | ||
| update(data: any): this; | ||
| delete(cond?: QBFilterQuery): this; | ||
| truncate(): this; | ||
| count(field?: string | string[], distinct?: boolean): this; | ||
| join(field: string, alias: string, cond?: QBFilterQuery, type?: JoinType, path?: string): this; | ||
| innerJoin(field: string, alias: string, cond?: QBFilterQuery): this; | ||
| leftJoin(field: string, alias: string, cond?: QBFilterQuery): this; | ||
| joinAndSelect(field: string, alias: string, cond?: QBFilterQuery): this; | ||
| leftJoinAndSelect(field: string, alias: string, cond?: QBFilterQuery, fields?: string[]): this; | ||
| innerJoinAndSelect(field: string, alias: string, cond?: QBFilterQuery, fields?: string[]): this; | ||
| withSubQuery(subQuery: RawQueryFragment | NativeQueryBuilder, alias: string): this; | ||
| where(cond: QBFilterQuery<T>, operator?: keyof typeof GroupOperator): this; | ||
| where(cond: string, params?: any[], operator?: keyof typeof GroupOperator): this; | ||
| andWhere(cond: QBFilterQuery<T>): this; | ||
| andWhere(cond: string, params?: any[]): this; | ||
| orWhere(cond: QBFilterQuery<T>): this; | ||
| orWhere(cond: string, params?: any[]): this; | ||
| orderBy(orderBy: QueryOrderMap<T>): this; | ||
| groupBy(fields: (string | keyof T) | (string | keyof T)[]): this; | ||
| having(cond?: QBFilterQuery | string, params?: any[]): this; | ||
| getAliasForJoinPath(path: string, options?: ICriteriaNodeProcessOptions): string | undefined; | ||
| getJoinForPath(path?: string, options?: ICriteriaNodeProcessOptions): JoinOptions | undefined; | ||
| getNextAlias(entityName?: string): string; | ||
| clone(reset?: boolean): IQueryBuilder<T>; | ||
| setFlag(flag: QueryFlag): this; | ||
| unsetFlag(flag: QueryFlag): this; | ||
| hasFlag(flag: QueryFlag): boolean; | ||
| scheduleFilterCheck(path: string): void; | ||
| } | ||
| export interface ICriteriaNodeProcessOptions { | ||
| alias?: string; | ||
| matchPopulateJoins?: boolean; | ||
| ignoreBranching?: boolean; | ||
| preferNoBranch?: boolean; | ||
| type?: 'orderBy'; | ||
| filter?: boolean; | ||
| } | ||
| export interface ICriteriaNode<T extends object> { | ||
| readonly entityName: string; | ||
| readonly parent?: ICriteriaNode<T> | undefined; | ||
| readonly key?: string | undefined; | ||
| readonly strict?: boolean; | ||
| payload: any; | ||
| prop?: EntityProperty; | ||
| index?: number; | ||
| process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any; | ||
| shouldInline(payload: any): boolean; | ||
| willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean; | ||
| shouldRename(payload: any): boolean; | ||
| renameFieldToPK<T>(qb: IQueryBuilder<T>, ownerAlias?: string): string; | ||
| getPath(addIndex?: boolean): string; | ||
| getPivotPath(path: string): string; | ||
| } | ||
| export type MaybeReturnType<T> = T extends (...args: any[]) => infer R ? R : T; | ||
| export type InferEntityProperties<Schema> = Schema extends EntitySchemaWithMeta<any, any, any, any, infer Properties> ? Properties : never; | ||
| export type InferKyselyDB<TEntities extends { | ||
| name: string; | ||
| }, TOptions extends MikroKyselyPluginOptions = {}> = MapValueAsTable<MapByName<TEntities>, TOptions>; | ||
| export type InferDBFromKysely<TKysely extends Kysely<any>> = TKysely extends Kysely<infer TDB> ? TDB : never; | ||
| type PreferStringLiteral<TCandidate, TFallback> = [ | ||
| TCandidate | ||
| ] extends [never] ? TFallback : string extends TCandidate ? TFallback : TCandidate extends string ? TCandidate : TFallback; | ||
| export type MapByName<T extends { | ||
| name: string; | ||
| tableName?: string; | ||
| }> = { | ||
| [P in T as PreferStringLiteral<NonNullable<P['tableName']>, P['name']>]: P; | ||
| }; | ||
| export type MapValueAsTable<TMap extends Record<string, any>, TOptions extends MikroKyselyPluginOptions = {}> = { | ||
| [K in keyof TMap as TransformName<K, TOptions['tableNamingStrategy'] extends 'entity' ? 'entity' : 'underscore'>]: InferKyselyTable<TMap[K], TOptions>; | ||
| }; | ||
| export type InferKyselyTable<TSchema extends EntitySchemaWithMeta, TOptions extends MikroKyselyPluginOptions = {}> = ExcludeNever<{ | ||
| -readonly [K in keyof InferEntityProperties<TSchema> as TransformColumnName<K, TOptions['columnNamingStrategy'] extends 'property' ? 'property' : 'underscore', MaybeReturnType<InferEntityProperties<TSchema>[K]>>]: InferColumnValue<MaybeReturnType<InferEntityProperties<TSchema>[K]>, TOptions['processOnCreateHooks'] extends true ? true : false>; | ||
| }>; | ||
| type TransformName<TName, TNamingStrategy extends 'underscore' | 'entity'> = TNamingStrategy extends 'underscore' ? TName extends string ? SnakeCase<TName> : TName : TName; | ||
| type TransformColumnName<TName, TNamingStrategy extends 'underscore' | 'property', TBuilder> = TNamingStrategy extends 'property' ? TName : TBuilder extends { | ||
| '~options': { | ||
| fieldName: string; | ||
| }; | ||
| } ? TBuilder['~options']['fieldName'] : TName extends string ? MaybeJoinColumnName<SnakeCase<TName>, TBuilder> : never; | ||
| type MaybeJoinColumnName<TName extends string, TBuilder> = TBuilder extends { | ||
| '~type'?: { | ||
| value: infer Value; | ||
| }; | ||
| '~options': { | ||
| kind: 'm:1'; | ||
| }; | ||
| } ? PrimaryProperty<Value> extends string ? `${TName}_${SnakeCase<PrimaryProperty<Value>>}` : never : TBuilder extends { | ||
| '~type'?: { | ||
| value: infer Value; | ||
| }; | ||
| '~options': { | ||
| kind: '1:1'; | ||
| owner: true; | ||
| }; | ||
| } ? PrimaryProperty<Value> extends string ? `${TName}_${SnakeCase<PrimaryProperty<Value>>}` : never : TName; | ||
| export type SnakeCase<TName extends string> = TName extends `${infer P1}${infer P2}` ? P2 extends Uncapitalize<P2> ? `${Uncapitalize<P1>}${SnakeCase<P2>}` : `${Uncapitalize<P1>}_${SnakeCase<Uncapitalize<P2>>}` : TName; | ||
| type InferColumnValue<TBuilder, TProcessOnCreate extends boolean> = TBuilder extends { | ||
| '~type'?: { | ||
| value: infer Value; | ||
| }; | ||
| '~options': infer TOptions; | ||
| } ? MaybeNever<MaybeGenerated<MaybeJoinKey<Value, TOptions>, TOptions, TProcessOnCreate>, TOptions> : never; | ||
| type MaybeGenerated<TValue, TOptions, TProcessOnCreate extends boolean> = TOptions extends { | ||
| nullable: true; | ||
| } ? (TValue | null) : TOptions extends { | ||
| autoincrement: true; | ||
| } ? Generated<TValue> : TOptions extends { | ||
| default: true; | ||
| } ? Generated<TValue> : TOptions extends { | ||
| defaultRaw: true; | ||
| } ? Generated<TValue> : TProcessOnCreate extends false ? TValue : TOptions extends { | ||
| onCreate: Function; | ||
| } ? Generated<TValue> : TValue; | ||
| type MaybeJoinKey<TValue, TOptions> = TOptions extends { | ||
| kind: 'm:1'; | ||
| } ? UnwrapOpt<Primary<TValue>> : TOptions extends { | ||
| kind: '1:1'; | ||
| } ? TOptions extends { | ||
| owner: true; | ||
| } ? UnwrapOpt<Primary<TValue>> : never : TValue; | ||
| type UnwrapOpt<TValue> = TValue extends Opt<infer OriginalValue> ? OriginalValue : TValue; | ||
| type MaybeNever<TValue, TOptions> = TOptions extends { | ||
| persist: true; | ||
| } ? never : TOptions extends { | ||
| kind: 'm:n'; | ||
| } ? never : TValue; | ||
| type ExcludeNever<TMap extends Record<string, any>> = { | ||
| [K in keyof TMap as TMap[K] extends never ? never : K]: TMap[K]; | ||
| }; | ||
| export {}; |
| export {}; |
+3
-3
| { | ||
| "name": "@mikro-orm/sql", | ||
| "version": "7.0.0-dev.97", | ||
| "version": "7.0.0-dev.98", | ||
| "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.", | ||
@@ -8,3 +8,3 @@ "type": "module", | ||
| "./package.json": "./package.json", | ||
| ".": "./src/index.ts" | ||
| ".": "./index.js" | ||
| }, | ||
@@ -61,4 +61,4 @@ "repository": { | ||
| "peerDependencies": { | ||
| "@mikro-orm/core": "7.0.0-dev.97" | ||
| "@mikro-orm/core": "7.0.0-dev.98" | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
574495
39657.44%91
9000%12131
Infinity%0
-100%391
Infinity%33
Infinity%2
100%