Skip to content

Commit

Permalink
feat(minato): support subquery, fix koishijs/koishi#595 (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hieuzest authored Feb 2, 2024
1 parent 70109f6 commit dcbdf81
Show file tree
Hide file tree
Showing 12 changed files with 512 additions and 186 deletions.
8 changes: 5 additions & 3 deletions packages/core/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ export class Database<S = any> {
this.extend(name, fields, { callback })
}

select<T extends Keys<S>>(table: T, query?: Query<S[T]>): Selection<S[T]> {
select<T>(table: Selection<T>, query?: Query<T>): Selection<T>
select<T extends Keys<S>>(table: T, query?: Query<S[T]>): Selection<S[T]>
select(table: any, query?: any) {
return new Selection(this.getDriver(table), table, query)
}

Expand All @@ -128,14 +130,14 @@ export class Database<S = any> {
sel.args[0].having = Eval.and(query(...tables.map(name => sel.row[name])))
}
sel.args[0].optional = Object.fromEntries(tables.map((name, index) => [name, optional?.[index]]))
return new Selection(this.getDriver(sel), sel)
return this.select(sel)
} else {
const sel = new Selection(this.getDriver(Object.values(tables)[0]), valueMap(tables, (t: TableLike<S>) => typeof t === 'string' ? this.select(t) : t))
if (typeof query === 'function') {
sel.args[0].having = Eval.and(query(sel.row))
}
sel.args[0].optional = optional
return new Selection(this.getDriver(sel), sel)
return this.select(sel)
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ Eval.object = (fields) => {
Eval.array = unary('array', (expr, table) => Array.isArray(table)
? table.map(data => executeAggr(expr, data))
: Array.from(executeEval(table, expr)))

Eval.exec = unary('exec', (expr, data) => (expr.driver as any).executeSelection(expr, data))

export { Eval as $ }

type MapUneval<S> = {
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,18 @@ function executeFieldQuery(query: Query.FieldQuery, data: any) {
return true
}

export function executeQuery(data: any, query: Query.Expr, ref: string): boolean {
export function executeQuery(data: any, query: Query.Expr, ref: string, env: any = {}): boolean {
const entries: [string, any][] = Object.entries(query)
return entries.every(([key, value]) => {
// execute logical query
if (key === '$and') {
return (value as Query.Expr[]).reduce((prev, query) => prev && executeQuery(data, query, ref), true)
return (value as Query.Expr[]).reduce((prev, query) => prev && executeQuery(data, query, ref, env), true)
} else if (key === '$or') {
return (value as Query.Expr[]).reduce((prev, query) => prev || executeQuery(data, query, ref), false)
return (value as Query.Expr[]).reduce((prev, query) => prev || executeQuery(data, query, ref, env), false)
} else if (key === '$not') {
return !executeQuery(data, value, ref)
return !executeQuery(data, value, ref, env)
} else if (key === '$expr') {
return executeEval({ [ref]: data, _: data }, value)
return executeEval({ ...env, [ref]: data, _: data }, value)
}

// execute field query
Expand Down
30 changes: 17 additions & 13 deletions packages/core/src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { Model } from './model'
import { Query } from './query'
import { Keys, randomId, Row } from './utils'

declare module '.' {
export namespace Eval {
export interface Static {
exec<S, T>(value: Executable<S, T>): Expr<T>
}
}
}

export type Direction = 'asc' | 'desc'

export interface Modifier {
Expand Down Expand Up @@ -47,15 +55,9 @@ class Executable<S = any, T = any> {

constructor(driver: Driver, payload: Executable.Payload) {
Object.assign(this, payload)
defineProperty(this, 'model', driver.model(this.table))
const expr = { $model: this.model }
if (typeof payload.table !== 'string' && !(payload.table instanceof Selection)) {
for (const key in payload.table) {
expr[key] = createRow(key, {}, '', this.model)
}
}
defineProperty(this, 'driver', driver)
defineProperty(this, 'row', createRow(this.ref, expr, '', this.model))
defineProperty(this, 'model', driver.model(this.table))
defineProperty(this, 'row', createRow(this.ref, {}, '', this.model))
}

protected resolveQuery(query?: Query<S>): Query.Expr<S>
Expand Down Expand Up @@ -210,19 +212,21 @@ export class Selection<S = any> extends Executable<S, S[]> {
return new Executable(this.driver, { ...this, type, args })
}

/** @deprecated use `selection.execute()` instead */
evaluate<T>(callback: Selection.Callback<S, T, true>): Eval.Expr<T, true>
evaluate<T>(callback: Selection.Callback<S, T>): Eval.Expr<T[], boolean>
evaluate<T>(callback: Selection.Callback<S, T>): any {
evaluate<K extends Keys<S>>(field: K): Eval.Expr<S[K][], boolean>
evaluate(): Eval.Expr<S[], boolean>
evaluate(callback?: any): any {
const selection = new Selection(this.driver, this)
return selection._action('eval', this.resolveField(callback))
if (!callback) callback = (row) => Eval.array(Eval.object(row))
return Eval('exec', selection._action('eval', this.resolveField(callback)))
}

execute<K extends Keys<S> = Keys<S>>(cursor?: Driver.Cursor<K>): Promise<Pick<S, K>[]>
execute<T>(callback: Selection.Callback<S, T, true>): Promise<T>
execute(cursor?: any) {
if (typeof cursor === 'function') {
return (this.evaluate(cursor) as any).execute()
const selection = new Selection(this.driver, this)
return selection._action('eval', this.resolveField(cursor)).execute()
}
if (Array.isArray(cursor)) {
cursor = { fields: cursor }
Expand Down
36 changes: 27 additions & 9 deletions packages/memory/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,34 @@ export class MemoryDriver extends Driver {
// await this.#loader?.stop(this.#store)
}

table(sel: string | Selection.Immutable | Dict<string | Selection.Immutable>, expr?: any): any[] {
table(sel: string | Selection.Immutable | Dict<string | Selection.Immutable>, env: any = {}): any[] {
if (typeof sel === 'string') {
return this.#store[sel] ||= []
}

if (!(sel instanceof Selection)) {
const entries = Object.entries(sel).map(([name, sel]) => [name, this.table(sel)] as const)
throw new Error('Should not reach here')
}

const { ref, query, table, args, model } = sel
const { fields, group, having } = sel.args[0]

let data: any[]

if (typeof table === 'object' && !(table instanceof Selection)) {
const entries = Object.entries(table).map(([name, sel]) => [name, this.table(sel, env)] as const)
const catesian = (entries: (readonly [string, any[]])[]): any[] => {
if (!entries.length) return []
const [[name, rows], ...tail] = entries
if (!tail.length) return rows.map(row => ({ [name]: row }))
return rows.flatMap(row => catesian(tail).map(tail => ({ ...tail, [name]: row })))
}
return catesian(entries).filter(data => executeEval(data, expr))
data = catesian(entries).map(x => ({ ...env, [ref]: x })).filter(data => executeEval(data, having)).map(x => x[ref])
} else {
data = this.table(table, env).filter(row => executeQuery(row, query, ref, env))
}

const { ref, query, table, args, model } = sel
const { fields, group, having } = sel.args[0]
const data = this.table(table, having).filter(row => executeQuery(row, query, ref))
env[ref] = data

const branches: { index: Dict; table: any[] }[] = []
const groupFields = group ? pick(fields!, group) : fields
Expand All @@ -58,7 +67,7 @@ export class MemoryDriver extends Driver {
}
let index = row
if (fields) {
index = valueMap(groupFields!, (expr) => executeEval({ [ref]: row }, expr))
index = valueMap(groupFields!, (expr) => executeEval({ ...env, [ref]: row }, expr))
}
let branch = branches.find((branch) => {
if (!group || !groupFields) return false
Expand All @@ -76,11 +85,11 @@ export class MemoryDriver extends Driver {
return branches.map(({ index, table }) => {
if (group) {
if (having) {
const value = executeEval(table.map(row => ({ [ref]: row, _: row })), having)
const value = executeEval(table.map(row => ({ ...env, [ref]: row, _: row })), having)
if (!value) return
}
for (const key in omit(fields!, group)) {
index[key] = executeEval(table.map(row => ({ [ref]: row, _: row })), fields![key])
index[key] = executeEval(table.map(row => ({ ...env, [ref]: row, _: row })), fields![key])
}
}
return model.parse(index, false)
Expand Down Expand Up @@ -173,6 +182,15 @@ export class MemoryDriver extends Driver {
return result
}

executeSelection(sel: Selection.Immutable, env: any = {}) {
const expr = sel.args[0], table = sel.table as Selection
if (Array.isArray(env)) env = { [sel.ref]: env }
const data = this.table(sel.table, env)
const res = expr.$ ? data.map(row => executeEval({ ...env, [table.ref]: row, _: row }, expr))
: executeEval(Object.assign(data.map(row => ({ [table.ref]: row, _: row })), env), expr)
return res
}

async withTransaction(callback: (session: Driver) => Promise<void>) {
const data = clone(this.#store)
await callback(this).then(undefined, (e) => {
Expand Down
Loading

0 comments on commit dcbdf81

Please sign in to comment.