From cb612add858877f7b218b2e008a84f22bdc2ed4d Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Fri, 21 Jun 2024 16:00:43 +0200 Subject: [PATCH] feat(orm): new selector API, still work in progress 2 --- packages/orm/package.json | 3 +- packages/orm/src/database-adapter.ts | 18 +- packages/orm/src/database-session.ts | 66 ++--- packages/orm/src/database.ts | 62 +++-- packages/orm/src/formatter.ts | 155 ++++++----- packages/orm/src/memory-db.ts | 248 ++++++++---------- packages/orm/src/query.ts | 3 + packages/orm/src/select.ts | 165 +++++++----- packages/orm/src/utils.ts | 13 - .../orm/src/virtual-foreign-key-constraint.ts | 105 ++++---- packages/orm/tests/dql.spec.ts | 147 +++++++++-- packages/sql/src/select.ts | 10 +- packages/type-compiler/src/compiler.ts | 52 +++- .../type-compiler/tests/transpile.spec.ts | 1 + packages/type/src/reflection/reflection.ts | 6 +- packages/type/tests/type.spec.ts | 45 ++++ 16 files changed, 636 insertions(+), 463 deletions(-) diff --git a/packages/orm/package.json b/packages/orm/package.json index 13b46e4fe..6493cbc42 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -32,8 +32,7 @@ "@deepkit/type": "^1.0.1-alpha.13" }, "dependencies": { - "@deepkit/topsort": "^1.0.1-alpha.121", - "sift": "^7.0.1" + "@deepkit/topsort": "^1.0.1-alpha.121" }, "devDependencies": { "@deepkit/core": "^1.0.1-alpha.147", diff --git a/packages/orm/src/database-adapter.ts b/packages/orm/src/database-adapter.ts index ebe423597..9a220a1cd 100644 --- a/packages/orm/src/database-adapter.ts +++ b/packages/orm/src/database-adapter.ts @@ -31,7 +31,7 @@ import { } from '@deepkit/type'; import { Query } from './query.js'; import { DatabaseSession, DatabaseTransaction } from './database-session.js'; -import { Query2Resolver } from './select.js'; +import { SelectorResolver } from './select.js'; export abstract class DatabaseAdapterQueryFactory { abstract createQuery(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass): Query; @@ -110,13 +110,15 @@ export class MigrateOptions { * You can specify a more specialized adapter like MysqlDatabaseAdapter/MongoDatabaseAdapter with special API for MySQL/Mongo. */ export abstract class DatabaseAdapter { - abstract queryFactory(session: DatabaseSession): DatabaseAdapterQueryFactory; - - createQuery2Resolver?(session: DatabaseSession): Query2Resolver; - - rawFactory(session: DatabaseSession): RawFactory { - return new RawFactory(); - }; + // abstract queryFactory(session: DatabaseSession): DatabaseAdapterQueryFactory; + // + // createQuery2Resolver?(session: DatabaseSession): Query2Resolver; + // + // rawFactory(session: DatabaseSession): RawFactory { + // return new RawFactory(); + // }; + + abstract createSelectorResolver(session: DatabaseSession): SelectorResolver; abstract createPersistence(session: DatabaseSession): DatabasePersistence; diff --git a/packages/orm/src/database-session.ts b/packages/orm/src/database-session.ts index 005ea2748..116f01aa1 100644 --- a/packages/orm/src/database-session.ts +++ b/packages/orm/src/database-session.ts @@ -11,13 +11,12 @@ import type { DatabaseAdapter, DatabasePersistence, DatabasePersistenceChangeSet } from './database-adapter.js'; import { DatabaseEntityRegistry } from './database-adapter.js'; import { DatabaseValidationError, OrmEntity } from './type.js'; -import { AbstractClassType, ClassType, CustomError } from '@deepkit/core'; +import { ClassType, CustomError, isFunction } from '@deepkit/core'; import { getPrimaryKeyExtractor, isReferenceInstance, markAsHydrated, PrimaryKeyFields, - ReceiveType, ReflectionClass, typeSettings, UnpopulatedCheck, @@ -40,6 +39,7 @@ import { DatabaseLogger } from './logger.js'; import { Stopwatch } from '@deepkit/stopwatch'; import { EventDispatcher, EventDispatcherInterface, EventToken } from '@deepkit/event'; import { DatabasePluginRegistry } from './plugin/plugin.js'; +import { query, Query2, SelectorInferredState, SelectorRefs } from './select.js'; let SESSION_IDS = 0; @@ -307,9 +307,9 @@ export class DatabaseSession /** * Creates a new DatabaseQuery instance which can be used to query and manipulate data. */ - public readonly query: ReturnType['createQuery']; - - public readonly raw!: ReturnType['create']; + // public readonly query: ReturnType['createQuery']; + // + // public readonly raw!: ReturnType['create']; protected rounds: DatabaseSessionRound[] = []; @@ -336,16 +336,16 @@ export class DatabaseSession public logger: DatabaseLogger = new DatabaseLogger, public stopwatch?: Stopwatch, ) { - const queryFactory = this.adapter.queryFactory(this); - - //we cannot use arrow functions, since they can't have ReceiveType - function query(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass) { - const result = queryFactory.createQuery(type); - result.model.adapterName = adapter.getName(); - return result; - } - - this.query = query as any; + // const queryFactory = this.adapter.queryFactory(this); + // + // //we cannot use arrow functions, since they can't have ReceiveType + // function query(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass) { + // const result = queryFactory.createQuery(type); + // result.model.adapterName = adapter.getName(); + // return result; + // } + // + // this.query = query as any; // this.query = {} as any; // const factory = this.adapter.rawFactory(this); @@ -355,6 +355,12 @@ export class DatabaseSession // }; } + query2 | ((main: SelectorRefs, ...args: SelectorRefs[]) => R | undefined)>(cbOrQ?: Q): Query2 { + if (!cbOrQ) throw new Error('Query2 needs a callback or query object'); + const state: SelectorInferredState = isFunction(cbOrQ) ? query(cbOrQ) : cbOrQ; + return new Query2(state.state, this, this.adapter.createSelectorResolver(this)); + } + /** * Marks this session as transactional. On the next query or flush/commit() a transaction on the database adapter is started. * Use flush(), commit(), and rollback() to control the transaction behavior. All created query objects from this session @@ -518,21 +524,21 @@ export class DatabaseSession const classSchema = this.entityRegistry.getFromInstance(item); const pk = getPrimaryKeyExtractor(classSchema)(item); - const itemDB = await this.query(classSchema).filter(pk).findOne(); - - for (const property of classSchema.getProperties()) { - if (property.isPrimaryKey()) continue; - if (property.isReference() || property.isBackReference()) continue; - - //we set only not overwritten values - if (!item.hasOwnProperty(property.symbol)) { - Object.defineProperty(item, property.symbol, { - enumerable: false, - configurable: true, - value: itemDB[property.getNameAsString() as keyof T], - }); - } - } + // const itemDB = await this.query(classSchema).filter(pk).findOne(); + // + // for (const property of classSchema.getProperties()) { + // if (property.isPrimaryKey()) continue; + // if (property.isReference() || property.isBackReference()) continue; + // + // //we set only not overwritten values + // if (!item.hasOwnProperty(property.symbol)) { + // Object.defineProperty(item, property.symbol, { + // enumerable: false, + // configurable: true, + // value: itemDB[property.getNameAsString() as keyof T], + // }); + // } + // } markAsHydrated(item); } diff --git a/packages/orm/src/database.ts b/packages/orm/src/database.ts index 123ca8047..93dd048c0 100644 --- a/packages/orm/src/database.ts +++ b/packages/orm/src/database.ts @@ -8,13 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ -import { - AbstractClassType, - ClassType, - forwardTypeArguments, - getClassName, - getClassTypeFromInstance, -} from '@deepkit/core'; +import { AbstractClassType, ClassType, getClassName, getClassTypeFromInstance } from '@deepkit/core'; import { entityAnnotation, EntityOptions, @@ -38,7 +32,7 @@ import { Stopwatch } from '@deepkit/stopwatch'; import { getClassState, getInstanceState, getNormalizedPrimaryKey } from './identity-map.js'; import { EventDispatcher, EventDispatcherUnsubscribe, EventListenerCallback, EventToken } from '@deepkit/event'; import { DatabasePlugin, DatabasePluginRegistry } from './plugin/plugin.js'; -import { Query2 } from './select.js'; +import { Query2, SelectorInferredState, SelectorRefs } from './select.js'; /** * Hydrates not completely populated item and makes it completely accessible. @@ -135,9 +129,9 @@ export class Database { * await session.commit(); //only necessary when you changed items received by this session * ``` */ - public readonly query: ReturnType['createQuery']; - - public readonly raw: ReturnType['create']; + // public readonly query: ReturnType['createQuery']; + // + // public readonly raw: ReturnType['create']; protected virtualForeignKeyConstraint: VirtualForeignKeyConstraint = new VirtualForeignKeyConstraint(this); @@ -154,24 +148,24 @@ export class Database { this.entityRegistry.add(...schemas); if (Database.registry) Database.registry.push(this); - const self = this; - - //we cannot use arrow functions, since they can't have ReceiveType - function query(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass) { - const session = self.createSession(); - session.withIdentityMap = false; - return session.query(type); - } - - this.query = query; - - this.raw = (...args: any[]) => { - const session = this.createSession(); - session.withIdentityMap = false; - if (!session.raw) throw new Error('Adapter has no raw mode'); - forwardTypeArguments(this.raw, session.raw); - return session.raw(...args); - }; + // const self = this; + + // //we cannot use arrow functions, since they can't have ReceiveType + // function query(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass) { + // const session = self.createSession(); + // session.withIdentityMap = false; + // return session.query(type); + // } + + // this.query = query; + // + // this.raw = (...args: any[]) => { + // const session = this.createSession(); + // session.withIdentityMap = false; + // if (!session.raw) throw new Error('Adapter has no raw mode'); + // forwardTypeArguments(this.raw, session.raw); + // return session.raw(...args); + // }; this.registerEntity(...schemas); @@ -180,10 +174,14 @@ export class Database { } } - from(type?: ReceiveType) { + query(...args: any[]): any { + throw new Error('Deprecated'); + } + + query2 | ((main: SelectorRefs, ...args: SelectorRefs[]) => R | undefined)>(cbOrQ?: Q): Query2 { const session = this.createSession(); - if (!this.adapter.createQuery2Resolver) throw new Error('Adapter has no createQuery2Resolver method'); - return new Query2(ReflectionClass.fromType(resolveReceiveType(type)), session, this.adapter.createQuery2Resolver(session)); + session.withIdentityMap = false; + return session.query2(cbOrQ as any); } registerPlugin(...plugins: DatabasePlugin[]): void { diff --git a/packages/orm/src/formatter.ts b/packages/orm/src/formatter.ts index 89f177c9a..375d0e985 100644 --- a/packages/orm/src/formatter.ts +++ b/packages/orm/src/formatter.ts @@ -20,18 +20,17 @@ import { markAsHydrated, ReflectionClass, ReflectionProperty, - resolveForeignReflectionClass, SerializeFunction, Serializer, typeSettings, UnpopulatedCheck, unpopulatedSymbol, } from '@deepkit/type'; -import { DatabaseQueryModel } from './query.js'; import { capitalize, ClassType } from '@deepkit/core'; import { ClassState, getClassState, getInstanceState, IdentityMap, PKHash } from './identity-map.js'; import { getReference } from './reference.js'; import { OrmEntity } from './type.js'; +import { selectorIsPartial, SelectorState } from './select.js'; export type HydratorFn = (item: any) => Promise; @@ -77,8 +76,8 @@ export class Formatter { return this.instancePools.get(classType)!; } - public hydrate(model: DatabaseQueryModel, dbRecord: DBRecord): any { - return this.hydrateModel(model, this.rootClassSchema, dbRecord); + public hydrate(state: SelectorState, dbRecord: DBRecord): any { + return this.hydrateModel(state, this.rootClassSchema, dbRecord); } protected makeInvalidReference( @@ -182,10 +181,10 @@ export class Formatter { return ref; } - protected hydrateModel(model: DatabaseQueryModel, classSchema: ReflectionClass, dbRecord: DBRecord) { + protected hydrateModel(model: SelectorState, classSchema: ReflectionClass, dbRecord: DBRecord) { let pool: Map | undefined = undefined; let pkHash: any = undefined; - const partial = model.isPartial(); + const partial = selectorIsPartial(model); const classState = classSchema === this.rootClassSchema ? this.rootClassState : getClassState(classSchema); const singleTableInheritanceMap = classSchema.getAssignedSingleTableInheritanceSubClassesByIdentifier(); @@ -283,54 +282,54 @@ export class Formatter { return converted; } - protected assignJoins(model: DatabaseQueryModel, classSchema: ReflectionClass, dbRecord: DBRecord, item: any): { [name: string]: true } { + protected assignJoins(model: SelectorState, classSchema: ReflectionClass, dbRecord: DBRecord, item: any): { [name: string]: true } { const handledRelation: { [name: string]: true } = {}; - for (const join of model.joins) { - handledRelation[join.propertySchema.name] = true; - const refName = join.as || join.propertySchema.name; - - //When the item is NOT from the database or property was overwritten, we don't overwrite it again. - if (item.hasOwnProperty(join.propertySchema.symbol)) { - continue; - } - - if (join.populate) { - const hasValue = dbRecord[refName] !== undefined && dbRecord[refName] !== null; - if (join.propertySchema.isBackReference() && join.propertySchema.isArray()) { - if (hasValue) { - item[join.propertySchema.name] = dbRecord[refName].map((item: any) => { - return this.hydrateModel(join.query.model, resolveForeignReflectionClass(join.propertySchema), item); - }); - } else if (!item[join.propertySchema.name]) { - item[join.propertySchema.name] = []; - } - } else if (hasValue) { - item[join.propertySchema.name] = this.hydrateModel( - join.query.model, resolveForeignReflectionClass(join.propertySchema), dbRecord[refName] - ); - } else { - item[join.propertySchema.name] = undefined; - } - } else { - //not populated - if (join.propertySchema.isReference()) { - const reference = this.getReference(classSchema, dbRecord, join.propertySchema, model.isPartial()); - if (reference) item[join.propertySchema.name] = reference; - } else { - //unpopulated backReferences are inaccessible - if (!model.isPartial()) { - this.makeInvalidReference(item, classSchema, join.propertySchema); - } - } - } - } + // for (const join of model.joins) { + // handledRelation[join.propertySchema.name] = true; + // const refName = join.as || join.propertySchema.name; + // + // //When the item is NOT from the database or property was overwritten, we don't overwrite it again. + // if (item.hasOwnProperty(join.propertySchema.symbol)) { + // continue; + // } + // + // if (join.populate) { + // const hasValue = dbRecord[refName] !== undefined && dbRecord[refName] !== null; + // if (join.propertySchema.isBackReference() && join.propertySchema.isArray()) { + // if (hasValue) { + // item[join.propertySchema.name] = dbRecord[refName].map((item: any) => { + // return this.hydrateModel(join.query.model, resolveForeignReflectionClass(join.propertySchema), item); + // }); + // } else if (!item[join.propertySchema.name]) { + // item[join.propertySchema.name] = []; + // } + // } else if (hasValue) { + // item[join.propertySchema.name] = this.hydrateModel( + // join.query.model, resolveForeignReflectionClass(join.propertySchema), dbRecord[refName] + // ); + // } else { + // item[join.propertySchema.name] = undefined; + // } + // } else { + // //not populated + // if (join.propertySchema.isReference()) { + // const reference = this.getReference(classSchema, dbRecord, join.propertySchema, model.isPartial()); + // if (reference) item[join.propertySchema.name] = reference; + // } else { + // //unpopulated backReferences are inaccessible + // if (!model.isPartial()) { + // this.makeInvalidReference(item, classSchema, join.propertySchema); + // } + // } + // } + // } return handledRelation; } - protected createObject(model: DatabaseQueryModel, classState: ClassState, classSchema: ReflectionClass, dbRecord: DBRecord) { - const partial = model.isPartial(); + protected createObject(model: SelectorState, classState: ClassState, classSchema: ReflectionClass, dbRecord: DBRecord) { + const partial = selectorIsPartial(model); const converted = classSchema === this.rootClassSchema ? (partial ? this.partialDeserialize(dbRecord) : this.deserialize(dbRecord)) @@ -340,36 +339,36 @@ export class Formatter { if (model.withChangeDetection) getInstanceState(classState, converted).markAsFromDatabase(); } - if (!partial && model.lazyLoad.size) { - for (const lazy of model.lazyLoad.values()) { - const property = classSchema.getProperty(lazy); - - //relations should be handled in getReferences() - if (property.isReference() || property.isBackReference()) continue; - - this.makeInvalidReference(converted, classSchema, property, 'lazy'); - } - - getInstanceState(getClassState(classSchema), converted).hydrator = this.hydrator; - } - - if (classSchema.getReferences().length > 0) { - const handledRelation = model.joins.length ? this.assignJoins(model, classSchema, dbRecord, converted) : undefined; - - //all non-populated owning references will be just proxy references - for (const property of classSchema.getReferences()) { - if (model.select.size && !model.select.has(property.name)) continue; - if (handledRelation && handledRelation[property.name]) continue; - if (property.isReference()) { - converted[property.name] = this.getReference(classSchema, dbRecord, property, partial); - } else { - //unpopulated backReferences are inaccessible - if (!partial) { - this.makeInvalidReference(converted, classSchema, property); - } - } - } - } + // if (!partial && model.lazyLoad.size) { + // for (const lazy of model.lazyLoad.values()) { + // const property = classSchema.getProperty(lazy); + // + // //relations should be handled in getReferences() + // if (property.isReference() || property.isBackReference()) continue; + // + // this.makeInvalidReference(converted, classSchema, property, 'lazy'); + // } + // + // getInstanceState(getClassState(classSchema), converted).hydrator = this.hydrator; + // } + + // if (classSchema.getReferences().length > 0) { + // const handledRelation = model.joins.length ? this.assignJoins(model, classSchema, dbRecord, converted) : undefined; + // + // //all non-populated owning references will be just proxy references + // for (const property of classSchema.getReferences()) { + // if (model.select.size && !model.select.has(property.name)) continue; + // if (handledRelation && handledRelation[property.name]) continue; + // if (property.isReference()) { + // converted[property.name] = this.getReference(classSchema, dbRecord, property, partial); + // } else { + // //unpopulated backReferences are inaccessible + // if (!partial) { + // this.makeInvalidReference(converted, classSchema, property); + // } + // } + // } + // } return converted; } diff --git a/packages/orm/src/memory-db.ts b/packages/orm/src/memory-db.ts index 61d2a1d90..4d88fcdbb 100644 --- a/packages/orm/src/memory-db.ts +++ b/packages/orm/src/memory-db.ts @@ -7,31 +7,31 @@ * * You should have received a copy of the MIT License along with this program. */ - +/** @reflection never */ import { DatabaseSession, DatabaseTransaction } from './database-session.js'; -import { DatabaseQueryModel, GenericQueryResolver, Query } from './query.js'; -import { - Changes, - getSerializeFunction, - ReceiveType, - ReflectionClass, - resolvePath, - serialize, - Serializer, -} from '@deepkit/type'; -import { AbstractClassType, deletePathValue, getPathValue, setPathValue } from '@deepkit/core'; +import { Changes, getSerializeFunction, ReflectionClass, Serializer } from '@deepkit/type'; +import { deletePathValue, getPathValue, setPathValue } from '@deepkit/core'; import { DatabaseAdapter, - DatabaseAdapterQueryFactory, DatabaseEntityRegistry, DatabasePersistence, DatabasePersistenceChangeSet, MigrateOptions, } from './database-adapter.js'; import { DeleteResult, OrmEntity, PatchResult } from './type.js'; -import { findQueryList } from './utils.js'; -import { convertQueryFilter } from './query-filter.js'; import { Formatter } from './formatter.js'; +import { + and, + eq, + isOp, + isProperty, + OpExpression, + opTag, + SelectorProperty, + SelectorResolver, + SelectorState, + where, +} from './select.js'; type SimpleStore = { items: Map, autoIncrement: number }; @@ -41,30 +41,6 @@ class MemorySerializer extends Serializer { const memorySerializer = new MemorySerializer(); -// memorySerializer.fromClass.register('undefined', (property: PropertySchema, state: CompilerState) => { -// //mongo does not support 'undefined' as column type, so we convert automatically to null -// state.addSetter(`null`); -// }); -// -// memorySerializer.toClass.register('undefined', (property: PropertySchema, state: CompilerState) => { -// //mongo does not support 'undefined' as column type, so we store always null. depending on the property definition -// //we convert back to undefined or keep it null -// if (property.isOptional) return state.addSetter(`undefined`); -// if (property.isNullable) return state.addSetter(`null`); -// }); -// -// memorySerializer.fromClass.register('null', (property: PropertySchema, state: CompilerState) => { -// //mongo does not support 'undefined' as column type, so we convert automatically to null -// state.addSetter(`null`); -// }); -// -// memorySerializer.toClass.register('null', (property: PropertySchema, state: CompilerState) => { -// //mongo does not support 'undefined' as column type, so we store always null. depending on the property definition -// //we convert back to undefined or keep it null -// if (property.isOptional) return state.addSetter(`undefined`); -// if (property.isNullable) return state.addSetter(`null`); -// }); - function sortAsc(a: any, b: any) { if (a > b) return +1; if (a < b) return -1; @@ -77,57 +53,74 @@ function sortDesc(a: any, b: any) { return 0; } -function sort(items: any[], field: string, sortFn: typeof sortAsc | typeof sortAsc): void { +type Accessor = (record: any) => any; +export type MemoryOpRegistry = { [tag: symbol]: (expression: OpExpression) => Accessor }; + +export const memoryOps: MemoryOpRegistry = { + [eq.id](expression: OpExpression) { + const [a, b] = expression.args.map(e => buildAccessor(e)); + return (record: any) => a(record) === b(record); + }, + [and.id](expression: OpExpression) { + const lines = expression.args.map(e => buildAccessor(e)); + return (record: any) => lines.every(v => v(record)); + }, + [where.id](expression: OpExpression) { + const lines = expression.args.map(e => buildAccessor(e)); + return (record: any) => lines.every(v => v(record)); + }, +}; + +function buildAccessor(op: OpExpression | SelectorProperty | unknown): Accessor { + if (isOp(op)) { + const fn = memoryOps[op[opTag].id]; + if (!fn) throw new Error(`No memory op registered for ${op[opTag].id.toString()}`); + return fn(op); + } + + if (isProperty(op)) { + return (record: any) => { + //todo: handle if selector of joined table + // and deep json path + return record[op.name]; + }; + } + + return () => op; +} + +function sort(items: any[], accessor: Accessor, sortFn: typeof sortAsc | typeof sortAsc): void { items.sort((a, b) => { - return sortFn(a[field], b[field]); + return sortFn(accessor(a), accessor(b)); }); } -export class MemoryQuery extends Query { - protected isMemory = true; - - isMemoryDb() { - return this.isMemory; - } +function filterWhere(items: T[], where: OpExpression): T[] { + const accessor = buildAccessor(where); + console.log('accessor', accessor.toString()); + return items.filter(v => !!accessor(v)); } -const find = (adapter: MemoryDatabaseAdapter, classSchema: ReflectionClass, model: DatabaseQueryModel): T[] => { +const find = (adapter: MemoryDatabaseAdapter, classSchema: ReflectionClass, model: SelectorState): T[] => { const rawItems = [...adapter.getStore(classSchema).items.values()]; - const serializer = getSerializeFunction(classSchema.type, memorySerializer.deserializeRegistry); - const items = rawItems.map(v => serializer(v)); - - if (model.filter) { - model.filter = convertQueryFilter(classSchema, model.filter, (convertClassType: ReflectionClass, path: string, value: any) => { - //this is important to convert relations to its foreignKey value - return serialize(value, undefined, memorySerializer, undefined, resolvePath(path, classSchema.type)); - }, {}, { - $parameter: (name, value) => { - if (undefined === model.parameters[value]) { - throw new Error(`Parameter ${value} not defined in ${classSchema.getClassName()} query.`); - } - return model.parameters[value]; - } - }); - } + const deserializer = getSerializeFunction(classSchema.type, memorySerializer.deserializeRegistry); + const items = rawItems.map(v => deserializer(v)); - let filtered = model.filter ? findQueryList(items, model.filter) : items; - - if (model.hasJoins()) { - console.log('MemoryDatabaseAdapter does not support joins. Please use another lightweight adapter like SQLite.'); - } + console.log(items); + let filtered = model.where ? filterWhere(items, model.where) : items; - if (model.sort) { - for (const [name, direction] of Object.entries(model.sort)) { - sort(filtered, name, direction === 'asc' ? sortAsc : sortDesc); + if (model.orderBy) { + for (const order of model.orderBy) { + sort(filtered, buildAccessor(order.a), order.direction === 'asc' ? sortAsc : sortDesc); } } - if (model.skip && model.limit) { - filtered = filtered.slice(model.skip, model.skip + model.limit); + if (model.offset && model.limit) { + filtered = filtered.slice(model.offset, model.offset + model.limit); } else if (model.limit) { filtered = filtered.slice(0, model.limit); - } else if (model.skip) { - filtered = filtered.slice(model.skip); + } else if (model.offset) { + filtered = filtered.slice(model.offset); } return filtered; }; @@ -141,72 +134,56 @@ const remove = (adapter: MemoryDatabaseAdapter, classSchema: ReflectionClass< } }; -class Resolver extends GenericQueryResolver { - constructor( - protected classSchema: ReflectionClass, - protected session: DatabaseSession, - ) { - super(classSchema, session); - } - +class Resolver extends SelectorResolver { get adapter() { return this.session.adapter as any as MemoryDatabaseAdapter; } - protected createFormatter(withIdentityMap: boolean = false) { + protected createFormatter(state: SelectorState, withIdentityMap: boolean = false) { return new Formatter( - this.classSchema, + state.schema, memorySerializer, this.session.getHydrator(), - withIdentityMap ? this.session.identityMap : undefined + withIdentityMap ? this.session.identityMap : undefined, ); } - async count(model: DatabaseQueryModel): Promise { - if (this.session.logger.logger) { - this.session.logger.logger.log('count', model.filter); - } - const items = find(this.adapter, this.classSchema, model); + async count(state: SelectorState): Promise { + const items = find(this.adapter, state.schema, state); return items.length; } - async delete(model: DatabaseQueryModel, deleteResult: DeleteResult): Promise { - if (this.session.logger.logger) { - this.session.logger.logger.log('delete', model.filter); - } - const items = find(this.adapter, this.classSchema, model); + async delete(state: SelectorState, deleteResult: DeleteResult): Promise { + const items = find(this.adapter, state.schema, state); for (const item of items) { deleteResult.primaryKeys.push(item); } - remove(this.adapter, this.classSchema, items); + remove(this.adapter, state.schema, items); } - async find(model: DatabaseQueryModel): Promise { - const items = find(this.adapter, this.classSchema, model); - if (this.session.logger.logger) { - this.session.logger.logger.log('find', model.filter); - } - const formatter = this.createFormatter(model.withIdentityMap); - return items.map(v => formatter.hydrate(model, v)); + async find(state: SelectorState): Promise { + const items = find(this.adapter, state.schema, state); + const formatter = this.createFormatter(state); + return items.map(v => formatter.hydrate(state, v)); } - async findOneOrUndefined(model: DatabaseQueryModel): Promise { - const items = find(this.adapter, this.classSchema, model); + async findOneOrUndefined(state: SelectorState): Promise { + const items = find(this.adapter, state.schema, state); - if (items[0]) return this.createFormatter(model.withIdentityMap).hydrate(model, items[0]); + if (items[0]) return this.createFormatter(state).hydrate(state, items[0]); return undefined; } - async has(model: DatabaseQueryModel): Promise { - const items = find(this.adapter, this.classSchema, model); + async has(state: SelectorState): Promise { + const items = find(this.adapter, state.schema, state); return items.length > 0; } - async patch(model: DatabaseQueryModel, changes: Changes, patchResult: PatchResult): Promise { - const items = find(this.adapter, this.classSchema, model); - const store = this.adapter.getStore(this.classSchema); - const primaryKey = this.classSchema.getPrimary().name as keyof T; - const serializer = getSerializeFunction(this.classSchema.type, memorySerializer.serializeRegistry); + async patch(state: SelectorState, changes: Changes, patchResult: PatchResult): Promise { + const items = find(this.adapter, state.schema, state); + const store = this.adapter.getStore(state.schema); + const primaryKey = state.schema.getPrimary().name as keyof T; + const serializer = getSerializeFunction(state.schema.type, memorySerializer.serializeRegistry); patchResult.modified = items.length; for (const item of items) { @@ -229,13 +206,14 @@ class Resolver extends Generi } } - if (model.returning) { - for (const f of model.returning) { - if (!patchResult.returning[f]) patchResult.returning[f] = []; - const v = patchResult.returning[f]; - if (v) v.push(item[f]); - } - } + // todo add returning support + // if (model.returning) { + // for (const f of model.returning) { + // if (!patchResult.returning[f]) patchResult.returning[f] = []; + // const v = patchResult.returning[f]; + // if (v) v.push(item[f]); + // } + // } patchResult.primaryKeys.push(item); store.items.set(item[primaryKey] as any, serializer(item)); @@ -243,17 +221,6 @@ class Resolver extends Generi } } - -export class MemoryQueryFactory extends DatabaseAdapterQueryFactory { - constructor(protected adapter: MemoryDatabaseAdapter, protected databaseSession: DatabaseSession) { - super(); - } - - createQuery(classType?: ReceiveType | AbstractClassType | ReflectionClass): MemoryQuery { - return new MemoryQuery(ReflectionClass.from(classType), this.databaseSession, new Resolver(ReflectionClass.from(classType), this.databaseSession)); - } -} - export class MemoryDatabaseTransaction extends DatabaseTransaction { async begin(): Promise { } @@ -310,11 +277,15 @@ export class MemoryPersistence extends DatabasePersistence { } export class MemoryDatabaseAdapter extends DatabaseAdapter { - protected store = new Map, SimpleStore>(); + protected store = new Map>(); async migrate(options: MigrateOptions, entityRegistry: DatabaseEntityRegistry) { } + createSelectorResolver(session: DatabaseSession): SelectorResolver { + return new Resolver(session); + } + isNativeForeignKeyConstraintSupported(): boolean { return false; } @@ -324,10 +295,11 @@ export class MemoryDatabaseAdapter extends DatabaseAdapter { } getStore(classSchema: ReflectionClass): SimpleStore { - let store = this.store.get(classSchema); + const id = classSchema.type.id || 0; + let store = this.store.get(id); if (!store) { store = { items: new Map, autoIncrement: 0 }; - this.store.set(classSchema, store); + this.store.set(id, store); } return store; } @@ -346,8 +318,4 @@ export class MemoryDatabaseAdapter extends DatabaseAdapter { getSchemaName(): string { return ''; } - - queryFactory(databaseSession: DatabaseSession): MemoryQueryFactory { - return new MemoryQueryFactory(this, databaseSession); - } } diff --git a/packages/orm/src/query.ts b/packages/orm/src/query.ts index 69ca2dc6a..f484202f1 100644 --- a/packages/orm/src/query.ts +++ b/packages/orm/src/query.ts @@ -650,6 +650,9 @@ export class BaseQuery { } } +/** + * @deprecated use SelectorResolver instead + */ export abstract class GenericQueryResolver = DatabaseQueryModel> { constructor( protected classSchema: ReflectionClass, diff --git a/packages/orm/src/select.ts b/packages/orm/src/select.ts index c80c990f2..c6f1410e4 100644 --- a/packages/orm/src/select.ts +++ b/packages/orm/src/select.ts @@ -1,5 +1,5 @@ -/** @reflection never */ import { + assertType, Changes, ChangesInterface, DeepPartial, @@ -7,7 +7,6 @@ import { getTypeJitContainer, PrimaryKeyFields, PrimaryKeyType, - ReceiveType, ReflectionClass, ReflectionKind, resolveReceiveType, @@ -32,31 +31,38 @@ import { DatabaseSession } from './database-session.js'; import { FieldName } from './utils.js'; import { FrameCategory } from '@deepkit/stopwatch'; import { ItemNotFound } from './query.js'; -import { DatabaseAdapter } from './database-adapter.js'; -let graphId = 10; -type Graph = { id: number, nodes: { [id: number]: Graph }, cache?: { [name: string]: any } }; +let treeId = 10; +type ExpressionTree = { id: number, nodes: { [id: number]: ExpressionTree }, cache?: { [name: string]: any } }; -export type SelectorProperty = { +/** @reflection never */ +export type SelectorProperty = { [propertyTag]: 'property'; model: SelectorState; name: string; // as?: string; - graph: Graph, + tree: ExpressionTree, property: TypeProperty | TypePropertySignature; toString(): string; } export type SelectorRefs = { - [P in keyof T]: SelectorProperty; + [P in keyof T]-?: SelectorProperty; } & { $$fields: SelectorProperty[] }; export interface SelectStateExpression { kind: string; } +export function selectorIsPartial(state: SelectorState): boolean { + return state.select.length > 0; +} + export type SelectorState = { params: any[]; + /** + * The main origin schema (first position of query() call). + */ schema: ReflectionClass; fields: SelectorRefs; as?: string; @@ -65,13 +71,14 @@ export type SelectorState = { //join // TODO: this is not cacheable/deterministic // or how should we determine whether select, where, joins, offset, limit, etc is all the same - // -> solution: just build a new graph tree, where.graph[joins[0].graph.id], ... + // -> solution: just build a new expression tree, where.tree[joins[0].tree.id], ... where?: OpExpression; joins?: SelectorState[]; lazyLoaded?: SelectorProperty[]; groupBy?: (SelectorProperty | OpExpression)[]; + orderBy?: { a: OpExpression | SelectorProperty, direction: 'asc' | 'desc' }[]; offset?: number; limit?: number; @@ -80,6 +87,7 @@ export type SelectorState = { previous?: SelectorState; withIdentityMap?: boolean; + withChangeDetection?: boolean; } let state: SelectorState | undefined; @@ -114,14 +122,16 @@ export const limit = (limit?: number): any => { state.limit = limit; }; -export const orderBy = (a: any, direction: 'asc' | 'desc' = 'asc'): any => { -// ensureState(state); -// state.orderBy.push({ kind: 'order', a, direction }); +export const orderBy = (a: OpExpression | SelectorProperty, direction: 'asc' | 'desc' = 'asc'): any => { + ensureState(state); + state.orderBy = state.orderBy || []; + state.orderBy.push({ a, direction }); }; -// -export const groupBy = (a: any): any => { -// ensureState(state); -// state.groupBy.push({ kind: 'group', by: a }); + +export const groupBy = (expression: OpExpression | SelectorProperty): any => { + ensureState(state); + state.groupBy = state.groupBy || []; + state.groupBy.push(expression); }; export const inArray = makeOp('inArray', (expression, args: any[]) => { @@ -170,47 +180,47 @@ export function isOp(value: any): value is OpExpression { } -function getGraph(...args: any[]) { - let graph: Graph | undefined; +function getTree(...args: any[]) { + let tree: ExpressionTree | undefined; for (const arg of args) { if (isProperty(arg) || isOp(arg)) { - if (graph) { - const a = graph.nodes[arg.graph.id]; + if (tree) { + const a = tree.nodes[arg.tree.id]; if (a) { - graph = a; + tree = a; } else { - graph = graph.nodes[arg.graph.id] = arg.graph; + tree = tree.nodes[arg.tree.id] = arg.tree; } } else { - graph = arg.graph; + tree = arg.tree; } } else { - if (graph) { - const a = graph.nodes[0]; + if (tree) { + const a = tree.nodes[0]; if (a) { - graph = a; + tree = a; } else { - graph = graph.nodes[0] = { id: graphId++, nodes: {} }; + tree = tree.nodes[0] = { id: treeId++, nodes: {} }; } } } } - return graph; + return tree; } -export type OpExpression = { [opTag]: Op, graph: Graph, args: any[] }; +export type OpExpression = { [opTag]: Op, tree: ExpressionTree, args: (OpExpression | SelectorProperty | unknown)[] }; export type Op = ((...args: any[]) => OpExpression) & { id: symbol }; function makeOp(name: string, cb: (expression: OpExpression, args: any[]) => any): Op { - const opGraph: Graph = { id: graphId++, nodes: {} }; + const opTree: ExpressionTree = { id: treeId++, nodes: {} }; const id = Symbol('op:' + name); /** * @reflection never */ function operation(...args: any[]) { - const graph = getGraph(...args) || opGraph; - const opExpression = { [opTag]: operation, graph, args }; + const tree = getTree(...args) || opTree; + const opExpression = { [opTag]: operation, tree, args }; cb(opExpression, args); return opExpression; } @@ -236,13 +246,13 @@ export const where = makeOp('where', (expression, args: any[]) => { } }); -export const or = makeOp('or', (graph, args: any[]) => { +export const or = makeOp('or', (exp, args: any[]) => { }); -export const and = makeOp('and', (graph, args: any[]) => { +export const and = makeOp('and', (exp, args: any[]) => { }); -export const joinOp = makeOp('join', (graph, args: any[]) => { +export const joinOp = makeOp('join', (exp, args: any[]) => { ensureState(state); if (state.joins) { state.joins.push(state); @@ -260,11 +270,11 @@ function resolveReferencedSchema(property: TypePropertySignature | TypeProperty) return type; } -export const asOp = makeOp('as', (graph, args: any[]) => { +export const asOp = makeOp('as', (exp, args: any[]) => { }); -export function as(a: T, name: string): T { - if (isOp(a)) { +export function as>(a: T, name: string): T { + if (isOp(a) || isProperty(a)) { return asOp(a, [name]) as T; } else { a.as = name; @@ -278,8 +288,8 @@ export const join = (a: SelectorProperty, cb?: (m: SelectorRefs { state: SelectorState; } -export interface From { - select(cb?: (m: SelectorRefs) => R): SelectorInferredState>; -} - -export const from = (type?: ReceiveType): From => { - return { - select(cb?: (m: SelectorRefs) => R) { - const state = createModel(resolveReceiveType(type)); - if (cb) applySelect(cb, state); - return {state}; - } +export function query(cb: (main: SelectorRefs, ...args: SelectorRefs[]) => R | undefined): SelectorInferredState> { + const fnType = resolveReceiveType(cb); + assertType(fnType, ReflectionKind.function); + const selectorRefs = fnType.parameters.map(v => { + const arg = v.type.originTypes?.[0].typeArguments?.[0]; + return createModel(arg || v.type); + }); + if (selectorRefs.length === 0) { + throw new Error('No main selector found in query callback'); + } + let previous = state; + const nextSelect = selectorRefs[0]; + state = nextSelect; + const args = selectorRefs.map(v => v.fields); + try { + (cb as any)(...args); + } finally { + state = previous; } + return { state: nextSelect }; } +export type Select = SelectorRefs; + export const applySelect = (a: (m: SelectorRefs) => any, nextSelect: SelectorState) => { let previous = state; state = nextSelect; @@ -345,8 +365,8 @@ export function createModel(type: Type): SelectorState { [propertyTag]: 'property', model, property: member, - graph: { - id: graphId++, + tree: { + id: treeId++, nodes: {}, }, name: String(member.name), @@ -358,10 +378,9 @@ export function createModel(type: Type): SelectorState { return jit.query2Model = model; } -export abstract class Query2Resolver { +export abstract class SelectorResolver { constructor( - protected model: SelectorState, - protected session: DatabaseSession, + protected session: DatabaseSession, ) { } @@ -376,12 +395,15 @@ export abstract class Query2Resolver { abstract patch(model: SelectorState, value: Changes, patchResult: PatchResult): Promise; } -export class Query2 { +export class Query2 { + classSchema: ReflectionClass; + constructor( - public classSchema: ReflectionClass, + public state: SelectorState, protected session: DatabaseSession, - protected resolver: Query2Resolver, + protected resolver: SelectorResolver, ) { + this.classSchema = state.schema; } createResolver() { @@ -426,7 +448,6 @@ export class Query2 { */ public async count(fromHas: boolean = false): Promise { let query: Query2 | undefined = undefined; - const frame = this.session .stopwatch?.start((fromHas ? 'Has:' : 'Count:') + this.classSchema.getClassName(), FrameCategory.database); @@ -438,7 +459,7 @@ export class Query2 { const eventFrame = this.session.stopwatch?.start('Events'); query = this.onQueryResolve(await this.callOnFetchEvent(this)); eventFrame?.end(); - return await this.resolver.count(query.model); + return await this.resolver.count(query.state); } catch (error: any) { await this.session.eventDispatcher.dispatch(onDatabaseError, new DatabaseErrorEvent(error, this.session, query?.classSchema, query)); throw error; @@ -453,8 +474,6 @@ export class Query2 { * @throws DatabaseError */ public async find(selector?: SelectorInferredState): Promise { - const state = selector ? selector.state : createModel(this.classSchema.type); - const frame = this.session .stopwatch?.start('Find:' + this.classSchema.getClassName(), FrameCategory.database); @@ -468,7 +487,7 @@ export class Query2 { const eventFrame = this.session.stopwatch?.start('Events'); query = this.onQueryResolve(await this.callOnFetchEvent(this)); eventFrame?.end(); - return await query.resolver.find(state) as R[]; + return await query.resolver.find(query.state) as R[]; } catch (error: any) { await this.session.eventDispatcher.dispatch(onDatabaseError, new DatabaseErrorEvent(error, this.session, query?.classSchema, query)); throw error; @@ -494,7 +513,7 @@ export class Query2 { const eventFrame = this.session.stopwatch?.start('Events'); query = this.onQueryResolve(await this.callOnFetchEvent(this)); eventFrame?.end(); - return await query.resolver.findOneOrUndefined(query.model); + return await query.resolver.findOneOrUndefined(query.state); } catch (error: any) { await this.session.eventDispatcher.dispatch(onDatabaseError, new DatabaseErrorEvent(error, this.session, query?.classSchema, query)); throw error; @@ -550,7 +569,7 @@ export class Query2 { try { if (!hasEvents) { query = this.onQueryResolve(query); - await this.resolver.delete(query.model, deleteResult); + await this.resolver.delete(query.state, deleteResult); this.session.identityMap.deleteManyBySimplePK(this.classSchema, deleteResult.primaryKeys); return deleteResult; } @@ -636,7 +655,7 @@ export class Query2 { const hasEvents = this.session.eventDispatcher.hasListeners(onPatchPre) || this.session.eventDispatcher.hasListeners(onPatchPost); if (!hasEvents) { query = this.onQueryResolve(query); - await this.resolver.patch(query.model, changes, patchResult); + await this.resolver.patch(query.state, changes, patchResult); return patchResult; } @@ -656,7 +675,7 @@ export class Query2 { query = this.onQueryResolve(query); await event.query.resolver.patch(event.query.model, changes, patchResult); - if (query.model.withIdentityMap) { + if (query.state.withIdentityMap) { const pkHashGenerator = getSimplePrimaryKeyHashGenerator(this.classSchema); for (let i = 0; i < patchResult.primaryKeys.length; i++) { const item = this.session.identityMap.getByHash(this.classSchema, pkHashGenerator(patchResult.primaryKeys[i])); @@ -698,7 +717,9 @@ export class Query2 { } protected patchModel(patch: Partial): Query2 { - return new Query2(Object.assign({}, this.model, patch), this.session, this.resolver as any); + //todo + return this as any; + // return new Query2(Object.assign({}, this.state, patch), this.session, this.resolver as any); } /** @@ -740,7 +761,7 @@ export class Query2 { * @throws DatabaseError */ public async findField>(name: K): Promise { - const query = this.patchModel({ select: [this.model.fields[name]] }); + const query = this.patchModel({ select: [this.state.fields[name]] }); const items = await query.find() as T[]; return items.map(v => v[name]); } @@ -756,7 +777,7 @@ export class Query2 { * @throws DatabaseError */ public async findOneField>(name: K): Promise { - const query = this.patchModel({ select: [this.model.fields[name]] }); + const query = this.patchModel({ select: [this.state.fields[name]] }); const item = await query.findOne() as T; return item[name]; } @@ -767,7 +788,7 @@ export class Query2 { * @throws DatabaseError */ public async findOneFieldOrUndefined>(name: K): Promise { - const query = this.patchModel({ select: [this.model.fields[name]] }); + const query = this.patchModel({ select: [this.state.fields[name]] }); const item = await query.findOneOrUndefined() as T; if (item) return item[name]; return; diff --git a/packages/orm/src/utils.ts b/packages/orm/src/utils.ts index 609c150be..447ecae6b 100644 --- a/packages/orm/src/utils.ts +++ b/packages/orm/src/utils.ts @@ -10,8 +10,6 @@ import { Changes, getSerializeFunction, PrimaryKeyFields, ReflectionClass, TemplateRegistry } from '@deepkit/type'; import { OrmEntity } from './type.js'; -import sift from 'sift'; -import { FilterQuery } from './query.js'; import { getInstanceStateFromItem } from './identity-map.js'; import { getClassTypeFromInstance } from '@deepkit/core'; @@ -34,17 +32,6 @@ export function getClassSchemaInstancePairs(items: Iterable return map; } - -export function findQuerySatisfied(target: T, query: FilterQuery): boolean { - //get rid of "Excessive stack depth comparing types 'any' and 'SiftQuery'." - return (sift as any)(query as any, [target] as any[]).length > 0; -} - -export function findQueryList(items: T[], query: FilterQuery): T[] { - //get rid of "Excessive stack depth comparing types 'any' and 'SiftQuery'." - return (sift as any)(query as any, items as any[]); -} - export type Placeholder = () => T; export type Resolve }> = ReturnType; export type Replace = T & { _: Placeholder }; diff --git a/packages/orm/src/virtual-foreign-key-constraint.ts b/packages/orm/src/virtual-foreign-key-constraint.ts index aa963c115..07b644848 100644 --- a/packages/orm/src/virtual-foreign-key-constraint.ts +++ b/packages/orm/src/virtual-foreign-key-constraint.ts @@ -8,7 +8,12 @@ * You should have received a copy of the MIT License along with this program. */ -import type { QueryDatabaseDeleteEvent, QueryDatabasePatchEvent, UnitOfWorkEvent, UnitOfWorkUpdateEvent } from './event.js'; +import type { + QueryDatabaseDeleteEvent, + QueryDatabasePatchEvent, + UnitOfWorkEvent, + UnitOfWorkUpdateEvent, +} from './event.js'; import type { Database } from './database.js'; import { ReflectionClass, ReflectionProperty } from '@deepkit/type'; @@ -50,15 +55,16 @@ export class VirtualForeignKeyConstraint { if (!event.deleteResult.primaryKeys.length) return; for (const { classSchema, property } of references) { - const query = event.databaseSession.query(classSchema).filter({ [property.name]: { $in: event.deleteResult.primaryKeys } }); - const options = property.getReference()!; - if (options.onDelete === undefined || options.onDelete === 'CASCADE') { - await query.deleteMany(); - } else if (options.onDelete === 'SET NULL') { - await query.patchMany({ [property.name]: null }); - } else if (options.onDelete === 'SET DEFAULT') { - await query.patchMany({ [property.name]: property.getDefaultValue() }); - } + //todo + // const query = event.databaseSession.query(classSchema).filter({ [property.name]: { $in: event.deleteResult.primaryKeys } }); + // const options = property.getReference()!; + // if (options.onDelete === undefined || options.onDelete === 'CASCADE') { + // await query.deleteMany(); + // } else if (options.onDelete === 'SET NULL') { + // await query.patchMany({ [property.name]: null }); + // } else if (options.onDelete === 'SET DEFAULT') { + // await query.patchMany({ [property.name]: property.getDefaultValue() }); + // } } } @@ -71,16 +77,17 @@ export class VirtualForeignKeyConstraint { for (const { classSchema, property } of references) { if (!event.patch.has(property.name)) continue; - const query = event.databaseSession.query(classSchema).filter({ [property.name]: { $in: event.patchResult.primaryKeys } }); - const options = property.getReference()!; - - if (options.onDelete === undefined || options.onDelete === 'CASCADE') { - await query.patchMany({ [property.name]: event.patch.$set[primaryKeyName] }); - } else if (options.onDelete === 'SET NULL') { - await query.patchMany({ [property.name]: null }); - } else if (options.onDelete === 'SET DEFAULT') { - await query.patchMany({ [property.name]: property.getDefaultValue() }); - } + //todo + // const query = event.databaseSession.query(classSchema).filter({ [property.name]: { $in: event.patchResult.primaryKeys } }); + // const options = property.getReference()!; + // + // if (options.onDelete === undefined || options.onDelete === 'CASCADE') { + // await query.patchMany({ [property.name]: event.patch.$set[primaryKeyName] }); + // } else if (options.onDelete === 'SET NULL') { + // await query.patchMany({ [property.name]: null }); + // } else if (options.onDelete === 'SET DEFAULT') { + // await query.patchMany({ [property.name]: property.getDefaultValue() }); + // } } } @@ -88,19 +95,20 @@ export class VirtualForeignKeyConstraint { const references = this.resolveReferencesTo(event.classSchema); if (!references.length) return; - for (const { classSchema, property } of references) { - const query = event.databaseSession.query(classSchema).filter({ [property.name]: { $in: event.items } }); - const options = property.getReference()!; - - if (options.onDelete === undefined || options.onDelete === 'CASCADE') { - await query.deleteMany(); - } else if (options.onDelete === 'SET NULL') { - await query.patchMany({ [property.name]: null }); - } else if (options.onDelete === 'SET DEFAULT') { - await query.patchMany({ [property.name]: property.getDefaultValue() }); - } - //RESTRICT needs to be handled in Pre - } + //todo + // for (const { classSchema, property } of references) { + // const query = event.databaseSession.query(classSchema).filter({ [property.name]: { $in: event.items } }); + // const options = property.getReference()!; + // + // if (options.onDelete === undefined || options.onDelete === 'CASCADE') { + // await query.deleteMany(); + // } else if (options.onDelete === 'SET NULL') { + // await query.patchMany({ [property.name]: null }); + // } else if (options.onDelete === 'SET DEFAULT') { + // await query.patchMany({ [property.name]: property.getDefaultValue() }); + // } + // //RESTRICT needs to be handled in Pre + // } } async onUoWUpdate(event: UnitOfWorkUpdateEvent) { @@ -119,20 +127,21 @@ export class VirtualForeignKeyConstraint { } } - for (const { classSchema, property } of references) { - for (const { oldPK, newPK } of primaryKeys) { - const query = await event.databaseSession.query(classSchema).filter({ [property.name]: oldPK }); - const options = property.getReference()!; - - if (options.onDelete === undefined || options.onDelete === 'CASCADE') { - await query.patchMany({ [property.name]: newPK }); - } else if (options.onDelete === 'SET NULL') { - await query.patchMany({ [property.name]: null }); - } else if (options.onDelete === 'SET DEFAULT') { - await query.patchMany({ [property.name]: property.getDefaultValue() }); - } - } - //RESTRICT needs to be handled in Pre - } + //todo + // for (const { classSchema, property } of references) { + // for (const { oldPK, newPK } of primaryKeys) { + // const query = await event.databaseSession.query(classSchema).filter({ [property.name]: oldPK }); + // const options = property.getReference()!; + // + // if (options.onDelete === undefined || options.onDelete === 'CASCADE') { + // await query.patchMany({ [property.name]: newPK }); + // } else if (options.onDelete === 'SET NULL') { + // await query.patchMany({ [property.name]: null }); + // } else if (options.onDelete === 'SET DEFAULT') { + // await query.patchMany({ [property.name]: property.getDefaultValue() }); + // } + // } + // //RESTRICT needs to be handled in Pre + // } } } diff --git a/packages/orm/tests/dql.spec.ts b/packages/orm/tests/dql.spec.ts index 53a798b52..bdeb9b5df 100644 --- a/packages/orm/tests/dql.spec.ts +++ b/packages/orm/tests/dql.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@jest/globals'; -import { as, count, eq, from, groupBy, groupConcat, inArray, join, l2Distance, lower, lt, or, orderBy, SelectorRefs, where } from '../src/select.js'; +import { as, count, eq, groupBy, inArray, join, l2Distance, lower, lt, or, orderBy, query, Select, SelectorRefs, where } from '../src/select.js'; import { Database } from '../src/database.js'; import { AutoIncrement, BackReference, PrimaryKey, Reference, Vector } from '@deepkit/type'; import { MemoryDatabaseAdapter } from '../src/memory-db.js'; @@ -13,39 +13,37 @@ interface Group { interface User { id: number & AutoIncrement & PrimaryKey; name: string; - age: number; - group: Group & Reference; + group?: Group & Reference; + birthday: Date; } test('basics', () => { function filterByAge(m: SelectorRefs, age: number) { } - const users = from().select(user => { - const group = join(user.group, group => { - where(eq(group.name, 'Admin')); - }); - where(eq(user.name, 'Peter')); - groupBy(user.id); - return [user.id, user.name, groupConcat(group.name)]; + // SELECT name, COUNT(id) FROM user + // GROUP BY name + const users = query((user: Select) => { + groupBy(user.name); + return [user.name, count(user.id)]; }); // SELECT * FROM user // JOIN group ON (user.group_id = group.id AND name = 'Admin') // WHERE name = 'Peter' - const users2 = from().select(user => { + const users2 = query((user: Select) => { join(user.group, group => { where(eq(group.name, 'Admin')); }); where(eq(user.name, 'Peter')); }); - const users4 = from().select(user => { + const users4 = query((user: Select) => { return [user.id, lower(user.name)]; }); - const users3 = from().select(m => { - return [m.id, m.name]; + const users3 = query((user: Select) => { + return [user.id, user.name]; }); // from().update(m => { @@ -55,7 +53,7 @@ test('basics', () => { // SELECT * FROM group // JOIN user ON user.group_id = group.id // WHERE name = 'Admin' OR name = 'Moderator' - const groups = from().select(group => { + const groups = query((group: Select) => { where(or(eq(group.name, 'Admin'), eq(group.name, 'Moderator'))); // or where(inArray(group.name, ['Admin', 'Moderator'])); @@ -65,11 +63,63 @@ test('basics', () => { }); }); - // SELECT age, COUNT(id) FROM user GROUP BY age - const ageGroup = from().select(user => { - groupBy(user.age); - return [user.age, count(user.id)]; - }); + // // SELECT age, COUNT(id) FROM user GROUP BY age + // const ageGroup = query((user: Select) => { + // groupBy(user.age); + // return [user.age, count(user.id)]; + // }); +}); + +test('ideal world', async () => { + // SELECT * FROM user + // JOIN group ON group.id = user.group_id + // WHERE group.name = 'Admin' + // GROUP BY user.id + // ORDER BY user.name + // LIMIT 10 + // function userQuery(user: Select) { + // const group = join(user.group); + // where(eq(group.name, 'Admin')); + // groupBy(user.id); + // orderBy(user.name); + // limit(10); + // return [user.id, user.name]; + // } + // + // const db = new Database(new MemoryDatabaseAdapter()); + // db.query2(userQuery); + + // const rows = await db.select(userQuery); + // const rows = await db.select(userQuery).find(); + // const rows = await db.select(userQuery).findOne(); + // const rows = await db.select(userQuery).findOneOrUndefined(); + // const rows = await db.select(userQuery).findOneField(); + // const rows = await db.select(userQuery).findField(); + // const rows = await db.select(userQuery).patch(); + // const rows = await db.select(userQuery).patchMany(); + // const rows = await db.select(userQuery).delete(); + // const rows = await db.select(userQuery).deleteMany(); + // + // const res1 = await db.update((user: Select) => { + // set(user.name, 'asdasd'); + // }); + // + // const res2 = await db.delete(userQuery); +}); + +test('memory db', async () => { + const db = new Database(new MemoryDatabaseAdapter()); + db.register(); + + const user1: User = { id: 1, name: 'Peter', birthday: new Date() }; + const user2: User = { id: 2, name: 'John', birthday: new Date() }; + const user3: User = { id: 3, name: 'Jane', birthday: new Date() }; + await db.persist(user1, user2, user3); + + const user = await db.query2((user: Select) => { + where(eq(user.name, 'John')); + }).findOne(); + expect(user.name).toBe('John'); }); test('vector search', () => { @@ -84,7 +134,7 @@ test('vector search', () => { // FROM sentence // WHERE embedding <=> $1 < 0.5 // ORDER BY embedding <=> $1 - const sentences = from().select(sentence => { + const sentences = query((sentence: Select) => { const score = l2Distance(sentence.embedding, queryEmbedding); where(lt(score, 0.5)); orderBy(score); @@ -113,7 +163,7 @@ function bench(title: string, cb: () => void) { } test('asd', () => { - from().select(m => { + query((m: Select) => { const a = eq(m.name, 'Peter'); const b = eq(m.name, 'Peter'); const c = eq('Peter', m.name); @@ -123,23 +173,68 @@ test('asd', () => { }); test('graph', () => { - const a = from().select(m => { + const a = query((m: Select) => { + where(eq(m.name, 'Peter1')); + }); + + const b = query((m: Select) => { + where(eq(m.name, 'Peter2')); + }); + + console.log(a.state.params, b.state.where); + console.log(b.state.params, a.state.where); + + expect(a.state.where!.tree === b.state.where!.tree).toBe(true); + + function filterByAge(model: Select<{ birthday: Date }>, age: number) { + const target = new Date(); + target.setFullYear(target.getFullYear() - age); + where(lt(model.birthday, target)); + } + + const userIds = query((user: Select) => { + filterByAge(user, 30); + return [user.id]; + }); + + const result = query((user: Select) => { + const group = join(user.group); + return [user.id, as(group.id, 'groupId')]; + }); + + type Result = Pick & { group: Pick }; + + const result2: Result = { + id: 0, + group: { id: 0 }, + }; + + // SELECT user.id, group.id as group_id FROM user + // JOIN group ON group.id = user.group_id + // const result3 = query((user: Select) => { + // const group = join(user.group); + // return [user.id, pick(group, 'id')]; + // }); +}); + +test('tree', () => { + const a = query((m: Select) => { where(eq(m.name, 'Peter1')); }); - const b = from().select(m => { + const b = query((m: Select) => { where(eq(m.name, 'Peter2')); }); console.log(a.state.params, b.state.where); console.log(b.state.params, a.state.where); - expect(a.state.where!.graph === b.state.where!.graph).toBe(true); + expect(a.state.where!.tree === b.state.where!.tree).toBe(true); }); test('performance', () => { bench('select', () => { - from().select(user => { + query((user: Select) => { join(user.group, group => { where(eq(group.name, 'Admin')); }); diff --git a/packages/sql/src/select.ts b/packages/sql/src/select.ts index 7aac8e664..7a76616e6 100644 --- a/packages/sql/src/select.ts +++ b/packages/sql/src/select.ts @@ -1,20 +1,20 @@ -import { DatabaseSession, DeleteResult, PatchResult, Query2Resolver, SelectorState } from '@deepkit/orm'; +import { DatabaseSession, DeleteResult, PatchResult, SelectorResolver, SelectorState } from '@deepkit/orm'; import { castFunction, Changes, ReflectionClass } from '@deepkit/type'; import { SQLConnectionPool, SQLDatabaseAdapter } from './sql-adapter.js'; import { isArray } from '@deepkit/core'; import { SqlFormatter } from './sql-formatter.js'; import { SqlBuilder } from './sql-builder.js'; -export class SQLQuery2Resolver extends Query2Resolver { +export class SQLQuery2Resolver extends SelectorResolver { classSchema: ReflectionClass; constructor( - protected model: SelectorState, + protected state: SelectorState, protected session: DatabaseSession, protected connectionPool: SQLConnectionPool, ) { - super(model, session); - this.classSchema = this.model.schema; + super(state, session); + this.classSchema = this.state.schema; } count(model: SelectorState): Promise { diff --git a/packages/type-compiler/src/compiler.ts b/packages/type-compiler/src/compiler.ts index 96a828bf1..fca2e69fc 100644 --- a/packages/type-compiler/src/compiler.ts +++ b/packages/type-compiler/src/compiler.ts @@ -526,6 +526,8 @@ export class ReflectionTransformer implements CustomTransformer { protected tempResultIdentifier?: Identifier; protected parseConfigHost: ParseConfigHost; + protected rootScopedVariables: string[] = []; + constructor( protected context: TransformationContext, protected cache: Cache = new Cache, @@ -534,7 +536,7 @@ export class ReflectionTransformer implements CustomTransformer { this.nodeConverter = new NodeConverter(this.f); // It is important to not have undefined values like {paths: undefined} because it would override the read tsconfig.json. // Important to create a copy since we will modify it. - this.compilerOptions = {...filterUndefined(context.getCompilerOptions())}; + this.compilerOptions = { ...filterUndefined(context.getCompilerOptions()) }; // compilerHost has no internal cache and is cheap to build, so no cache needed. // Resolver loads SourceFile which has cache implemented. this.host = createCompilerHost(this.compilerOptions); @@ -562,8 +564,8 @@ export class ReflectionTransformer implements CustomTransformer { const mode = reflectionModeMatcher(config, path); return { mode, tsConfigPath: '' }; }; - const configResolver: ResolvedConfig = {...config, path: '', mergeStrategy: 'replace', compilerOptions: this.compilerOptions}; - this.overriddenConfigResolver = {config: configResolver, match}; + const configResolver: ResolvedConfig = { ...config, path: '', mergeStrategy: 'replace', compilerOptions: this.compilerOptions }; + this.overriddenConfigResolver = { config: configResolver, match }; return this; } @@ -629,7 +631,7 @@ export class ReflectionTransformer implements CustomTransformer { Object.assign(this.compilerOptions, configResolver.config.compilerOptions); if (reflection.mode === 'never') { - debug(`Transform file with reflection=${reflection.mode} took ${Date.now()-start}ms (${this.getModuleType()}) ${sourceFile.fileName} via config ${reflection.tsConfigPath || 'none'}.`); + debug(`Transform file with reflection=${reflection.mode} took ${Date.now() - start}ms (${this.getModuleType()}) ${sourceFile.fileName} via config ${reflection.tsConfigPath || 'none'}.`); return sourceFile; } @@ -1023,6 +1025,23 @@ export class ReflectionTransformer implements CustomTransformer { ); } + for (const varName of this.rootScopedVariables) { + newTopStatements.push( + this.f.createVariableStatement( + undefined, + this.f.createVariableDeclarationList( + [this.f.createVariableDeclaration( + this.f.createIdentifier(varName), + undefined, + undefined, + undefined, + )], + ts.NodeFlags.None, + ), + ), + ); + } + if (newTopStatements.length) { // we want to keep "use strict", or "use client", etc at the very top const indexOfFirstLiteralExpression = this.sourceFile.statements.findIndex(v => isExpressionStatement(v) && isStringLiteral(v.expression)); @@ -2550,10 +2569,11 @@ export class ReflectionTransformer implements CustomTransformer { const importOrExport = declaration.parent.parent.parent; const found = this.resolveImportSpecifier( element.propertyName ? element.propertyName.escapedText : declarationName, - importOrExport, sourceFile + importOrExport, sourceFile, ); if (found) return found; - } else if (declaration) {} + } else if (declaration) { + } return declaration; } } @@ -2693,19 +2713,35 @@ export class ReflectionTransformer implements CustomTransformer { /** * Object.assign(fn, {__type: []}) is much slower than a custom implementation like * - * assignType(fn, []) + * ``` + * var __type42; + * assignType(fn, __type42 || (__type42 = [34])); + * ``` * * where we embed assignType() at the beginning of the type. + * + * This also adds a unique root scoped variable so that second argument + * is cached */ protected wrapWithAssignType(fn: Expression, type: Expression) { this.embedAssignType = true; + const __typeName = '__type' + this.rootScopedVariables.length; + this.rootScopedVariables.push(__typeName); + + const secondArgExpression = this.f.createBinaryExpression( + this.f.createIdentifier(__typeName), + SyntaxKind.BarBarToken, + this.f.createParenthesizedExpression(this.f.createAssignment( + this.f.createIdentifier(__typeName), type, + )), + ); return this.f.createCallExpression( this.f.createIdentifier('__assignType'), undefined, [ fn, - type, + secondArgExpression, ], ); } diff --git a/packages/type-compiler/tests/transpile.spec.ts b/packages/type-compiler/tests/transpile.spec.ts index 6cc8c1ba1..32c0cbb16 100644 --- a/packages/type-compiler/tests/transpile.spec.ts +++ b/packages/type-compiler/tests/transpile.spec.ts @@ -550,6 +550,7 @@ export const typeValidation = (type?: ReceiveType): ValidatorFn => (contro }); console.log(res.app); expect(res.app).toContain(`exports.typeValidation.Ω = undefined; return __assignType((control) =>`); + expect(res.app).toContain(`__type0 || (__type0 = [`); }); test('ReceiveType forward to type passing', () => { diff --git a/packages/type/src/reflection/reflection.ts b/packages/type/src/reflection/reflection.ts index 50096eb3e..657bc28ef 100644 --- a/packages/type/src/reflection/reflection.ts +++ b/packages/type/src/reflection/reflection.ts @@ -84,10 +84,11 @@ import { SerializedTypes, serializeType } from '../type-serialization.js'; */ export type ReceiveType = Packed | Type | ClassType; -export function resolveReceiveType(type?: Packed | Type | ClassType | AbstractClassType | ReflectionClass): Type { +export function resolveReceiveType(type?: Packed | Type | ClassType | AbstractClassType | ReflectionClass | Function): Type { if (!type) throw new NoTypeReceived(); if (isType(type)) return type as Type; let typeFn: Function | undefined = undefined; + if (isFunction(type) && (type as any).__type) type = (type as any).__type as []; if (isArray(type)) { if (type.__type) return type.__type; @@ -115,6 +116,9 @@ export function resolveReceiveType(type?: Packed | Type | ClassType | AbstractCl } return resolveRuntimeType(type) as Type; } + if (isFunction(type)) { + return (type as any).__cached_type = { kind: ReflectionKind.function, function: type as any, return: { kind: ReflectionKind.any }, parameters: [] } as any; + } return resolvePacked(type, undefined, { reuseCached: true }); } diff --git a/packages/type/tests/type.spec.ts b/packages/type/tests/type.spec.ts index b391e3d98..fd99b6507 100644 --- a/packages/type/tests/type.spec.ts +++ b/packages/type/tests/type.spec.ts @@ -6,6 +6,7 @@ import { Excluded, excludedAnnotation, findMember, + getTypeJitContainer, Group, groupAnnotation, indexAccess, @@ -1586,6 +1587,50 @@ test('function extends non-empty object literal', () => { expect(stringifyResolvedType(typeOf())).toBe('false'); }); +test('function caches type', () => { + function a(b: string): number { + return 0; + } + + const type2 = resolveReceiveType(a); + const type3 = resolveReceiveType(a); + expect(type2 === type3).toBe(true); +}); + +test('function factory caches type', () => { + function factory() { + return function a(b: string): number { + return 0; + } + } + + const a = factory(); + const b = factory(); + const type2 = resolveReceiveType(a); + const type3 = resolveReceiveType(b); + + expect(type2 === type3).toBe(true); +}); + +test('function callback caches type', () => { + // this is import for database.query() to have actual cache + function query(cb: Function): Type { + return resolveReceiveType(cb); + } + + function factory() { + return query((a: string): number => { + return 0; + }); + } + + const type2 = factory(); + const type3 = factory(); + + expect(type2 === type3).toBe(true); + expect(getTypeJitContainer(type2) === getTypeJitContainer(type3)).toBe(true); +}); + test('issue-429: invalid function detection', () => { interface IDTOInner { subfieldA: string;