Gone some way toward getting time-only fields to work. Still test failures though.

This commit is contained in:
Sam Rose 2024-07-29 16:54:59 +01:00
parent 5cb294f33e
commit ad414b982e
No known key found for this signature in database
4 changed files with 206 additions and 204 deletions

View File

@ -42,10 +42,45 @@ const envLimit = environment.SQL_MAX_ROWS
: null : null
const BASE_LIMIT = envLimit || 5000 const BASE_LIMIT = envLimit || 5000
function getTableName(table?: Table): string | undefined {
// SQS uses the table ID rather than the table name
if (
table?.sourceType === TableSourceType.INTERNAL ||
table?.sourceId === INTERNAL_TABLE_SOURCE_ID
) {
return table?._id
} else {
return table?.name
}
}
function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
if (Array.isArray(query)) {
return query.map((q: SqlQuery) => convertBooleans(q) as SqlQuery)
} else {
if (query.bindings) {
query.bindings = query.bindings.map(binding => {
if (typeof binding === "boolean") {
return binding ? 1 : 0
}
return binding
})
}
}
return query
}
class InternalBuilder {
private readonly client: SqlClient
constructor(client: SqlClient) {
this.client = client
}
// Takes a string like foo and returns a quoted string like [foo] for SQL Server // Takes a string like foo and returns a quoted string like [foo] for SQL Server
// and "foo" for Postgres. // and "foo" for Postgres.
function quote(client: SqlClient, str: string): string { private quote(str: string): string {
switch (client) { switch (this.client) {
case SqlClient.SQL_LITE: case SqlClient.SQL_LITE:
case SqlClient.ORACLE: case SqlClient.ORACLE:
case SqlClient.POSTGRES: case SqlClient.POSTGRES:
@ -59,77 +94,14 @@ function quote(client: SqlClient, str: string): string {
// Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c] // 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. // for SQL Server and `a`.`b`.`c` for MySQL.
function quotedIdentifier(client: SqlClient, key: string): string { private quotedIdentifier(key: string): string {
return key return key
.split(".") .split(".")
.map(part => quote(client, part)) .map(part => this.quote(part))
.join(".") .join(".")
} }
function parse(input: any) { private generateSelectStatement(
if (Array.isArray(input)) {
return JSON.stringify(input)
}
if (input == undefined) {
return null
}
if (typeof input !== "string") {
return input
}
if (isInvalidISODateString(input)) {
return null
}
if (isValidISODateString(input)) {
return new Date(input.trim())
}
return input
}
function parseBody(body: any) {
for (let [key, value] of Object.entries(body)) {
body[key] = parse(value)
}
return body
}
function parseFilters(filters: SearchFilters | undefined): SearchFilters {
if (!filters) {
return {}
}
for (let [key, value] of Object.entries(filters)) {
let parsed
if (typeof value === "object") {
parsed = parseFilters(value)
} else {
parsed = parse(value)
}
// @ts-ignore
filters[key] = parsed
}
return filters
}
// OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
// so when we use them we need to wrap them in to_char(). This function
// converts a field name to the appropriate identifier.
function convertClobs(client: SqlClient, table: Table, field: string): string {
const parts = field.split(".")
const col = parts.pop()!
const schema = table.schema[col]
let identifier = quotedIdentifier(client, field)
if (
schema.type === FieldType.STRING ||
schema.type === FieldType.LONGFORM ||
schema.type === FieldType.BB_REFERENCE_SINGLE ||
schema.type === FieldType.OPTIONS ||
schema.type === FieldType.BARCODEQR
) {
identifier = `to_char(${identifier})`
}
return identifier
}
function generateSelectStatement(
json: QueryJson, json: QueryJson,
knex: Knex knex: Knex
): (string | Knex.Raw)[] | "*" { ): (string | Knex.Raw)[] | "*" {
@ -174,10 +146,9 @@ function generateSelectStatement(
columnSchema?.externalType?.includes("money") columnSchema?.externalType?.includes("money")
) { ) {
return knex.raw( return knex.raw(
`${quotedIdentifier( `${this.quotedIdentifier(
client,
[table, column].join(".") [table, column].join(".")
)}::money::numeric as ${quote(client, field)}` )}::money::numeric as ${this.quote(field)}`
) )
} }
@ -201,50 +172,84 @@ function generateSelectStatement(
// sample data tests. // sample data tests.
if (table) { if (table) {
return knex.raw( return knex.raw(
`${quote(client, table)}.${quote(client, column)} as ${quote( `${this.quote(table)}.${this.quote(column)} as ${this.quote(field)}`
client,
field
)}`
) )
} else { } else {
return knex.raw(`${quote(client, field)} as ${quote(client, field)}`) return knex.raw(`${this.quote(field)} as ${this.quote(field)}`)
} }
}) })
} }
function getTableName(table?: Table): string | undefined { // OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
// SQS uses the table ID rather than the table name // so when we use them we need to wrap them in to_char(). This function
// converts a field name to the appropriate identifier.
private convertClobs(table: Table, field: string): string {
const parts = field.split(".")
const col = parts.pop()!
const schema = table.schema[col]
let identifier = this.quotedIdentifier(field)
if ( if (
table?.sourceType === TableSourceType.INTERNAL || schema.type === FieldType.STRING ||
table?.sourceId === INTERNAL_TABLE_SOURCE_ID schema.type === FieldType.LONGFORM ||
schema.type === FieldType.BB_REFERENCE_SINGLE ||
schema.type === FieldType.OPTIONS ||
schema.type === FieldType.BARCODEQR
) { ) {
return table?._id identifier = `to_char(${identifier})`
}
return identifier
}
private parse(input: any, schema: FieldSchema) {
if (schema.type === FieldType.DATETIME && schema.timeOnly) {
if (this.client === SqlClient.ORACLE) {
return new Date(`1970-01-01 ${input}`)
}
}
if (Array.isArray(input)) {
return JSON.stringify(input)
}
if (input == undefined) {
return null
}
if (typeof input !== "string") {
return input
}
if (isInvalidISODateString(input)) {
return null
}
if (isValidISODateString(input)) {
return new Date(input.trim())
}
return input
}
private parseBody(body: any, table: Table) {
for (let [key, value] of Object.entries(body)) {
body[key] = this.parse(value, table.schema[key])
}
return body
}
private parseFilters(
filters: SearchFilters | undefined,
table: Table
): SearchFilters {
if (!filters) {
return {}
}
for (let [key, value] of Object.entries(filters)) {
let parsed
if (typeof value === "object") {
parsed = this.parseFilters(value, table)
} else { } else {
return table?.name parsed = this.parse(value, table.schema[key])
} }
// @ts-ignore
filters[key] = parsed
} }
return filters
function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] {
if (Array.isArray(query)) {
return query.map((q: SqlQuery) => convertBooleans(q) as SqlQuery)
} else {
if (query.bindings) {
query.bindings = query.bindings.map(binding => {
if (typeof binding === "boolean") {
return binding ? 1 : 0
}
return binding
})
}
}
return query
}
class InternalBuilder {
private readonly client: SqlClient
constructor(client: SqlClient) {
this.client = client
} }
// right now we only do filters on the specific table being queried // right now we only do filters on the specific table being queried
@ -261,7 +266,7 @@ class InternalBuilder {
if (!filters) { if (!filters) {
return query return query
} }
filters = parseFilters(filters) filters = this.parseFilters(filters, table)
// if all or specified in filters, then everything is an or // if all or specified in filters, then everything is an or
const allOr = filters.allOr const allOr = filters.allOr
const sqlStatements = new SqlStatements(this.client, table, { const sqlStatements = new SqlStatements(this.client, table, {
@ -318,10 +323,9 @@ class InternalBuilder {
} else { } else {
const rawFnc = `${fnc}Raw` const rawFnc = `${fnc}Raw`
// @ts-ignore // @ts-ignore
query = query[rawFnc]( query = query[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
`LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`, `%${value.toLowerCase()}%`,
[`%${value.toLowerCase()}%`] ])
)
} }
} }
@ -371,10 +375,7 @@ class InternalBuilder {
} }
statement += statement +=
(statement ? andOr : "") + (statement ? andOr : "") +
`COALESCE(LOWER(${quotedIdentifier( `COALESCE(LOWER(${this.quotedIdentifier(key)}), '') LIKE ?`
this.client,
key
)}), '') LIKE ?`
} }
if (statement === "") { if (statement === "") {
@ -393,7 +394,7 @@ class InternalBuilder {
filters.oneOf, filters.oneOf,
(key: string, array) => { (key: string, array) => {
if (this.client === SqlClient.ORACLE) { if (this.client === SqlClient.ORACLE) {
key = convertClobs(this.client, table, key) key = this.convertClobs(table, key)
array = Array.isArray(array) ? array : [array] array = Array.isArray(array) ? array : [array]
const binding = new Array(array.length).fill("?").join(",") const binding = new Array(array.length).fill("?").join(",")
query = query.whereRaw(`${key} IN (${binding})`, array) query = query.whereRaw(`${key} IN (${binding})`, array)
@ -415,10 +416,9 @@ class InternalBuilder {
} else { } else {
const rawFnc = `${fnc}Raw` const rawFnc = `${fnc}Raw`
// @ts-ignore // @ts-ignore
query = query[rawFnc]( query = query[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
`LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`, `${value.toLowerCase()}%`,
[`${value.toLowerCase()}%`] ])
)
} }
}) })
} }
@ -456,21 +456,18 @@ 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 ${quotedIdentifier( `CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 1`,
this.client,
key
)} = ? THEN 1 ELSE 0 END = 1`,
[value] [value]
) )
} else if (this.client === SqlClient.ORACLE) { } else if (this.client === SqlClient.ORACLE) {
const identifier = convertClobs(this.client, table, key) const identifier = this.convertClobs(table, key)
query = query[fnc]( query = query[fnc](
`(${identifier} IS NOT NULL AND ${identifier} = ?)`, `(${identifier} IS NOT NULL AND ${identifier} = ?)`,
[value] [value]
) )
} else { } else {
query = query[fnc]( query = query[fnc](
`COALESCE(${quotedIdentifier(this.client, key)} = ?, FALSE)`, `COALESCE(${this.quotedIdentifier(key)} = ?, FALSE)`,
[value] [value]
) )
} }
@ -481,21 +478,18 @@ 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 ${quotedIdentifier( `CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 0`,
this.client,
key
)} = ? THEN 1 ELSE 0 END = 0`,
[value] [value]
) )
} else if (this.client === SqlClient.ORACLE) { } else if (this.client === SqlClient.ORACLE) {
const identifier = convertClobs(this.client, table, key) const identifier = this.convertClobs(table, key)
query = query[fnc]( query = query[fnc](
`(${identifier} IS NOT NULL AND ${identifier} != ?)`, `(${identifier} IS NOT NULL AND ${identifier} != ?)`,
[value] [value]
) )
} else { } else {
query = query[fnc]( query = query[fnc](
`COALESCE(${quotedIdentifier(this.client, key)} != ?, TRUE)`, `COALESCE(${this.quotedIdentifier(key)} != ?, TRUE)`,
[value] [value]
) )
} }
@ -692,7 +686,7 @@ class InternalBuilder {
create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder { create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, body } = json const { endpoint, body } = json
let query = this.knexWithAlias(knex, endpoint) let query = this.knexWithAlias(knex, endpoint)
const parsedBody = parseBody(body) const parsedBody = this.parseBody(body, json.meta.table)
// make sure no null values in body for creation // make sure no null values in body for creation
for (let [key, value] of Object.entries(parsedBody)) { for (let [key, value] of Object.entries(parsedBody)) {
if (value == null) { if (value == null) {
@ -714,7 +708,7 @@ class InternalBuilder {
if (!Array.isArray(body)) { if (!Array.isArray(body)) {
return query return query
} }
const parsedBody = body.map(row => parseBody(row)) const parsedBody = body.map(row => this.parseBody(row, json.meta.table))
return query.insert(parsedBody) return query.insert(parsedBody)
} }
@ -724,7 +718,7 @@ class InternalBuilder {
if (!Array.isArray(body)) { if (!Array.isArray(body)) {
return query return query
} }
const parsedBody = body.map(row => parseBody(row)) const parsedBody = body.map(row => this.parseBody(row, json.meta.table))
if ( if (
this.client === SqlClient.POSTGRES || this.client === SqlClient.POSTGRES ||
this.client === SqlClient.SQL_LITE || this.client === SqlClient.SQL_LITE ||
@ -806,7 +800,7 @@ class InternalBuilder {
}) })
// if counting, use distinct count, else select // if counting, use distinct count, else select
preQuery = !counting preQuery = !counting
? preQuery.select(generateSelectStatement(json, knex)) ? preQuery.select(this.generateSelectStatement(json, knex))
: this.addDistinctCount(preQuery, json) : this.addDistinctCount(preQuery, json)
// have to add after as well (this breaks MS-SQL) // have to add after as well (this breaks MS-SQL)
if (this.client !== SqlClient.MS_SQL && !counting) { if (this.client !== SqlClient.MS_SQL && !counting) {
@ -837,7 +831,7 @@ class InternalBuilder {
update(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder { update(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, body, filters, tableAliases } = json const { endpoint, body, filters, tableAliases } = json
let query = this.knexWithAlias(knex, endpoint, tableAliases) let query = this.knexWithAlias(knex, endpoint, tableAliases)
const parsedBody = parseBody(body) const parsedBody = this.parseBody(body, json.meta.table)
query = this.addFilters(query, filters, json.meta.table, { query = this.addFilters(query, filters, json.meta.table, {
columnPrefix: json.meta.columnPrefix, columnPrefix: json.meta.columnPrefix,
aliases: tableAliases, aliases: tableAliases,
@ -861,7 +855,7 @@ class InternalBuilder {
if (opts.disableReturning) { if (opts.disableReturning) {
return query.delete() return query.delete()
} else { } else {
return query.delete().returning(generateSelectStatement(json, knex)) return query.delete().returning(this.generateSelectStatement(json, knex))
} }
} }
} }

View File

@ -1318,7 +1318,7 @@ describe.each([
}) })
!isInternal && !isInternal &&
describe("datetime - time only", () => { describe.only("datetime - time only", () => {
const T_1000 = "10:00:00" const T_1000 = "10:00:00"
const T_1045 = "10:45:00" const T_1045 = "10:45:00"
const T_1200 = "12:00:00" const T_1200 = "12:00:00"

View File

@ -398,7 +398,6 @@ class OracleIntegration extends Sql implements DatasourcePlus {
} }
private getConnection = async (): Promise<Connection> => { private getConnection = async (): Promise<Connection> => {
//connectString : "(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))(CONNECT_DATA =(SID= ORCL)))"
const connectString = `${this.config.host}:${this.config.port || 1521}/${ const connectString = `${this.config.host}:${this.config.port || 1521}/${
this.config.database this.config.database
}` }`

View File

@ -315,6 +315,15 @@ export async function outputProcessing<T extends Row[] | Row>(
column.subtype column.subtype
) )
} }
} else if (column.type === FieldType.DATETIME && column.timeOnly) {
for (let row of enriched) {
if (row[property] instanceof Date) {
const hours = row[property].getHours().toString().padStart(2, "0")
const minutes = row[property].getMinutes().toString().padStart(2, "0")
const seconds = row[property].getSeconds().toString().padStart(2, "0")
row[property] = `${hours}:${minutes}:${seconds}`
}
}
} }
} }