diff --git a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts index 4a545b253e..59a9b98ff9 100644 --- a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts +++ b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts @@ -16,7 +16,7 @@ const descriptions = datasourceDescribe({ if (descriptions.length) { describe.each(descriptions)( "queries ($dbName)", - ({ config, dsProvider, isOracle, isMSSQL, isPostgres }) => { + ({ config, dsProvider, isOracle, isMSSQL, isPostgres, isMySQL }) => { let rawDatasource: Datasource let datasource: Datasource let client: Knex @@ -217,6 +217,38 @@ if (descriptions.length) { expect(res).toBeDefined() }) }) + + isMySQL && + it("should handle ANSI_QUOTE=off MySQL queries with bindings", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName) + .select("*") + .where({ + name: client.raw("'{{ name }}'"), + }) + .toString(), + }, + parameters: [ + { + name: "name", + default: "", + }, + ], + queryVerb: "read", + }) + const res = await config.api.query.execute( + query._id!, + { + parameters: { name: "one" }, + }, + { + status: 200, + } + ) + expect(res.data.length).toEqual(1) + expect(res.data[0].name).toEqual("one") + }) }) describe("preview", () => { diff --git a/packages/server/src/integrations/queries/sql.ts b/packages/server/src/integrations/queries/sql.ts index f6b0d68e7f..4b6fd378fb 100644 --- a/packages/server/src/integrations/queries/sql.ts +++ b/packages/server/src/integrations/queries/sql.ts @@ -1,10 +1,30 @@ import { findHBSBlocks } from "@budibase/string-templates" -import { DatasourcePlus } from "@budibase/types" +import { DatasourcePlus, SourceName } from "@budibase/types" import sdk from "../../sdk" -const CONST_CHAR_REGEX = new RegExp("'[^']*'", "g") +function getConstCharRegex(sourceName: SourceName) { + // MySQL clients support ANSI_QUOTES mode off, this is by default + // but " and ' count as string literals + if (sourceName === SourceName.MYSQL) { + return new RegExp(`"[^"]*"|'[^']*'`, "g") + } else { + return new RegExp(`'[^']*'`, "g") + } +} + +function getBindingWithinConstCharRegex( + sourceName: SourceName, + binding: string +) { + if (sourceName === SourceName.MYSQL) { + return new RegExp(`[^']*${binding}[^']*'|"[^"]*${binding}[^"]*"`, "g") + } else { + return new RegExp(`'[^']*${binding}[^']*'`) + } +} export async function interpolateSQL( + sourceName: SourceName, fields: { sql: string; bindings: any[] }, parameters: { [key: string]: any }, integration: DatasourcePlus, @@ -24,10 +44,10 @@ export async function interpolateSQL( ) // check if the variable was used as part of a string concat e.g. 'Hello {{binding}}' // start by finding all the instances of const character strings - const charConstMatch = sql.match(CONST_CHAR_REGEX) || [] + const charConstMatch = sql.match(getConstCharRegex(sourceName)) || [] // now look within them to see if a binding is used const charConstBindingMatch = charConstMatch.find((string: any) => - string.match(new RegExp(`'[^']*${binding}[^']*'`)) + string.match(getBindingWithinConstCharRegex(sourceName, binding)) ) if (charConstBindingMatch) { let [part1, part2] = charConstBindingMatch.split(binding) diff --git a/packages/server/src/threads/query.ts b/packages/server/src/threads/query.ts index 3ba4995b2c..59c4b6dc89 100644 --- a/packages/server/src/threads/query.ts +++ b/packages/server/src/threads/query.ts @@ -112,9 +112,15 @@ class QueryRunner { let query: Record // handle SQL injections by interpolating the variables if (isSQL(datasourceClone)) { - query = await interpolateSQL(fieldsClone, enrichedContext, integration, { - nullDefaultSupport, - }) + query = await interpolateSQL( + datasource.source, + fieldsClone, + enrichedContext, + integration, + { + nullDefaultSupport, + } + ) } else { query = await sdk.queries.enrichContext(fieldsClone, enrichedContext) }