From e0d66b2fd01302ff2d176d34bcafbce15a7e1db6 Mon Sep 17 00:00:00 2001 From: Peter Baker Date: Wed, 18 Mar 2026 15:38:06 +0000 Subject: [PATCH] Fix ambiguous column in SQLite FTS multi-term search (#2688) * Fix multi-term SQLite FTS5 search causing a 500 error When filtering cards by more than one term, `matching` was called once per term via an association join. Rails did not deduplicate association joins, resulting in two JOINs to `search_records_fts` in a single query. SQLite FTS5 then raised "ambiguous column name" when evaluating the MATCH condition. Fix by using a string join instead, which Rails deduplicates so only one JOIN to `search_records_fts` is generated regardless of how many terms are searched. Fixes: https://github.com/basecamp/fizzy/discussions/2354 Co-Authored-By: Claude Sonnet 4.6 * test: verify multi-term search requires ALL terms to match --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Mike Dalessio --- app/models/search/record/sqlite.rb | 5 ++++- test/models/filter/search_test.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/models/search/record/sqlite.rb b/app/models/search/record/sqlite.rb index ae0d34281..857e858a7 100644 --- a/app/models/search/record/sqlite.rb +++ b/app/models/search/record/sqlite.rb @@ -10,7 +10,10 @@ module Search::Record::SQLite after_save :upsert_to_fts5_table - scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", query) } + scope :matching, ->(query, account_id) { + joins("INNER JOIN search_records_fts ON search_records_fts.rowid = #{table_name}.id") + .where("search_records_fts MATCH ?", query) + } end class_methods do diff --git a/test/models/filter/search_test.rb b/test/models/filter/search_test.rb index cbdd9558d..8e3344114 100644 --- a/test/models/filter/search_test.rb +++ b/test/models/filter/search_test.rb @@ -12,4 +12,14 @@ class Filter::SearchTest < ActiveSupport::TestCase assert_equal [ card ], filter.cards.to_a end + + test "multiple terms all match" do + matching_card = @board.cards.create!(title: "haggis neeps tatties", creator: @user, status: "published") + @board.cards.create!(title: "haggis only", creator: @user, status: "published") + @board.cards.create!(title: "neeps only", creator: @user, status: "published") + + filter = @user.filters.new(terms: [ "haggis", "neeps" ], indexed_by: "all", sorted_by: "latest") + + assert_equal [ matching_card ], filter.cards.to_a + end end