Skip to content

Commit

Permalink
feat(minato): impl bitwise operations (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hieuzest committed May 8, 2024
1 parent 72ea137 commit 4ad01c0
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 29 deletions.
38 changes: 30 additions & 8 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ export namespace Eval {
concat: Multi<string, string>
regex<A extends boolean>(x: Term<string, A>, y: Term<string, A> | Term<RegExp, A>): Expr<boolean, A>

// logical
and: Multi<boolean, boolean>
or: Multi<boolean, boolean>
not: Unary<boolean, boolean>
// logical / bitwise
and: Multi<boolean, boolean> & Multi<number, number> & Multi<bigint, bigint>
or: Multi<boolean, boolean> & Multi<number, number> & Multi<bigint, bigint>
not: Unary<boolean, boolean> & Unary<number, number> & Unary<bigint, bigint>
xor: Multi<boolean, boolean> & Multi<number, number> & Multi<bigint, bigint>

// typecast
literal<T>(value: T, type?: Type<T> | Field.Type<T> | Field.NewType<T> | string): Expr<T, false>
Expand Down Expand Up @@ -215,10 +216,31 @@ Eval.nin = multary('nin', ([value, array], data) => !executeEval(data, array).in
Eval.concat = multary('concat', (args, data) => args.map(arg => executeEval(data, arg)).join(''), Type.String)
Eval.regex = multary('regex', ([value, regex], data) => makeRegExp(executeEval(data, regex)).test(executeEval(data, value)), Type.Boolean)

// logical
Eval.and = multary('and', (args, data) => args.every(arg => executeEval(data, arg)), Type.Boolean)
Eval.or = multary('or', (args, data) => args.some(arg => executeEval(data, arg)), Type.Boolean)
Eval.not = unary('not', (value, data) => !executeEval(data, value), Type.Boolean)
// logical / bitwise
Eval.and = multary('and', (args, data) => {
const type = Type.fromTerms(args, Type.Boolean)
if (Field.boolean.includes(type.type)) return args.every(arg => executeEval(data, arg))
else if (Field.number.includes(type.type)) return args.map(arg => executeEval(data, arg)).reduce((prev, curr) => prev & curr)
else if (type.type === 'bigint') return args.map(arg => BigInt(executeEval(data, arg) ?? 0)).reduce((prev, curr) => prev & curr)
}, (...args) => Type.fromTerms(args, Type.Boolean))
Eval.or = multary('or', (args, data) => {
const type = Type.fromTerms(args, Type.Boolean)
if (Field.boolean.includes(type.type)) return args.some(arg => executeEval(data, arg))
else if (Field.number.includes(type.type)) return args.map(arg => executeEval(data, arg)).reduce((prev, curr) => prev | curr)
else if (type.type === 'bigint') return args.map(arg => BigInt(executeEval(data, arg) ?? 0)).reduce((prev, curr) => prev | curr)
}, (...args) => Type.fromTerms(args, Type.Boolean))
Eval.not = unary('not', (value, data) => {
const type = Type.fromTerms([value], Type.Boolean)
if (Field.boolean.includes(type.type)) return !executeEval(data, value)
else if (Field.number.includes(type.type)) return ~executeEval(data, value) as any
else if (type.type === 'bigint') return ~BigInt(executeEval(data, value) ?? 0)
}, (value) => Type.fromTerms([value], Type.Boolean))
Eval.xor = multary('xor', (args, data) => {
const type = Type.fromTerms(args, Type.Boolean)
if (Field.boolean.includes(type.type)) return args.map(arg => executeEval(data, arg)).reduce((prev, curr) => prev !== curr)
else if (Field.number.includes(type.type)) return args.map(arg => executeEval(data, arg)).reduce((prev, curr) => prev ^ curr)
else if (type.type === 'bigint') return args.map(arg => BigInt(executeEval(data, arg) ?? 0)).reduce((prev, curr) => prev ^ curr)
}, (...args) => Type.fromTerms(args, Type.Boolean))

// typecast
Eval.literal = multary('literal', ([value, type]) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export namespace Type {
else return fromPrimitive(value as T)
}

export function fromTerms(values: Eval.Term<any>[], initial?: Type): Type {
return values.map(fromTerm).find((type) => type.type !== 'expr') ?? initial ?? fromField('expr')
}

export function isType(value: any): value is Type {
return value?.[kType] === true
}
Expand Down
115 changes: 107 additions & 8 deletions packages/mongo/src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Dict, isNullable, mapValues } from 'cosmokit'
import { Driver, Eval, isComparable, isEvalExpr, Model, Query, Selection, Type, unravel } from 'minato'
import { Eval, Field, isComparable, isEvalExpr, Model, Query, Selection, Type, unravel } from 'minato'
import { Filter, FilterOperators, ObjectId } from 'mongodb'
import MongoDriver from '.'

function createFieldFilter(query: Query.FieldQuery, key: string) {
function createFieldFilter(query: Query.Field, key: string) {
const filters: Filter<any>[] = []
const result: Filter<any> = {}
const child = transformFieldQuery(query, key, filters)
Expand All @@ -14,7 +14,7 @@ function createFieldFilter(query: Query.FieldQuery, key: string) {
return true
}

function transformFieldQuery(query: Query.FieldQuery, key: string, filters: Filter<any>[]) {
function transformFieldQuery(query: Query.Field, key: string, filters: Filter<any>[]) {
// shorthand syntax
if (isComparable(query) || query instanceof ObjectId) {
return { $eq: query }
Expand All @@ -31,15 +31,15 @@ function transformFieldQuery(query: Query.FieldQuery, key: string, filters: Filt
const result: FilterOperators<any> = {}
for (const prop in query) {
if (prop === '$and') {
for (const item of query[prop]) {
for (const item of query[prop]!) {
const child = createFieldFilter(item, key)
if (child === false) return false
if (child !== true) filters.push(child)
}
} else if (prop === '$or') {
const $or: Filter<any>[] = []
if (!query[prop].length) return false
const always = query[prop].some((item) => {
if (!query[prop]!.length) return false
const always = query[prop]!.some((item) => {
const child = createFieldFilter(item, key)
if (typeof child === 'boolean') return child
$or.push(child)
Expand All @@ -50,7 +50,7 @@ function transformFieldQuery(query: Query.FieldQuery, key: string, filters: Filt
if (child === true) return false
if (child !== false) filters.push({ $nor: [child] })
} else if (prop === '$el') {
const child = transformFieldQuery(query[prop], key, filters)
const child = transformFieldQuery(query[prop]!, key, filters)
if (child === false) return false
if (child !== true) result.$elemMatch = child!
} else if (prop === '$regexFor') {
Expand Down Expand Up @@ -88,14 +88,15 @@ export class Builder {
public pipeline: any[] = []
protected lookups: any[] = []
public evalKey?: string
private evalType?: Type
private refTables: string[] = []
private refVirtualKeys: Dict<string> = {}
private joinTables: Dict<string> = {}
public aggrDefault: any

private evalOperators: EvalOperators

constructor(private driver: Driver, private tables: string[], public virtualKey?: string, public recursivePrefix: string = '$') {
constructor(private driver: MongoDriver, private tables: string[], public virtualKey?: string, public recursivePrefix: string = '$') {
this.walkedKeys = []

this.evalOperators = {
Expand All @@ -120,6 +121,103 @@ export class Builder {
},
$if: (arg, group) => ({ $cond: arg.map(val => this.eval(val, group)) }),

$and: (args, group) => {
const type = this.evalType!
if (Field.boolean.includes(type.type)) return { $and: args.map(arg => this.eval(arg, group)) }
else if (this.driver.version >= 7) return { $bitAnd: args.map(arg => this.eval(arg, group)) }
else if (Field.number.includes(type.type)) {
return {
$function: {
body: function (...args: number[]) { return args.reduce((prev, curr) => prev & curr) }.toString(),
args: args.map(arg => this.eval(arg, group)),
lang: 'js',
},
}
} else {
return {
$toLong: {
$function: {
body: function (...args: string[]) { return args.reduce((prev, curr) => String(BigInt(prev ?? 0) & BigInt(curr ?? 0))) }.toString(),
args: args.map(arg => ({ $toString: this.eval(arg, group) })),
lang: 'js',
},
},
}
}
},
$or: (args, group) => {
const type = this.evalType!
if (Field.boolean.includes(type.type)) return { $or: args.map(arg => this.eval(arg, group)) }
else if (this.driver.version >= 7) return { $bitOr: args.map(arg => this.eval(arg, group)) }
else if (Field.number.includes(type.type)) {
return {
$function: {
body: function (...args: number[]) { return args.reduce((prev, curr) => prev | curr) }.toString(),
args: args.map(arg => this.eval(arg, group)),
lang: 'js',
},
}
} else {
return {
$toLong: {
$function: {
body: function (...args: string[]) { return args.reduce((prev, curr) => String(BigInt(prev ?? 0) | BigInt(curr ?? 0))) }.toString(),
args: args.map(arg => ({ $toString: this.eval(arg, group) })),
lang: 'js',
},
},
}
}
},
$not: (arg, group) => {
const type = this.evalType!
if (Field.boolean.includes(type.type)) return { $not: this.eval(arg, group) }
else if (this.driver.version >= 7) return { $bitNot: this.eval(arg, group) }
else if (Field.number.includes(type.type)) {
return {
$function: {
body: function (arg: number) { return ~arg }.toString(),
args: [this.eval(arg, group)],
lang: 'js',
},
}
} else {
return {
$toLong: {
$function: {
body: function (arg: string) { return String(~BigInt(arg ?? 0)) }.toString(),
args: [{ $toString: this.eval(arg, group) }],
lang: 'js',
},
},
}
}
},
$xor: (args, group) => {
const type = this.evalType!
if (Field.boolean.includes(type.type)) return args.map(arg => this.eval(arg, group)).reduce((prev, curr) => ({ $ne: [prev, curr] }))
else if (this.driver.version >= 7) return { $bitXor: args.map(arg => this.eval(arg, group)) }
else if (Field.number.includes(type.type)) {
return {
$function: {
body: function (...args: number[]) { return args.reduce((prev, curr) => prev ^ curr) }.toString(),
args: args.map(arg => this.eval(arg, group)),
lang: 'js',
},
}
} else {
return {
$toLong: {
$function: {
body: function (...args: string[]) { return args.reduce((prev, curr) => String(BigInt(prev ?? 0) ^ BigInt(curr ?? 0))) }.toString(),
args: args.map(arg => ({ $toString: this.eval(arg, group) })),
lang: 'js',
},
},
}
}
},

$object: (arg, group) => mapValues(arg as any, x => this.transformEvalExpr(x)),

$regex: (arg, group) => ({ $regexMatch: { input: this.eval(arg[0], group), regex: this.eval(arg[1], group) } }),
Expand Down Expand Up @@ -213,6 +311,7 @@ export class Builder {

for (const key in expr) {
if (this.evalOperators[key]) {
this.evalType = Type.fromTerm(expr)
return this.evalOperators[key](expr[key], group)
} else if (key?.startsWith('$') && Eval[key.slice(1)]) {
return mapValues(expr, (value) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/mongo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class MongoDriver extends Driver<MongoDriver.Config> {
public client!: MongoClient
public db!: Db
public mongo = this
public version = 0

private builder: Builder = new Builder(this, [])
private session?: ClientSession
Expand Down Expand Up @@ -47,6 +48,7 @@ export class MongoDriver extends Driver<MongoDriver.Config> {
]))
this.db = this.client.db(this.config.database)

this.db.admin().serverInfo().then((doc) => this.version = +doc.version.split('.')[0]).catch(noop)
await this.client.withSession((session) => session.withTransaction(
() => this.db.collection('_fields').findOne({}, { session }),
)).catch(() => this._replSet = false)
Expand Down
23 changes: 22 additions & 1 deletion packages/mysql/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ export class MySQLBuilder extends Builder {
return this.asEncoded(`ifnull(${res}, 0)`, false)
}

this.evalOperators.$or = (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalOr(args.map(arg => this.parseEval(arg)))
else return `cast(${args.map(arg => this.parseEval(arg)).join(' | ')} as signed)`
}
this.evalOperators.$and = (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalAnd(args.map(arg => this.parseEval(arg)))
else return `cast(${args.map(arg => this.parseEval(arg)).join(' & ')} as signed)`
}
this.evalOperators.$not = (arg) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalNot(this.parseEval(arg))
else return `cast(~(${this.parseEval(arg)}) as signed)`
}
this.evalOperators.$xor = (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return args.map(arg => this.parseEval(arg)).reduce((prev, curr) => `(${prev} != ${curr})`)
else return `cast(${args.map(arg => this.parseEval(arg)).join(' ^ ')} as signed)`
}

this.transformers['boolean'] = {
encode: value => `if(${value}=b'1', 1, 0)`,
decode: value => `if(${value}=1, b'1', b'0')`,
Expand All @@ -55,7 +76,7 @@ export class MySQLBuilder extends Builder {

this.transformers['bigint'] = {
encode: value => `cast(${value} as char)`,
decode: value => `cast(${value} as bigint)`,
decode: value => `cast(${value} as signed)`,
load: value => isNullable(value) ? value : BigInt(value),
dump: value => isNullable(value) ? value : `${value}`,
}
Expand Down
29 changes: 26 additions & 3 deletions packages/postgres/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ export class PostgresBuilder extends Builder {
: `ln(${this.parseEval(left, 'double precision')}) / ln(${this.parseEval(right, 'double precision')})`,
$random: () => `random()`,

$or: (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalOr(args.map(arg => this.parseEval(arg, 'boolean')))
else return `(${args.map(arg => this.parseEval(arg, 'bigint')).join(' | ')})`
},
$and: (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalAnd(args.map(arg => this.parseEval(arg, 'boolean')))
else return `(${args.map(arg => this.parseEval(arg, 'bigint')).join(' & ')})`
},
$not: (arg) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalNot(this.parseEval(arg, 'boolean'))
else return `(~(${this.parseEval(arg, 'bigint')}))`
},
$xor: (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return args.map(arg => this.parseEval(arg, 'boolean')).reduce((prev, curr) => `(${prev} != ${curr})`)
else return `(${args.map(arg => this.parseEval(arg, 'bigint')).join(' # ')})`
},

$eq: this.binary('=', 'text'),

$number: (arg) => {
Expand Down Expand Up @@ -153,9 +174,11 @@ export class PostgresBuilder extends Builder {
}

private getLiteralType(expr: any) {
if (typeof expr === 'string') return 'text'
else if (typeof expr === 'number') return 'double precision'
else if (typeof expr === 'string') return 'boolean'
const type = Type.fromTerm(expr)
if (Field.string.includes(type.type) || typeof expr === 'string') return 'text'
else if (Field.number.includes(type.type) || typeof expr === 'number') return 'double precision'
else if (Field.boolean.includes(type.type) || typeof expr === 'boolean') return 'boolean'
else if (type.type === 'json') return 'jsonb'
}

parseEval(expr: any, outtype: boolean | string = true): string {
Expand Down
27 changes: 19 additions & 8 deletions packages/sql-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,22 @@ export class Builder {
$concat: (args) => `concat(${args.map(arg => this.parseEval(arg)).join(', ')})`,
$regex: ([key, value]) => `${this.parseEval(key)} regexp ${this.parseEval(value)}`,

// logical
$or: (args) => this.logicalOr(args.map(arg => this.parseEval(arg))),
$and: (args) => this.logicalAnd(args.map(arg => this.parseEval(arg))),
$not: (arg) => this.logicalNot(this.parseEval(arg)),
// logical / bitwise
$or: (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalOr(args.map(arg => this.parseEval(arg)))
else return `(${args.map(arg => this.parseEval(arg)).join(' | ')})`
},
$and: (args) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalAnd(args.map(arg => this.parseEval(arg)))
else return `(${args.map(arg => this.parseEval(arg)).join(' & ')})`
},
$not: (arg) => {
const type = this.state.type!
if (Field.boolean.includes(type.type)) return this.logicalNot(this.parseEval(arg))
else return `(~(${this.parseEval(arg)}))`
},

// boolean
$eq: this.binary('='),
Expand Down Expand Up @@ -323,7 +335,7 @@ export class Builder {
return this.asEncoded(`ifnull(json_arrayagg(${value}), json_array())`, true)
}

protected parseFieldQuery(key: string, query: Query.FieldExpr) {
protected parseFieldQuery(key: string, query: Query.Field) {
const conditions: string[] = []
if (this.modifiedTable) key = `${this.escapeId(this.modifiedTable)}.${key}`

Expand Down Expand Up @@ -409,9 +421,7 @@ export class Builder {
}
}
const prefix = this.modifiedTable ? `${this.escapeId(this.state.tables?.[table]?.name ?? this.modifiedTable)}.`
: (!this.state.tables || table === '_' || key in fields
// the only table must be the main table
|| (Object.keys(this.state.tables).length === 1 && table in this.state.tables) ? '' : `${this.escapeId(table)}.`)
: (!this.state.tables || table === '_' || key in fields || table in this.state.tables ? '' : `${this.escapeId(table)}.`)

if (!(table in (this.state.tables || {})) && (table in (this.state.innerTables || {}))) {
const fields = this.state.innerTables?.[table]?.fields || {}
Expand Down Expand Up @@ -628,6 +638,7 @@ export class Builder {
switch (typeof value) {
case 'boolean':
case 'number':
case 'bigint':
return value + ''
case 'object':
return this.quote(JSON.stringify(value))
Expand Down
Loading

0 comments on commit 4ad01c0

Please sign in to comment.