From 8b48eea4e18fec7d6d82fc40330592a135e9f0cd Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 1 Oct 2026 21:11:38 +0200 Subject: [PATCH] fix(09): WR-08 search non-text list columns as text instead of failing in SQL --- modules/cabana/query.go | 4 +++- modules/cabana/query_test.go | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/modules/cabana/query.go b/modules/cabana/query.go index 193e18b..2f7c8cc 100644 --- a/modules/cabana/query.go +++ b/modules/cabana/query.go @@ -291,7 +291,9 @@ func applyListSearch(db *gorm.DB, controller pact.AdminController, schema *ListS } else if !identifier(column) { return nil, listInvalid("search", "is not searchable") } - parts = append(parts, fmt.Sprintf("LOWER(%s) LIKE ? ESCAPE '\\'", qualifiedColumn(db, table, column))) + // CAST makes a numeric, date or boolean column searchable as its text + // form (Winter casts to TEXT too); PostgreSQL has no lower(integer). + parts = append(parts, fmt.Sprintf("LOWER(CAST(%s AS TEXT)) LIKE ? ESCAPE '\\'", qualifiedColumn(db, table, column))) args = append(args, pattern) } return db.Where(strings.Join(parts, " OR "), args...), nil diff --git a/modules/cabana/query_test.go b/modules/cabana/query_test.go index a9974a5..223f497 100644 --- a/modules/cabana/query_test.go +++ b/modules/cabana/query_test.go @@ -435,3 +435,45 @@ var _ pact.AdminRecordSource = queryController{} var _ pact.AdminPermissioned = queryController{} var _ pact.FilterScope = queryRow{} var _ pact.FilterOptions = queryRow{} + +// TestListSearchNonTextColumns pins WR-08: a searchable numeric or date column +// is searched as text instead of failing with an SQL error (PostgreSQL has no +// lower(integer) or lower(timestamp)). +func TestListSearchNonTextColumns(t *testing.T) { + svc, db := newListService(t) + seedListRows(t, db) + const columns = `columns: + name: + label: Name + searchable: true + group_id: + label: Group + searchable: true + created_at: + label: Created + type: datetime + searchable: true + active: + label: Active + type: switch + searchable: true +` + schema, err := CompileList("acme.demo", queryController{}, filterFS(queryListConfig, columns, queryFilters)) + if err != nil { + t.Fatal(err) + } + svc.reg.byID["acme.demo.widgets"].List = schema + + byGroup := decodeList(t, getList(t, svc, "search=9"), http.StatusOK) + if ids := rowIDs(byGroup.Data); len(ids) != 1 || ids[0] != 3 { + t.Fatalf("integer column search = %v, want [3]", ids) + } + byDate := decodeList(t, getList(t, svc, "search=2024-01"), http.StatusOK) + if byDate.Meta.Total != 2 { + t.Fatalf("timestamp column search total = %d, want 2", byDate.Meta.Total) + } + byName := decodeList(t, getList(t, svc, "search=other"), http.StatusOK) + if ids := rowIDs(byName.Data); len(ids) != 1 || ids[0] != 4 { + t.Fatalf("text column search = %v, want [4]", ids) + } +}