From ee65089061e10361ba9e69e5e09c77e3162b2eee Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 02:45:31 +0800 Subject: [PATCH 1/9] feat: support object query --- packages/postgres/src/builder.ts | 36 ++++++++++++++++---------------- packages/sql-utils/src/index.ts | 11 ++++------ packages/sqlite/src/index.ts | 13 ++++++------ packages/tests/src/model.ts | 19 +++++++++++++++++ 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/packages/postgres/src/builder.ts b/packages/postgres/src/builder.ts index 9c52f2e0..f0d8a711 100644 --- a/packages/postgres/src/builder.ts +++ b/packages/postgres/src/builder.ts @@ -46,28 +46,28 @@ export class PostgresBuilder extends Builder { this.evalOperators = { ...this.evalOperators, - $select: (args) => `${args.map(arg => this.parseEval(arg, this.getLiteralType(arg))).join(', ')}`, + $select: (args) => `${args.map(arg => this.parseEval(arg, this.transformType(arg))).join(', ')}`, $if: (args) => { - const type = this.getLiteralType(args[1]) ?? this.getLiteralType(args[2]) ?? 'text' + const type = this.transformType(args[1]) ?? this.transformType(args[2]) ?? 'text' return `(SELECT CASE WHEN ${this.parseEval(args[0], 'boolean')} THEN ${this.parseEval(args[1], type)} ELSE ${this.parseEval(args[2], type)} END)` }, $ifNull: (args) => { - const type = args.map(this.getLiteralType).find(x => x) ?? 'text' + const type = args.map(this.transformType).find(x => x) ?? 'text' return `coalesce(${args.map(arg => this.parseEval(arg, type)).join(', ')})` }, - $regex: ([key, value]) => `${this.parseEval(key)} ~ ${this.parseEval(value)}`, + $regex: ([key, value]) => `(${this.parseEval(key)} ~ ${this.parseEval(value)})`, // number $add: (args) => `(${args.map(arg => this.parseEval(arg, 'double precision')).join(' + ')})`, $multiply: (args) => `(${args.map(arg => this.parseEval(arg, 'double precision')).join(' * ')})`, $modulo: ([left, right]) => { const dividend = this.parseEval(left, 'double precision'), divisor = this.parseEval(right, 'double precision') - return `${dividend} - (${divisor} * floor(${dividend} / ${divisor}))` + return `(${dividend} - (${divisor} * floor(${dividend} / ${divisor})))` }, $log: ([left, right]) => isNullable(right) ? `ln(${this.parseEval(left, 'double precision')})` - : `ln(${this.parseEval(left, 'double precision')}) / ln(${this.parseEval(right, 'double precision')})`, + : `(ln(${this.parseEval(left, 'double precision')}) / ln(${this.parseEval(right, 'double precision')}))`, $random: () => `random()`, $or: (args) => { @@ -91,8 +91,6 @@ export class PostgresBuilder extends Builder { else return `(${args.map(arg => this.parseEval(arg, 'bigint')).join(' # ')})` }, - $eq: this.binary('=', 'text'), - $number: (arg) => { const value = this.parseEval(arg) const type = Type.fromTerm(arg) @@ -109,7 +107,7 @@ export class PostgresBuilder extends Builder { value => this.isEncoded() ? this.jsonLength(value) : this.asEncoded(`COALESCE(ARRAY_LENGTH(${value}, 1), 0)`, false), ), - $concat: (args) => `${args.map(arg => this.parseEval(arg, 'text')).join('||')}`, + $concat: (args) => `(${args.map(arg => this.parseEval(arg, 'text')).join('||')})`, } this.transformers['boolean'] = { @@ -167,19 +165,20 @@ export class PostgresBuilder extends Builder { this.modifiedTable = table } - protected binary(operator: string, eltype: string = 'double precision') { + protected binary(operator: string, eltype: true | string = 'double precision') { return ([left, right]) => { - const type = this.getLiteralType(left) ?? this.getLiteralType(right) ?? eltype + const type = this.transformType(left) ?? this.transformType(right) ?? eltype return `(${this.parseEval(left, type)} ${operator} ${this.parseEval(right, type)})` } } - private getLiteralType(expr: any) { - 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' + private transformType(source: any) { + const type = Type.isType(source) ? source : Type.fromTerm(source) + if (Field.string.includes(type.type) || typeof source === 'string') return 'text' + else if (Field.number.includes(type.type) || typeof source === 'number') return 'double precision' + else if (Field.boolean.includes(type.type) || typeof source === 'boolean') return 'boolean' else if (type.type === 'json') return 'jsonb' + else if (type.type !== 'expr') return true } parseEval(expr: any, outtype: boolean | string = true): string { @@ -225,8 +224,9 @@ export class PostgresBuilder extends Builder { return this.asEncoded(`(${obj} @> ${value})`, false) } - protected encode(value: string, encoded: boolean, pure: boolean = false, type?: Type, outtype?: string) { - return this.asEncoded((encoded === this.isEncoded() && !pure) ? value + protected encode(value: string, encoded: boolean, pure: boolean = false, type?: Type, outtype?: true | string) { + outtype ??= this.transformType(type) + return this.asEncoded((encoded === this.isEncoded() && !pure) ? value // `${value}${typeof outtype === 'string' ? `::${outtype}` : ''}` : encoded ? `to_jsonb(${this.transform(value, type, 'encode')})` : this.transform(`(jsonb_build_object('v', ${value})->>'v')`, type, 'decode') + `${typeof outtype === 'string' ? `::${outtype}` : ''}` , pure ? undefined : encoded) diff --git a/packages/sql-utils/src/index.ts b/packages/sql-utils/src/index.ts index 6d5c341c..624d1759 100644 --- a/packages/sql-utils/src/index.ts +++ b/packages/sql-utils/src/index.ts @@ -195,10 +195,6 @@ export class Builder { } } - protected unescapeId(value: string) { - return value.slice(1, value.length - 1) - } - protected createNullQuery(key: string, value: boolean) { return `${key} is ${value ? 'not ' : ''}null` } @@ -231,7 +227,7 @@ export class Builder { } protected isJsonQuery(key: string) { - return isSqlJson(this.state.tables![this.state.table!].fields![this.unescapeId(key)]?.type) + return Type.fromTerm(this.state.expr)?.type === 'json' || this.isEncoded(key) } protected comparator(operator: string) { @@ -347,7 +343,6 @@ export class Builder { protected parseFieldQuery(key: string, query: Query.Field) { const conditions: string[] = [] - if (this.modifiedTable) key = `${this.escapeId(this.modifiedTable)}.${key}` // query shorthand if (Array.isArray(query)) { @@ -383,7 +378,9 @@ export class Builder { } else if (key === '$expr') { conditions.push(this.parseEval(query.$expr)) } else { - conditions.push(this.parseFieldQuery(this.escapeId(key), query[key])) + const model = this.state.tables![this.state.table!] ?? Object.values(this.state.tables!)[0] + const expr = Eval('', [Object.keys(this.state.tables!)[0], key], model.getType(key)!) + conditions.push(this.parseFieldQuery(this.parseEval(expr), query[key])) } } diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts index 61bb70ad..8a33ad43 100644 --- a/packages/sqlite/src/index.ts +++ b/packages/sqlite/src/index.ts @@ -305,8 +305,9 @@ export class SQLiteDriver extends Driver { } async remove(sel: Selection.Mutable) { - const { query, table } = sel - const filter = this.sql.parseQuery(query) + const { query, table, tables } = sel + const builder = new SQLiteBuilder(this, tables) + const filter = builder.parseQuery(query) if (filter === '0') return {} const result = this._run(`DELETE FROM ${escapeId(table)} WHERE ${filter}`, [], () => this._get(`SELECT changes() AS count`)) return { matched: result.count, removed: result.count } @@ -330,13 +331,13 @@ export class SQLiteDriver extends Driver { } _update(sel: Selection.Mutable, indexFields: string[], updateFields: string[], update: {}, data: {}) { - const { ref, table } = sel - const model = this.model(table) + const { ref, table, tables, model } = sel + const builder = new SQLiteBuilder(this, tables) executeUpdate(data, update, ref) - const row = this.sql.dump(data, model) + const row = builder.dump(data, model) const assignment = updateFields.map((key) => `${escapeId(key)} = ?`).join(',') const query = Object.fromEntries(indexFields.map(key => [key, row[key]])) - const filter = this.sql.parseQuery(query) + const filter = builder.parseQuery(query) this._run(`UPDATE ${escapeId(table)} SET ${assignment} WHERE ${filter}`, updateFields.map((key) => row[key] ?? null)) } diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index f32f7a5f..69ecc770 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -464,6 +464,25 @@ namespace ModelOperations { )) }) + it('object query', async () => { + const table = await setup(database, 'dtypes', dtypeTable) + await expect(database.get('dtypes', { + 'object.num': 10, + })).to.eventually.have.shape([table[4]]) + + await expect(database.get('dtypes', { + 'object.num': { + $gte: 10, + }, + })).to.eventually.have.shape([table[4]]) + + // await expect(database.get('dtypes', { + // object: { + // num: 10, + // }, + // })).to.eventually.deep.equal(table[4]) + }) + it('recursive type', async () => { const table = await setup(database, 'recurxs', [{ id: 1, y: { id: 2, x: { id: 3, y: { id: 4, x: { id: 5 } } } } }]) await expect(database.get('recurxs', {})).to.eventually.have.deep.members(table) From d97f934d2da5fc09124b1504bec032fe27641470 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 12:53:48 +0800 Subject: [PATCH 2/9] feat: support nested object query --- packages/core/src/model.ts | 10 +++++----- packages/core/src/query.ts | 5 +++-- packages/core/src/utils.ts | 28 ++++++++++++++++++++++++++-- packages/mongo/src/builder.ts | 14 +++++++++----- packages/postgres/src/builder.ts | 1 + packages/sql-utils/src/index.ts | 14 ++++++++++---- packages/tests/src/model.ts | 29 +++++++++++++++++++++++------ 7 files changed, 77 insertions(+), 24 deletions(-) diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 3644f34b..7185a55b 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,7 +1,7 @@ -import { Binary, clone, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit' +import { clone, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit' import { Context } from 'cordis' -import { Eval, isEvalExpr, Update } from './eval.ts' -import { DeepPartial, FlatKeys, Flatten, Keys, Row, unravel } from './utils.ts' +import { Eval, Update } from './eval.ts' +import { DeepPartial, FlatKeys, Flatten, isFlat, Keys, Row, unravel } from './utils.ts' import { Type } from './type.ts' import { Driver } from './driver.ts' import { Query } from './query.ts' @@ -335,7 +335,7 @@ export class Model { const field = fields.find(field => key.startsWith(field + '.')) if (field) { result[key] = value - } else if (!value || typeof value !== 'object' || isEvalExpr(value) || Object.keys(value).length === 0) { + } else if (isFlat(value)) { if (strict && (typeof value !== 'object' || Object.keys(value).length)) { throw new TypeError(`unknown field "${key}" in model ${this.name}`) } @@ -367,7 +367,7 @@ export class Model { const field = fields.find(field => fullKey === field || fullKey.startsWith(field + '.')) if (field) { node[segments[0]] = value - } else if (!value || typeof value !== 'object' || isEvalExpr(value) || Array.isArray(value) || Binary.is(value) || Object.keys(value).length === 0) { + } else if (isFlat(value)) { if (strict) { throw new TypeError(`unknown field "${fullKey}" in model ${this.name}`) } else { diff --git a/packages/core/src/query.ts b/packages/core/src/query.ts index 5aef44bf..1bf72b68 100644 --- a/packages/core/src/query.ts +++ b/packages/core/src/query.ts @@ -1,6 +1,6 @@ import { Extract, isNullable } from 'cosmokit' import { Eval, executeEval } from './eval.ts' -import { AtomicTypes, Comparable, Flatten, Indexable, isComparable, makeRegExp, Values } from './utils.ts' +import { AtomicTypes, Comparable, Flatten, flatten, Indexable, isComparable, isFlat, makeRegExp, Values } from './utils.ts' import { Selection } from './selection.ts' export type Query = Query.Expr> | Query.Shorthand | Selection.Callback @@ -148,7 +148,8 @@ export function executeQuery(data: any, query: Query.Expr, ref: string, env: any // execute field query try { - return executeFieldQuery(value, getCell(data, key)) + const flattenQuery = isFlat(query[key]) ? { [key]: query[key] } : flatten(query[key], `${key}.`) + return Object.entries(flattenQuery).every(([key, value]) => executeFieldQuery(value, getCell(data, key))) } catch { return false } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index a30987a2..07f4d472 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,5 +1,5 @@ -import { Intersect, isNullable } from 'cosmokit' -import { Eval } from './eval.ts' +import { Binary, Intersect, isNullable } from 'cosmokit' +import { Eval, isEvalExpr } from './eval.ts' export type Values = S[keyof S] @@ -73,6 +73,17 @@ export function isComparable(value: any): value is Comparable { || value instanceof Date } +export function isFlat(value: any): value is Values { + return !value + || typeof value !== 'object' + || isEvalExpr(value) + || Object.keys(value).length === 0 + || Array.isArray(value) + || value instanceof Date + || value instanceof RegExp + || Binary.isSource(value) +} + const letters = 'abcdefghijklmnopqrstuvwxyz' export function randomId() { @@ -98,6 +109,19 @@ export function unravel(source: object, init?: (value) => any) { return result } +export function flatten(source: object, prefix = '', ignore: (value: any) => boolean = isFlat) { + const result = {} + for (const key in source) { + const value = source[key] + if (ignore(value)) { + result[`${prefix}${key}`] = value + } else { + Object.assign(result, flatten(value, `${prefix}${key}.`)) + } + } + return result +} + export function isEmpty(value: any) { if (isNullable(value)) return true if (typeof value !== 'object') return false diff --git a/packages/mongo/src/builder.ts b/packages/mongo/src/builder.ts index d4009ee1..2af32037 100644 --- a/packages/mongo/src/builder.ts +++ b/packages/mongo/src/builder.ts @@ -1,5 +1,5 @@ import { Dict, isNullable, mapValues } from 'cosmokit' -import { Eval, Field, isAggrExpr, isComparable, isEvalExpr, Model, Query, Selection, Type, unravel } from 'minato' +import { Eval, Field, flatten, isAggrExpr, isComparable, isEvalExpr, isFlat, Model, Query, Selection, Type, unravel } from 'minato' import { Filter, FilterOperators, ObjectId } from 'mongodb' import MongoDriver from '.' @@ -406,10 +406,14 @@ export class Builder { } else if (key === '$expr') { additional.push({ $expr: this.eval(value) }) } else { - const actualKey = this.getActualKey(key) - const query = transformFieldQuery(value, actualKey, additional) - if (query === false) return - if (query !== true) filter[actualKey] = query + const ignore = (value: any) => isFlat(value) || value instanceof ObjectId + const flattenQuery = ignore(value) ? { [key]: value } : flatten(value, `${key}.`, ignore) + for (const key in flattenQuery) { + const value = flattenQuery[key], actualKey = this.getActualKey(key) + const query = transformFieldQuery(value, actualKey, additional) + if (query === false) return + if (query !== true) filter[actualKey] = query + } } } if (additional.length) { diff --git a/packages/postgres/src/builder.ts b/packages/postgres/src/builder.ts index f0d8a711..6a8f6521 100644 --- a/packages/postgres/src/builder.ts +++ b/packages/postgres/src/builder.ts @@ -175,6 +175,7 @@ export class PostgresBuilder extends Builder { private transformType(source: any) { const type = Type.isType(source) ? source : Type.fromTerm(source) if (Field.string.includes(type.type) || typeof source === 'string') return 'text' + else if (['integer', 'unsigned', 'bigint'].includes(type.type) || typeof source === 'bigint') return 'bigint' else if (Field.number.includes(type.type) || typeof source === 'number') return 'double precision' else if (Field.boolean.includes(type.type) || typeof source === 'boolean') return 'boolean' else if (type.type === 'json') return 'jsonb' diff --git a/packages/sql-utils/src/index.ts b/packages/sql-utils/src/index.ts index 624d1759..dfd293f9 100644 --- a/packages/sql-utils/src/index.ts +++ b/packages/sql-utils/src/index.ts @@ -1,5 +1,8 @@ import { Dict, isNullable } from 'cosmokit' -import { Driver, Eval, Field, isAggrExpr, isComparable, isEvalExpr, Model, Modifier, Query, randomId, Selection, Type, unravel } from 'minato' +import { + Driver, Eval, Field, flatten, isAggrExpr, isComparable, isEvalExpr, isFlat, + Model, Modifier, Query, randomId, Selection, Type, unravel, +} from 'minato' export function escapeId(value: string) { return '`' + value + '`' @@ -378,9 +381,12 @@ export class Builder { } else if (key === '$expr') { conditions.push(this.parseEval(query.$expr)) } else { - const model = this.state.tables![this.state.table!] ?? Object.values(this.state.tables!)[0] - const expr = Eval('', [Object.keys(this.state.tables!)[0], key], model.getType(key)!) - conditions.push(this.parseFieldQuery(this.parseEval(expr), query[key])) + const flattenQuery = isFlat(query[key]) ? { [key]: query[key] } : flatten(query[key], `${key}.`) + for (const key in flattenQuery) { + const model = this.state.tables![this.state.table!] ?? Object.values(this.state.tables!)[0] + const expr = Eval('', [Object.keys(this.state.tables!)[0], key], model.getType(key)!) + conditions.push(this.parseFieldQuery(this.parseEval(expr), flattenQuery[key])) + } } } diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index 69ecc770..938cf34f 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -476,11 +476,28 @@ namespace ModelOperations { }, })).to.eventually.have.shape([table[4]]) - // await expect(database.get('dtypes', { - // object: { - // num: 10, - // }, - // })).to.eventually.deep.equal(table[4]) + await expect(database.get('dtypes', { + object: { + num: 10, + }, + })).to.eventually.deep.equal([table[4]]) + + await expect(database.get('dtypes', { + $or: [ + { + object: { + num: 10, + }, + }, + { + object2: { + num: { + $gte: 10, + }, + }, + }, + ], + })).to.eventually.deep.equal([table[4], table[5]]) }) it('recursive type', async () => { @@ -489,7 +506,7 @@ namespace ModelOperations { }) } - export const object = function ObjectFields(database: Database, options: ModelOptions = {}) { + const object = function ObjectFields(database: Database, options: ModelOptions = {}) { const { aggregateNull = true, typeModel = true } = options it('basic', async () => { From cd979cc803c1ada00fca39f84b2e7806c450afd7 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 12:58:59 +0800 Subject: [PATCH 3/9] chore: clean --- packages/postgres/src/builder.ts | 2 +- packages/tests/src/model.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/postgres/src/builder.ts b/packages/postgres/src/builder.ts index 6a8f6521..6f8a20d0 100644 --- a/packages/postgres/src/builder.ts +++ b/packages/postgres/src/builder.ts @@ -227,7 +227,7 @@ export class PostgresBuilder extends Builder { protected encode(value: string, encoded: boolean, pure: boolean = false, type?: Type, outtype?: true | string) { outtype ??= this.transformType(type) - return this.asEncoded((encoded === this.isEncoded() && !pure) ? value // `${value}${typeof outtype === 'string' ? `::${outtype}` : ''}` + return this.asEncoded((encoded === this.isEncoded() && !pure) ? value : encoded ? `to_jsonb(${this.transform(value, type, 'encode')})` : this.transform(`(jsonb_build_object('v', ${value})->>'v')`, type, 'decode') + `${typeof outtype === 'string' ? `::${outtype}` : ''}` , pure ? undefined : encoded) diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index 938cf34f..59944311 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -506,7 +506,7 @@ namespace ModelOperations { }) } - const object = function ObjectFields(database: Database, options: ModelOptions = {}) { + export const object = function ObjectFields(database: Database, options: ModelOptions = {}) { const { aggregateNull = true, typeModel = true } = options it('basic', async () => { From bbe9a0d5c84e98efa2df272946655cfbdd949bee Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 14:26:30 +0800 Subject: [PATCH 4/9] fix(sqlite): transform on decode, format query model in upsert --- packages/core/src/database.ts | 8 ++------ packages/core/src/query.ts | 6 +----- packages/core/src/utils.ts | 8 +++++++- packages/sql-utils/src/index.ts | 2 +- packages/sqlite/src/builder.ts | 2 +- packages/sqlite/src/index.ts | 6 +++++- packages/tests/src/model.ts | 10 +++++++--- 7 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index bb2d3127..b06d72b9 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -1,6 +1,6 @@ import { defineProperty, Dict, filterKeys, makeArray, mapValues, MaybeArray, noop, omit } from 'cosmokit' import { Context, Service, Spread } from 'cordis' -import { DeepPartial, FlatKeys, FlatPick, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' +import { DeepPartial, FlatKeys, FlatPick, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' import { Selection } from './selection.ts' import { Field, Model, Relation } from './model.ts' import { Driver } from './driver.ts' @@ -44,10 +44,6 @@ export namespace Join2 { export type Predicate> = (args: Parameters) => Eval.Expr } -function getCell(row: any, key: any): any { - return key.split('.').reduce((r, k) => r[k], row) -} - export class Database extends Service { static [Service.provide] = 'model' static [Service.immediate] = true @@ -476,7 +472,7 @@ export class Database extends Servi const { primary, autoInc, fields } = sel.model if (!autoInc) { const keys = makeArray(primary) - if (keys.some(key => !(key in data))) { + if (keys.some(key => getCell(data, key) === undefined)) { throw new Error('missing primary key') } } diff --git a/packages/core/src/query.ts b/packages/core/src/query.ts index 1bf72b68..4c3b5358 100644 --- a/packages/core/src/query.ts +++ b/packages/core/src/query.ts @@ -1,6 +1,6 @@ import { Extract, isNullable } from 'cosmokit' import { Eval, executeEval } from './eval.ts' -import { AtomicTypes, Comparable, Flatten, flatten, Indexable, isComparable, isFlat, makeRegExp, Values } from './utils.ts' +import { AtomicTypes, Comparable, Flatten, flatten, getCell, Indexable, isComparable, isFlat, makeRegExp, Values } from './utils.ts' import { Selection } from './selection.ts' export type Query = Query.Expr> | Query.Shorthand | Selection.Callback @@ -155,7 +155,3 @@ export function executeQuery(data: any, query: Query.Expr, ref: string, env: any } }) } - -function getCell(row: any, key: any): any { - return key.split('.').reduce((r, k) => r[k], row) -} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 07f4d472..ba267dca 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -70,6 +70,7 @@ export function isComparable(value: any): value is Comparable { return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' + || typeof value === 'bigint' || value instanceof Date } @@ -116,12 +117,17 @@ export function flatten(source: object, prefix = '', ignore: (value: any) => boo if (ignore(value)) { result[`${prefix}${key}`] = value } else { - Object.assign(result, flatten(value, `${prefix}${key}.`)) + Object.assign(result, flatten(value, `${prefix}${key}.`, ignore)) } } return result } +export function getCell(row: any, key: any): any { + if (key in row) return row[key] + return key.split('.').reduce((r, k) => r === undefined ? undefined : r[k], row) +} + export function isEmpty(value: any) { if (isNullable(value)) return true if (typeof value !== 'object') return false diff --git a/packages/sql-utils/src/index.ts b/packages/sql-utils/src/index.ts index dfd293f9..23310cfd 100644 --- a/packages/sql-utils/src/index.ts +++ b/packages/sql-utils/src/index.ts @@ -295,7 +295,7 @@ export class Builder { protected encode(value: string, encoded: boolean, pure: boolean = false, type?: Type) { return this.asEncoded((encoded === this.isEncoded() && !pure) ? value : encoded ? `cast(${this.transform(value, type, 'encode')} as json)` - : `json_unquote(${this.transform(value, type, 'decode')})`, pure ? undefined : encoded) + : this.transform(`json_unquote(${value})`, type, 'decode'), pure ? undefined : encoded) } protected isEncoded(key?: string) { diff --git a/packages/sqlite/src/builder.ts b/packages/sqlite/src/builder.ts index 0cb5e3de..d37b1796 100644 --- a/packages/sqlite/src/builder.ts +++ b/packages/sqlite/src/builder.ts @@ -74,7 +74,7 @@ export class SQLiteBuilder extends Builder { protected encode(value: string, encoded: boolean, pure: boolean = false, type?: Type) { return encoded ? super.encode(value, encoded, pure, type) : (encoded === this.isEncoded() && !pure) ? value - : this.asEncoded(`(${value} ->> '$')`, pure ? undefined : false) + : this.asEncoded(this.transform(`(${value} ->> '$')`, type, 'decode'), pure ? undefined : false) } protected createAggr(expr: any, aggr: (value: string) => string, nonaggr?: (value: string) => string) { diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts index 8a33ad43..52fb9ce3 100644 --- a/packages/sqlite/src/index.ts +++ b/packages/sqlite/src/index.ts @@ -403,7 +403,11 @@ export class SQLiteDriver extends Driver { $or: chunk.map(item => Object.fromEntries(keys.map(key => [key, item[key]]))), }) for (const item of chunk) { - const row = results.find(row => keys.every(key => deepEqual(row[key], item[key], true))) + const row = results.find(row => { + // flatten key to respect model + row = model.format(row) + return keys.every(key => deepEqual(row[key], item[key], true)) + }) if (row) { this._update(sel, keys, updateFields, item, row) result.matched++ diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index 59944311..e41fca49 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -22,6 +22,7 @@ interface DType { embed?: { bool?: boolean bigint?: bigint + int64?: bigint custom?: Custom bstr?: string } @@ -189,6 +190,7 @@ function ModelOperations(database: Database) { type: 'boolean', initial: false, }, + int64: 'bigint', bigint: 'bigint2', custom: { type: 'custom' }, bstr: bstr, @@ -280,7 +282,7 @@ namespace ModelOperations { { id: 2, text: 'pku' }, { id: 3, num: 1989 }, { id: 4, list: ['1', '1', '4'], array: [1, 1, 4] }, - { id: 5, object: { num: 10, text: 'ab', embed: { bool: false, bigint: 90n, bstr: 'world' } } }, + { id: 5, object: { num: 10, text: 'ab', embed: { bool: true, bigint: 90n, int64: 100n, bstr: 'world' } } }, { id: 6, object2: { num: 10, text: 'ab', embed: { bool: false, bigint: 90n } } }, { id: 7, timestamp: magicBorn }, { id: 8, date: magicBorn }, @@ -467,7 +469,7 @@ namespace ModelOperations { it('object query', async () => { const table = await setup(database, 'dtypes', dtypeTable) await expect(database.get('dtypes', { - 'object.num': 10, + 'object.embed.bool': true, })).to.eventually.have.shape([table[4]]) await expect(database.get('dtypes', { @@ -478,7 +480,9 @@ namespace ModelOperations { await expect(database.get('dtypes', { object: { - num: 10, + embed: { + int64: 100n, + }, }, })).to.eventually.deep.equal([table[4]]) From 6aff4028d72632bd023af5f913e9e24808a71781 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 14:28:00 +0800 Subject: [PATCH 5/9] feat: rename manyToMany association table name --- packages/core/src/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 7185a55b..9ee7da83 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -51,7 +51,7 @@ export namespace Relation { } export function buildAssociationTable(...tables: [string, string]) { - return '_' + tables.sort().join('To') + return '_' + tables.sort().join('_') } export function buildAssociationKey(key: string, table: string) { From d08a4389cda204cf91ef08cafe9b8df0156fc817 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 14:35:39 +0800 Subject: [PATCH 6/9] fix(maria): boolean encoding --- packages/mysql/src/builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mysql/src/builder.ts b/packages/mysql/src/builder.ts index d5443518..e2483a94 100644 --- a/packages/mysql/src/builder.ts +++ b/packages/mysql/src/builder.ts @@ -76,8 +76,8 @@ export class MySQLBuilder extends Builder { } this.transformers['boolean'] = { - encode: value => `if(${value}=b'1', 1, 0)`, - decode: value => `if(${value}=1, b'1', b'0')`, + encode: value => `if(${value}=true, 1, 0)`, + decode: value => `if(${value}=1, true, false)`, load: value => isNullable(value) ? value : !!value, dump: value => isNullable(value) ? value : value ? 1 : 0, } From 80fc85b2402b50d4cb75b450e22dcb73580afc4c Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 14:37:59 +0800 Subject: [PATCH 7/9] chore: postgres order --- packages/tests/src/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index e41fca49..11bf0e24 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -501,7 +501,7 @@ namespace ModelOperations { }, }, ], - })).to.eventually.deep.equal([table[4], table[5]]) + })).to.eventually.have.deep.members([table[4], table[5]]) }) it('recursive type', async () => { From 72f47d9e863e301cd28d4bcedcbfb17647da57a6 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 20:32:32 +0800 Subject: [PATCH 8/9] fix(memory): dotted primary --- packages/core/src/model.ts | 2 +- packages/memory/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 9ee7da83..7185a55b 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -51,7 +51,7 @@ export namespace Relation { } export function buildAssociationTable(...tables: [string, string]) { - return '_' + tables.sort().join('_') + return '_' + tables.sort().join('To') } export function buildAssociationKey(key: string, table: string) { diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts index c3237fd7..e6029ad9 100644 --- a/packages/memory/src/index.ts +++ b/packages/memory/src/index.ts @@ -151,7 +151,7 @@ export class MemoryDriver extends Driver { meta.autoInc += 1 data[primary] = meta.autoInc } else { - const duplicated = await this.database.get(table, pick(data, makeArray(primary))) + const duplicated = await this.database.get(table, pick(model.format(data), makeArray(primary))) if (duplicated.length) { throw new RuntimeError('duplicate-entry') } From ca6bda8727a6e7d214e32e438fd2d29489b4b799 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 20 May 2024 20:42:52 +0800 Subject: [PATCH 9/9] test: add modify test --- packages/tests/src/model.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index 11bf0e24..512b747e 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -478,6 +478,17 @@ namespace ModelOperations { }, })).to.eventually.have.shape([table[4]]) + table[4].object!.embed!.bool = false + await expect(database.set('dtypes', { + object: { + embed: { + int64: 100n, + }, + }, + }, { + 'object.embed.bool': false, + })).to.eventually.fulfilled + await expect(database.get('dtypes', { object: { embed: {