This commit is contained in:
Sam Rose 2024-07-29 09:57:24 +01:00
parent 20bad903cc
commit 50d1972127
No known key found for this signature in database
6 changed files with 114 additions and 72 deletions

View File

@ -109,6 +109,26 @@ function parseFilters(filters: SearchFilters | undefined): SearchFilters {
return filters 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( function generateSelectStatement(
json: QueryJson, json: QueryJson,
knex: Knex knex: Knex
@ -372,7 +392,15 @@ class InternalBuilder {
iterate( iterate(
filters.oneOf, filters.oneOf,
(key: string, array) => { (key: string, array) => {
if (this.client === SqlClient.ORACLE) {
key = convertClobs(this.client, table, key)
query = query.whereRaw(
`${key} IN (?)`,
Array.isArray(array) ? array : [array]
)
} else {
query = query[fnc](key, Array.isArray(array) ? array : [array]) query = query[fnc](key, Array.isArray(array) ? array : [array])
}
}, },
(key: string[], array) => { (key: string[], array) => {
query = query[fnc](key, Array.isArray(array) ? array : [array]) query = query[fnc](key, Array.isArray(array) ? array : [array])
@ -436,8 +464,9 @@ class InternalBuilder {
[value] [value]
) )
} else if (this.client === SqlClient.ORACLE) { } else if (this.client === SqlClient.ORACLE) {
const identifier = convertClobs(this.client, table, key)
query = query[fnc]( query = query[fnc](
`COALESCE(${quotedIdentifier(this.client, key)}, -1) = ?`, `(${identifier} IS NOT NULL AND ${identifier} = ?)`,
[value] [value]
) )
} else { } else {
@ -460,8 +489,9 @@ class InternalBuilder {
[value] [value]
) )
} else if (this.client === SqlClient.ORACLE) { } else if (this.client === SqlClient.ORACLE) {
const identifier = convertClobs(this.client, table, key)
query = query[fnc]( query = query[fnc](
`COALESCE(${quotedIdentifier(this.client, key)}, -1) != ?`, `(${identifier} IS NOT NULL AND ${identifier} != ?)`,
[value] [value]
) )
} else { } else {
@ -707,8 +737,11 @@ class InternalBuilder {
} }
const ret = query.insert(parsedBody).onConflict(primary).merge() const ret = query.insert(parsedBody).onConflict(primary).merge()
return ret return ret
} else if (this.client === SqlClient.MS_SQL) { } else if (
// No upsert or onConflict support in MSSQL yet, see: this.client === SqlClient.MS_SQL ||
this.client === SqlClient.ORACLE
) {
// No upsert or onConflict support in MSSQL/Oracle yet, see:
// https://github.com/knex/knex/pull/6050 // https://github.com/knex/knex/pull/6050
return query.insert(parsedBody) return query.insert(parsedBody)
} }
@ -867,7 +900,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
const config: Knex.Config = { const config: Knex.Config = {
client: sqlClient, client: sqlClient,
} }
if (sqlClient === SqlClient.SQL_LITE) { if (sqlClient === SqlClient.SQL_LITE || sqlClient === SqlClient.ORACLE) {
config.useNullAsDefault = true config.useNullAsDefault = true
} }

View File

@ -1,21 +0,0 @@
const executeMock = jest.fn(() => ({
rows: [
{
a: "string",
b: 1,
},
],
}))
const closeMock = jest.fn()
class Connection {
execute = executeMock
close = closeMock
}
module.exports = {
getConnection: jest.fn(() => new Connection()),
executeMock,
closeMock,
}

View File

@ -292,7 +292,7 @@ describe.each([
}) })
describe("equal", () => { describe("equal", () => {
it.only("successfully finds true row", async () => { it("successfully finds true row", async () => {
await expectQuery({ equal: { isTrue: true } }).toMatchExactly([ await expectQuery({ equal: { isTrue: true } }).toMatchExactly([
{ isTrue: true }, { isTrue: true },
]) ])
@ -1577,12 +1577,15 @@ describe.each([
}) })
}) })
describe("bigints", () => { describe.only("bigints", () => {
const SMALL = "1" const SMALL = "1"
const MEDIUM = "10000000" const MEDIUM = "10000000"
// Our bigints are int64s in most datasources. // Our bigints are int64s in most datasources.
const BIG = "9223372036854775807" let BIG = "9223372036854775807"
if (name === DatabaseName.ORACLE) {
// BIG = "9223372036854775808"
}
beforeAll(async () => { beforeAll(async () => {
table = await createTable({ table = await createTable({
@ -2415,25 +2418,25 @@ describe.each([
describe.each([ describe.each([
"名前", // Japanese for "name" "名前", // Japanese for "name"
"Benutzer-ID", // German for "user ID", includes a hyphen // "Benutzer-ID", // German for "user ID", includes a hyphen
"numéro", // French for "number", includes an accent // "numéro", // French for "number", includes an accent
"år", // Swedish for "year", includes a ring above // "år", // Swedish for "year", includes a ring above
"naïve", // English word borrowed from French, includes an umlaut // "naïve", // English word borrowed from French, includes an umlaut
"الاسم", // Arabic for "name" // "الاسم", // Arabic for "name"
"оплата", // Russian for "payment" // "оплата", // Russian for "payment"
"पता", // Hindi for "address" // "पता", // Hindi for "address"
"用戶名", // Chinese for "username" // "用戶名", // Chinese for "username"
"çalışma_zamanı", // Turkish for "runtime", includes an underscore and a cedilla // "çalışma_zamanı", // Turkish for "runtime", includes an underscore and a cedilla
"preço", // Portuguese for "price", includes a cedilla // "preço", // Portuguese for "price", includes a cedilla
"사용자명", // Korean for "username" // "사용자명", // Korean for "username"
"usuario_ñoño", // Spanish, uses an underscore and includes "ñ" // "usuario_ñoño", // Spanish, uses an underscore and includes "ñ"
"файл", // Bulgarian for "file" // "файл", // Bulgarian for "file"
"δεδομένα", // Greek for "data" // "δεδομένα", // Greek for "data"
"geändert_am", // German for "modified on", includes an umlaut // "geändert_am", // German for "modified on", includes an umlaut
"ব্যবহারকারীর_নাম", // Bengali for "user name", includes an underscore // "ব্যবহারকারীর_নাম", // Bengali for "user name", includes an underscore
"São_Paulo", // Portuguese, includes an underscore and a tilde // "São_Paulo", // Portuguese, includes an underscore and a tilde
"età", // Italian for "age", includes an accent // "età", // Italian for "age", includes an accent
"ชื่อผู้ใช้", // Thai for "username" // "ชื่อผู้ใช้", // Thai for "username"
])("non-ascii column name: %s", name => { ])("non-ascii column name: %s", name => {
beforeAll(async () => { beforeAll(async () => {
table = await createTable({ table = await createTable({

View File

@ -360,11 +360,20 @@ class OracleIntegration extends Sql implements DatasourcePlus {
this.index = 1 this.index = 1
connection = await this.getConnection() connection = await this.getConnection()
const options: ExecuteOptions = { autoCommit: true } const options: ExecuteOptions = {
autoCommit: true,
fetchTypeHandler: function (metaData) {
if (metaData.dbType === oracledb.CLOB) {
return { type: oracledb.STRING }
}
return undefined
},
}
const bindings: BindParameters = query.bindings || [] const bindings: BindParameters = query.bindings || []
this.log(query.sql, bindings) this.log(query.sql, bindings)
return await connection.execute<T>(query.sql, bindings, options) const result = await connection.execute(query.sql, bindings, options)
return result as Result<T>
} finally { } finally {
if (connection) { if (connection) {
try { try {

View File

@ -8,7 +8,7 @@ let ports: Promise<testContainerUtils.Port[]>
export async function getDatasource(): Promise<Datasource> { export async function getDatasource(): Promise<Datasource> {
if (!ports) { if (!ports) {
let image = "oracle/database:19.3.0.0-ee" let image = "oracle/database:19.3.0.0-ee-slim-faststart"
if (process.arch.startsWith("arm")) { if (process.arch.startsWith("arm")) {
image = "samhuang78/oracle-database:19.3.0-ee-slim-faststart" image = "samhuang78/oracle-database:19.3.0-ee-slim-faststart"
} }
@ -17,7 +17,7 @@ export async function getDatasource(): Promise<Datasource> {
new GenericContainer(image) new GenericContainer(image)
.withExposedPorts(1521) .withExposedPorts(1521)
.withEnvironment({ ORACLE_PASSWORD: "password" }) .withEnvironment({ ORACLE_PASSWORD: "password" })
.withWaitStrategy(Wait.forHealthCheck().withStartupTimeout(10000)) .withWaitStrategy(Wait.forHealthCheck().withStartupTimeout(60000))
) )
} }
@ -26,23 +26,25 @@ export async function getDatasource(): Promise<Datasource> {
throw new Error("Oracle port not found") throw new Error("Oracle port not found")
} }
const host = "127.0.0.1"
const user = "SYSTEM"
const password = "password"
const datasource: Datasource = { const datasource: Datasource = {
type: "datasource_plus", type: "datasource_plus",
source: SourceName.ORACLE, source: SourceName.ORACLE,
plus: true, plus: true,
config: { config: { host, port, user, password, database: "FREEPDB1" },
host: "127.0.0.1",
port,
database: "postgres",
user: "SYS",
password: "password",
},
} }
const database = generator.guid().replaceAll("-", "") const newUser = "a" + generator.guid().replaceAll("-", "")
const client = await knexClient(datasource) const client = await knexClient(datasource)
await client.raw(`CREATE DATABASE "${database}"`) await client.raw(`CREATE USER ${newUser} IDENTIFIED BY password`)
datasource.config!.database = database await client.raw(
`GRANT CONNECT, RESOURCE, CREATE VIEW, CREATE SESSION TO ${newUser}`
)
await client.raw(`GRANT UNLIMITED TABLESPACE TO ${newUser}`)
datasource.config!.user = newUser
return datasource return datasource
} }
@ -55,8 +57,17 @@ export async function knexClient(ds: Datasource) {
throw new Error("Datasource source is not Oracle") throw new Error("Datasource source is not Oracle")
} }
return knex({ const db = ds.config.database || "FREEPDB1"
const connectString = `${ds.config.host}:${ds.config.port}/${db}`
const c = knex({
client: "oracledb", client: "oracledb",
connection: ds.config, connection: {
connectString,
user: ds.config.user,
password: ds.config.password,
},
}) })
return c
} }

View File

@ -315,6 +315,13 @@ export async function outputProcessing<T extends Row[] | Row>(
column.subtype column.subtype
) )
} }
} else if (column.type === FieldType.BIGINT) {
for (const row of enriched) {
if (row[property] == null) {
continue
}
row[property] = row[property].toString()
}
} }
} }