Add FTS ActiveRecord model
Needs some special handling for the FTS virtual table format. Set the primary key to rowid and manually specify the columns.
This commit is contained in:
@@ -2,21 +2,17 @@ module Search::Record::SQLite
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Override the UUID id attribute from ApplicationRecord
|
||||
# FTS tables require integer rowids
|
||||
# Override default UUID id attribute, as FTS5 uses rowid integer primary key
|
||||
attribute :id, :integer, default: nil
|
||||
|
||||
# Virtual attributes from FTS5 functions
|
||||
attribute :result_title, :string
|
||||
attribute :result_content, :string
|
||||
|
||||
has_one :search_records_fts, -> { with_rowid }, class_name: "Search::Record::SQLite::Fts", foreign_key: :rowid, primary_key: :id
|
||||
|
||||
after_save :upsert_to_fts5_table
|
||||
after_destroy :delete_from_fts5_table
|
||||
|
||||
scope :matching, ->(query, account_id) do
|
||||
joins("INNER JOIN search_records_fts ON search_records_fts.rowid = #{table_name}.id")
|
||||
.where("search_records_fts MATCH ?", query)
|
||||
end
|
||||
scope :matching, ->(query, account_id) { joins(:search_records_fts).where("search_records_fts MATCH ?", query) }
|
||||
end
|
||||
|
||||
class_methods do
|
||||
@@ -54,18 +50,10 @@ module Search::Record::SQLite
|
||||
end
|
||||
|
||||
def upsert_to_fts5_table
|
||||
self.class.connection.exec_query(
|
||||
"INSERT OR REPLACE INTO search_records_fts(rowid, title, content) VALUES (?, ?, ?)",
|
||||
"Search::Record Upsert FTS5",
|
||||
[id, title, content]
|
||||
)
|
||||
Fts.upsert(id, title, content)
|
||||
end
|
||||
|
||||
def delete_from_fts5_table
|
||||
self.class.connection.exec_query(
|
||||
"DELETE FROM search_records_fts WHERE rowid = ?",
|
||||
"Search::Record Delete FTS5",
|
||||
[id]
|
||||
)
|
||||
search_records_fts&.destroy
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class Search::Record::SQLite::Fts < ApplicationRecord
|
||||
self.table_name = "search_records_fts"
|
||||
self.primary_key = "rowid"
|
||||
|
||||
# FTS5 virtual table columns
|
||||
attribute :rowid, :integer
|
||||
attribute :title, :string
|
||||
attribute :content, :string
|
||||
|
||||
# FTS5 virtual tables don't expose rowid in the schema by default
|
||||
# We need to explicitly select it when loading records
|
||||
scope :with_rowid, -> { select(:rowid, :title, :content) }
|
||||
|
||||
def self.upsert(rowid, title, content)
|
||||
connection.exec_query(
|
||||
"INSERT OR REPLACE INTO search_records_fts(rowid, title, content) VALUES (?, ?, ?)",
|
||||
"Search::Record::SqliteFts Upsert",
|
||||
[rowid, title, content]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -41,4 +41,32 @@ class Card::SearchableTest < ActiveSupport::TestCase
|
||||
|
||||
assert_match(/Current.account must be set to save Card/, error.message)
|
||||
end
|
||||
|
||||
test "deleting card removes search record and FTS entry" do
|
||||
card = @board.cards.create!(title: "Card to delete", creator: @user)
|
||||
|
||||
# Verify search record exists
|
||||
search_record = Search::Record.find_by(searchable_type: "Card", searchable_id: card.id)
|
||||
assert_not_nil search_record, "Search record should exist after card creation"
|
||||
|
||||
# For SQLite, verify FTS entry exists
|
||||
if Search::Record.connection.adapter_name == "SQLite"
|
||||
fts_entry = search_record.search_records_fts
|
||||
assert_not_nil fts_entry, "FTS entry should exist"
|
||||
assert_equal card.title, fts_entry.title
|
||||
end
|
||||
|
||||
# Delete the card
|
||||
card.destroy
|
||||
|
||||
# Verify search record is deleted
|
||||
search_record = Search::Record.find_by(searchable_type: "Card", searchable_id: card.id)
|
||||
assert_nil search_record, "Search record should be deleted after card deletion"
|
||||
|
||||
# For SQLite, verify FTS entry is deleted
|
||||
if Search::Record.connection.adapter_name == "SQLite"
|
||||
fts_count = Search::Record::SQLite::Fts.where(rowid: card.id).count
|
||||
assert_equal 0, fts_count, "FTS entry should be deleted"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,10 +20,24 @@ class Comment::SearchableTest < ActiveSupport::TestCase
|
||||
|
||||
# Comment is removed from index on destroy
|
||||
comment_id = comment.id
|
||||
search_record_id = record.id
|
||||
|
||||
# For SQLite, verify FTS entry exists before deletion
|
||||
if Search::Record.connection.adapter_name == "SQLite"
|
||||
fts_entry = record.search_records_fts
|
||||
assert_not_nil fts_entry, "FTS entry should exist before comment deletion"
|
||||
end
|
||||
|
||||
comment.destroy
|
||||
record = Search::Record.find_by(searchable_type: "Comment", searchable_id: comment_id)
|
||||
assert_nil record
|
||||
|
||||
# For SQLite, verify FTS entry is also deleted
|
||||
if Search::Record.connection.adapter_name == "SQLite"
|
||||
fts_count = Search::Record::SQLite::Fts.where(rowid: search_record_id).count
|
||||
assert_equal 0, fts_count, "FTS entry should be deleted after comment deletion"
|
||||
end
|
||||
|
||||
# Finding cards via comment search
|
||||
card_with_comment = @board.cards.create!(title: "Card One", creator: @user)
|
||||
card_with_comment.comments.create!(body: "unique searchable phrase", creator: @user)
|
||||
|
||||
Reference in New Issue
Block a user