Merge pull request #13614 from Budibase/search-tests-boolean

Tests for searching boolean fields, including fixes for various bugs.
This commit is contained in:
Sam Rose 2024-05-08 16:02:07 +01:00 committed by GitHub
commit 17eece5ef2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 133 additions and 34 deletions

View File

@ -12,6 +12,10 @@ import { dataFilters } from "@budibase/shared-core"
export const removeKeyNumbering = dataFilters.removeKeyNumbering export const removeKeyNumbering = dataFilters.removeKeyNumbering
function isEmpty(value: any) {
return value == null || value === ""
}
/** /**
* Class to build lucene query URLs. * Class to build lucene query URLs.
* Optionally takes a base lucene query object. * Optionally takes a base lucene query object.
@ -282,15 +286,14 @@ export class QueryBuilder<T> {
} }
const equal = (key: string, value: any) => { const equal = (key: string, value: any) => {
// 0 evaluates to false, which means we would return all rows if we don't check it if (isEmpty(value)) {
if (!value && value !== 0) {
return null return null
} }
return `${key}:${builder.preprocess(value, allPreProcessingOpts)}` return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
} }
const contains = (key: string, value: any, mode = "AND") => { const contains = (key: string, value: any, mode = "AND") => {
if (!value || (Array.isArray(value) && value.length === 0)) { if (isEmpty(value)) {
return null return null
} }
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
@ -306,7 +309,7 @@ export class QueryBuilder<T> {
} }
const fuzzy = (key: string, value: any) => { const fuzzy = (key: string, value: any) => {
if (!value) { if (isEmpty(value)) {
return null return null
} }
value = builder.preprocess(value, { value = builder.preprocess(value, {
@ -328,7 +331,7 @@ export class QueryBuilder<T> {
} }
const oneOf = (key: string, value: any) => { const oneOf = (key: string, value: any) => {
if (!value) { if (isEmpty(value)) {
return `*:*` return `*:*`
} }
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
@ -386,7 +389,7 @@ export class QueryBuilder<T> {
// Construct the actual lucene search query string from JSON structure // Construct the actual lucene search query string from JSON structure
if (this.#query.string) { if (this.#query.string) {
build(this.#query.string, (key: string, value: any) => { build(this.#query.string, (key: string, value: any) => {
if (!value) { if (isEmpty(value)) {
return null return null
} }
value = builder.preprocess(value, { value = builder.preprocess(value, {
@ -399,7 +402,7 @@ export class QueryBuilder<T> {
} }
if (this.#query.range) { if (this.#query.range) {
build(this.#query.range, (key: string, value: any) => { build(this.#query.range, (key: string, value: any) => {
if (!value) { if (isEmpty(value)) {
return null return null
} }
if (value.low == null || value.low === "") { if (value.low == null || value.low === "") {
@ -421,7 +424,7 @@ export class QueryBuilder<T> {
} }
if (this.#query.notEqual) { if (this.#query.notEqual) {
build(this.#query.notEqual, (key: string, value: any) => { build(this.#query.notEqual, (key: string, value: any) => {
if (!value) { if (isEmpty(value)) {
return null return null
} }
if (typeof value === "boolean") { if (typeof value === "boolean") {

View File

@ -117,6 +117,19 @@ export async function validate(
}) })
} }
function fixBooleanFields({ row, table }: { row: Row; table: Table }) {
for (let col of Object.values(table.schema)) {
if (col.type === FieldType.BOOLEAN) {
if (row[col.name] === 1) {
row[col.name] = true
} else if (row[col.name] === 0) {
row[col.name] = false
}
}
}
return row
}
export async function sqlOutputProcessing( export async function sqlOutputProcessing(
rows: DatasourcePlusQueryResponse, rows: DatasourcePlusQueryResponse,
table: Table, table: Table,
@ -161,7 +174,9 @@ export async function sqlOutputProcessing(
if (thisRow._id == null) { if (thisRow._id == null) {
throw new Error("Unable to generate row ID for SQL rows") throw new Error("Unable to generate row ID for SQL rows")
} }
finalRows[thisRow._id] = thisRow
finalRows[thisRow._id] = fixBooleanFields({ row: thisRow, table })
// do this at end once its been added to the final rows // do this at end once its been added to the final rows
finalRows = await updateRelationshipColumns( finalRows = await updateRelationshipColumns(
table, table,

View File

@ -67,6 +67,22 @@ describe.each([
class SearchAssertion { class SearchAssertion {
constructor(private readonly query: RowSearchParams) {} constructor(private readonly query: RowSearchParams) {}
private findRow(expectedRow: any, foundRows: any[]) {
const row = foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
if (!row) {
const fields = Object.keys(expectedRow)
// To make the error message more readable, we only include the fields
// that are present in the expected row.
const searchedObjects = foundRows.map(row => _.pick(row, fields))
throw new Error(
`Failed to find row: ${JSON.stringify(
expectedRow
)} in ${JSON.stringify(searchedObjects)}`
)
}
return row
}
// Asserts that the query returns rows matching exactly the set of rows // Asserts that the query returns rows matching exactly the set of rows
// passed in. The order of the rows matters. Rows returned in an order // passed in. The order of the rows matters. Rows returned in an order
// different to the one passed in will cause the assertion to fail. Extra // different to the one passed in will cause the assertion to fail. Extra
@ -82,9 +98,7 @@ describe.each([
// eslint-disable-next-line jest/no-standalone-expect // eslint-disable-next-line jest/no-standalone-expect
expect(foundRows).toEqual( expect(foundRows).toEqual(
expectedRows.map((expectedRow: any) => expectedRows.map((expectedRow: any) =>
expect.objectContaining( expect.objectContaining(this.findRow(expectedRow, foundRows))
foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
)
) )
) )
} }
@ -104,9 +118,7 @@ describe.each([
expect(foundRows).toEqual( expect(foundRows).toEqual(
expect.arrayContaining( expect.arrayContaining(
expectedRows.map((expectedRow: any) => expectedRows.map((expectedRow: any) =>
expect.objectContaining( expect.objectContaining(this.findRow(expectedRow, foundRows))
foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
)
) )
) )
) )
@ -125,9 +137,7 @@ describe.each([
expect(foundRows).toEqual( expect(foundRows).toEqual(
expect.arrayContaining( expect.arrayContaining(
expectedRows.map((expectedRow: any) => expectedRows.map((expectedRow: any) =>
expect.objectContaining( expect.objectContaining(this.findRow(expectedRow, foundRows))
foundRows.find(foundRow => _.isMatch(foundRow, expectedRow))
)
) )
) )
) )
@ -156,6 +166,67 @@ describe.each([
return expectSearch({ query }) return expectSearch({ query })
} }
describe("boolean", () => {
beforeAll(async () => {
await createTable({
isTrue: { name: "isTrue", type: FieldType.BOOLEAN },
})
await createRows([{ isTrue: true }, { isTrue: false }])
})
describe("equal", () => {
it("successfully finds true row", () =>
expectQuery({ equal: { isTrue: true } }).toMatchExactly([
{ isTrue: true },
]))
it("successfully finds false row", () =>
expectQuery({ equal: { isTrue: false } }).toMatchExactly([
{ isTrue: false },
]))
})
describe("notEqual", () => {
it("successfully finds false row", () =>
expectQuery({ notEqual: { isTrue: true } }).toContainExactly([
{ isTrue: false },
]))
it("successfully finds true row", () =>
expectQuery({ notEqual: { isTrue: false } }).toContainExactly([
{ isTrue: true },
]))
})
describe("oneOf", () => {
it("successfully finds true row", () =>
expectQuery({ oneOf: { isTrue: [true] } }).toContainExactly([
{ isTrue: true },
]))
it("successfully finds false row", () =>
expectQuery({ oneOf: { isTrue: [false] } }).toContainExactly([
{ isTrue: false },
]))
})
describe("sort", () => {
it("sorts ascending", () =>
expectSearch({
query: {},
sort: "isTrue",
sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([{ isTrue: false }, { isTrue: true }]))
it("sorts descending", () =>
expectSearch({
query: {},
sort: "isTrue",
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ isTrue: true }, { isTrue: false }]))
})
})
describe.each([FieldType.STRING, FieldType.LONGFORM])("%s", () => { describe.each([FieldType.STRING, FieldType.LONGFORM])("%s", () => {
beforeAll(async () => { beforeAll(async () => {
await createTable({ await createTable({

View File

@ -1,14 +1,4 @@
// lucene searching not supported in test due to use of PouchDB import { Table } from "@budibase/types"
let rows: Row[] = []
jest.mock("../../sdk/app/rows/search/internalSearch", () => ({
fullSearch: jest.fn(() => {
return {
rows,
}
}),
paginatedSearch: jest.fn(),
}))
import { Row, Table } from "@budibase/types"
import * as setup from "./utilities" import * as setup from "./utilities"
const NAME = "Test" const NAME = "Test"
@ -25,8 +15,8 @@ describe("Test a query step automation", () => {
description: "original description", description: "original description",
tableId: table._id, tableId: table._id,
} }
rows.push(await config.createRow(row)) await config.createRow(row)
rows.push(await config.createRow(row)) await config.createRow(row)
}) })
afterAll(setup.afterAll) afterAll(setup.afterAll)

View File

@ -150,6 +150,22 @@ function getTableName(table?: Table): string | undefined {
} }
} }
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 { class InternalBuilder {
private readonly client: string private readonly client: string
@ -654,7 +670,11 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
if (opts?.disableBindings) { if (opts?.disableBindings) {
return { sql: query.toString() } return { sql: query.toString() }
} else { } else {
return getNativeSql(query) let native = getNativeSql(query)
if (sqlClient === SqlClient.SQL_LITE) {
native = convertBooleans(native)
}
return native
} }
} }

View File

@ -164,8 +164,8 @@ export async function search(
throw new Error("SQS cannot currently handle multiple queries") throw new Error("SQS cannot currently handle multiple queries")
} }
let sql = query.sql, let sql = query.sql
bindings = query.bindings let bindings = query.bindings
// quick hack for docIds // quick hack for docIds
sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`") sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")