From 233924bde90e150d7208d0dc2fa430a2d015b7c0 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 18 May 2026 20:04:24 +0800 Subject: [PATCH] feat(mysql): support charset config, refactor regexp ops --- packages/mysql/src/builder.ts | 35 ++++++++++++++++++++++++++++++-- packages/mysql/src/index.ts | 4 ++-- packages/postgres/src/builder.ts | 1 - packages/sql-utils/src/index.ts | 15 ++------------ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/mysql/src/builder.ts b/packages/mysql/src/builder.ts index b6e490ea..6858bf12 100644 --- a/packages/mysql/src/builder.ts +++ b/packages/mysql/src/builder.ts @@ -1,11 +1,11 @@ import { Builder, escapeId, isBracketed } from '@cordisjs/sql-utils' import { Binary, Dict, isNullable, Time } from 'cosmokit' -import { Driver, Field, isAggrExpr, isEvalExpr, Model, randomId, Selection, Type } from '@cordisjs/plugin-database' +import { Driver, Field, isAggrExpr, isEvalExpr, Model, randomId, RegExpLike, Selection, Type } from '@cordisjs/plugin-database' export interface Compat { maria?: boolean - maria105?: boolean mysql57?: boolean + ci?: boolean timezone?: string } @@ -33,6 +33,21 @@ export class MySQLBuilder extends Builder { super(driver, tables) this._dbTimezone = compat.timezone ?? 'SYSTEM' + 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) + ? '' : this.compat.ci ? 'collate utf8mb4_bin' : 'collate utf8mb4_general_ci'} regexp ${key}` + this.evalOperators.$regex = ([key, value, flags]) => `(${this.parseEval(key)} ${ + ((!!flags?.includes('i') || (value instanceof RegExp && value.flags.includes('i'))) === !!this.compat.ci) + ? '' : this.compat.ci ? 'collate utf8mb4_bin' : 'collate utf8mb4_general_ci' + } regexp ${this.parseEval(value)})` + } else { + this.queryOperators.$regexFor = (key, value) => typeof value === 'string' ? `regexp_like(${this.escape(value)}, ${key}, 'c')` + : `regexp_like(${this.escape(value.input)}, ${key}, ${value.flags?.includes('i') ? `'i'` : `'c'`})` + this.evalOperators.$regex = ([key, value, flags]) => `regexp_like(${this.parseEval(key)}, ${this.parseEval(value)}, ${ + (flags?.includes('i') || (value instanceof RegExp && value.flags.includes('i'))) ? `'i'` : `'c'`})` + } + this.evalOperators.$select = (args) => { if (compat.maria || compat.mysql57) { return this.asEncoded(`json_object(${args.map(arg => this.parseEval(arg, false)).flatMap((x, i) => [`${i}`, x]).join(', ')})`, true) @@ -130,6 +145,22 @@ export class MySQLBuilder extends Builder { } } + protected createRegExpQuery(key: string, value: string | RegExpLike) { + if (this.compat.mysql57) { + if (typeof value !== 'string' && value.flags?.includes('i')) { + return `${key} ${this.compat.ci ? '' : 'collate utf8mb4_general_ci'} regexp ${this.escape(value.source)}` + } else { + return `${key} ${this.compat.ci ? 'collate utf8mb4_bin' : ''} regexp ${this.escape(typeof value === 'string' ? value : value.source)}` + } + } else { + if (typeof value !== 'string' && value.flags?.includes('i')) { + return `${key} regexp ${this.escape('(?i)' + value.source)}` + } else { + return `${key} regexp ${this.escape('(?-i)' + (typeof value === 'string' ? value : value.source))}` + } + } + } + protected createMemberQuery(key: string, value: any, notStr = '') { if (Array.isArray(value) && Array.isArray(value[0]) && (this.compat.maria || this.compat.mysql57)) { const vals = `json_array(${value.map((val: any[]) => `(${this.evalOperators.$select!(val)})`).join(', ')})` diff --git a/packages/mysql/src/index.ts b/packages/mysql/src/index.ts index 605f9f80..3e73d72d 100644 --- a/packages/mysql/src/index.ts +++ b/packages/mysql/src/index.ts @@ -76,11 +76,10 @@ export class MySQLDriver extends Driver { const [version, timezone] = Object.values((await this.query(`SELECT version(), @@GLOBAL.time_zone`))[0]) as string[] // https://jira.mariadb.org/browse/MDEV-30623 this._compat.maria = version.includes('MariaDB') - // https://jira.mariadb.org/browse/MDEV-26506 - this._compat.maria105 = !!version.match(/10.5.\d+-MariaDB/) // For json_table this._compat.mysql57 = !!version.match(/5.7.\d+/) + this._compat.ci = this.config.charset?.toLowerCase().endsWith('ci') this._compat.timezone = timezone if (this._compat.mysql57 || this._compat.maria) { @@ -600,6 +599,7 @@ export namespace MySQLDriver { user: z.string().default('root'), password: z.string().role('secret'), database: z.string().required(), + charset: z.string().default('utf8mb4_general_ci'), }), z.object({ ssl: z.union([ diff --git a/packages/postgres/src/builder.ts b/packages/postgres/src/builder.ts index ff23690a..396e78a3 100644 --- a/packages/postgres/src/builder.ts +++ b/packages/postgres/src/builder.ts @@ -32,7 +32,6 @@ export class PostgresBuilder extends Builder { this.queryOperators = { ...this.queryOperators, - $regex: (key, value) => this.createRegExpQuery(key, value), $regexFor: (key, value) => typeof value === 'string' ? `${this.escape(value)} ~ ${key}` : `${this.escape(value.input)} ${value.flags?.includes('i') ? '~*' : '~'} ${key}`, $size: (key, value) => { diff --git a/packages/sql-utils/src/index.ts b/packages/sql-utils/src/index.ts index d9f0e008..94d76c3b 100644 --- a/packages/sql-utils/src/index.ts +++ b/packages/sql-utils/src/index.ts @@ -56,7 +56,7 @@ interface State { wrappedSubquery?: boolean } -export class Builder { +export abstract class Builder { protected escapeMap = {} protected escapeRegExp?: RegExp protected createEqualQuery = this.comparator('=') @@ -94,8 +94,6 @@ export class Builder { // regexp $regex: (key, value) => this.createRegExpQuery(key, value), - $regexFor: (key, value) => typeof value === 'string' ? `${this.escape(value)} collate utf8mb4_bin regexp ${key}` - : `${this.escape(value.input)} ${value.flags?.includes('i') ? 'regexp' : 'collate utf8mb4_bin regexp'} ${key}`, // bitwise $bitsAllSet: (key, value) => `${key} & ${this.escape(value)} = ${this.escape(value)}`, @@ -149,9 +147,6 @@ export class Builder { // string $concat: (args) => `concat(${args.map(arg => this.parseEval(arg)).join(', ')})`, - $regex: ([key, value, flags]) => `(${this.parseEval(key)} ${ - (flags?.includes('i') || (value instanceof RegExp && value.flags.includes('i'))) ? 'regexp' : 'collate utf8mb4_bin regexp' - } ${this.parseEval(value)})`, // logical / bitwise $or: (args) => { @@ -226,13 +221,7 @@ export class Builder { } } - protected createRegExpQuery(key: string, value: string | RegExpLike) { - if (typeof value !== 'string' && value.flags?.includes('i')) { - return `${key} regexp ${this.escape(value.source)}` - } else { - return `${key} collate utf8mb4_bin regexp ${this.escape(typeof value === 'string' ? value : value.source)}` - } - } + protected abstract createRegExpQuery(key: string, value: string | RegExpLike): string protected listContains(list: any, value: string) { return `find_in_set(${value}, ${list})`