diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 626ab3bf8e..6181d18046 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -7,6 +7,7 @@ import { isValidFilter, isValidISODateString, sqlLog, + validateManyToMany, } from "./utils" import SqlTableQueryBuilder from "./sqlTable" import { @@ -133,80 +134,78 @@ class InternalBuilder { return "*" } - // no relationships - select everything in SQLite - if (this.client === SqlClient.SQL_LITE) { - const alias = tableAliases?.[endpoint.entityId] - ? tableAliases?.[endpoint.entityId] - : endpoint.entityId - return [this.knex.raw(`${this.quote(alias)}.*`)] - } - - const schema = meta.table.schema - return resource.fields.map(field => { - const parts = field.split(/\./g) - let table: string | undefined = undefined - let column: string | undefined = undefined - - // Just a column name, e.g.: "column" - if (parts.length === 1) { - column = parts[0] - } - - // A table name and a column name, e.g.: "table.column" - if (parts.length === 2) { - table = parts[0] - column = parts[1] - } - - // A link doc, e.g.: "table.doc1.fieldName" - if (parts.length > 2) { - table = parts[0] - column = parts.slice(1).join(".") - } - - if (!column) { - throw new Error(`Invalid field name: ${field}`) - } - - const columnSchema = schema[column] - - if ( - this.client === SqlClient.POSTGRES && - columnSchema?.externalType?.includes("money") - ) { - return this.knex.raw( - `${this.quotedIdentifier( - [table, column].join(".") - )}::money::numeric as ${this.quote(field)}` - ) - } - - if ( - this.client === SqlClient.MS_SQL && - columnSchema?.type === FieldType.DATETIME && - columnSchema.timeOnly - ) { - // Time gets returned as timestamp from mssql, not matching the expected - // HH:mm format - return this.knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`) - } - - // There's at least two edge cases being handled in the expression below. - // 1. The column name could start/end with a space, and in that case we - // want to preseve that space. - // 2. Almost all column names are specified in the form table.column, except - // in the case of relationships, where it's table.doc1.column. In that - // case, we want to split it into `table`.`doc1.column` for reasons that - // aren't actually clear to me, but `table`.`doc1` breaks things with the - // sample data tests. - if (table) { - return this.knex.raw( - `${this.quote(table)}.${this.quote(column)} as ${this.quote(field)}` - ) - } else { - return this.knex.raw(`${this.quote(field)} as ${this.quote(field)}`) - } - }) + const alias = tableAliases?.[endpoint.entityId] + ? tableAliases?.[endpoint.entityId] + : endpoint.entityId + return [this.knex.raw(`${this.quote(alias)}.*`)] + // + // + // const schema = meta.table.schema + // return resource.fields.map(field => { + // const parts = field.split(/\./g) + // let table: string | undefined = undefined + // let column: string | undefined = undefined + // + // // Just a column name, e.g.: "column" + // if (parts.length === 1) { + // column = parts[0] + // } + // + // // A table name and a column name, e.g.: "table.column" + // if (parts.length === 2) { + // table = parts[0] + // column = parts[1] + // } + // + // // A link doc, e.g.: "table.doc1.fieldName" + // if (parts.length > 2) { + // table = parts[0] + // column = parts.slice(1).join(".") + // } + // + // if (!column) { + // throw new Error(`Invalid field name: ${field}`) + // } + // + // const columnSchema = schema[column] + // + // if ( + // this.client === SqlClient.POSTGRES && + // columnSchema?.externalType?.includes("money") + // ) { + // return this.knex.raw( + // `${this.quotedIdentifier( + // [table, column].join(".") + // )}::money::numeric as ${this.quote(field)}` + // ) + // } + // + // if ( + // this.client === SqlClient.MS_SQL && + // columnSchema?.type === FieldType.DATETIME && + // columnSchema.timeOnly + // ) { + // // Time gets returned as timestamp from mssql, not matching the expected + // // HH:mm format + // return this.knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`) + // } + // + // // There's at least two edge cases being handled in the expression below. + // // 1. The column name could start/end with a space, and in that case we + // // want to preseve that space. + // // 2. Almost all column names are specified in the form table.column, except + // // in the case of relationships, where it's table.doc1.column. In that + // // case, we want to split it into `table`.`doc1.column` for reasons that + // // aren't actually clear to me, but `table`.`doc1` breaks things with the + // // sample data tests. + // if (table) { + // return this.knex.raw( + // `${this.quote(table)}.${this.quote(column)} as ${this.quote(field)}` + // ) + // } else { + // return this.knex.raw(`${this.quote(field)} as ${this.quote(field)}`) + // } + // }) } // OracleDB can't use character-large-objects (CLOBs) in WHERE clauses, @@ -368,35 +367,47 @@ class InternalBuilder { let subQuery = mainKnex .select(mainKnex.raw(1)) .from({ [toAlias]: relatedTableName }) - let mainTableRelatesTo = toAlias - if (relationship.through) { + const manyToMany = validateManyToMany(relationship) + if (manyToMany) { const throughAlias = - aliases?.[relationship.through] || relationship.through - let throughTable = this.tableNameWithSchema(relationship.through, { + aliases?.[manyToMany.through] || relationship.through + let throughTable = this.tableNameWithSchema(manyToMany.through, { alias: throughAlias, schema: endpoint.schema, }) - subQuery = subQuery.innerJoin(throughTable, function () { - // @ts-ignore - this.on( - `${toAlias}.${relationship.toPrimary}`, + subQuery = subQuery + // add a join through the junction table + .innerJoin(throughTable, function () { + // @ts-ignore + this.on( + `${toAlias}.${manyToMany.toPrimary}`, + "=", + `${throughAlias}.${manyToMany.to}` + ) + }) + // check the document in the junction table points to the main table + .where( + `${throughAlias}.${manyToMany.from}`, "=", - `${throughAlias}.${relationship.to}` + mainKnex.raw( + this.quotedIdentifier(`${fromAlias}.${manyToMany.fromPrimary}`) + ) ) - }) + // in SQS the same junction table is used for different many-to-many relationships between the + // two same tables, this is needed to avoid rows ending up in all columns if (this.client === SqlClient.SQL_LITE) { - subQuery = this.addJoinFieldCheck(subQuery, relationship) + subQuery = this.addJoinFieldCheck(subQuery, manyToMany) } - mainTableRelatesTo = throughAlias - } - // "join" to the main table, making sure the ID matches that of the main - subQuery = subQuery.where( - `${mainTableRelatesTo}.${relationship.from}`, - "=", - mainKnex.raw( - this.quotedIdentifier(`${fromAlias}.${relationship.fromPrimary}`) + } else { + // "join" to the main table, making sure the ID matches that of the main + subQuery = subQuery.where( + `${toAlias}.${relationship.to}`, + "=", + mainKnex.raw( + this.quotedIdentifier(`${fromAlias}.${relationship.from}`) + ) ) - ) + } query = query.whereExists(whereCb(subQuery)) break } @@ -478,12 +489,10 @@ class InternalBuilder { alias ? `${alias}.${updatedKey}` : updatedKey, value ) - } else if (isSqlite && shouldProcessRelationship) { + } else if (shouldProcessRelationship) { query = builder.addRelationshipForFilter(query, updatedKey, q => { return handleRelationship(q, updatedKey, value) }) - } else if (shouldProcessRelationship) { - query = handleRelationship(query, updatedKey, value) } } } @@ -849,6 +858,7 @@ class InternalBuilder { fromTable: string, relationships: RelationshipsJson[] ): Knex.QueryBuilder { + const sqlClient = this.client const { resource, tableAliases: aliases, endpoint } = this.query const fields = resource?.fields || [] const jsonField = (field: string) => { @@ -862,7 +872,15 @@ class InternalBuilder { unaliased = parts.join(".") tableField = this.quote(unaliased) } - return `'${unaliased}',${tableField}` + let separator = "," + switch (sqlClient) { + case SqlClient.ORACLE: + separator = " VALUE " + break + case SqlClient.MS_SQL: + separator = ":" + } + return `'${unaliased}'${separator}${tableField}` } for (let relationship of relationships) { const { @@ -874,23 +892,15 @@ class InternalBuilder { toPrimary, } = relationship // skip invalid relationships - if (!toTable || !fromTable || !fromPrimary || !toPrimary) { + if (!toTable || !fromTable) { continue } - if (!throughTable) { - throw new Error("Only many-to-many implemented for JSON relationships") - } const toAlias = aliases?.[toTable] || toTable, - throughAlias = aliases?.[throughTable] || throughTable, fromAlias = aliases?.[fromTable] || fromTable let toTableWithSchema = this.tableNameWithSchema(toTable, { alias: toAlias, schema: endpoint.schema, }) - let throughTableWithSchema = this.tableNameWithSchema(throughTable, { - alias: throughAlias, - schema: endpoint.schema, - }) let relationshipFields = fields.filter( field => field.split(".")[0] === toAlias ) @@ -903,32 +913,75 @@ class InternalBuilder { const fieldList: string = relationshipFields .map(field => jsonField(field)) .join(",") - let rawJsonArray: Knex.Raw - switch (this.client) { + let rawJsonArray: Knex.Raw, limit: number + switch (sqlClient) { case SqlClient.SQL_LITE: rawJsonArray = this.knex.raw( `json_group_array(json_object(${fieldList}))` ) + limit = getBaseLimit() + break + case SqlClient.POSTGRES: + rawJsonArray = this.knex.raw( + `json_agg(json_build_object(${fieldList}))` + ) + limit = 1 + break + case SqlClient.MY_SQL: + case SqlClient.ORACLE: + rawJsonArray = this.knex.raw( + `json_arrayagg(json_object(${fieldList}))` + ) + limit = getBaseLimit() + break + case SqlClient.MS_SQL: + rawJsonArray = this.knex.raw(`json_array(json_object(${fieldList}))`) + limit = 1 break default: throw new Error(`JSON relationships not implement for ${this.client}`) } + // SQL Server uses TOP - which performs a little differently to the normal LIMIT syntax + // it reduces the result set rather than limiting how much data it filters over + const primaryKey = `${toAlias}.${toPrimary || toKey}` let subQuery = this.knex .select(rawJsonArray) .from(toTableWithSchema) - .join(throughTableWithSchema, function () { - this.on(`${toAlias}.${toPrimary}`, "=", `${throughAlias}.${toKey}`) - }) - .where( - `${throughAlias}.${fromKey}`, - "=", - this.knex.raw(this.quotedIdentifier(`${fromAlias}.${fromPrimary}`)) - ) - // relationships should never have more than the base limit - .limit(getBaseLimit()) + .limit(limit) // add sorting to get consistent order - .orderBy(`${toAlias}.${toPrimary}`) - // need to check the junction table document is to the right column + .orderBy(primaryKey) + + if (sqlClient === SqlClient.POSTGRES) { + subQuery = subQuery.groupBy(primaryKey) + } + + // many-to-many relationship with junction table + if (throughTable && toPrimary && fromPrimary) { + const throughAlias = aliases?.[throughTable] || throughTable + let throughTableWithSchema = this.tableNameWithSchema(throughTable, { + alias: throughAlias, + schema: endpoint.schema, + }) + subQuery = subQuery + .join(throughTableWithSchema, function () { + this.on(`${toAlias}.${toPrimary}`, "=", `${throughAlias}.${toKey}`) + }) + .where( + `${throughAlias}.${fromKey}`, + "=", + this.knex.raw(this.quotedIdentifier(`${fromAlias}.${fromPrimary}`)) + ) + } + // one-to-many relationship with foreign key + else { + subQuery = subQuery.where( + `${toAlias}.${toKey}`, + "=", + this.knex.raw(this.quotedIdentifier(`${fromAlias}.${fromKey}`)) + ) + } + + // need to check the junction table document is to the right column, this is just for SQS if (this.client === SqlClient.SQL_LITE) { subQuery = this.addJoinFieldCheck(subQuery, relationship) } @@ -1179,14 +1232,12 @@ class InternalBuilder { ? query.select(this.generateSelectStatement()) : this.addDistinctCount(query) // have to add after as well (this breaks MS-SQL) - if (this.client !== SqlClient.MS_SQL && !counting) { + if (!counting) { query = this.addSorting(query) } // handle joins - if (relationships && this.client === SqlClient.SQL_LITE) { + if (relationships) { query = this.addJsonRelationships(query, tableName, relationships) - } else if (relationships) { - query = this.addRelationships(query, tableName, relationships) } return this.addFilters(query, filters, { relationship: true }) diff --git a/packages/backend-core/src/sql/utils.ts b/packages/backend-core/src/sql/utils.ts index 1b32cc6da7..1b80ff337d 100644 --- a/packages/backend-core/src/sql/utils.ts +++ b/packages/backend-core/src/sql/utils.ts @@ -1,4 +1,11 @@ -import { DocumentType, SqlQuery, Table, TableSourceType } from "@budibase/types" +import { + DocumentType, + ManyToManyRelationshipJson, + RelationshipsJson, + SqlQuery, + Table, + TableSourceType, +} from "@budibase/types" import { DEFAULT_BB_DATASOURCE_ID } from "../constants" import { Knex } from "knex" import { SEPARATOR } from "../db" @@ -163,3 +170,24 @@ export function sqlLog(client: string, query: string, values?: any[]) { } console.log(string) } + +function isValidManyToManyRelationship( + relationship: RelationshipsJson +): relationship is ManyToManyRelationshipJson { + return ( + !!relationship.through && + !!relationship.fromPrimary && + !!relationship.from && + !!relationship.toPrimary && + !!relationship.to + ) +} + +export function validateManyToMany( + relationship: RelationshipsJson +): ManyToManyRelationshipJson | undefined { + if (isValidManyToManyRelationship(relationship)) { + return relationship + } + return undefined +} diff --git a/packages/server/src/api/controllers/row/utils/basic.ts b/packages/server/src/api/controllers/row/utils/basic.ts index 8f3607bc73..9d5a315628 100644 --- a/packages/server/src/api/controllers/row/utils/basic.ts +++ b/packages/server/src/api/controllers/row/utils/basic.ts @@ -1,6 +1,10 @@ // need to handle table name + field or just field, depending on if relationships used import { FieldSchema, FieldType, Row, Table } from "@budibase/types" -import { helpers, PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core" +import { + helpers, + PROTECTED_EXTERNAL_COLUMNS, + PROTECTED_INTERNAL_COLUMNS, +} from "@budibase/shared-core" import { generateRowIdField } from "../../../../integrations/utils" function extractFieldValue({ @@ -61,11 +65,13 @@ export function generateIdForRow( export function basicProcessing({ row, table, + tables, isLinked, sqs, }: { row: Row table: Table + tables: Table[] isLinked: boolean sqs?: boolean }): Row { @@ -86,24 +92,65 @@ export function basicProcessing({ thisRow[fieldName] = value } } + let columns: string[] = Object.keys(table.schema) if (!sqs) { thisRow._id = generateIdForRow(row, table, isLinked) thisRow.tableId = table._id thisRow._rev = "rev" + columns = columns.concat(PROTECTED_EXTERNAL_COLUMNS) } else { - const columns = Object.keys(table.schema) + columns = columns.concat(PROTECTED_EXTERNAL_COLUMNS) for (let internalColumn of [...PROTECTED_INTERNAL_COLUMNS, ...columns]) { - const schema: FieldSchema | undefined = table.schema[internalColumn] - let value = extractFieldValue({ + thisRow[internalColumn] = extractFieldValue({ row, tableName: table._id!, fieldName: internalColumn, isLinked, }) - if (sqs && schema?.type === FieldType.LINK && typeof value === "string") { - value = JSON.parse(value) + } + } + for (let col of columns) { + const schema: FieldSchema | undefined = table.schema[col] + if (schema?.type !== FieldType.LINK) { + continue + } + const relatedTable = tables.find(tbl => tbl._id === schema.tableId) + if (!relatedTable) { + continue + } + const value = extractFieldValue({ + row, + tableName: table._id!, + fieldName: col, + isLinked, + }) + const array: Row[] = Array.isArray(value) + ? value + : typeof value === "string" + ? JSON.parse(value) + : undefined + if (array) { + thisRow[col] = array + // make sure all of them have an _id + if (Array.isArray(thisRow[col])) { + const sortField = + relatedTable.primaryDisplay || relatedTable.primary![0]! + thisRow[col] = (thisRow[col] as Row[]) + .map(relatedRow => { + relatedRow._id = relatedRow._id + ? relatedRow._id + : generateIdForRow(relatedRow, relatedTable) + return relatedRow + }) + .sort((a, b) => { + if (!a?.[sortField]) { + return 1 + } else if (!b?.[sortField]) { + return -1 + } + return a[sortField].localeCompare(b[sortField]) + }) } - thisRow[internalColumn] = value } } return thisRow diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts index a24ec17c26..d6e5c3e8f1 100644 --- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts +++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts @@ -36,73 +36,6 @@ function isCorrectRelationship( 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 - * to SQL and the way that SQL relationships are returned based on joins. - * This is complicated, but the idea is that when a SQL query returns all the relations - * will be separate rows, with all of the data in each row. We have to decipher what comes - * from where (which tables) and how to convert that into budibase columns. - */ -export async function updateRelationshipColumns( - table: Table, - tables: TableMap, - row: Row, - rows: { [key: string]: Row }, - relationships: RelationshipsJson[], - opts?: { sqs?: boolean } -) { - const columns: { [key: string]: any } = {} - for (let relationship of relationships) { - const linkedTable = tables[relationship.tableName] - if (!linkedTable) { - continue - } - const fromColumn = `${table.name}.${relationship.from}` - const toColumn = `${linkedTable.name}.${relationship.to}` - // this is important when working with multiple relationships - // between the same tables, don't want to overlap/multiply the relations - if ( - !relationship.through && - row[fromColumn]?.toString() !== row[toColumn]?.toString() - ) { - continue - } - - let linked = basicProcessing({ - row, - table: linkedTable, - isLinked: true, - sqs: opts?.sqs, - }) - if (!linked._id) { - continue - } - if ( - !opts?.sqs || - isCorrectRelationship(relationship, table, linkedTable, row) - ) { - columns[relationship.column] = linked - } - } - for (let [column, related] of Object.entries(columns)) { - if (!row._id) { - continue - } - const rowId: string = row._id - if (!Array.isArray(rows[rowId][column])) { - rows[rowId][column] = [] - } - // make sure relationship hasn't been found already - if ( - !rows[rowId][column].find((relation: Row) => relation._id === related._id) - ) { - rows[rowId][column].push(related) - } - } - return rows -} - /** * Gets the list of relationship JSON structures based on the columns in the table, * this will be used by the underlying library to build whatever relationship mechanism diff --git a/packages/server/src/api/controllers/row/utils/utils.ts b/packages/server/src/api/controllers/row/utils/utils.ts index e52ee63e42..45f0cef085 100644 --- a/packages/server/src/api/controllers/row/utils/utils.ts +++ b/packages/server/src/api/controllers/row/utils/utils.ts @@ -13,7 +13,7 @@ import { processDates, processFormulas, } from "../../../../utilities/rowProcessor" -import { isKnexEmptyReadResponse, updateRelationshipColumns } from "./sqlUtils" +import { isKnexEmptyReadResponse } from "./sqlUtils" import { basicProcessing, generateIdForRow, @@ -149,22 +149,11 @@ export async function sqlOutputProcessing( rowId = generateIdForRow(row, table) row._id = rowId } - // this is a relationship of some sort - if (!opts?.sqs && finalRows[rowId]) { - finalRows = await updateRelationshipColumns( - table, - tables, - row, - finalRows, - relationships, - opts - ) - continue - } const thisRow = fixArrayTypes( basicProcessing({ row, table, + tables: Object.values(tables), isLinked: false, sqs: opts?.sqs, }), @@ -175,18 +164,6 @@ export async function sqlOutputProcessing( } finalRows[thisRow._id] = fixBooleanFields({ row: thisRow, table }) - - // do this at end once its been added to the final rows - if (!opts?.sqs) { - finalRows = await updateRelationshipColumns( - table, - tables, - row, - finalRows, - relationships, - opts - ) - } } // make sure all related rows are correct diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index de7fac4f1b..86d86b1161 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -2126,81 +2126,76 @@ describe.each([ }) }) - // This will never work for Lucene. - !isLucene && - // It also can't work for in-memory searching because the related table name - // isn't available. - !isInMemory && - describe("relations", () => { - let productCategoryTable: Table, productCatRows: Row[] + describe("relations", () => { + let productCategoryTable: Table, productCatRows: Row[] - beforeAll(async () => { - productCategoryTable = await createTable( - { - name: { name: "name", type: FieldType.STRING }, - }, - "productCategory" - ) - table = await createTable( - { - name: { name: "name", type: FieldType.STRING }, - productCat: { - type: FieldType.LINK, - relationshipType: RelationshipType.ONE_TO_MANY, - name: "productCat", - fieldName: "product", - tableId: productCategoryTable._id!, - constraints: { - type: "array", - }, + beforeAll(async () => { + productCategoryTable = await createTable( + { + name: { name: "name", type: FieldType.STRING }, + }, + "productCategory" + ) + table = await createTable( + { + name: { name: "name", type: FieldType.STRING }, + productCat: { + type: FieldType.LINK, + relationshipType: RelationshipType.ONE_TO_MANY, + name: "productCat", + fieldName: "product", + tableId: productCategoryTable._id!, + constraints: { + type: "array", }, }, - "product" - ) + }, + "product" + ) - productCatRows = await Promise.all([ - config.api.row.save(productCategoryTable._id!, { name: "foo" }), - config.api.row.save(productCategoryTable._id!, { name: "bar" }), - ]) + productCatRows = await Promise.all([ + config.api.row.save(productCategoryTable._id!, { name: "foo" }), + config.api.row.save(productCategoryTable._id!, { name: "bar" }), + ]) - await Promise.all([ - config.api.row.save(table._id!, { - name: "foo", - productCat: [productCatRows[0]._id], - }), - config.api.row.save(table._id!, { - name: "bar", - productCat: [productCatRows[1]._id], - }), - config.api.row.save(table._id!, { - name: "baz", - productCat: [], - }), - ]) - }) - - it("should be able to filter by relationship using column name", async () => { - await expectQuery({ - equal: { ["productCat.name"]: "foo" }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - ]) - }) - - it("should be able to filter by relationship using table name", async () => { - await expectQuery({ - equal: { ["productCategory.name"]: "foo" }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - ]) - }) - - it("shouldn't return any relationship for last row", async () => { - await expectQuery({ - equal: { ["name"]: "baz" }, - }).toContainExactly([{ name: "baz", productCat: undefined }]) - }) + await Promise.all([ + config.api.row.save(table._id!, { + name: "foo", + productCat: [productCatRows[0]._id], + }), + config.api.row.save(table._id!, { + name: "bar", + productCat: [productCatRows[1]._id], + }), + config.api.row.save(table._id!, { + name: "baz", + productCat: [], + }), + ]) }) + + it("should be able to filter by relationship using column name", async () => { + await expectQuery({ + equal: { ["productCat.name"]: "foo" }, + }).toContainExactly([ + { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, + ]) + }) + + it("should be able to filter by relationship using table name", async () => { + await expectQuery({ + equal: { ["productCategory.name"]: "foo" }, + }).toContainExactly([ + { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, + ]) + }) + + it("shouldn't return any relationship for last row", async () => { + await expectQuery({ + equal: { ["name"]: "baz" }, + }).toContainExactly([{ name: "baz", productCat: undefined }]) + }) + }) ;(isSqs || isLucene) && describe("relations to same table", () => { let relatedTable: Table, relatedRows: Row[] @@ -2610,50 +2605,50 @@ describe.each([ }) }) - !isInMemory && - describe("search by _id", () => { - let row: Row + // !isInMemory && + describe("search by _id", () => { + let row: Row - beforeAll(async () => { - const toRelateTable = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - table = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - rel: { - name: "rel", - type: FieldType.LINK, - relationshipType: RelationshipType.MANY_TO_MANY, - tableId: toRelateTable._id!, - fieldName: "rel", - }, - }) - const [row1, row2] = await Promise.all([ - config.api.row.save(toRelateTable._id!, { name: "tag 1" }), - config.api.row.save(toRelateTable._id!, { name: "tag 2" }), - ]) - row = await config.api.row.save(table._id!, { - name: "product 1", - rel: [row1._id, row2._id], - }) + beforeAll(async () => { + const toRelateTable = await createTable({ + name: { + name: "name", + type: FieldType.STRING, + }, }) - - it("can filter by the row ID with limit 1", async () => { - await expectSearch({ - query: { - equal: { _id: row._id }, - }, - limit: 1, - }).toContainExactly([row]) + table = await createTable({ + name: { + name: "name", + type: FieldType.STRING, + }, + rel: { + name: "rel", + type: FieldType.LINK, + relationshipType: RelationshipType.MANY_TO_MANY, + tableId: toRelateTable._id!, + fieldName: "rel", + }, + }) + const [row1, row2] = await Promise.all([ + config.api.row.save(toRelateTable._id!, { name: "tag 1" }), + config.api.row.save(toRelateTable._id!, { name: "tag 2" }), + ]) + row = await config.api.row.save(table._id!, { + name: "product 1", + rel: [row1._id, row2._id], }) }) + it("can filter by the row ID with limit 1", async () => { + await expectSearch({ + query: { + equal: { _id: row._id }, + }, + limit: 1, + }).toContainExactly([row]) + }) + }) + !isInternal && describe("search by composite key", () => { beforeAll(async () => { @@ -2690,82 +2685,6 @@ describe.each([ }) }) - // TODO: when all SQL databases use the same mechanism - remove this test, new relationship system doesn't have this problem - !isInternal && - describe("pagination edge case with relationships", () => { - let mainRows: Row[] = [] - - beforeAll(async () => { - const toRelateTable = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - table = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - rel: { - name: "rel", - type: FieldType.LINK, - relationshipType: RelationshipType.MANY_TO_ONE, - tableId: toRelateTable._id!, - fieldName: "rel", - }, - }) - const relatedRows = await Promise.all([ - config.api.row.save(toRelateTable._id!, { name: "tag 1" }), - config.api.row.save(toRelateTable._id!, { name: "tag 2" }), - config.api.row.save(toRelateTable._id!, { name: "tag 3" }), - config.api.row.save(toRelateTable._id!, { name: "tag 4" }), - config.api.row.save(toRelateTable._id!, { name: "tag 5" }), - config.api.row.save(toRelateTable._id!, { name: "tag 6" }), - ]) - mainRows = await Promise.all([ - config.api.row.save(table._id!, { - name: "product 1", - rel: relatedRows.map(row => row._id), - }), - config.api.row.save(table._id!, { - name: "product 2", - rel: [], - }), - config.api.row.save(table._id!, { - name: "product 3", - rel: [], - }), - ]) - }) - - it("can still page when the hard limit is hit", async () => { - await withCoreEnv( - { - SQL_MAX_ROWS: "6", - }, - async () => { - const params: Omit = { - query: {}, - paginate: true, - limit: 3, - sort: "name", - sortType: SortType.STRING, - sortOrder: SortOrder.ASCENDING, - } - const page1 = await expectSearch(params).toContain([mainRows[0]]) - expect(page1.hasNextPage).toBe(true) - expect(page1.bookmark).toBeDefined() - const page2 = await expectSearch({ - ...params, - bookmark: page1.bookmark, - }).toContain([mainRows[1], mainRows[2]]) - expect(page2.hasNextPage).toBe(false) - } - ) - }) - }) - isSql && describe("primaryDisplay", () => { beforeAll(async () => { diff --git a/packages/types/src/sdk/search.ts b/packages/types/src/sdk/search.ts index 6feea40766..7d61aebdfb 100644 --- a/packages/types/src/sdk/search.ts +++ b/packages/types/src/sdk/search.ts @@ -134,6 +134,17 @@ export interface RelationshipsJson { column: string } +// TODO - this can be combined with the above type +export interface ManyToManyRelationshipJson { + through: string + from: string + to: string + fromPrimary: string + toPrimary: string + tableName: string + column: string +} + export interface QueryJson { endpoint: { datasourceId: string