diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index d9dddd0097..f60ef5a978 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -283,7 +283,7 @@ export class QueryBuilder { const equal = (key: string, value: any) => { // 0 evaluates to false, which means we would return all rows if we don't check it - if (!value && value !== 0) { + if (value === null || value === undefined) { return null } return `${key}:${builder.preprocess(value, allPreProcessingOpts)}` @@ -421,7 +421,7 @@ export class QueryBuilder { } if (this.#query.notEqual) { build(this.#query.notEqual, (key: string, value: any) => { - if (!value) { + if (value === null || value === undefined) { return null } if (typeof value === "boolean") { diff --git a/packages/server/src/api/controllers/row/utils/utils.ts b/packages/server/src/api/controllers/row/utils/utils.ts index bf9ede6fe3..d6113d8b7e 100644 --- a/packages/server/src/api/controllers/row/utils/utils.ts +++ b/packages/server/src/api/controllers/row/utils/utils.ts @@ -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( rows: DatasourcePlusQueryResponse, table: Table, @@ -161,7 +174,13 @@ export async function sqlOutputProcessing( if (thisRow._id == null) { throw new Error("Unable to generate row ID for SQL rows") } - finalRows[thisRow._id] = thisRow + + if (opts?.sqs) { + finalRows[thisRow._id] = fixBooleanFields({ row: thisRow, table }) + } else { + finalRows[thisRow._id] = thisRow + } + // do this at end once its been added to the final rows finalRows = await updateRelationshipColumns( table, diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 6ab7ac61d4..b38f187404 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -15,16 +15,17 @@ import { TableSchema, } from "@budibase/types" import _ from "lodash" +import exp from "constants" jest.unmock("mssql") describe.each([ - ["lucene", undefined], + // ["lucene", undefined], ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], + // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], ])("/api/:sourceId/search (%s)", (name, dsProvider) => { const isSqs = name === "sqs" const isLucene = name === "lucene" @@ -67,6 +68,22 @@ describe.each([ class SearchAssertion { 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 // 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 @@ -82,9 +99,7 @@ describe.each([ // eslint-disable-next-line jest/no-standalone-expect expect(foundRows).toEqual( expectedRows.map((expectedRow: any) => - expect.objectContaining( - foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) - ) + expect.objectContaining(this.findRow(expectedRow, foundRows)) ) ) } @@ -104,9 +119,7 @@ describe.each([ expect(foundRows).toEqual( expect.arrayContaining( expectedRows.map((expectedRow: any) => - expect.objectContaining( - foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) - ) + expect.objectContaining(this.findRow(expectedRow, foundRows)) ) ) ) @@ -125,9 +138,7 @@ describe.each([ expect(foundRows).toEqual( expect.arrayContaining( expectedRows.map((expectedRow: any) => - expect.objectContaining( - foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) - ) + expect.objectContaining(this.findRow(expectedRow, foundRows)) ) ) ) @@ -156,6 +167,67 @@ describe.each([ return expectSearch({ query }) } + describe.only("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", () => { beforeAll(async () => { await createTable({ diff --git a/packages/server/src/sdk/app/rows/search/internalSearch.ts b/packages/server/src/sdk/app/rows/search/internalSearch.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index dabccc4b55..f6b79ce968 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -164,8 +164,13 @@ export async function search( throw new Error("SQS cannot currently handle multiple queries") } - let sql = query.sql, - bindings = query.bindings + let sql = query.sql + let bindings = query.bindings?.map(b => { + if (typeof b === "boolean") { + return b ? 1 : 0 + } + return b + }) // quick hack for docIds sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")