diff --git a/packages/mysql/src/builder.ts b/packages/mysql/src/builder.ts index 3b3cae7a..704b48d7 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 { bufferToUuid, Driver, Field, isAggrExpr, isEvalExpr, Model, randomId, Selection, Type, uuidToBuffer } from '@cordisjs/plugin-database' +import { bufferToUuid, Driver, Field, isAggrExpr, isEvalExpr, Model, randomId, RegExpLike, Selection, Type, uuidToBuffer } from '@cordisjs/plugin-database' export interface Compat { maria?: boolean - maria105?: boolean mysql57?: boolean + ci?: boolean uuid?: boolean timezone?: string } @@ -34,6 +34,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) @@ -143,6 +158,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 63dda6b7..61d5c3d2 100644 --- a/packages/mysql/src/index.ts +++ b/packages/mysql/src/index.ts @@ -76,8 +76,6 @@ 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+/) // MariaDB 10.7+ has the native UUID data type @@ -89,6 +87,7 @@ export class MySQLDriver extends Driver { this._compat.uuid = major > 10 || (major === 10 && minor >= 7) } + this._compat.ci = this.config.charset?.toLowerCase().endsWith('ci') this._compat.timezone = timezone this.sql = new MySQLBuilder(this, undefined, this._compat) @@ -641,6 +640,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 ed897a05..d81ee9a3 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 5fde4209..a3656f11 100644 --- a/packages/sql-utils/src/index.ts +++ b/packages/sql-utils/src/index.ts @@ -59,7 +59,7 @@ interface State { wrappedSubquery?: boolean } -export class Builder { +export abstract class Builder { protected escapeMap = {} protected escapeRegExp?: RegExp protected createEqualQuery = this.comparator('=') @@ -97,8 +97,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)}`, @@ -152,9 +150,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) => { @@ -230,13 +225,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})`