diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index b579454f3a..684f276145 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -99,6 +99,23 @@ function isSqs(table: Table): boolean { ) } +function escapeQuotes(value: string, quoteChar = '"'): string { + return value.replace(new RegExp(quoteChar, "g"), `${quoteChar}${quoteChar}`) +} + +function wrap(value: string, quoteChar = '"'): string { + return `${quoteChar}${escapeQuotes(value, quoteChar)}${quoteChar}` +} + +function stringifyArray(value: any[], quoteStyle = '"'): string { + for (let i in value) { + if (typeof value[i] === "string") { + value[i] = wrap(value[i], quoteStyle) + } + } + return `[${value.join(",")}]` +} + const allowEmptyRelationships: Record = { [BasicOperator.EQUAL]: false, [BasicOperator.NOT_EQUAL]: true, @@ -194,6 +211,15 @@ class InternalBuilder { return key.map(part => this.quote(part)).join(".") } + private quotedValue(value: string): string { + const formatter = this.knexClient.formatter(this.knexClient.queryBuilder()) + return formatter.wrap(value, false) + } + + private rawQuotedValue(value: string): Knex.Raw { + return this.knex.raw(this.quotedValue(value)) + } + // Unfortuantely we cannot rely on knex's identifier escaping because it trims // the identifier string before escaping it, which breaks cases for us where // columns that start or end with a space aren't referenced correctly anymore. @@ -682,25 +708,20 @@ class InternalBuilder { return q } - function stringifyArray(value: any[], quoteStyle = '"'): string { - for (let i in value) { - if (typeof value[i] === "string") { - value[i] = `${quoteStyle}${value[i]}${quoteStyle}` - } - } - return `[${value.join(",")}]` - } - if (this.client === SqlClient.POSTGRES) { iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => { - const wrap = any ? "" : "'" - const op = any ? "\\?| array" : "@>" - - const stringifiedArray = stringifyArray(value, any ? "'" : '"') - return addModifiers(q).whereRaw( - `COALESCE(??::jsonb ${op} ${wrap}${stringifiedArray}${wrap}, FALSE)`, - [this.rawQuotedIdentifier(key)] - ) + q = addModifiers(q) + if (any) { + return q.whereRaw(`COALESCE(??::jsonb \\?| array??, FALSE)`, [ + this.rawQuotedIdentifier(key), + this.knex.raw(stringifyArray(value, "'")), + ]) + } else { + return q.whereRaw(`COALESCE(??::jsonb @> '??', FALSE)`, [ + this.rawQuotedIdentifier(key), + this.knex.raw(stringifyArray(value)), + ]) + } }) } else if ( this.client === SqlClient.MY_SQL || @@ -710,7 +731,7 @@ class InternalBuilder { return addModifiers(q).whereRaw(`COALESCE(?(??, ?), FALSE)`, [ this.knex.raw(any ? "JSON_OVERLAPS" : "JSON_CONTAINS"), this.rawQuotedIdentifier(key), - stringifyArray(value), + this.knex.raw(wrap(stringifyArray(value))), ]) }) } else { diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 74bf0eaf53..b22b1545be 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -793,7 +793,7 @@ describe.each([ }) const stringTypes = [FieldType.STRING, FieldType.LONGFORM] as const - describe.only.each(stringTypes)("%s", type => { + describe.each(stringTypes)("%s", type => { beforeAll(async () => { tableOrViewId = await createTableOrView({ name: { name: "name", type }, @@ -2163,7 +2163,7 @@ describe.each([ }) }) - describe("multi user", () => { + describe.only("multi user", () => { let user1: User let user2: User