Skip to content
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
2 changes: 2 additions & 0 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export namespace Eval {
concat: Multi<string, string>
regex<A extends boolean>(x: Term<string, A>, y: RegExp): Expr<boolean, A>
regex<A extends boolean>(x: Term<string, A>, y: Term<string, A>, flags?: string): Expr<boolean, A>
startsWith<A extends boolean>(x: Term<string, A>, y: Term<string, A>): Expr<boolean, A>

// logical / bitwise
and: Multi<boolean, boolean> & Multi<number, number> & Multi<bigint, bigint>
Expand Down Expand Up @@ -250,6 +251,7 @@ operators.$nin = ([value, array], data) => {
// string
Eval.concat = multary('concat', (args, data) => args.map(arg => executeEval(data, arg)).join(''), Type.String)
Eval.regex = multary('regex', ([value, regex, flags], data) => makeRegExp(executeEval(data, regex), flags).test(executeEval(data, value)), Type.Boolean)
Eval.startsWith = comparator('startsWith', (str, prefix) => str.startsWith(prefix))

// logical / bitwise
Eval.and = multary('and', (args, data) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export namespace Query {
// regexp
$regex?: Extract<T, string, string | RegExpLike>
$regexFor?: Extract<T, string, string | { input: string; flags?: string }>
$startsWith?: Extract<T, string, string>

// bitwise
$bitsAllClear?: Extract<T, number>
Expand Down Expand Up @@ -99,6 +100,7 @@ const queryOperators: QueryOperators = {
// regexp
$regex: (query, data) => makeRegExp(query).test(data),
$regexFor: (query, data) => typeof query === 'string' ? makeRegExp(data).test(query) : makeRegExp(data, query.flags).test(query.input),
$startsWith: (query, data) => data.startsWith(query),

// bitwise
$bitsAllSet: (query, data) => (query & data) === query,
Expand Down
12 changes: 12 additions & 0 deletions packages/mongo/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ export class Builder {
},
}),

$startsWith: ([str, prefix], group) => {
const input = this.eval(str, group)
if (typeof prefix === 'string') {
const escaped = '^' + String(prefix).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
return { $regexMatch: { input, regex: { $literal: escaped } } }
}
const p = this.eval(prefix, group)
return { $eq: [{ $substrCP: [input, 0, { $strLenCP: p }] }, p] }
},

$length: (arg, group) => ({ $size: this.eval(arg, group) }),
$nin: (arg, group) => ({ $not: { $in: arg.map(val => this.eval(val, group)) } }),

Expand Down Expand Up @@ -315,6 +325,8 @@ export class Builder {
},
},
})
} else if (prop === '$startsWith') {
return { $regex: '^' + String(query[prop]).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') }
} else if (prop === '$exists') {
if (query[prop]) return { $ne: null }
else return null
Expand Down
15 changes: 15 additions & 0 deletions packages/mysql/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ export class MySQLBuilder extends Builder {
super(driver, tables)
this._dbTimezone = compat.timezone ?? 'SYSTEM'

this.queryOperators.$startsWith = (key, value) => {
const escaped = String(value).replace(/[!%_]/g, '!$&')
return `${key} LIKE BINARY ${this.escape(escaped + '%')} ESCAPE '!'`
}

this.evalOperators.$startsWith = ([key, prefix]) => {
const k = this.parseEval(key)
if (typeof prefix === 'string') {
const escaped = String(prefix).replace(/[!%_]/g, '!$&')
return `${k} LIKE BINARY ${this.escape(escaped + '%')} ESCAPE '!'`
}
const p = this.parseEval(prefix)
return `${k} LIKE BINARY concat(replace(replace(replace(${p}, '!', '!!'), '%', '!%'), '_', '!_'), '%') ESCAPE '!'`
}

if (this.compat.mysql57 || this.compat.maria) {
this.queryOperators.$regexFor = (key, value) => typeof value === 'string' ? `${this.escape(value)} ${this.compat.ci
? 'collate utf8mb4_bin' : ''} regexp ${key}` : `${this.escape(value.input)} ${(!!value.flags?.includes('i') === !!this.compat.ci)
Expand Down
13 changes: 13 additions & 0 deletions packages/postgres/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class PostgresBuilder extends Builder {
...this.queryOperators,
$regexFor: (key, value) => typeof value === 'string' ? `${this.escape(value)} ~ ${key}`
: `${this.escape(value.input)} ${value.flags?.includes('i') ? '~*' : '~'} ${key}`,
$startsWith: (key, value) => {
const escaped = String(value).replace(/[!%_]/g, '!$&')
return `${key} LIKE ${this.escape(escaped + '%')} ESCAPE '!'`
},
$size: (key, value) => {
if (this.isJsonQuery(key)) {
return `${this.jsonLength(key)} = ${this.escape(value)}`
Expand All @@ -59,6 +63,15 @@ export class PostgresBuilder extends Builder {
$regex: ([key, value, flags]) => `(${this.parseEval(key)} ${
(flags?.includes('i') || (value instanceof RegExp && value.flags.includes('i'))) ? '~*' : '~'
} ${this.parseEval(value)})`,
$startsWith: ([key, prefix]) => {
const k = this.parseEval(key)
if (typeof prefix === 'string') {
const escaped = String(prefix).replace(/[!%_]/g, '!$&')
return `${k} LIKE ${this.escape(escaped + '%')} ESCAPE '!'`
}
const p = this.parseEval(prefix)
return `${k} LIKE concat(replace(replace(replace(${p}, '!', '!!'), '%', '!%'), '_', '!_'), '%') ESCAPE '!'`
},

// number
$add: (args) => `(${args.map(arg => this.parseEval(arg, 'double precision')).join(' + ')})`,
Expand Down
14 changes: 14 additions & 0 deletions packages/sqlite/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,26 @@ export class SQLiteBuilder extends Builder {
: value.flags?.includes('i') ? `regexp2(${key}, ${this.escape(value.input)}, 'i')`
: `${this.escape(value.input)} regexp ${key}`

this.queryOperators.$startsWith = (key, value) => {
const escaped = String(value).replace(/[*?[]/g, '[$&]')
return `${key} GLOB ${this.escape(escaped + '*')}`
}

this.evalOperators.$if = (args) => `iif(${args.map(arg => this.parseEval(arg)).join(', ')})`
this.evalOperators.$regex = ([key, value, flags]) => (flags?.includes('i') || (value instanceof RegExp && value.flags?.includes('i')))
? `regexp2(${this.parseEval(value)}, ${this.parseEval(key)}, ${this.escape(flags ?? (value as any).flags)})`
: `regexp(${this.parseEval(value)}, ${this.parseEval(key)})`

this.evalOperators.$concat = (args) => `(${args.map(arg => this.parseEval(arg)).join('||')})`
this.evalOperators.$startsWith = ([key, prefix]) => {
const k = this.parseEval(key)
if (typeof prefix === 'string') {
const escaped = String(prefix).replace(/[*?[]/g, '[$&]')
return `${k} GLOB ${this.escape(escaped + '*')}`
}
const p = this.parseEval(prefix)
return `${k} GLOB replace(replace(replace(${p}, '[', '[[]'), '*', '[*]'), '?', '[?]') || '*'`
}
this.evalOperators.$modulo = ([left, right]) => `modulo(${this.parseEval(left)}, ${this.parseEval(right)})`
this.evalOperators.$log = ([left, right]) => isNullable(right)
? `ln(${this.parseEval(left)})`
Expand Down
56 changes: 56 additions & 0 deletions packages/tests/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface Foo {
date?: Date
time?: Date
regex?: string
pattern?: string
}

declare module '@cordisjs/plugin-database' {
Expand All @@ -31,6 +32,7 @@ function QueryOperators(database: Database) {
date: 'date',
time: 'time',
regex: 'string',
pattern: 'string',
}, {
autoInc: true,
})
Expand Down Expand Up @@ -275,6 +277,60 @@ namespace QueryOperators {
})
}

export const text = function Text(database: Database) {
beforeAll(async () => {
await database.remove('temp1', {})
await database.create('temp1', { text: 'hello world', pattern: 'hello' })
await database.create('temp1', { text: '100%juice', pattern: '100%' })
await database.create('temp1', { text: 'a_b', pattern: 'a_' })
await database.create('temp1', { text: '!important', pattern: '!' })
await database.create('temp1', { text: '!%_test', pattern: '!%_' })
await database.create('temp1', { text: 'Hello World', pattern: 'Hello' })
await database.create('temp1', { text: '*.log', pattern: '*' })
await database.create('temp1', { text: '?query', pattern: '?' })
await database.create('temp1', { text: '[tag]', pattern: '[' })
await database.create('temp1', { text: '.hidden', pattern: '.' })
await database.create('temp1', { text: '^anchor', pattern: '^' })
await database.create('temp1', { text: '$money', pattern: '$' })
})

it('$.startsWith', async () => {
await expect(database.get('temp1', row => $.startsWith(row.text, 'hello'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '100%'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, 'a_'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '!'))).eventually.to.have.length(2)
await expect(database.get('temp1', row => $.startsWith(row.text, '!%_'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, 'Hello'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, 'hello'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '.'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '^'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '$'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '*'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '?'))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, '['))).eventually.to.have.length(1)
await expect(database.get('temp1', row => $.startsWith(row.text, 'nomatch'))).eventually.to.have.length(0)
await expect(database.get('temp1', row => $.startsWith(row.text, row.pattern))).eventually.to.have.length(12)
await expect(database.get('temp1', row => $.startsWith('hello world', row.pattern))).eventually.to.have.length(1)
})

it('$startsWith', async () => {
await expect(database.get('temp1', { text: { $startsWith: 'hello' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '100%' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: 'a_' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '!' } })).eventually.to.have.length(2)
await expect(database.get('temp1', { text: { $startsWith: '!%_' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: 'Hello' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: 'hello' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '.' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '^' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '$' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '*' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '?' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: '[' } })).eventually.to.have.length(1)
await expect(database.get('temp1', { text: { $startsWith: 'nomatch' } })).eventually.to.have.length(0)
})
}

export const bitwise = function Bitwise(database: Database) {
beforeAll(async () => {
await database.remove('temp1', {})
Expand Down