@budibase/backend-core 2.29.21 → 2.29.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sql/sql.ts CHANGED
@@ -42,27 +42,28 @@ const envLimit = environment.SQL_MAX_ROWS
42
42
  : null
43
43
  const BASE_LIMIT = envLimit || 5000
44
44
 
45
- function likeKey(client: string | string[], key: string): string {
46
- let start: string, end: string
45
+ // Takes a string like foo and returns a quoted string like [foo] for SQL Server
46
+ // and "foo" for Postgres.
47
+ function quote(client: SqlClient, str: string): string {
47
48
  switch (client) {
48
- case SqlClient.MY_SQL:
49
- start = end = "`"
50
- break
51
49
  case SqlClient.SQL_LITE:
52
50
  case SqlClient.ORACLE:
53
51
  case SqlClient.POSTGRES:
54
- start = end = '"'
55
- break
52
+ return `"${str}"`
56
53
  case SqlClient.MS_SQL:
57
- start = "["
58
- end = "]"
59
- break
60
- default:
61
- throw new Error("Unknown client generating like key")
62
- }
63
- const parts = key.split(".")
64
- key = parts.map(part => `${start}${part}${end}`).join(".")
54
+ return `[${str}]`
55
+ case SqlClient.MY_SQL:
56
+ return `\`${str}\``
57
+ }
58
+ }
59
+
60
+ // Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c]
61
+ // for SQL Server and `a`.`b`.`c` for MySQL.
62
+ function quotedIdentifier(client: SqlClient, key: string): string {
65
63
  return key
64
+ .split(".")
65
+ .map(part => quote(client, part))
66
+ .join(".")
66
67
  }
67
68
 
