import { Context } from '@aphro/context-runtime-ts'; import { invariant } from '@strut/utils'; import { count, EmptySourceExpression, Expression, filter, filterAsync, HopExpression, map, mapAsync, orderBy, orderByLambda, SourceExpression, union, } from './Expression.js'; import HopPlan from './HopPlan.js'; import LiveResult from './live/LiveResult.js'; import Plan, { IPlan } from './Plan.js'; import P from './Predicate.js'; import tracer from './trace.js'; export enum UpdateType { CREATE = 1, UPDATE = 2, CREATE_OR_UPDATE = UpdateType.CREATE | UpdateType.UPDATE, DELETE = 4, CREATE_OR_DELETE = UpdateType.CREATE | UpdateType.DELETE, DELETE_OR_UPDATE = UpdateType.DELETE | UpdateType.UPDATE, ANY = UpdateType.CREATE | UpdateType.UPDATE | UpdateType.DELETE, } /** * See https://tantaman.com/2022-05-26-query-builder for more details * on query building. */ export interface Query { gen(): Promise; genOnlyValue(): Promise; genxOnlyValue(): Promise; /** * Returns a live result that is updated whenever the data that matches the backing query changes. * @param on */ live(on: UpdateType): LiveResult; /** * Same as `live` except that it allows you to await a prefetch. * * Use this if you want to prefetch initial data and then subscribe to updates. * * @param on */ genLive(on: UpdateType): Promise>; // flatMap(fn: (t: T) => R[]): Query; // after(cursor: Cursor): Query count(): IterableDerivedQuery; plan(): IPlan; implicatedDatasets(): Set; } abstract class BaseQuery implements Query { protected readonly ctx: Context; constructor(ctx: Context) { this.ctx = ctx; } gen(): Promise { return tracer.genStartActiveSpan(this.constructor.name + '.gen', span => { return this.plan().optimize().gen(); }); } async genOnlyValue(): Promise { const values = await this.gen(); invariant(values.length <= 1, 'genOnlyValue returned more values than expected.'); return values[0] || null; } async genxOnlyValue(): Promise { const ret = await this.genOnlyValue(); if (ret == null) { throw new Error('genxOnlyValue did not return a value'); } return ret; } live(on: UpdateType): LiveResult { return new LiveResult(this.ctx, on, this); } async genLive(on: UpdateType = UpdateType.ANY): Promise> { const ret = new LiveResult(this.ctx, on, this); await ret.__currentHandle; return ret; } count(): IterableDerivedQuery { return new IterableDerivedQuery(this.ctx, this, count()); } // map(fn: (t: T) => R): Query { // return // } abstract plan(): IPlan; abstract implicatedDatasets(): Set; } export abstract class SourceQuery extends BaseQuery { // source query // expression // make a recursive data structure of queries and expressions. // then convert to plan which will collapse expression as needed. // How do expressions convert themselves to SQL or whatever? constructor(ctx: Context, public readonly expression: SourceExpression) { super(ctx); } // Expression could be null if we're hopping an edge? // That'd just be a change in query type rather than an expression? // abstract new>( // priorQuery: Query, // expression: Expression // ): Tq; plan() { return new Plan(this.expression, []); } implicatedDatasets(): Set { return new Set([this.expression.implicatedDataset()]); } } export abstract class HopQuery extends BaseQuery { constructor( ctx: Context, private priorQuery: Query, public readonly expression: HopExpression, ) { super(ctx); } plan() { return new HopPlan(this.priorQuery.plan(), this.expression, []); } implicatedDatasets(): Set { const s = this.priorQuery.implicatedDatasets(); s.add(this.expression.implicatedDataset()); return s; } } export abstract class DerivedQuery extends BaseQuery { #priorQuery: Query; #expression?: Expression; constructor(ctx: Context, priorQuery: Query, expression?: Expression) { super(ctx); this.#priorQuery = priorQuery; this.#expression = expression; } /** * This needs to match the constructor (sans priorQuery which is `this`) and serves the same purpose. * 1. TypeScript does not allow `ConsistentConstruct` * 2. TypeScript does not allow abstract static methods * 3. TypeScript does not allow `new self()` to instantiate * a child class from a parent class. I.e., polymorphism on constructor. * but an instance method that matches the constructor and returns `this` does * the trick. * * `derive` is used for lambda filters */ protected abstract derive(expression: Expression): any; /** * Be careful when using lambdas to filter/map/order. * Lambdas can't be run in the database and requires pulling all matched * records by the query into the application to perform filtering. * @param fn * @returns */ where(fn: (t: TOut) => boolean): this { return this.derive(filter(null, P.lambda(fn))); } whereAsync(fn: (t: TOut) => Promise): this { return this.derive(filterAsync(null, P.asyncLambda(fn))); } map(fn: (t: TOut) => TMapped): DerivedQuery { return this.derive(map(fn)); } mapAsync(fn: (t: TOut) => Promise): DerivedQuery { return this.derive(mapAsync(fn)); } orderBy(fn: (l: TOut, r: TOut) => number): this { return this.derive(orderByLambda(fn)); } union(other: this): this { return this.derive(union(other)); } // intersect, concat, etc. plan() { const plan = this.#priorQuery.plan(); if (this.#expression) { plan.addDerivation(this.#expression); } return plan; } implicatedDatasets(): Set { return this.#priorQuery.implicatedDatasets(); } } export class IterableDerivedQuery extends DerivedQuery { constructor(ctx: Context, priorQuery: Query, expression: Expression) { super(ctx, priorQuery, expression); } protected derive(expression: Expression): IterableDerivedQuery { return new IterableDerivedQuery(this.ctx, this, expression); } } // and regular old IterableQuery to allow making anything // into a query export class EmptyQuery extends SourceQuery { constructor(ctx: Context) { super(ctx, new EmptySourceExpression()); } }