Skip to content

Commit

Permalink
feat(orm): support evaluatation api
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Aug 21, 2021
1 parent 3cad36f commit 9172e95
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 44 deletions.
37 changes: 36 additions & 1 deletion packages/koishi-core/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,47 @@ export namespace Query {
return modifier || {}
}

type Projection<T extends TableType, K extends string> = Record<K, Evaluation.Aggregation<Tables[T]>>

export interface Database {
drop(table?: TableType): Promise<void>
get<T extends TableType, K extends Field<T>>(table: T, query: Query<T>, modifier?: Modifier<K>): Promise<Pick<Tables[T], K>[]>
remove<T extends TableType>(table: T, query: Query<T>): Promise<void>
create<T extends TableType>(table: T, data: Partial<Tables[T]>): Promise<Tables[T]>
update<T extends TableType>(table: T, data: Partial<Tables[T]>[], key?: Index<T>): Promise<void>
drop(table?: TableType): Promise<void>
aggregate<T extends TableType, K extends string>(table: T, fields: Projection<T, K>, query?: Query<T>): Promise<Record<K, number>>
}
}

export namespace Evaluation {
export type Numeric<T = any, U = never> = U | number | Keys<T, number> | NumericExpr<Numeric<T, U>>

export interface NumericExpr<N = Numeric> {
$add?: N[]
$multiply?: N[]
$subtract?: [N, N]
$divide?: [N, N]
}

export type Boolean<T = any, U = never> = U | boolean | Keys<T, boolean> | BooleanExpr<Numeric<T, U>>

export interface BooleanExpr<N = Numeric> {
$eq?: [N, N]
$ne?: [N, N]
$gt?: [N, N]
$gte?: [N, N]
$lt?: [N, N]
$lte?: [N, N]
}

export type Aggregation<T = any> = Numeric<{}, AggregationExpr<Numeric<T>>>

export interface AggregationExpr<N = Numeric> {
$sum?: N
$avg?: N
$max?: N
$min?: N
$count?: N
}
}

Expand Down
83 changes: 40 additions & 43 deletions packages/plugin-mysql/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import MysqlDatabase, { Config, TableType } from './database'
import { User, Channel, Database, Context, Query } from 'koishi-core'
import MysqlDatabase, { Config } from './database'
import { User, Channel, Database, Context, Query, Evaluation } from 'koishi-core'
import { difference } from 'koishi-utils'
import { OkPacket, escapeId, escape } from 'mysql'
import * as Koishi from 'koishi-core'
Expand Down Expand Up @@ -74,50 +74,47 @@ const queryOperators: QueryOperators = {
$bitsAnyClear: (key, value) => `${key} & ${escape(value)} != ${escape(value)}`,
}

export function createFilter<T extends TableType>(name: T, query: Query<T>) {
function parseQuery(query: Query.Expr) {
const conditions: string[] = []
for (const key in query) {
// logical expression
if (key === '$not') {
conditions.push(`!(${parseQuery(query.$not)})`)
continue
} else if (key === '$and') {
if (!query.$and.length) return '0'
conditions.push(...query.$and.map(parseQuery))
continue
} else if (key === '$or' && query.$or.length) {
conditions.push(`(${query.$or.map(parseQuery).join(' || ')})`)
continue
}
function parseQuery(query: Query.Expr) {
const conditions: string[] = []
for (const key in query) {
// logical expression
if (key === '$not') {
conditions.push(`!(${parseQuery(query.$not)})`)
continue
} else if (key === '$and') {
if (!query.$and.length) return '0'
conditions.push(...query.$and.map(parseQuery))
continue
} else if (key === '$or' && query.$or.length) {
conditions.push(`(${query.$or.map(parseQuery).join(' || ')})`)
continue
}

// query shorthand
const value = query[key]
const escKey = escapeId(key)
if (Array.isArray(value)) {
conditions.push(createMemberQuery(escKey, value))
continue
} else if (value instanceof RegExp) {
conditions.push(createRegExpQuery(escKey, value))
continue
} else if (typeof value === 'string' || typeof value === 'number' || value instanceof Date) {
conditions.push(createEqualQuery(escKey, value))
continue
}
// query shorthand
const value = query[key]
const escKey = escapeId(key)
if (Array.isArray(value)) {
conditions.push(createMemberQuery(escKey, value))
continue
} else if (value instanceof RegExp) {
conditions.push(createRegExpQuery(escKey, value))
continue
} else if (typeof value === 'string' || typeof value === 'number' || value instanceof Date) {
conditions.push(createEqualQuery(escKey, value))
continue
}

// query expression
for (const prop in value) {
if (prop in queryOperators) {
conditions.push(queryOperators[prop](escKey, value[prop]))
}
// query expression
for (const prop in value) {
if (prop in queryOperators) {
conditions.push(queryOperators[prop](escKey, value[prop]))
}
}

if (!conditions.length) return '1'
if (conditions.includes('0')) return '0'
return conditions.join(' && ')
}
return parseQuery(Query.resolve(name, query))

if (!conditions.length) return '1'
if (conditions.includes('0')) return '0'
return conditions.join(' && ')
}

Database.extend(MysqlDatabase, {
Expand All @@ -131,7 +128,7 @@ Database.extend(MysqlDatabase, {
},

async get(name, query, modifier) {
const filter = createFilter(name, query)
const filter = parseQuery(Query.resolve(name, query))
if (filter === '0') return []
const { fields, limit, offset } = Query.resolveModifier(modifier)
const keys = this.joinKeys(this.inferFields(name, fields))
Expand All @@ -142,7 +139,7 @@ Database.extend(MysqlDatabase, {
},

async remove(name, query) {
const filter = createFilter(name, query)
const filter = parseQuery(Query.resolve(name, query))
if (filter === '0') return
await this.query('DELETE FROM ?? WHERE ' + filter, [name])
},
Expand Down

0 comments on commit 9172e95

Please sign in to comment.