68
69
  function parse(input: any) {
@@ -113,34 +114,81 @@ function generateSelectStatement(
113
114
  knex: Knex
114
115
  ): (string | Knex.Raw)[] | "*" {
115
116
  const { resource, meta } = json
117
+ const client = knex.client.config.client as SqlClient
116
118
 
117
119
  if (!resource || !resource.fields || resource.fields.length === 0) {
118
120
  return "*"
119
121
  }
120
122
 
121
- const schema = meta?.table?.schema
123
+ const schema = meta.table.schema
122
124
  return resource.fields.map(field => {
123
- const fieldNames = field.split(/\./g)
124
- const tableName = fieldNames[0]
125
- const columnName = fieldNames[1]
126
- const columnSchema = schema?.[columnName]
127
- if (columnSchema && knex.client.config.client === SqlClient.POSTGRES) {
128
- const externalType = schema[columnName].externalType
129
- if (externalType?.includes("money")) {
130
- return knex.raw(
131
- `"${tableName}"."${columnName}"::money::numeric as "${field}"`
132
- )
133
- }
125
+ const parts = field.split(/\./g)
126
+ let table: string | undefined = undefined
127
+ let column: string | undefined = undefined
128
+
129
+ // Just a column name, e.g.: "column"
130
+ if (parts.length === 1) {
131
+ column = parts[0]
132
+ }
133
+
134
+ // A table name and a column name, e.g.: "table.column"
135
+ if (parts.length === 2) {
136
+ table = parts[0]
137
+ column = parts[1]
138
+ }
139
+
140
+ // A link doc, e.g.: "table.doc1.fieldName"
141
+ if (parts.length > 2) {
142
+ table = parts[0]
143
+ column = parts.slice(1).join(".")
144
+ }
145
+
146
+ if (!column) {
147
+ throw new Error(`Invalid field name: ${field}`)
134
148
  }
149
+
150
+ const columnSchema = schema[column]
151
+
152
+ if (
153
+ client === SqlClient.POSTGRES &&
154
+ columnSchema?.externalType?.includes("money")
155
+ ) {
156
+ return knex.raw(
157
+ `${quotedIdentifier(
158
+ client,
159
+ [table, column].join(".")
160
+ )}::money::numeric as ${quote(client, field)}`
161
+ )
162
+ }
163
+
135
164
  if (
136
- knex.client.config.client === SqlClient.MS_SQL &&
165
+ client === SqlClient.MS_SQL &&
137
166
  columnSchema?.type === FieldType.DATETIME &&
138
167
  columnSchema.timeOnly
139
168
  ) {
140
- // Time gets returned as timestamp from mssql, not matching the expected HH:mm format
169
+ // Time gets returned as timestamp from mssql, not matching the expected
170
+ // HH:mm format
141
171
  return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`)
142
172
  }
143
- return `${field} as ${field}`
173
+
174
+ // There's at least two edge cases being handled in the expression below.
175
+ // 1. The column name could start/end with a space, and in that case we
176
+ // want to preseve that space.
177
+ // 2. Almost all column names are specified in the form table.column, except
178
+ // in the case of relationships, where it's table.doc1.column. In that
179
+ // case, we want to split it into `table`.`doc1.column` for reasons that
180
+ // aren't actually clear to me, but `table`.`doc1` breaks things with the
181
+ // sample data tests.
182
+ if (table) {
183
+ return knex.raw(
184
+ `${quote(client, table)}.${quote(client, column)} as ${quote(
185
+ client,
186
+ field
187
+ )}`
188
+ )
189
+ } else {
190
+ return knex.raw(`${quote(client, field)} as ${quote(client, field)}`)
191
+ }
144
192
  })
145
193
  }
146
194
 
@@ -173,9 +221,9 @@ function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
173
221
  }
174
222
 
175
223
  class InternalBuilder {
176
- private readonly client: string
224
+ private readonly client: SqlClient
177
225
 
178
- constructor(client: string) {
226
+ constructor(client: SqlClient) {
179
227
  this.client = client
180
228
  }
181
229
 
@@ -250,9 +298,10 @@ class InternalBuilder {
250
298
  } else {
251
299
  const rawFnc = `${fnc}Raw`
252
300
  // @ts-ignore
253
- query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
254
- `%${value.toLowerCase()}%`,
255
- ])
301
+ query = query[rawFnc](
302
+ `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`,
303
+ [`%${value.toLowerCase()}%`]
304
+ )
256
305
  }
257
306
  }
258
307
 
@@ -302,7 +351,10 @@ class InternalBuilder {
302
351
  }
303
352
  statement +=
304
353
  (statement ? andOr : "") +
305
- `COALESCE(LOWER(${likeKey(this.client, key)}), '') LIKE ?`
354
+ `COALESCE(LOWER(${quotedIdentifier(
355
+ this.client,
356
+ key
357
+ )}), '') LIKE ?`
306
358
  }
307
359
 
308
360
  if (statement === "") {
@@ -336,9 +388,10 @@ class InternalBuilder {
336
388
  } else {
337
389
  const rawFnc = `${fnc}Raw`
338
390
  // @ts-ignore
339
- query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
340
- `${value.toLowerCase()}%`,
341
- ])
391
+ query = query[rawFnc](
392
+ `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`,
393
+ [`${value.toLowerCase()}%`]
394
+ )
342
395
  }
343
396
  })
344
397
  }
@@ -376,12 +429,15 @@ class InternalBuilder {
376
429
  const fnc = allOr ? "orWhereRaw" : "whereRaw"
377
430
  if (this.client === SqlClient.MS_SQL) {
378
431
  query = query[fnc](
379
- `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 1`,
432
+ `CASE WHEN ${quotedIdentifier(
433
+ this.client,
434
+ key
435
+ )} = ? THEN 1 ELSE 0 END = 1`,
380
436
  [value]
381
437
  )
382
438
  } else {
383
439
  query = query[fnc](
384
- `COALESCE(${likeKey(this.client, key)} = ?, FALSE)`,
440
+ `COALESCE(${quotedIdentifier(this.client, key)} = ?, FALSE)`,
385
441
  [value]
386
442
  )
387
443
  }
@@ -392,12 +448,15 @@ class InternalBuilder {
392
448
  const fnc = allOr ? "orWhereRaw" : "whereRaw"
393
449
  if (this.client === SqlClient.MS_SQL) {
394
450
  query = query[fnc](
395
- `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 0`,
451
+ `CASE WHEN ${quotedIdentifier(
452
+ this.client,
453
+ key
454
+ )} = ? THEN 1 ELSE 0 END = 0`,
396
455
  [value]
397
456
  )
398
457
  } else {
399
458
  query = query[fnc](
400
- `COALESCE(${likeKey(this.client, key)} != ?, TRUE)`,
459
+ `COALESCE(${quotedIdentifier(this.client, key)} != ?, TRUE)`,
401
460
  [value]
402
461
  )
403
462
  }
@@ -769,7 +828,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
769
828
  private readonly limit: number
770
829
 
771
830
  // pass through client to get flavour of SQL
772
- constructor(client: string, limit: number = BASE_LIMIT) {
831
+ constructor(client: SqlClient, limit: number = BASE_LIMIT) {
773
832
  super(client)
774
833
  this.limit = limit
775
834
  }
@@ -195,14 +195,14 @@ function buildDeleteTable(knex: SchemaBuilder, table: Table): SchemaBuilder {
195
195
  }
196
196
 
197
197
  class SqlTableQueryBuilder {
198
- private readonly sqlClient: string
198
+ private readonly sqlClient: SqlClient
199
199
 
200
200
  // pass through client to get flavour of SQL
201
- constructor(client: string) {
201
+ constructor(client: SqlClient) {
202
202
  this.sqlClient = client
203
203
  }
204
204
 
205
- getSqlClient(): string {
205
+ getSqlClient(): SqlClient {
206
206
  return this.sqlClient
207
207
  }
208
208