Merge pull request #13238 from Budibase/budi-8067-sql-testing-more-datasource-types

Update row.spec.ts to test against more real databases.
This commit is contained in:
Sam Rose 2024-03-13 13:48:17 +00:00 committed by GitHub
commit 5f563a9b93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 187 additions and 15 deletions

View File

@ -38,11 +38,18 @@ import * as uuid from "uuid"
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
tk.freeze(timestamp) tk.freeze(timestamp)
jest.unmock("mysql2")
jest.unmock("mysql2/promise")
jest.unmock("mssql")
const { basicRow } = setup.structures const { basicRow } = setup.structures
describe.each([ describe.each([
["internal", undefined], ["internal", undefined],
["postgres", databaseTestProviders.postgres], ["postgres", databaseTestProviders.postgres],
["mysql", databaseTestProviders.mysql],
["mssql", databaseTestProviders.mssql],
["mariadb", databaseTestProviders.mariadb],
])("/rows (%s)", (__, dsProvider) => { ])("/rows (%s)", (__, dsProvider) => {
const isInternal = !dsProvider const isInternal = !dsProvider
@ -70,7 +77,7 @@ describe.each([
const generateTableConfig: () => SaveTableRequest = () => { const generateTableConfig: () => SaveTableRequest = () => {
return { return {
name: uuid.v4(), name: uuid.v4().substring(0, 16),
type: "table", type: "table",
primary: ["id"], primary: ["id"],
primaryDisplay: "name", primaryDisplay: "name",
@ -467,7 +474,6 @@ describe.each([
const createRowResponse = await config.api.row.save( const createRowResponse = await config.api.row.save(
createViewResponse.id, createViewResponse.id,
{ {
OrderID: "1111",
Country: "Aussy", Country: "Aussy",
Story: "aaaaa", Story: "aaaaa",
} }
@ -477,7 +483,7 @@ describe.each([
expect(row.Story).toBeUndefined() expect(row.Story).toBeUndefined()
expect(row).toEqual({ expect(row).toEqual({
...defaultRowFields, ...defaultRowFields,
OrderID: 1111, OrderID: createRowResponse.OrderID,
Country: "Aussy", Country: "Aussy",
_id: createRowResponse._id, _id: createRowResponse._id,
_rev: createRowResponse._rev, _rev: createRowResponse._rev,
@ -641,7 +647,7 @@ describe.each([
const createdRow = await config.createRow() const createdRow = await config.createRow()
const res = await config.api.row.bulkDelete(table._id!, { const res = await config.api.row.bulkDelete(table._id!, {
rows: [createdRow, { _id: "2" }], rows: [createdRow, { _id: "9999999" }],
}) })
expect(res[0]._id).toEqual(createdRow._id) expect(res[0]._id).toEqual(createdRow._id)

View File

@ -4,11 +4,17 @@ import { QueryOptions } from "../../definitions/datasource"
import { isIsoDateString, SqlClient, isValidFilter } from "../utils" import { isIsoDateString, SqlClient, isValidFilter } from "../utils"
import SqlTableQueryBuilder from "./sqlTable" import SqlTableQueryBuilder from "./sqlTable"
import { import {
BBReferenceFieldMetadata,
FieldSchema,
FieldSubtype,
FieldType,
JsonFieldMetadata,
Operation, Operation,
QueryJson, QueryJson,
RelationshipsJson, RelationshipsJson,
SearchFilters, SearchFilters,
SortDirection, SortDirection,
Table,
} from "@budibase/types" } from "@budibase/types"
import environment from "../../environment" import environment from "../../environment"
@ -691,6 +697,37 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
return results.length ? results : [{ [operation.toLowerCase()]: true }] return results.length ? results : [{ [operation.toLowerCase()]: true }]
} }
convertJsonStringColumns(
table: Table,
results: Record<string, any>[]
): Record<string, any>[] {
for (const [name, field] of Object.entries(table.schema)) {
if (!this._isJsonColumn(field)) {
continue
}
const fullName = `${table.name}.${name}`
for (let row of results) {
if (typeof row[fullName] === "string") {
row[fullName] = JSON.parse(row[fullName])
}
if (typeof row[name] === "string") {
row[name] = JSON.parse(row[name])
}
}
}
return results
}
_isJsonColumn(
field: FieldSchema
): field is JsonFieldMetadata | BBReferenceFieldMetadata {
return (
field.type === FieldType.JSON ||
(field.type === FieldType.BB_REFERENCE &&
field.subtype === FieldSubtype.USERS)
)
}
log(query: string, values?: any[]) { log(query: string, values?: any[]) {
if (!environment.SQL_LOGGING_ENABLE) { if (!environment.SQL_LOGGING_ENABLE) {
return return

View File

@ -14,6 +14,8 @@ import {
Schema, Schema,
TableSourceType, TableSourceType,
DatasourcePlusQueryResponse, DatasourcePlusQueryResponse,
FieldType,
FieldSubtype,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -502,8 +504,14 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
} }
const operation = this._operation(json) const operation = this._operation(json)
const queryFn = (query: any, op: string) => this.internalQuery(query, op) const queryFn = (query: any, op: string) => this.internalQuery(query, op)
const processFn = (result: any) => const processFn = (result: any) => {
result.recordset ? result.recordset : [{ [operation]: true }] if (json?.meta?.table && result.recordset) {
return this.convertJsonStringColumns(json.meta.table, result.recordset)
} else if (result.recordset) {
return result.recordset
}
return [{ [operation]: true }]
}
return this.queryWithReturning(json, queryFn, processFn) return this.queryWithReturning(json, queryFn, processFn)
} }

View File

@ -13,6 +13,8 @@ import {
Schema, Schema,
TableSourceType, TableSourceType,
DatasourcePlusQueryResponse, DatasourcePlusQueryResponse,
FieldType,
FieldSubtype,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -386,7 +388,13 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
try { try {
const queryFn = (query: any) => const queryFn = (query: any) =>
this.internalQuery(query, { connect: false, disableCoercion: true }) this.internalQuery(query, { connect: false, disableCoercion: true })
return await this.queryWithReturning(json, queryFn) const processFn = (result: any) => {
if (json?.meta?.table && Array.isArray(result)) {
return this.convertJsonStringColumns(json.meta.table, result)
}
return result
}
return await this.queryWithReturning(json, queryFn, processFn)
} finally { } finally {
await this.disconnect() await this.disconnect()
} }

View File

@ -4,6 +4,8 @@ import { Datasource } from "@budibase/types"
import * as postgres from "./postgres" import * as postgres from "./postgres"
import * as mongodb from "./mongodb" import * as mongodb from "./mongodb"
import * as mysql from "./mysql" import * as mysql from "./mysql"
import * as mssql from "./mssql"
import * as mariadb from "./mariadb"
import { StartedTestContainer } from "testcontainers" import { StartedTestContainer } from "testcontainers"
jest.setTimeout(30000) jest.setTimeout(30000)
@ -14,4 +16,10 @@ export interface DatabaseProvider {
datasource(): Promise<Datasource> datasource(): Promise<Datasource>
} }
export const databaseTestProviders = { postgres, mongodb, mysql } export const databaseTestProviders = {
postgres,
mongodb,
mysql,
mssql,
mariadb,
}

View File

@ -0,0 +1,58 @@
import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
let container: StartedTestContainer | undefined
class MariaDBWaitStrategy extends AbstractWaitStrategy {
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
// Because MariaDB first starts itself up, runs an init script, then restarts,
// it's possible for the mysqladmin ping to succeed early and then tests to
// run against a MariaDB that's mid-restart and fail. To get around this, we
// wait for logs and then do a ping check.
const logs = Wait.forLogMessage("mariadbd: ready for connections", 2)
await logs.waitUntilReady(container, boundPorts, startTime)
const command = Wait.forSuccessfulCommand(
`mysqladmin ping -h localhost -P 3306 -u root -ppassword`
)
await command.waitUntilReady(container)
}
}
export async function start(): Promise<StartedTestContainer> {
return await new GenericContainer("mariadb:lts")
.withExposedPorts(3306)
.withEnvironment({ MARIADB_ROOT_PASSWORD: "password" })
.withWaitStrategy(new MariaDBWaitStrategy())
.start()
}
export async function datasource(): Promise<Datasource> {
if (!container) {
container = await start()
}
const host = container.getHost()
const port = container.getMappedPort(3306)
return {
type: "datasource_plus",
source: SourceName.MYSQL,
plus: true,
config: {
host,
port,
user: "root",
password: "password",
database: "mysql",
},
}
}
export async function stop() {
if (container) {
await container.stop()
container = undefined
}
}

View File

@ -0,0 +1,53 @@
import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
let container: StartedTestContainer | undefined
export async function start(): Promise<StartedTestContainer> {
return await new GenericContainer(
"mcr.microsoft.com/mssql/server:2022-latest"
)
.withExposedPorts(1433)
.withEnvironment({
ACCEPT_EULA: "Y",
MSSQL_SA_PASSWORD: "Password_123",
// This is important, as Microsoft allow us to use the "Developer" edition
// of SQL Server for development and testing purposes. We can't use other
// versions without a valid license, and we cannot use the Developer
// version in production.
MSSQL_PID: "Developer",
})
.withWaitStrategy(
Wait.forSuccessfulCommand(
"/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'"
)
)
.start()
}
export async function datasource(): Promise<Datasource> {
if (!container) {
container = await start()
}
const host = container.getHost()
const port = container.getMappedPort(1433)
return {
type: "datasource_plus",
source: SourceName.SQL_SERVER,
plus: true,
config: {
server: host,
port,
user: "sa",
password: "Password_123",
},
}
}
export async function stop() {
if (container) {
await container.stop()
container = undefined
}
}

View File

@ -1,10 +1,4 @@
import { import { Row, SearchFilters, SearchParams, SortOrder } from "@budibase/types"
Row,
SearchFilters,
SearchParams,
SortOrder,
SortType,
} from "@budibase/types"
import { isExternalTableID } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import * as internal from "./search/internal" import * as internal from "./search/internal"
import * as external from "./search/external" import * as external from "./search/external"