From c0b85c63797d7bacb59f75f458eef5f3db090871 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 8 Jul 2024 18:42:11 +0100 Subject: [PATCH 1/7] Initial implementation - needs testing. --- .../src/api/controllers/row/utils/sqlUtils.ts | 16 +++++- .../server/src/sdk/app/rows/search/sqs.ts | 55 ++++++++++++++----- packages/server/src/sdk/app/rows/sqlAlias.ts | 3 +- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts index 767916616c..76cf4d01ff 100644 --- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts +++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts @@ -22,6 +22,18 @@ export function isManyToMany( return !!(field as ManyToManyRelationshipFieldMetadata).through } +function isCorrectRelationship( + relationship: RelationshipsJson, + row: Row +): boolean { + const junctionTableId = relationship.through! + const possibleColumns = [ + `${junctionTableId}.doc1.fieldName`, + `${junctionTableId}.doc2.fieldName`, + ] + return !!possibleColumns.find(col => row[col] === relationship.column) +} + /** * This iterates through the returned rows and works out what elements of the rows * actually match up to another row (based on primary keys) - this is pretty specific @@ -64,7 +76,9 @@ export async function updateRelationshipColumns( if (!linked._id) { continue } - columns[relationship.column] = linked + if (opts?.sqs && isCorrectRelationship(relationship, row)) { + columns[relationship.column] = linked + } } for (let [column, related] of Object.entries(columns)) { if (!row._id) { diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 72a1557cc9..b7af8705a2 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -5,6 +5,7 @@ import { Operation, QueryJson, RelationshipFieldMetadata, + RelationshipsJson, Row, RowSearchParams, SearchFilters, @@ -30,7 +31,10 @@ import { SQLITE_DESIGN_DOC_ID, SQS_DATASOURCE_INTERNAL, } from "@budibase/backend-core" -import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils" +import { + CONSTANT_INTERNAL_ROW_COLS, + generateJunctionTableID, +} from "../../../../db/utils" import AliasTables from "../sqlAlias" import { outputProcessing } from "../../../../utilities/rowProcessor" import pick from "lodash/pick" @@ -52,7 +56,7 @@ const USER_COLUMN_PREFIX_REGEX = new RegExp( function buildInternalFieldList( table: Table, tables: Table[], - opts: { relationships: boolean } = { relationships: true } + opts?: { relationships?: RelationshipsJson[] } ) { let fieldList: string[] = [] fieldList = fieldList.concat( @@ -60,20 +64,31 @@ function buildInternalFieldList( ) for (let col of Object.values(table.schema)) { const isRelationship = col.type === FieldType.LINK - if (!opts.relationships && isRelationship) { + if (!opts?.relationships && isRelationship) { continue } if (isRelationship) { const linkCol = col as RelationshipFieldMetadata const relatedTable = tables.find(table => table._id === linkCol.tableId)! - fieldList = fieldList.concat( - buildInternalFieldList(relatedTable, tables, { relationships: false }) + // no relationships provided, don't go more than a layer deep + fieldList = fieldList.concat(buildInternalFieldList(relatedTable, tables)) + fieldList.push( + `${generateJunctionTableID( + table._id!, + relatedTable._id! + )}.doc1.fieldName` + ) + fieldList.push( + `${generateJunctionTableID( + table._id!, + relatedTable._id! + )}.doc2.fieldName` ) } else { fieldList.push(`${table._id}.${mapToUserColumn(col.name)}`) } } - return fieldList + return [...new Set(fieldList)] } function cleanupFilters( @@ -165,18 +180,27 @@ function reverseUserColumnMapping(rows: Row[]) { }) } -function runSqlQuery(json: QueryJson, tables: Table[]): Promise function runSqlQuery( json: QueryJson, tables: Table[], + relationships: RelationshipsJson[] +): Promise +function runSqlQuery( + json: QueryJson, + tables: Table[], + relationships: RelationshipsJson[], opts: { countTotalRows: true } ): Promise async function runSqlQuery( json: QueryJson, tables: Table[], + relationships: RelationshipsJson[], opts?: { countTotalRows?: boolean } ) { - const alias = new AliasTables(tables.map(table => table.name)) + const relationshipJunctionTableIds = relationships.map(rel => rel.through!) + const alias = new AliasTables( + tables.map(table => table.name).concat(relationshipJunctionTableIds) + ) if (opts?.countTotalRows) { json.endpoint.operation = Operation.COUNT } @@ -193,8 +217,13 @@ async function runSqlQuery( let bindings = query.bindings // quick hack for docIds - sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`") - sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`") + + const fixJunctionDocs = (field: string) => + ["doc1", "doc2"].forEach(doc => { + sql = sql.replaceAll(`\`${doc}\`.\`${field}\``, `\`${doc}.${field}\``) + }) + fixJunctionDocs("rowId") + fixJunctionDocs("fieldName") if (Array.isArray(query)) { throw new Error("SQS cannot currently handle multiple queries") @@ -260,7 +289,7 @@ export async function search( columnPrefix: USER_COLUMN_PREFIX, }, resource: { - fields: buildInternalFieldList(table, allTables), + fields: buildInternalFieldList(table, allTables, { relationships }), }, relationships, } @@ -292,11 +321,11 @@ export async function search( try { const queries: Promise[] = [] - queries.push(runSqlQuery(request, allTables)) + queries.push(runSqlQuery(request, allTables, relationships)) if (options.countRows) { // get the total count of rows queries.push( - runSqlQuery(request, allTables, { + runSqlQuery(request, allTables, relationships, { countTotalRows: true, }) ) diff --git a/packages/server/src/sdk/app/rows/sqlAlias.ts b/packages/server/src/sdk/app/rows/sqlAlias.ts index bc8fc56d5e..664e64057b 100644 --- a/packages/server/src/sdk/app/rows/sqlAlias.ts +++ b/packages/server/src/sdk/app/rows/sqlAlias.ts @@ -111,7 +111,8 @@ export default class AliasTables { aliasField(field: string) { const tableNames = this.tableNames if (field.includes(".")) { - const [tableName, column] = field.split(".") + const [tableName, ...rest] = field.split(".") + const column = rest.join(".") const foundTableName = tableNames.find(name => { const idx = tableName.indexOf(name) if (idx === -1 || idx > 1) { From cd192020429bda774d1d8e05b8a087c5f4569ea0 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Jul 2024 13:39:49 +0100 Subject: [PATCH 2/7] Fix external relationships. --- packages/server/src/api/controllers/row/utils/sqlUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts index 76cf4d01ff..0d64113788 100644 --- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts +++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts @@ -76,7 +76,7 @@ export async function updateRelationshipColumns( if (!linked._id) { continue } - if (opts?.sqs && isCorrectRelationship(relationship, row)) { + if (!opts?.sqs || isCorrectRelationship(relationship, row)) { columns[relationship.column] = linked } } From 6e699a163d696ef251afc21b60730ae4a6388714 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Jul 2024 16:32:35 +0100 Subject: [PATCH 3/7] Cleaning up how junction fields are added to query. --- .../server/src/sdk/app/rows/search/sqs.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index b7af8705a2..4745aee7fb 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -59,6 +59,13 @@ function buildInternalFieldList( opts?: { relationships?: RelationshipsJson[] } ) { let fieldList: string[] = [] + const addJunctionFields = (relatedTable: Table, fields: string[]) => { + fields.forEach(field => { + fieldList.push( + `${generateJunctionTableID(table._id!, relatedTable._id!)}.${field}` + ) + }) + } fieldList = fieldList.concat( CONSTANT_INTERNAL_ROW_COLS.map(col => `${table._id}.${col}`) ) @@ -72,18 +79,7 @@ function buildInternalFieldList( const relatedTable = tables.find(table => table._id === linkCol.tableId)! // no relationships provided, don't go more than a layer deep fieldList = fieldList.concat(buildInternalFieldList(relatedTable, tables)) - fieldList.push( - `${generateJunctionTableID( - table._id!, - relatedTable._id! - )}.doc1.fieldName` - ) - fieldList.push( - `${generateJunctionTableID( - table._id!, - relatedTable._id! - )}.doc2.fieldName` - ) + addJunctionFields(relatedTable, ["doc1.fieldName", "doc2.fieldName"]) } else { fieldList.push(`${table._id}.${mapToUserColumn(col.name)}`) } From 4cb23759a3c3aeca2cc6a8a642c50c9948ea3356 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Jul 2024 16:33:10 +0100 Subject: [PATCH 4/7] Removing tables and their related table definitions. --- .../server/src/sdk/app/tables/internal/sqs.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts index 2d49adf96e..706e0ca167 100644 --- a/packages/server/src/sdk/app/tables/internal/sqs.ts +++ b/packages/server/src/sdk/app/tables/internal/sqs.ts @@ -176,9 +176,24 @@ export async function addTable(table: Table) { export async function removeTable(table: Table) { const db = context.getAppDB() try { - const definition = await db.get(SQLITE_DESIGN_DOC_ID) - if (definition.sql?.tables?.[table._id!]) { - delete definition.sql.tables[table._id!] + let response = await Promise.all([ + tablesSdk.getAllInternalTables(), + db.get(SQLITE_DESIGN_DOC_ID), + ]) + const tables: Table[] = response[0], + definition: SQLiteDefinition = response[1] + const tableIds = tables + .map(tbl => tbl._id!) + .filter(id => !id.includes(table._id!)) + let cleanup = false + for (let tableKey of Object.keys(definition.sql?.tables || {})) { + // there are no tables matching anymore + if (!tableIds.find(id => tableKey.includes(id))) { + delete definition.sql.tables[tableKey] + cleanup = true + } + } + if (cleanup) { await db.put(definition) // make sure SQS is cleaned up, tables removed await db.sqlDiskCleanup() From 9e8a855d14616b6ba8fa20526f648ac4b1bad7b7 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 9 Jul 2024 19:09:01 +0100 Subject: [PATCH 5/7] Adding test case for separating columns to rows in same table. --- .../src/api/routes/tests/search.spec.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index d9036b22fb..9c76d90e24 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -2085,6 +2085,58 @@ describe.each([ }) }) + isInternal && + describe("relations to same table", () => { + let relatedTable: Table, relatedRows: Row[] + + beforeAll(async () => { + relatedTable = await createTable( + { + name: { name: "name", type: FieldType.STRING }, + }, + "productCategory" + ) + table = await createTable({ + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTable._id!, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTable._id!, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }) + relatedRows = await Promise.all([ + config.api.row.save(relatedTable._id!, { name: "foo" }), + config.api.row.save(relatedTable._id!, { name: "bar" }), + ]) + await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + }) + + it("should be able to relate to same table", async () => { + await expectSearch({ + query: {}, + }).toContainExactly([ + { + name: "test", + related1: [{ _id: relatedRows[0]._id }], + related2: [{ _id: relatedRows[1]._id }], + }, + ]) + }) + }) + isInternal && describe("no column error backwards compat", () => { beforeAll(async () => { From 4ab3aef02058f63f2eff700cd6822f472959bf12 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Jul 2024 11:05:06 +0100 Subject: [PATCH 6/7] PR comments. --- .../src/api/controllers/row/utils/sqlUtils.ts | 9 +- .../src/api/routes/tests/search.spec.ts | 119 ++++++++++++------ .../server/src/sdk/app/tables/internal/sqs.ts | 4 +- 3 files changed, 91 insertions(+), 41 deletions(-) diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts index 0d64113788..32124fa79d 100644 --- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts +++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts @@ -24,9 +24,11 @@ export function isManyToMany( function isCorrectRelationship( relationship: RelationshipsJson, + table1: Table, + table2: Table, row: Row ): boolean { - const junctionTableId = relationship.through! + const junctionTableId = generateJunctionTableID(table1._id!, table2._id!) const possibleColumns = [ `${junctionTableId}.doc1.fieldName`, `${junctionTableId}.doc2.fieldName`, @@ -76,7 +78,10 @@ export async function updateRelationshipColumns( if (!linked._id) { continue } - if (!opts?.sqs || isCorrectRelationship(relationship, row)) { + if ( + !opts?.sqs || + isCorrectRelationship(relationship, table, linkedTable, row) + ) { columns[relationship.column] = linked } } diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 9c76d90e24..d27d0e677c 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -2085,57 +2085,104 @@ describe.each([ }) }) - isInternal && - describe("relations to same table", () => { - let relatedTable: Table, relatedRows: Row[] + describe("relations to same table", () => { + let relatedTable: Table, relatedRows: Row[] - beforeAll(async () => { - relatedTable = await createTable( - { - name: { name: "name", type: FieldType.STRING }, - }, - "productCategory" - ) - table = await createTable({ + beforeAll(async () => { + relatedTable = await createTable( + { name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTable._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTable._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }) - relatedRows = await Promise.all([ - config.api.row.save(relatedTable._id!, { name: "foo" }), - config.api.row.save(relatedTable._id!, { name: "bar" }), - ]) - await config.api.row.save(table._id!, { + }, + "productCategory" + ) + table = await createTable({ + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTable._id!, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTable._id!, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }) + relatedRows = await Promise.all([ + config.api.row.save(relatedTable._id!, { name: "foo" }), + config.api.row.save(relatedTable._id!, { name: "bar" }), + config.api.row.save(relatedTable._id!, { name: "baz" }), + config.api.row.save(relatedTable._id!, { name: "boo" }), + ]) + await Promise.all([ + config.api.row.save(table._id!, { name: "test", related1: [relatedRows[0]._id!], related2: [relatedRows[1]._id!], - }) + }), + config.api.row.save(table._id!, { + name: "test2", + related1: [relatedRows[2]._id!], + related2: [relatedRows[3]._id!], + }), + ]) + }) + + it("should be able to relate to same table", async () => { + await expectSearch({ + query: {}, + }).toContainExactly([ + { + name: "test", + related1: [{ _id: relatedRows[0]._id }], + related2: [{ _id: relatedRows[1]._id }], + }, + { + name: "test2", + related1: [{ _id: relatedRows[2]._id }], + related2: [{ _id: relatedRows[3]._id }], + }, + ]) + }) + + isSqs && + it("should be able to filter down to second row with equal", async () => { + await expectSearch({ + query: { + equal: { + ["related1.name"]: "baz", + }, + }, + }).toContainExactly([ + { + name: "test2", + related1: [{ _id: relatedRows[2]._id }], + }, + ]) }) - it("should be able to relate to same table", async () => { + isSqs && + it("should be able to filter down to first row with not equal", async () => { await expectSearch({ - query: {}, + query: { + notEqual: { + ["1:related2.name"]: "bar", + ["2:related2.name"]: "baz", + ["3:related2.name"]: "boo", + }, + }, }).toContainExactly([ { name: "test", related1: [{ _id: relatedRows[0]._id }], - related2: [{ _id: relatedRows[1]._id }], }, ]) }) - }) + }) isInternal && describe("no column error backwards compat", () => { diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts index 706e0ca167..fc0ee8fc0b 100644 --- a/packages/server/src/sdk/app/tables/internal/sqs.ts +++ b/packages/server/src/sdk/app/tables/internal/sqs.ts @@ -176,12 +176,10 @@ export async function addTable(table: Table) { export async function removeTable(table: Table) { const db = context.getAppDB() try { - let response = await Promise.all([ + const [tables, definition] = await Promise.all([ tablesSdk.getAllInternalTables(), db.get(SQLITE_DESIGN_DOC_ID), ]) - const tables: Table[] = response[0], - definition: SQLiteDefinition = response[1] const tableIds = tables .map(tbl => tbl._id!) .filter(id => !id.includes(table._id!)) From d6ad6a46860a3d17223bb6351cea152a95a82f1d Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 10 Jul 2024 11:21:41 +0100 Subject: [PATCH 7/7] Missing internal check. --- .../src/api/routes/tests/search.spec.ts | 169 +++++++++--------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index d27d0e677c..ce9ef8034b 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -2085,104 +2085,105 @@ describe.each([ }) }) - describe("relations to same table", () => { - let relatedTable: Table, relatedRows: Row[] + isInternal && + describe("relations to same table", () => { + let relatedTable: Table, relatedRows: Row[] - beforeAll(async () => { - relatedTable = await createTable( - { - name: { name: "name", type: FieldType.STRING }, - }, - "productCategory" - ) - table = await createTable({ - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTable._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTable._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }) - relatedRows = await Promise.all([ - config.api.row.save(relatedTable._id!, { name: "foo" }), - config.api.row.save(relatedTable._id!, { name: "bar" }), - config.api.row.save(relatedTable._id!, { name: "baz" }), - config.api.row.save(relatedTable._id!, { name: "boo" }), - ]) - await Promise.all([ - config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }), - config.api.row.save(table._id!, { - name: "test2", - related1: [relatedRows[2]._id!], - related2: [relatedRows[3]._id!], - }), - ]) - }) - - it("should be able to relate to same table", async () => { - await expectSearch({ - query: {}, - }).toContainExactly([ - { - name: "test", - related1: [{ _id: relatedRows[0]._id }], - related2: [{ _id: relatedRows[1]._id }], - }, - { - name: "test2", - related1: [{ _id: relatedRows[2]._id }], - related2: [{ _id: relatedRows[3]._id }], - }, - ]) - }) - - isSqs && - it("should be able to filter down to second row with equal", async () => { - await expectSearch({ - query: { - equal: { - ["related1.name"]: "baz", - }, - }, - }).toContainExactly([ + beforeAll(async () => { + relatedTable = await createTable( { - name: "test2", - related1: [{ _id: relatedRows[2]._id }], + name: { name: "name", type: FieldType.STRING }, }, + "productCategory" + ) + table = await createTable({ + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTable._id!, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTable._id!, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }) + relatedRows = await Promise.all([ + config.api.row.save(relatedTable._id!, { name: "foo" }), + config.api.row.save(relatedTable._id!, { name: "bar" }), + config.api.row.save(relatedTable._id!, { name: "baz" }), + config.api.row.save(relatedTable._id!, { name: "boo" }), + ]) + await Promise.all([ + config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }), + config.api.row.save(table._id!, { + name: "test2", + related1: [relatedRows[2]._id!], + related2: [relatedRows[3]._id!], + }), ]) }) - isSqs && - it("should be able to filter down to first row with not equal", async () => { + it("should be able to relate to same table", async () => { await expectSearch({ - query: { - notEqual: { - ["1:related2.name"]: "bar", - ["2:related2.name"]: "baz", - ["3:related2.name"]: "boo", - }, - }, + query: {}, }).toContainExactly([ { name: "test", related1: [{ _id: relatedRows[0]._id }], + related2: [{ _id: relatedRows[1]._id }], + }, + { + name: "test2", + related1: [{ _id: relatedRows[2]._id }], + related2: [{ _id: relatedRows[3]._id }], }, ]) }) - }) + + isSqs && + it("should be able to filter down to second row with equal", async () => { + await expectSearch({ + query: { + equal: { + ["related1.name"]: "baz", + }, + }, + }).toContainExactly([ + { + name: "test2", + related1: [{ _id: relatedRows[2]._id }], + }, + ]) + }) + + isSqs && + it("should be able to filter down to first row with not equal", async () => { + await expectSearch({ + query: { + notEqual: { + ["1:related2.name"]: "bar", + ["2:related2.name"]: "baz", + ["3:related2.name"]: "boo", + }, + }, + }).toContainExactly([ + { + name: "test", + related1: [{ _id: relatedRows[0]._id }], + }, + ]) + }) + }) isInternal && describe("no column error backwards compat", () => {