Merge pull request #15795 from Budibase/fix/my-sql-ansi-quotes

MySQL ANSI quotes support for queries
This commit is contained in:
Michael Drury 2025-03-24 15:02:42 +00:00 committed by GitHub
commit 449978d8f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 66 additions and 8 deletions

View File

@ -16,7 +16,7 @@ const descriptions = datasourceDescribe({
if (descriptions.length) { if (descriptions.length) {
describe.each(descriptions)( describe.each(descriptions)(
"queries ($dbName)", "queries ($dbName)",
({ config, dsProvider, isOracle, isMSSQL, isPostgres }) => { ({ config, dsProvider, isOracle, isMSSQL, isPostgres, isMySQL }) => {
let rawDatasource: Datasource let rawDatasource: Datasource
let datasource: Datasource let datasource: Datasource
let client: Knex let client: Knex
@ -217,6 +217,38 @@ if (descriptions.length) {
expect(res).toBeDefined() 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", () => { describe("preview", () => {

View File

@ -1,10 +1,30 @@
import { findHBSBlocks } from "@budibase/string-templates" import { findHBSBlocks } from "@budibase/string-templates"
import { DatasourcePlus } from "@budibase/types" import { DatasourcePlus, SourceName } from "@budibase/types"
import sdk from "../../sdk" 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( export async function interpolateSQL(
sourceName: SourceName,
fields: { sql: string; bindings: any[] }, fields: { sql: string; bindings: any[] },
parameters: { [key: string]: any }, parameters: { [key: string]: any },
integration: DatasourcePlus, 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}}' // 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 // 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 // now look within them to see if a binding is used
const charConstBindingMatch = charConstMatch.find((string: any) => const charConstBindingMatch = charConstMatch.find((string: any) =>
string.match(new RegExp(`'[^']*${binding}[^']*'`)) string.match(getBindingWithinConstCharRegex(sourceName, binding))
) )
if (charConstBindingMatch) { if (charConstBindingMatch) {
let [part1, part2] = charConstBindingMatch.split(binding) let [part1, part2] = charConstBindingMatch.split(binding)

View File

@ -112,9 +112,15 @@ class QueryRunner {
let query: Record<string, any> let query: Record<string, any>
// handle SQL injections by interpolating the variables // handle SQL injections by interpolating the variables
if (isSQL(datasourceClone)) { if (isSQL(datasourceClone)) {
query = await interpolateSQL(fieldsClone, enrichedContext, integration, { query = await interpolateSQL(
nullDefaultSupport, datasource.source,
}) fieldsClone,
enrichedContext,
integration,
{
nullDefaultSupport,
}
)
} else { } else {
query = await sdk.queries.enrichContext(fieldsClone, enrichedContext) query = await sdk.queries.enrichContext(fieldsClone, enrichedContext)
} }