diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 96c3855f00..3fabbfbef9 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -6,6 +6,7 @@ import { Datasource, EmptyFilterOption, FieldType, + Row, SearchFilters, Table, } from "@budibase/types" @@ -47,7 +48,7 @@ describe.each([ }) describe("strings", () => { - beforeEach(async () => { + beforeAll(async () => { table = await config.api.table.save( tableForDatasource(datasource, { schema: { @@ -61,6 +62,13 @@ describe.each([ }) const rows = [{ name: "foo" }, { name: "bar" }] + let savedRows: Row[] + + beforeAll(async () => { + savedRows = await Promise.all( + rows.map(r => config.api.row.save(table._id!, r)) + ) + }) interface StringSearchTest { query: SearchFilters @@ -68,6 +76,8 @@ describe.each([ } const stringSearchTests: StringSearchTest[] = [ + // These three test cases are generic and don't really need + // to be repeated for all data types, so we just do them here. { query: {}, expected: rows }, { query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL }, @@ -77,6 +87,7 @@ describe.each([ query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE }, expected: [], }, + // The rest of these tests are specific to strings. { query: { string: { name: "foo" } }, expected: [rows[0]] }, { query: { string: { name: "none" } }, expected: [] }, { query: { fuzzy: { name: "oo" } }, expected: [rows[0]] }, @@ -88,13 +99,11 @@ describe.each([ it.each(stringSearchTests)( `should be able to run query: $query`, async ({ query, expected }) => { - const savedRows = await Promise.all( - rows.map(r => config.api.row.save(table._id!, r)) - ) const { rows: foundRows } = await config.api.row.search(table._id!, { tableId: table._id!, query, }) + expect(foundRows).toHaveLength(expected.length) expect(foundRows).toEqual( expect.arrayContaining( expected.map(r => @@ -107,7 +116,7 @@ describe.each([ }) describe("number", () => { - beforeEach(async () => { + beforeAll(async () => { table = await config.api.table.save( tableForDatasource(datasource, { schema: { @@ -121,6 +130,13 @@ describe.each([ }) const rows = [{ age: 1 }, { age: 10 }] + let savedRows: Row[] + + beforeAll(async () => { + savedRows = await Promise.all( + rows.map(r => config.api.row.save(table._id!, r)) + ) + }) interface NumberSearchTest { query: SearchFilters @@ -128,15 +144,6 @@ describe.each([ } const numberSearchTests: NumberSearchTest[] = [ - { query: {}, expected: rows }, - { - query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL }, - expected: rows, - }, - { - query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE }, - expected: [], - }, { query: { equal: { age: 1 } }, expected: [rows[0]] }, { query: { equal: { age: 2 } }, expected: [] }, { query: { notEqual: { age: 1 } }, expected: [rows[1]] }, @@ -150,13 +157,11 @@ describe.each([ it.each(numberSearchTests)( `should be able to run query: $query`, async ({ query, expected }) => { - const savedRows = await Promise.all( - rows.map(r => config.api.row.save(table._id!, r)) - ) const { rows: foundRows } = await config.api.row.search(table._id!, { tableId: table._id!, query, }) + expect(foundRows).toHaveLength(expected.length) expect(foundRows).toEqual( expect.arrayContaining( expected.map(r => @@ -186,6 +191,13 @@ describe.each([ { dob: new Date("2020-01-01").toISOString() }, { dob: new Date("2020-01-10").toISOString() }, ] + let savedRows: Row[] + + beforeEach(async () => { + savedRows = await Promise.all( + rows.map(r => config.api.row.save(table._id!, r)) + ) + }) interface DateSearchTest { query: SearchFilters @@ -193,15 +205,6 @@ describe.each([ } const dateSearchTests: DateSearchTest[] = [ - { query: {}, expected: rows }, - { - query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL }, - expected: rows, - }, - { - query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE }, - expected: [], - }, { query: { equal: { dob: new Date("2020-01-01").toISOString() } }, expected: [rows[0]], @@ -256,13 +259,11 @@ describe.each([ it.each(dateSearchTests)( `should be able to run query: $query`, async ({ query, expected }) => { - const savedRows = await Promise.all( - rows.map(r => config.api.row.save(table._id!, r)) - ) const { rows: foundRows } = await config.api.row.search(table._id!, { tableId: table._id!, query, }) + expect(foundRows).toHaveLength(expected.length) expect(foundRows).toEqual( expect.arrayContaining( expected.map(r => diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index e99e34ab0f..abf12b35b2 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -22,6 +22,7 @@ import { SortDirection, SqlQueryBinding, Table, + EmptyFilterOption, } from "@budibase/types" import environment from "../../environment" @@ -243,6 +244,7 @@ class InternalBuilder { return query } filters = parseFilters(filters) + let noFilters = true // if all or specified in filters, then everything is an or const allOr = filters.allOr if (filters.oneOf) { @@ -250,6 +252,7 @@ class InternalBuilder { const fnc = allOr ? "orWhereIn" : "whereIn" query = query[fnc](key, Array.isArray(array) ? array : [array]) }) + noFilters = false } if (filters.string) { iterate(filters.string, (key, value) => { @@ -265,9 +268,11 @@ class InternalBuilder { ]) } }) + noFilters = false } if (filters.fuzzy) { iterate(filters.fuzzy, like) + noFilters = false } if (filters.range) { iterate(filters.range, (key, value) => { @@ -300,40 +305,61 @@ class InternalBuilder { query = query[fnc](key, "<", value.high) } }) + noFilters = false } if (filters.equal) { iterate(filters.equal, (key, value) => { const fnc = allOr ? "orWhere" : "where" query = query[fnc]({ [key]: value }) }) + + // Somewhere above us in the stack adds `{ type: "row" }` to the `equal` + // key before we get here, so we need to still consider it empty when + // that's the case. + const equalEmpty = + Object.keys(filters.equal).length === 1 && filters.equal.type === "row" + if (!equalEmpty) { + noFilters = false + } } if (filters.notEqual) { iterate(filters.notEqual, (key, value) => { const fnc = allOr ? "orWhereNot" : "whereNot" query = query[fnc]({ [key]: value }) }) + noFilters = false } if (filters.empty) { iterate(filters.empty, key => { const fnc = allOr ? "orWhereNull" : "whereNull" query = query[fnc](key) }) + noFilters = false } if (filters.notEmpty) { iterate(filters.notEmpty, key => { const fnc = allOr ? "orWhereNotNull" : "whereNotNull" query = query[fnc](key) }) + noFilters = false } if (filters.contains) { contains(filters.contains) + noFilters = false } if (filters.notContains) { contains(filters.notContains) + noFilters = false } if (filters.containsAny) { contains(filters.containsAny, true) + noFilters = false } + + if (noFilters && filters.onEmptyFilter === EmptyFilterOption.RETURN_NONE) { + query = query.whereRaw("1=0") + } + return query }