Skip to content

Commit

Permalink
feat(orm): new selector API, still work in progress 3
Browse files Browse the repository at this point in the history
Don't use TypeFunction.function per default to make GC work correctly
  • Loading branch information
marcj committed Jun 21, 2024
1 parent cb612ad commit cfba6ad
Show file tree
Hide file tree
Showing 23 changed files with 512 additions and 1,872 deletions.
2 changes: 0 additions & 2 deletions packages/orm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export * from './src/database-session.js';
export * from './src/database-registry.js';
export * from './src/identity-map.js';
export * from './src/formatter.js';
export * from './src/query.js';
export * from './src/query-filter.js';
export * from './src/utils.js';
export * from './src/memory-db.js';
export * from './src/type.js';
Expand Down
7 changes: 0 additions & 7 deletions packages/orm/src/database-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import { OrmEntity } from './type.js';
import {
AbstractClassType,
arrayRemoveItem,
ClassType,
getClassName,
Expand All @@ -23,20 +22,14 @@ import {
isSameType,
ItemChanges,
PrimaryKeyFields,
ReceiveType,
ReflectionClass,
ReflectionKind,
stringifyType,
Type,
} from '@deepkit/type';
import { Query } from './query.js';
import { DatabaseSession, DatabaseTransaction } from './database-session.js';
import { SelectorResolver } from './select.js';

export abstract class DatabaseAdapterQueryFactory {
abstract createQuery<T extends OrmEntity>(type?: ReceiveType<T> | ClassType<T> | AbstractClassType<T> | ReflectionClass<T>): Query<T>;
}

export interface DatabasePersistenceChangeSet<T extends object> {
changes: ItemChanges<T>;
item: T;
Expand Down
10 changes: 7 additions & 3 deletions packages/orm/src/database-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +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';
import { query, Query2, SelectorInferredState, SelectorRefs, SelectorState, singleQuery } from './select.js';

let SESSION_IDS = 0;

Expand Down Expand Up @@ -355,9 +355,13 @@ export class DatabaseSession<ADAPTER extends DatabaseAdapter = DatabaseAdapter>
// };
}

query2<const R extends any, T extends object, Q extends SelectorInferredState<T, R> | ((main: SelectorRefs<T>, ...args: SelectorRefs<unknown>[]) => R | undefined)>(cbOrQ?: Q): Query2<T, R> {
singleQuery<const R extends any, T extends object>(classType: ClassType<T>, cb?: (main: SelectorRefs<T>) => R | undefined): Query2<T, R> {
return this.query2(singleQuery(classType, cb));
}

query2<const R extends any, T extends object, Q extends SelectorInferredState<T, R> | SelectorState<T> | ((main: SelectorRefs<T>, ...args: SelectorRefs<unknown>[]) => R | undefined)>(cbOrQ?: Q): Query2<T, R> {
if (!cbOrQ) throw new Error('Query2 needs a callback or query object');
const state: SelectorInferredState<any, any> = isFunction(cbOrQ) ? query(cbOrQ) : cbOrQ;
const state: SelectorInferredState<any, any> = isFunction(cbOrQ) ? query(cbOrQ) : 'state' in cbOrQ ? cbOrQ : { state: cbOrQ };
return new Query2(state.state, this, this.adapter.createSelectorResolver(this));
}

Expand Down
30 changes: 19 additions & 11 deletions packages/orm/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ import {
import { DatabaseAdapter, DatabaseEntityRegistry, MigrateOptions } from './database-adapter.js';
import { DatabaseSession } from './database-session.js';
import { DatabaseLogger } from './logger.js';
import { Query } from './query.js';
import { getReference } from './reference.js';
import { OrmEntity } from './type.js';
import { VirtualForeignKeyConstraint } from './virtual-foreign-key-constraint.js';
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, SelectorInferredState, SelectorRefs } from './select.js';
import { Query2, SelectorInferredState, SelectorRefs, singleQuery } from './select.js';
import { onDeletePost, onPatchPost } from './event.js';

/**
* Hydrates not completely populated item and makes it completely accessible.
Expand Down Expand Up @@ -83,13 +83,14 @@ function setupVirtualForeignKey(database: Database, virtualForeignKeyConstraint:
database.listen(DatabaseSession.onUpdatePost, async (event) => {
await virtualForeignKeyConstraint.onUoWUpdate(event);
});
database.listen(Query.onPatchPost, async (event) => {
database.listen(onPatchPost, async (event) => {
await virtualForeignKeyConstraint.onQueryPatch(event);
});
database.listen(Query.onDeletePost, async (event) => {
database.listen(onDeletePost, async (event) => {
await virtualForeignKeyConstraint.onQueryDelete(event);
});
}

/**
* Database abstraction. Use createSession() to create a work session with transaction support.
*
Expand Down Expand Up @@ -129,9 +130,9 @@ export class Database<ADAPTER extends DatabaseAdapter = DatabaseAdapter> {
* await session.commit(); //only necessary when you changed items received by this session
* ```
*/
// public readonly query: ReturnType<this['adapter']['queryFactory']>['createQuery'];
//
// public readonly raw: ReturnType<this['adapter']['rawFactory']>['create'];
// public readonly query: ReturnType<this['adapter']['queryFactory']>['createQuery'];
//
// public readonly raw: ReturnType<this['adapter']['rawFactory']>['create'];

protected virtualForeignKeyConstraint: VirtualForeignKeyConstraint = new VirtualForeignKeyConstraint(this);

Expand All @@ -143,7 +144,7 @@ export class Database<ADAPTER extends DatabaseAdapter = DatabaseAdapter> {

constructor(
public readonly adapter: ADAPTER,
schemas: (Type | ClassType | ReflectionClass<any>)[] = []
schemas: (Type | ClassType | ReflectionClass<any>)[] = [],
) {
this.entityRegistry.add(...schemas);
if (Database.registry) Database.registry.push(this);
Expand Down Expand Up @@ -178,6 +179,12 @@ export class Database<ADAPTER extends DatabaseAdapter = DatabaseAdapter> {
throw new Error('Deprecated');
}

singleQuery<const R extends any, T extends object>(classType: ClassType<T>, cb?: (main: SelectorRefs<T>) => R | undefined): Query2<T, R> {
const session = this.createSession();
session.withIdentityMap = false;
return session.query2(singleQuery(classType, cb));
}

query2<const R extends any, T extends object, Q extends SelectorInferredState<T, R> | ((main: SelectorRefs<T>, ...args: SelectorRefs<unknown>[]) => R | undefined)>(cbOrQ?: Q): Query2<T, R> {
const session = this.createSession();
session.withIdentityMap = false;
Expand Down Expand Up @@ -419,9 +426,10 @@ export class ActiveRecord {
await db.remove(this);
}

public static query<T extends typeof ActiveRecord>(this: T): Query<InstanceType<T>> {
return this.getDatabase().query(this);
}
// todo implement query2
// public static query<T extends typeof ActiveRecord>(this: T): Query<InstanceType<T>> {
// return this.getDatabase().query(this);
// }

public static reference<T extends typeof ActiveRecord>(this: T, primaryKey: any | PrimaryKeyFields<InstanceType<T>>): InstanceType<T> {
return this.getDatabase().getReference(this, primaryKey) as InstanceType<T>;
Expand Down
17 changes: 8 additions & 9 deletions packages/orm/src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import type { DatabasePersistenceChangeSet } from './database-adapter.js';
import type { DatabaseSession } from './database-session.js';
import type { DeleteResult, PatchResult } from './type.js';
import { OrmEntity } from './type.js';
import { SelectorState } from './select.js';

export class ItemNotFound extends Error {
}

export class DatabaseEvent extends BaseEvent {
stopped = false;
Expand Down Expand Up @@ -78,8 +82,7 @@ export class QueryDatabaseEvent<T extends OrmEntity> extends DatabaseEvent {
constructor(
public readonly databaseSession: DatabaseSession<any>,
public readonly classSchema: ReflectionClass<T>,
// public query: Query2<T>,
public query: any, //TODO change back
public readonly query: SelectorState,
) {
super();
}
Expand All @@ -94,8 +97,7 @@ export class DatabaseErrorEvent extends DatabaseEvent {
public readonly error: Error,
public readonly databaseSession: DatabaseSession<any>,
public readonly classSchema?: ReflectionClass<any>,
// public readonly query?: Query2<any>,
public readonly query?: any, //TODO change back
public readonly query?: SelectorState,
) {
super();
}
Expand Down Expand Up @@ -123,13 +125,11 @@ export class DatabaseErrorUpdateEvent extends DatabaseErrorEvent {
*/
export const onDatabaseError = new EventToken<DatabaseErrorEvent>('database.error');


export class QueryDatabaseDeleteEvent<T extends OrmEntity> extends DatabaseEvent {
constructor(
public readonly databaseSession: DatabaseSession<any>,
public readonly classSchema: ReflectionClass<T>,
// public query: Query2<T>,
public query: any, //TODO change back
public readonly query: SelectorState,
public readonly deleteResult: DeleteResult<T>,
) {
super();
Expand All @@ -144,8 +144,7 @@ export class QueryDatabasePatchEvent<T extends object> extends DatabaseEvent {
constructor(
public readonly databaseSession: DatabaseSession<any>,
public readonly classSchema: ReflectionClass<T>,
// public query: Query2<T>,
public query: any, //TODO change back
public readonly query: SelectorState,
public readonly patch: Changes<T>,
public readonly patchResult: PatchResult<T>,
) {
Expand Down
85 changes: 50 additions & 35 deletions packages/orm/src/memory-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,76 +53,90 @@ function sortDesc(a: any, b: any) {
return 0;
}

type Accessor = (record: any) => any;
type Accessor = (record: any, params: 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);
return (record: any, params: any[]) => a(record, params) === b(record, params);
},
[and.id](expression: OpExpression) {
const lines = expression.args.map(e => buildAccessor(e));
return (record: any) => lines.every(v => v(record));
return (record: any, params: any[]) => lines.every(v => v(record, params));
},
[where.id](expression: OpExpression) {
const lines = expression.args.map(e => buildAccessor(e));
return (record: any) => lines.every(v => v(record));
return (record: any, params: any[]) => lines.every(v => v(record, params));
},
};

function buildAccessor(op: OpExpression | SelectorProperty | unknown): Accessor {
function buildAccessor(op: OpExpression | SelectorProperty | number): 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) => {
return (record: any, params: any[]) => {
//todo: handle if selector of joined table
// and deep json path
return record[op.name];
};
}

return () => op;
return (record: any, params: any[]) => {
return params[op];
}
}

function sort(items: any[], accessor: Accessor, sortFn: typeof sortAsc | typeof sortAsc): void {
items.sort((a, b) => {
return sortFn(accessor(a), accessor(b));
});
}
export type MemoryFinder<T> = (records: T[], params: any[]) => T[];

export function buildFinder<T>(model: SelectorState<T>, cache: { [id: string]: MemoryFinder<any> }): MemoryFinder<T> {
const cacheId = model.schema.type.id + '_' + model.where?.tree.id + '_' + model.orderBy?.map(v => v.a.tree.id).join(':');
let finder = cache[cacheId];
if (finder) return finder;

const whereCheck = model.where ? buildAccessor(model.where) : () => true;
const offset = model.offset || 0;
const limit = model.limit;
const limitCheck: (m: number) => boolean = 'undefined' === typeof limit ? () => false : ((m) => m >= limit);

const orderBy = model.orderBy ? model.orderBy.map(v => {
const accessor = buildAccessor(v.a);
const direction = v.direction === 'asc' ? sortAsc : sortDesc;
return (records: T[], params: any[]) => records.sort((a, b) => direction(accessor(a, params), accessor(b, params)));
}) : [];

finder = (records: T[], params: any[]) => {
const filtered: T[] = [];
let matched = 0;
for (const record of records) {
if (limitCheck(matched)) break;
if (whereCheck(record, params)) {
matched++;
if (matched <= offset) continue;
filtered.push(record);
}
}

for (const order of orderBy) {
order(filtered, params);
}

return filtered;
};

function filterWhere<T>(items: T[], where: OpExpression): T[] {
const accessor = buildAccessor(where);
console.log('accessor', accessor.toString());
return items.filter(v => !!accessor(v));
return cache[cacheId] = finder;
}

const find = <T extends OrmEntity>(adapter: MemoryDatabaseAdapter, classSchema: ReflectionClass<any>, model: SelectorState<T>): T[] => {
const rawItems = [...adapter.getStore(classSchema).items.values()];
const deserializer = getSerializeFunction(classSchema.type, memorySerializer.deserializeRegistry);
const items = rawItems.map(v => deserializer(v));

console.log(items);
let filtered = model.where ? filterWhere(items, model.where) : items;

if (model.orderBy) {
for (const order of model.orderBy) {
sort(filtered, buildAccessor(order.a), order.direction === 'asc' ? sortAsc : sortDesc);
}
}

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.offset) {
filtered = filtered.slice(model.offset);
}
return filtered;
const finder = buildFinder(model, adapter.finderCache);
return finder(items, model.params);
};

const remove = <T>(adapter: MemoryDatabaseAdapter, classSchema: ReflectionClass<T>, toDelete: T[]) => {
Expand All @@ -136,7 +150,7 @@ const remove = <T>(adapter: MemoryDatabaseAdapter, classSchema: ReflectionClass<

class Resolver<T extends object> extends SelectorResolver<T> {
get adapter() {
return this.session.adapter as any as MemoryDatabaseAdapter;
return this.session.adapter as MemoryDatabaseAdapter;
}

protected createFormatter(state: SelectorState<T>, withIdentityMap: boolean = false) {
Expand Down Expand Up @@ -278,6 +292,7 @@ export class MemoryPersistence extends DatabasePersistence {

export class MemoryDatabaseAdapter extends DatabaseAdapter {
protected store = new Map<number, SimpleStore<any>>();
finderCache: { [id: string]: MemoryFinder<any> } = {};

async migrate(options: MigrateOptions, entityRegistry: DatabaseEntityRegistry) {
}
Expand Down
Loading

0 comments on commit cfba6ad

Please sign in to comment.