Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(minato): impl $.get #109

Merged
merged 4 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ export class Database<S = {}, N = {}, C extends Context = Context> extends Servi
private _parseField(field: any, transformers: Driver.Transformer[] = [], setInitial?: (value) => void, setField?: (value) => void): Type {
if (field === 'object') {
setInitial?.({})
setField?.({ type: 'json', initial: {} })
setField?.({ initial: {}, deftype: 'json', type: Type.Object() })
return Type.Object()
} else if (field === 'array') {
setInitial?.([])
setField?.({ type: 'json', initial: [] })
setField?.({ initial: [], deftype: 'json', type: Type.Array() })
return Type.Array()
} else if (typeof field === 'string' && this.types[field]) {
transformers.push({
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export namespace Eval {
object<T extends any>(row: Row.Cell<T>): Expr<T, false>
object<T extends any>(row: Row<T>): Expr<T, false>
array<T>(value: Expr<T, false>): Expr<T[], true>

get<T extends object, K extends keyof T, A extends boolean>(x: Term<T, A>, key: K): Expr<T[K], A>
get<T extends any, A extends boolean>(x: Array<T, A>, index: Term<number, A>): Expr<T, A>
}
}

Expand Down Expand Up @@ -329,6 +332,8 @@ Eval.array = unary('array', (expr, table) => Array.isArray(table)
? table.map(data => executeAggr(expr, data)).filter(x => !expr[Type.kType]?.ignoreNull || !isEmpty(x))
: Array.from(executeEval(table, expr)).filter(x => !expr[Type.kType]?.ignoreNull || !isEmpty(x)), (expr) => Type.Array(Type.fromTerm(expr)))

Eval.get = multary('get', ([x, key], data) => executeEval(data, x)?.[executeEval(data, key)], (x, key) => Type.getInner(Type.fromTerm(x), key) ?? Type.Any)

export { Eval as $ }

export type Update<T = any> = UnevalObject<Flatten<T>>
Expand Down
16 changes: 14 additions & 2 deletions packages/core/src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,27 @@ namespace Executable {
}
}

const createRow = (ref: string, expr = {}, prefix = '', model?: Model) => new Proxy(expr, {
const createRow = (ref: string, expr = {}, prefix = '', model?: Model, intermediate?: Eval.Expr) => new Proxy(expr, {
get(target, key) {
if (key === '$prefix') return prefix
if (key === '$model') return model
if (typeof key === 'symbol' || key in target || key.startsWith('$')) return Reflect.get(target, key)

if (intermediate) {
if (Type.isArray(expr?.[Type.kType]) && Number.isInteger(+key)) {
return createRow(ref, Eval.get(expr as any, +key), '', model, Eval.get(expr as any, +key))
} else {
return createRow(ref, Eval.get(intermediate as any, `${prefix}${key}`), `${prefix}${key}.`, model, intermediate)
}
}

let type: Type
const field = model?.fields[prefix + key as string]
if (Type.getInner(expr?.[Type.kType], key)) {
if (Type.isArray(expr?.[Type.kType]) && Number.isInteger(+key)) {
// indexing array
type = Type.getInner(expr?.[Type.kType]) ?? Type.fromField('expr')
return createRow(ref, Eval.get(expr as any, +key), '', model, Eval.get(expr as any, +key))
} else if (Type.getInner(expr?.[Type.kType], key)) {
// type may conatins object layout
type = Type.getInner(expr?.[Type.kType], key)!
} else if (field) {
Expand Down
19 changes: 10 additions & 9 deletions packages/core/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ export interface Type<T = any, N = any> {
export namespace Type {
export const kType = Symbol.for('minato.type')

export const Boolean: Type<boolean> = defineProperty({ type: 'boolean' }, kType, true) as any
export const Number: Type<number> = defineProperty({ type: 'double' }, kType, true)
export const String: Type<string> = defineProperty({ type: 'string' }, kType, true)
export const Any: Type = fromField('expr')
export const Boolean: Type<boolean> = fromField('boolean')
export const Number: Type<number> = fromField('double')
export const String: Type<string> = fromField('string')

type Extract<T> =
| T extends Type<infer I> ? I
Expand Down Expand Up @@ -76,20 +77,20 @@ export namespace Type {
return value?.[kType] === true
}

export function isArray(type: Type) {
return (type.type === 'json') && type.array
export function isArray(type?: Type) {
return (type?.type === 'json') && type?.array
}

export function getInner(type?: Type, key?: string): Type | undefined {
if (!type?.inner) return
if (isArray(type) && isNullable(key)) return type.inner
if (isArray(type)) return type.inner
if (isNullable(key)) return
if (type.inner[key]) return type.inner[key]
if (key.includes('.')) return key.split('.').reduce((t, k) => getInner(t, k), type)
return Object(globalThis.Object.fromEntries(globalThis.Object.entries(type.inner)
const fields = globalThis.Object.entries(type.inner)
.filter(([k]) => k.startsWith(`${key}.`))
.map(([k, v]) => [k.slice(key.length + 1), v]),
))
.map(([k, v]) => [k.slice(key.length + 1), v])
return fields.length ? Object(globalThis.Object.fromEntries(fields)) : undefined
}

export function transform(value: any, type: Type, callback: (value: any, type?: Type) => any) {
Expand Down
6 changes: 6 additions & 0 deletions packages/memory/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ describe('@minatojs/driver-memory', () => {
update: {
index: false,
},
json: {
query: {
nullableComparator: false,
},
},
model: {
fields: {
cast: false,
typeModel: false,
},
object: {
nullableComparator: false,
typeModel: false,
},
},
Expand Down
4 changes: 4 additions & 0 deletions packages/mongo/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ export class Builder {
}
},

$get: ([x, key], group) => typeof key === 'string'
? (key as string).split('.').reduce((res, k) => ({ $getField: { input: res, field: k } }), this.eval(x, group))
: { $arrayElemAt: [this.eval(x, group), this.eval(key, group)] },

$exec: (arg, group) => {
const sel = arg as Selection
const transformer = this.createSubquery(sel)
Expand Down
8 changes: 8 additions & 0 deletions packages/postgres/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ export class PostgresBuilder extends Builder {
else return `(${args.map(arg => this.parseEval(arg, 'bigint')).join(' # ')})`
},

$get: ([x, key]) => {
const type = Type.fromTerm(this.state.expr, Type.Any)
const res = typeof key === 'string'
? this.asEncoded(`jsonb_extract_path(${this.parseEval(x, false)}, ${(key as string).split('.').map(this.escapeKey).join(',')})`, true)
: this.asEncoded(`(${this.parseEval(x, false)})->(${this.parseEval(key, 'integer')})`, true)
return type.type === 'expr' ? res : `(${res})::${this.transformType(type)}`
},

$number: (arg) => {
const value = this.parseEval(arg)
const type = Type.fromTerm(arg)
Expand Down
3 changes: 3 additions & 0 deletions packages/sql-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ export class Builder {

$object: (fields) => this.groupObject(fields),
$array: (expr) => this.groupArray(this.transform(this.parseEval(expr, false), expr, 'encode')),
$get: ([x, key]) => typeof key === 'string'
? this.asEncoded(`json_extract(${this.parseEval(x, false)}, '$.${key}')`, true)
: this.asEncoded(`json_extract(${this.parseEval(x, false)}, concat('$[', ${this.parseEval(key)}, ']'))`, true),

$exec: (sel) => this.parseSelection(sel as Selection),
}
Expand Down
3 changes: 3 additions & 0 deletions packages/sqlite/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export class SQLiteBuilder extends Builder {
if (Field.boolean.includes(type.type)) return args.map(arg => this.parseEval(arg)).reduce((prev, curr) => `(${prev} != ${curr})`)
else return args.map(arg => this.parseEval(arg)).reduce((prev, curr) => binaryXor(prev, curr))
}
this.evalOperators.$get = ([x, key]) => typeof key === 'string'
? this.asEncoded(`(${this.parseEval(x, false)} -> '$.${key}')`, true)
: this.asEncoded(`(${this.parseEval(x, false)} -> ('$[' || ${this.parseEval(key)} || ']'))`, true)

this.transformers['bigint'] = {
encode: value => `cast(${value} as text)`,
Expand Down
44 changes: 42 additions & 2 deletions packages/tests/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ function JsonTests(database: Database<Tables>) {

database.extend('baz', {
id: 'unsigned',
nums: { type: 'json', initial: [] },
nums: {
type: 'array',
inner: 'unsigned',
}
})

before(async () => {
Expand All @@ -81,7 +84,13 @@ function JsonTests(database: Database<Tables>) {
}

namespace JsonTests {
export function query(database: Database<Tables>) {
export interface RelationOptions {
nullableComparator?: boolean
}

export function query(database: Database<Tables>, options: RelationOptions = {}) {
const { nullableComparator = true } = options

it('$size', async () => {
await expect(database.get('baz', {
nums: { $size: 3 },
Expand Down Expand Up @@ -134,6 +143,37 @@ namespace JsonTests {
await expect(database.eval('bar', row => $.max($.add(1, row.value)))).to.eventually.deep.equal(2)
await expect(database.eval('bar', row => $.max($.add(1, row.obj.x)))).to.eventually.deep.equal(4)
})

it('$get array', async () => {
await expect(database.get('baz', row => $.eq($.get(row.nums, 0), 4)))
.to.eventually.deep.equal([
{ id: 1, nums: [4, 5, 6] },
])

await expect(database.get('baz', row => $.eq(row.nums[0], 4)))
.to.eventually.deep.equal([
{ id: 1, nums: [4, 5, 6] },
])
})

nullableComparator && it('$get array with expressions', async () => {
await expect(database.get('baz', row => $.eq($.get(row.nums, $.add(row.id, -1)), 4)))
.to.eventually.deep.equal([
{ id: 1, nums: [4, 5, 6] },
])
})

it('$get object', async () => {
await expect(database.get('bar', row => $.eq(row.obj.o.a, 2)))
.to.eventually.have.shape([
{ value: 1 },
])

await expect(database.get('bar', row => $.eq($.get(row.obj.o, 'a'), 2)))
.to.eventually.have.shape([
{ value: 1 },
])
})
}

export function selection(database: Database<Tables>) {
Expand Down
9 changes: 8 additions & 1 deletion packages/tests/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ namespace ModelOperations {
cast?: boolean
typeModel?: boolean
aggregateNull?: boolean
nullableComparator?: boolean
}

export const fields = function Fields(database: Database<Tables, Types>, options: ModelOptions = {}) {
Expand Down Expand Up @@ -513,7 +514,7 @@ namespace ModelOperations {
}

export const object = function ObjectFields(database: Database<Tables, Types>, options: ModelOptions = {}) {
const { aggregateNull = true, typeModel = true } = options
const { aggregateNull = true, nullableComparator = true, typeModel = true } = options

it('basic', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
Expand Down Expand Up @@ -635,6 +636,12 @@ namespace ModelOperations {
await expect(database.get('dobjects', row => $.eq($.xor(row.foo!.nested!.int64!, 2n), 121n))).to.eventually.have.length(1)
await expect(database.eval('dobjects', row => $.max($.or(row.foo!.nested!.int64!, 9223372036854775701n)))).eventually.to.deep.equal(9223372036854775807n)
})

nullableComparator && it('nested $get', async () => {
await setup(database, 'dobjects', dobjectTable)
await expect(database.get('dobjects', row => $.eq(row.baz[0].nested.id, 1))).to.eventually.have.length(2)
await expect(database.get('dobjects', row => $.eq(row.baz[0].nested.array[0], 1))).to.eventually.have.length(2)
})
}
}

Expand Down