diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts
index 68f61ba28d..e774158c23 100644
--- a/packages/server/src/api/routes/tests/search.spec.ts
+++ b/packages/server/src/api/routes/tests/search.spec.ts
@@ -6,9 +6,11 @@ import {
} from "../../../integrations/tests/utils"
import {
db as dbCore,
+ context,
MAX_VALID_DATE,
MIN_VALID_DATE,
utils,
+ SQLITE_DESIGN_DOC_ID,
} from "@budibase/backend-core"
import * as setup from "./utilities"
@@ -2524,4 +2526,38 @@ describe.each([
}).toContainExactly([{ [" name"]: "foo" }])
})
})
+
+ isSqs &&
+ describe("duplicate columns", () => {
+ beforeAll(async () => {
+ table = await createTable({
+ name: {
+ name: "name",
+ type: FieldType.STRING,
+ },
+ })
+ await context.doInAppContext(config.getAppId(), async () => {
+ const db = context.getAppDB()
+ const tableDoc = await db.get
(table._id!)
+ tableDoc.schema.Name = {
+ name: "Name",
+ type: FieldType.STRING,
+ }
+ try {
+ // remove the SQLite definitions so that they can be rebuilt as part of the search
+ const sqliteDoc = await db.get(SQLITE_DESIGN_DOC_ID)
+ await db.remove(sqliteDoc)
+ } catch (err) {
+ // no-op
+ }
+ })
+ await createRows([{ name: "foo", Name: "bar" }])
+ })
+
+ it("should handle invalid duplicate column names", async () => {
+ await expectSearch({
+ query: {},
+ }).toContainExactly([{ name: "foo" }])
+ })
+ })
})
diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts
index dada90b1be..44fd718871 100644
--- a/packages/server/src/sdk/app/rows/search/sqs.ts
+++ b/packages/server/src/sdk/app/rows/search/sqs.ts
@@ -49,6 +49,7 @@ import { dataFilters } from "@budibase/shared-core"
const builder = new sql.Sql(SqlClient.SQL_LITE)
const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
const MISSING_TABLE_REGX = new RegExp(`no such table: .+`)
+const DUPLICATE_COLUMN_REGEX = new RegExp(`duplicate column name: .+`)
function buildInternalFieldList(
table: Table,
@@ -237,9 +238,11 @@ function resyncDefinitionsRequired(status: number, message: string) {
// pre data_ prefix on column names, need to resync
return (
// there are tables missing - try a resync
- (status === 400 && message.match(MISSING_TABLE_REGX)) ||
+ (status === 400 && message?.match(MISSING_TABLE_REGX)) ||
// there are columns missing - try a resync
- (status === 400 && message.match(MISSING_COLUMN_REGEX)) ||
+ (status === 400 && message?.match(MISSING_COLUMN_REGEX)) ||
+ // duplicate column name in definitions - need to re-run definition sync
+ (status === 400 && message?.match(DUPLICATE_COLUMN_REGEX)) ||
// no design document found, needs a full sync
(status === 404 && message?.includes(SQLITE_DESIGN_DOC_ID))
)
diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts
index 9db10f2b41..3c14e2fc67 100644
--- a/packages/server/src/sdk/app/tables/internal/sqs.ts
+++ b/packages/server/src/sdk/app/tables/internal/sqs.ts
@@ -94,6 +94,9 @@ export function mapToUserColumn(key: string) {
function mapTable(table: Table): SQLiteTables {
const tables: SQLiteTables = {}
const fields: Record = {}
+ // a list to make sure no duplicates - the fields are mapped by SQS with case sensitivity
+ // but need to make sure there are no duplicate columns
+ const usedColumns: string[] = []
for (let [key, column] of Object.entries(table.schema)) {
// relationships should be handled differently
if (column.type === FieldType.LINK) {
@@ -106,6 +109,12 @@ function mapTable(table: Table): SQLiteTables {
if (!FieldTypeMap[column.type]) {
throw new Error(`Unable to map type "${column.type}" to SQLite type`)
}
+ const lcKey = key.toLowerCase()
+ // ignore duplicates
+ if (usedColumns.includes(lcKey)) {
+ continue
+ }
+ usedColumns.push(lcKey)
fields[mapToUserColumn(key)] = {
field: key,
type: FieldTypeMap[column.type],