This commit is contained in:
Sam Rose 2024-07-17 15:45:35 +01:00
parent 5b62ea529a
commit aea9cda8f5
No known key found for this signature in database
3 changed files with 90 additions and 52 deletions

View File

@ -42,27 +42,28 @@ const envLimit = environment.SQL_MAX_ROWS
: null : null
const BASE_LIMIT = envLimit || 5000 const BASE_LIMIT = envLimit || 5000
function likeKey(client: string | string[], key: string): string { // Takes a string like foo and returns a quoted string like [foo] for SQL Server
let start: string, end: string // and "foo" for Postgres.
function quote(client: SqlClient, str: string) {
switch (client) { switch (client) {
case SqlClient.MY_SQL:
start = end = "`"
break
case SqlClient.SQL_LITE: case SqlClient.SQL_LITE:
case SqlClient.ORACLE: case SqlClient.ORACLE:
case SqlClient.POSTGRES: case SqlClient.POSTGRES:
start = end = '"' return `"${str}"`
break
case SqlClient.MS_SQL: case SqlClient.MS_SQL:
start = "[" return `[${str}]`
end = "]"
break
default: default:
throw new Error("Unknown client generating like key") return `\`${str}\``
} }
const parts = key.split(".") }
key = parts.map(part => `${start}${part}${end}`).join(".")
// Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c]
// for SQL Server and `a`.`b`.`c` for MySQL.
function quotedIdentifier(client: SqlClient, key: string): string {
return key return key
.split(".")
.map(part => quote(client, part))
.join(".")
} }
function parse(input: any) { function parse(input: any) {
@ -113,34 +114,37 @@ function generateSelectStatement(
knex: Knex knex: Knex
): (string | Knex.Raw)[] | "*" { ): (string | Knex.Raw)[] | "*" {
const { resource, meta } = json const { resource, meta } = json
const client = knex.client.config.client as SqlClient
if (!resource || !resource.fields || resource.fields.length === 0) { if (!resource || !resource.fields || resource.fields.length === 0) {
return "*" return "*"
} }
const schema = meta?.table?.schema const schema = meta.table.schema
return resource.fields.map(field => { return resource.fields.map(field => {
const fieldNames = field.split(/\./g) const [table, column, ..._rest] = field.split(/\./g)
const tableName = fieldNames[0] if (
const columnName = fieldNames[1] client === SqlClient.POSTGRES &&
const columnSchema = schema?.[columnName] schema[column].externalType?.includes("money")
if (columnSchema && knex.client.config.client === SqlClient.POSTGRES) { ) {
const externalType = schema[columnName].externalType return knex.raw(`"${table}"."${column}"::money::numeric as "${field}"`)
if (externalType?.includes("money")) {
return knex.raw(
`"${tableName}"."${columnName}"::money::numeric as "${field}"`
)
}
} }
if ( if (
knex.client.config.client === SqlClient.MS_SQL && client === SqlClient.MS_SQL &&
columnSchema?.type === FieldType.DATETIME && schema[column]?.type === FieldType.DATETIME &&
columnSchema.timeOnly schema[column].timeOnly
) { ) {
// Time gets returned as timestamp from mssql, not matching the expected HH:mm format // Time gets returned as timestamp from mssql, not matching the expected
// HH:mm format
return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`) return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`)
} }
return `${field} as ${field}` return `${field} as ${field}`
// return knex.raw(
// `${quote(client, table)}.${quote(client, column)} as ${quote(
// client,
// field
// )}`
// )
}) })
} }
@ -173,9 +177,9 @@ function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
} }
class InternalBuilder { class InternalBuilder {
private readonly client: string private readonly client: SqlClient
constructor(client: string) { constructor(client: SqlClient) {
this.client = client this.client = client
} }
@ -250,9 +254,10 @@ class InternalBuilder {
} else { } else {
const rawFnc = `${fnc}Raw` const rawFnc = `${fnc}Raw`
// @ts-ignore // @ts-ignore
query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ query = query[rawFnc](
`%${value.toLowerCase()}%`, `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`,
]) [`%${value.toLowerCase()}%`]
)
} }
} }
@ -302,7 +307,10 @@ class InternalBuilder {
} }
statement += statement +=
(statement ? andOr : "") + (statement ? andOr : "") +
`COALESCE(LOWER(${likeKey(this.client, key)}), '') LIKE ?` `COALESCE(LOWER(${quotedIdentifier(
this.client,
key
)}), '') LIKE ?`
} }
if (statement === "") { if (statement === "") {
@ -336,9 +344,10 @@ class InternalBuilder {
} else { } else {
const rawFnc = `${fnc}Raw` const rawFnc = `${fnc}Raw`
// @ts-ignore // @ts-ignore
query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ query = query[rawFnc](
`${value.toLowerCase()}%`, `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`,
]) [`${value.toLowerCase()}%`]
)
} }
}) })
} }
@ -376,12 +385,15 @@ class InternalBuilder {
const fnc = allOr ? "orWhereRaw" : "whereRaw" const fnc = allOr ? "orWhereRaw" : "whereRaw"
if (this.client === SqlClient.MS_SQL) { if (this.client === SqlClient.MS_SQL) {
query = query[fnc]( query = query[fnc](
`CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 1`, `CASE WHEN ${quotedIdentifier(
this.client,
key
)} = ? THEN 1 ELSE 0 END = 1`,
[value] [value]
) )
} else { } else {
query = query[fnc]( query = query[fnc](
`COALESCE(${likeKey(this.client, key)} = ?, FALSE)`, `COALESCE(${quotedIdentifier(this.client, key)} = ?, FALSE)`,
[value] [value]
) )
} }
@ -392,12 +404,15 @@ class InternalBuilder {
const fnc = allOr ? "orWhereRaw" : "whereRaw" const fnc = allOr ? "orWhereRaw" : "whereRaw"
if (this.client === SqlClient.MS_SQL) { if (this.client === SqlClient.MS_SQL) {
query = query[fnc]( query = query[fnc](
`CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 0`, `CASE WHEN ${quotedIdentifier(
this.client,
key
)} = ? THEN 1 ELSE 0 END = 0`,
[value] [value]
) )
} else { } else {
query = query[fnc]( query = query[fnc](
`COALESCE(${likeKey(this.client, key)} != ?, TRUE)`, `COALESCE(${quotedIdentifier(this.client, key)} != ?, TRUE)`,
[value] [value]
) )
} }
@ -769,7 +784,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
private readonly limit: number private readonly limit: number
// pass through client to get flavour of SQL // pass through client to get flavour of SQL
constructor(client: string, limit: number = BASE_LIMIT) { constructor(client: SqlClient, limit: number = BASE_LIMIT) {
super(client) super(client)
this.limit = limit this.limit = limit
} }

View File

@ -195,14 +195,14 @@ function buildDeleteTable(knex: SchemaBuilder, table: Table): SchemaBuilder {
} }
class SqlTableQueryBuilder { class SqlTableQueryBuilder {
private readonly sqlClient: string private readonly sqlClient: SqlClient
// pass through client to get flavour of SQL // pass through client to get flavour of SQL
constructor(client: string) { constructor(client: SqlClient) {
this.sqlClient = client this.sqlClient = client
} }
getSqlClient(): string { getSqlClient(): SqlClient {
return this.sqlClient return this.sqlClient
} }

View File

@ -38,13 +38,13 @@ import { structures } from "@budibase/backend-core/tests"
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default" import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
describe.each([ describe.each([
["in-memory", undefined], // ["in-memory", undefined],
["lucene", undefined], // ["lucene", undefined],
["sqs", undefined], ["sqs", undefined],
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
])("search (%s)", (name, dsProvider) => { ])("search (%s)", (name, dsProvider) => {
const isSqs = name === "sqs" const isSqs = name === "sqs"
const isLucene = name === "lucene" const isLucene = name === "lucene"
@ -735,6 +735,29 @@ describe.each([
query: {}, query: {},
}).toHaveLength(1) }).toHaveLength(1)
}) })
isInternal &&
describe("space at end of column name", () => {
beforeAll(async () => {
table = await createTable({
"name ": {
name: "name ",
type: FieldType.STRING,
},
})
await createRows([{ ["name "]: "foo" }, { ["name "]: "bar" }])
})
it("should be able to query a column that starts with a space", async () => {
await expectSearch({
query: {
string: {
"1:name ": "foo",
},
},
}).toContainExactly([{ ["name "]: "foo" }])
})
})
}) })
describe("equal", () => { describe("equal", () => {