Support SQLite searching

This commit is contained in:
Donal McBreen
2025-11-20 13:40:14 +00:00
parent a2333d9a37
commit bb36b4846f
13 changed files with 925 additions and 37 deletions
+68 -10
View File
@@ -13,21 +13,79 @@ module Searchable
private
def create_in_search_index
Search::Record.for_account(account_id).create!(search_record_attributes)
search_class = Search::Record.for_account(account_id)
if Search::Record.sqlite?
# SQLite: create with unstemmed content, FTS5 handles stemming
record = search_class.create!(search_record_attributes)
upsert_to_fts5(record.id)
else
# MySQL: create with stemmed content for FULLTEXT search
attrs = search_record_attributes.merge(
title: Search::Stemmer.stem(search_record_attributes[:title]),
content: Search::Stemmer.stem(search_record_attributes[:content])
)
search_class.create!(attrs)
end
end
def update_in_search_index
Search::Record.for_account(account_id).upsert_all(
[ search_record_attributes.merge(id: ActiveRecord::Type::Uuid.generate) ],
search_class = Search::Record.for_account(account_id)
if Search::Record.sqlite?
# SQLite: find or create record, then upsert to FTS5
record = search_class.find_or_initialize_by(
searchable_type: self.class.name,
searchable_id: id
)
record.assign_attributes(search_record_attributes)
record.save!
upsert_to_fts5(record.id)
else
# MySQL: use upsert_all with stemmed content
attrs = search_record_attributes.merge(
id: ActiveRecord::Type::Uuid.generate,
title: Search::Stemmer.stem(search_record_attributes[:title]),
content: Search::Stemmer.stem(search_record_attributes[:content])
)
search_class.upsert_all(
[ attrs ],
update_only: [ :card_id, :board_id, :title, :content, :created_at ]
)
end
end
def remove_from_search_index
Search::Record.for_account(account_id).where(
searchable_type: self.class.name,
searchable_id: id
).delete_all
search_class = Search::Record.for_account(account_id)
record = search_class.find_by(searchable_type: self.class.name, searchable_id: id)
if record
# For SQLite, delete from FTS5 first
if Search::Record.sqlite?
delete_from_fts5(record.id)
end
record.delete
end
end
def upsert_to_fts5(record_id)
# Use raw unstemmed text - FTS5 Porter tokenizer handles stemming automatically
title = search_title
content = search_content
# Note: FTS5 virtual tables don't work properly with bound parameters in SQLite,
# so we need to use string interpolation with proper quoting
conn = ActiveRecord::Base.connection
sql = "INSERT OR REPLACE INTO search_records_fts(rowid, title, content) VALUES (#{record_id}, #{conn.quote(title)}, #{conn.quote(content)})"
conn.execute(sql)
end
def delete_from_fts5(record_id)
# Note: Use string interpolation for consistency (rowid is always an integer, so safe)
ActiveRecord::Base.connection.execute(
"DELETE FROM search_records_fts WHERE rowid = #{record_id}"
)
end
def search_record_attributes
@@ -37,9 +95,9 @@ module Searchable
searchable_id: id,
card_id: search_card_id,
board_id: search_board_id,
title: Search::Stemmer.stem(search_title),
content: Search::Stemmer.stem(search_content),
created_at: created_at
title: search_title,
content: search_content,
created_at: created_at || Time.current
}
end
+4 -1
View File
@@ -16,7 +16,10 @@ class Search::Query < ApplicationRecord
end
def to_s
Search::Stemmer.stem(terms.to_s)
# Return unstemmed terms - each adapter handles stemming appropriately:
# - SQLite: FTS5 Porter tokenizer stems automatically
# - MySQL: Search::Record.matching applies stemming
terms.to_s
end
private
+126 -8
View File
@@ -3,6 +3,7 @@ class Search::Record < ApplicationRecord
SHARD_COUNT = 16
# MySQL sharded table classes
SHARD_CLASSES = SHARD_COUNT.times.map do |shard_id|
Class.new(self) do
self.table_name = "search_records_#{shard_id}"
@@ -13,8 +14,8 @@ class Search::Record < ApplicationRecord
end
end.freeze
belongs_to :searchable, polymorphic: true
belongs_to :card
belongs_to :searchable, polymorphic: true, optional: true
belongs_to :card, optional: true
# Virtual attributes from search query
attribute :query, :string
@@ -22,9 +23,30 @@ class Search::Record < ApplicationRecord
validates :account_id, :searchable_type, :searchable_id, :card_id, :board_id, :created_at, presence: true
class << self
def sqlite?
connection.adapter_name == "SQLite"
end
def for_account(account_id)
if sqlite?
# SQLite uses a single table, no sharding - use a non-abstract subclass
@sqlite_class ||= Class.new(self) do
self.abstract_class = false
self.table_name = "search_records"
# Override the UUID id attribute from ApplicationRecord
# SQLite uses integer auto-increment primary key (no default)
attribute :id, :integer, default: nil
def self.name
"Search::Record"
end
end
else
# MySQL uses sharded tables
SHARD_CLASSES[shard_id_for_account(account_id)]
end
end
def shard_id_for_account(account_id)
Zlib.crc32(account_id.to_s) % SHARD_COUNT
@@ -33,6 +55,41 @@ class Search::Record < ApplicationRecord
def card_join
"INNER JOIN #{table_name} ON #{table_name}.card_id = cards.id"
end
# Convert MySQL BOOLEAN MODE syntax to SQLite FTS5 syntax
def convert_to_fts5_query(query)
query = query.to_s.dup
# Handle quoted phrases - these work the same in both
# Extract them to protect during processing
phrases = []
query.gsub!(/"([^"]+)"/) do
phrases << $1
"__PHRASE_#{phrases.length - 1}__"
end
# Split into tokens
tokens = query.split(/\s+/)
processed_tokens = tokens.map do |token|
if token.start_with?("__PHRASE_")
# Restore phrase
idx = token[/\d+/].to_i
"\"#{phrases[idx]}\""
elsif token.start_with?("+")
# Remove + prefix (FTS5 ANDs by default)
token[1..]
elsif token.start_with?("-")
# Convert -term to NOT term
"NOT #{token[1..]}"
else
token
end
end
# Join with AND (FTS5 uses AND/OR/NOT operators)
processed_tokens.reject(&:blank?).join(" ")
end
end
scope :for_query, ->(query:, user:) do
@@ -44,7 +101,18 @@ class Search::Record < ApplicationRecord
end
scope :matching, ->(query) do
where("MATCH(#{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query)
if sqlite?
# SQLite FTS5: join on rowid for fast lookup with native highlighting
# Porter tokenizer handles stemming automatically
fts_query = convert_to_fts5_query(query)
joins("INNER JOIN search_records_fts ON search_records_fts.rowid = #{table_name}.id")
.where("search_records_fts MATCH ?", fts_query)
else
# MySQL FULLTEXT: manually stem query terms
stemmed_query = Search::Stemmer.stem(query)
where("MATCH(#{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", stemmed_query)
end
end
scope :for_user, ->(user) do
@@ -52,10 +120,36 @@ class Search::Record < ApplicationRecord
end
scope :search, ->(query:, user:) do
for_query(query: query, user: user)
relation = for_query(query: query, user: user)
.includes(:searchable, card: [ :board, :creator ])
.select(:id, :searchable_type, :searchable_id, :card_id, :board_id, :account_id, :created_at, "#{connection.quote(query.terms)} AS query")
.order(created_at: :desc)
if sqlite?
# SQLite: matching scope already selected all columns + FTS5 highlight columns
# Re-select to add query terms (ActiveRecord replaces the select list)
opening_mark = Search::Highlighter::OPENING_MARK
closing_mark = Search::Highlighter::CLOSING_MARK
ellipsis = Search::Highlighter::ELIPSIS
relation.select(
"#{table_name}.id",
"#{table_name}.account_id",
"#{table_name}.searchable_type",
"#{table_name}.searchable_id",
"#{table_name}.card_id",
"#{table_name}.board_id",
"#{table_name}.title",
"#{table_name}.content",
"#{table_name}.created_at",
"highlight(search_records_fts, 0, #{connection.quote(opening_mark)}, #{connection.quote(closing_mark)}) AS highlighted_title",
"highlight(search_records_fts, 1, #{connection.quote(opening_mark)}, #{connection.quote(closing_mark)}) AS highlighted_content",
"snippet(search_records_fts, 1, #{connection.quote(opening_mark)}, #{connection.quote(closing_mark)}, #{connection.quote(ellipsis)}, 20) AS content_snippet",
"#{connection.quote(query.terms)} AS query"
)
else
# MySQL: select specific columns needed
relation.select(:id, :searchable_type, :searchable_id, :card_id, :board_id, :account_id, :created_at, "#{connection.quote(query.terms)} AS query")
end
end
def source
@@ -67,15 +161,39 @@ class Search::Record < ApplicationRecord
end
def card_title
highlight(card.title, show: :full) if card_id
if card_id
if self.class.sqlite? && attribute?(:highlighted_title)
# Use FTS5 native highlighting (already HTML-safe from FTS5)
highlighted_title.html_safe
else
# MySQL: use Ruby highlighter
highlight(card.title, show: :full)
end
end
end
def card_description
highlight(card.description.to_plain_text, show: :snippet) if card_id
if card_id
if self.class.sqlite? && attribute?(:content_snippet)
# Use FTS5 native snippet for content (already HTML-safe from FTS5)
content_snippet.html_safe if content_snippet.present?
else
# MySQL: use Ruby highlighter
highlight(card.description.to_plain_text, show: :snippet)
end
end
end
def comment_body
highlight(comment.body.to_plain_text, show: :snippet) if comment
if comment
if self.class.sqlite? && attribute?(:content_snippet)
# Use FTS5 native snippet for content (already HTML-safe from FTS5)
content_snippet.html_safe if content_snippet.present?
else
# MySQL: use Ruby highlighter
highlight(comment.body.to_plain_text, show: :snippet)
end
end
end
private
+4
View File
@@ -32,11 +32,13 @@ development:
primary:
<<: *default
database: db/development.sqlite3
schema_dump: schema_sqlite.rb
<% else %>
primary:
<<: *default
database: fizzy_development
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
schema_dump: schema.rb
replica:
<<: *default
database: fizzy_development
@@ -67,11 +69,13 @@ test:
primary:
<<: *default
database: db/test.sqlite3
schema_dump: schema_sqlite.rb
<% else %>
primary:
<<: *default
database: fizzy_test
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
schema_dump: schema.rb
replica:
<<: *default
database: fizzy_test
@@ -16,8 +16,6 @@ module SQLiteCompatibility
# Override index method to filter out MySQL-specific index options
def index(column_name, **options)
if @conn.is_a?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
# Skip fulltext indexes entirely for SQLite
return if options[:type] == :fulltext
# SQLite doesn't support length-limited indexes
options = options.except(:length)
end
@@ -0,0 +1,33 @@
# Fix for SQLite FTS5 virtual table schema dumping
# Rails has a bug where it doesn't handle FTS5 content= and content_rowid= options
module SQLiteFTS5SchemaDumperFix
# Override the virtual_tables method to handle FTS5 syntax properly
def virtual_tables(stream)
# Query sqlite_master for all virtual tables
virtual_table_sqls = @connection.select_rows(
"SELECT name, sql FROM sqlite_master WHERE type='table' AND sql LIKE 'CREATE VIRTUAL TABLE%'"
)
virtual_table_sqls.each do |table_name, sql|
# Just output the raw SQL since create_virtual_table doesn't handle our syntax
stream.puts " execute #{sql.inspect}"
stream.puts
end
end
end
if ENV.fetch("DATABASE_ADAPTER", "mysql") == "sqlite"
ActiveSupport.on_load(:active_record) do
# Ensure schema dumper is loaded
begin
require 'active_record/connection_adapters/sqlite3/schema_dumper'
rescue LoadError
# SQLite3 adapter not available
end
if defined?(ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper)
ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper.prepend(SQLiteFTS5SchemaDumperFix)
end
end
end
+39 -9
View File
@@ -1,4 +1,15 @@
# Automatically use UUID type for all binary(16) columns
# Define schema dumper module outside on_load so it can be prepended
module SchemaDumperUuidType
# Map binary(16) and blob(16) columns to :uuid type in schema.rb
def schema_type(column)
return :uuid if column.sql_type == "binary(16)"
return :uuid if column.sql_type == "blob(16)"
super
end
end
ActiveSupport.on_load(:active_record) do
module MysqlUuidAdapter
# Add UUID to MySQL's native database types
@@ -21,7 +32,7 @@ ActiveSupport.on_load(:active_record) do
end
module SqliteUuidAdapter
# Add UUID to SQLite's native database types
# Add UUID to SQLite's native database types (instance method)
def native_database_types
@native_database_types_with_uuid ||= super.merge(uuid: { name: "blob", limit: 16 })
end
@@ -51,23 +62,42 @@ ActiveSupport.on_load(:active_record) do
if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SqliteUuidAdapter)
# Also add UUID to class-level native_database_types
ActiveRecord::ConnectionAdapters::SQLite3Adapter.class_eval do
@native_database_types = nil # Clear cache
class << self
alias_method :original_native_database_types, :native_database_types
def native_database_types
@native_database_types_with_uuid ||= original_native_database_types.merge(uuid: { name: "blob", limit: 16 })
end
end
end
end
module SchemaDumperUuidType
# Map binary(16) columns to :uuid type in schema.rb
def schema_type(column)
if column.sql_type == "binary(16)"
:uuid
else
super
end
# Ensure schema dumper classes are loaded before prepending
begin
require 'active_record/connection_adapters/mysql/schema_dumper'
rescue LoadError
# MySQL adapter not available
end
begin
require 'active_record/connection_adapters/sqlite3/schema_dumper'
rescue LoadError
# SQLite3 adapter not available
end
if defined?(ActiveRecord::ConnectionAdapters::MySQL::SchemaDumper)
ActiveRecord::ConnectionAdapters::MySQL::SchemaDumper.prepend(SchemaDumperUuidType)
end
if defined?(ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper)
ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper.prepend(SchemaDumperUuidType)
end
module TableDefinitionUuidSupport
def uuid(name, **options)
column(name, :uuid, **options)
@@ -1,5 +1,8 @@
class CreateSearchIndices < ActiveRecord::Migration[8.2]
def up
# Skip for SQLite - it doesn't use these tables
return if connection.adapter_name == "SQLite"
16.times do |i|
create_table "search_index_#{i}".to_sym, id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci" do |t|
t.string :searchable_type, null: false
@@ -17,6 +20,9 @@ class CreateSearchIndices < ActiveRecord::Migration[8.2]
end
def down
# Skip for SQLite - it doesn't use these tables
return if connection.adapter_name == "SQLite"
16.times do |i|
drop_table "search_index_#{i}".to_sym
end
@@ -2,6 +2,9 @@ class CreateSearchRecordShards < ActiveRecord::Migration[8.2]
SHARD_COUNT = 16
def change
# Skip for SQLite - it uses a single search_records table instead
return if connection.adapter_name == "SQLite"
# Create 16 sharded search_records tables
SHARD_COUNT.times do |shard_id|
create_table "search_records_#{shard_id}", id: :uuid do |t|
@@ -0,0 +1,37 @@
class AddSearchRecords < ActiveRecord::Migration[8.2]
def up
return unless connection.adapter_name == "SQLite"
# Create regular table with integer primary key for FTS5 rowid compatibility
create_table :search_records do |t|
t.uuid :account_id, null: false
t.string :searchable_type, null: false
t.uuid :searchable_id, null: false
t.uuid :card_id, null: false
t.uuid :board_id, null: false
t.string :title
t.text :content
t.datetime :created_at, null: false
t.index [:searchable_type, :searchable_id], unique: true
t.index :account_id
end
# Create FTS5 virtual table using Porter stemmer
# No triggers needed - Searchable concern handles sync via callbacks
execute <<-SQL
CREATE VIRTUAL TABLE search_records_fts USING fts5(
title,
content,
tokenize='porter'
)
SQL
end
def down
return unless connection.adapter_name == "SQLite"
execute "DROP TABLE IF EXISTS search_records_fts"
drop_table :search_records, if_exists: true
end
end
Generated
+17 -2
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do
ActiveRecord::Schema[8.2].define(version: 2025_11_20_110206) do
create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
@@ -46,7 +46,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do
create_table "action_text_rich_texts", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.text "body", size: :long
t.text "body"
t.datetime "created_at", null: false
t.string "name", null: false
t.uuid "record_id", null: false
@@ -408,6 +408,21 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do
t.index ["user_id"], name: "index_search_queries_on_user_id"
end
create_table "search_records", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.string "account_key", null: false
t.uuid "board_id", null: false
t.uuid "card_id", null: false
t.text "content"
t.datetime "created_at", null: false
t.uuid "searchable_id", null: false
t.string "searchable_type", null: false
t.string "title"
t.index ["account_id"], name: "index_search_records_on_account_id"
t.index ["account_key", "content", "title"], name: "index_search_records_on_account_key_and_content_and_title", type: :fulltext
t.index ["searchable_type", "searchable_id"], name: "index_search_records_on_searchable_type_and_searchable_id", unique: true
end
create_table "search_records_0", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "board_id", null: false
+578
View File
@@ -0,0 +1,578 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_11_20_110206) do
create_table "accesses", id: :uuid, force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
t.uuid "board_id", null: false
t.datetime "created_at", null: false
t.string "involvement", default: "access_only", null: false
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.index ["account_id", "accessed_at"], name: "index_accesses_on_account_id_and_accessed_at"
t.index ["board_id", "user_id"], name: "index_accesses_on_board_id_and_user_id", unique: true
t.index ["board_id"], name: "index_accesses_on_board_id"
t.index ["user_id"], name: "index_accesses_on_user_id"
end
create_table "account_join_codes", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.string "code", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "usage_count", default: 0, null: false
t.bigint "usage_limit", default: 10, null: false
t.index ["account_id", "code"], name: "index_account_join_codes_on_account_id_and_code", unique: true
end
create_table "accounts", id: :uuid, force: :cascade do |t|
t.bigint "cards_count", default: 0, null: false
t.datetime "created_at", null: false
t.bigint "external_account_id"
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["external_account_id"], name: "index_accounts_on_external_account_id", unique: true
end
create_table "action_text_rich_texts", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.text "body"
t.datetime "created_at", null: false
t.string "name", null: false
t.uuid "record_id", null: false
t.string "record_type", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_action_text_rich_texts_on_account_id"
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
create_table "active_storage_attachments", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "blob_id", null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.uuid "record_id", null: false
t.string "record_type", null: false
t.index ["account_id"], name: "index_active_storage_attachments_on_account_id"
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.string "content_type"
t.datetime "created_at", null: false
t.string "filename", null: false
t.string "key", null: false
t.text "metadata"
t.string "service_name", null: false
t.index ["account_id"], name: "index_active_storage_blobs_on_account_id"
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "blob_id", null: false
t.string "variation_digest", null: false
t.index ["account_id"], name: "index_active_storage_variant_records_on_account_id"
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "assignees_filters", id: false, force: :cascade do |t|
t.uuid "assignee_id", null: false
t.uuid "filter_id", null: false
t.index ["assignee_id"], name: "index_assignees_filters_on_assignee_id"
t.index ["filter_id"], name: "index_assignees_filters_on_filter_id"
end
create_table "assigners_filters", id: false, force: :cascade do |t|
t.uuid "assigner_id", null: false
t.uuid "filter_id", null: false
t.index ["assigner_id"], name: "index_assigners_filters_on_assigner_id"
t.index ["filter_id"], name: "index_assigners_filters_on_filter_id"
end
create_table "assignments", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "assignee_id", null: false
t.uuid "assigner_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_assignments_on_account_id"
t.index ["assignee_id", "card_id"], name: "index_assignments_on_assignee_id_and_card_id", unique: true
t.index ["card_id"], name: "index_assignments_on_card_id"
end
create_table "board_publications", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "board_id", null: false
t.datetime "created_at", null: false
t.string "key"
t.datetime "updated_at", null: false
t.index ["account_id", "key"], name: "index_board_publications_on_account_id_and_key"
t.index ["board_id"], name: "index_board_publications_on_board_id"
end
create_table "boards", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.boolean "all_access", default: false, null: false
t.datetime "created_at", null: false
t.uuid "creator_id", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_boards_on_account_id"
t.index ["creator_id"], name: "index_boards_on_creator_id"
end
create_table "boards_filters", id: false, force: :cascade do |t|
t.uuid "board_id", null: false
t.uuid "filter_id", null: false
t.index ["board_id"], name: "index_boards_filters_on_board_id"
t.index ["filter_id"], name: "index_boards_filters_on_filter_id"
end
create_table "card_activity_spikes", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_card_activity_spikes_on_account_id"
t.index ["card_id"], name: "index_card_activity_spikes_on_card_id"
end
create_table "card_engagements", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id"
t.datetime "created_at", null: false
t.string "status", default: "doing", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "status"], name: "index_card_engagements_on_account_id_and_status"
t.index ["card_id"], name: "index_card_engagements_on_card_id"
end
create_table "card_goldnesses", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_card_goldnesses_on_account_id"
t.index ["card_id"], name: "index_card_goldnesses_on_card_id", unique: true
end
create_table "card_not_nows", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.uuid "user_id"
t.index ["account_id"], name: "index_card_not_nows_on_account_id"
t.index ["card_id"], name: "index_card_not_nows_on_card_id", unique: true
t.index ["user_id"], name: "index_card_not_nows_on_user_id"
end
create_table "cards", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "board_id", null: false
t.uuid "column_id"
t.datetime "created_at", null: false
t.uuid "creator_id", null: false
t.date "due_on"
t.datetime "last_active_at", null: false
t.bigint "number", null: false
t.string "status", default: "drafted", null: false
t.string "title"
t.datetime "updated_at", null: false
t.index ["account_id", "last_active_at", "status"], name: "index_cards_on_account_id_and_last_active_at_and_status"
t.index ["account_id", "number"], name: "index_cards_on_account_id_and_number", unique: true
t.index ["board_id"], name: "index_cards_on_board_id"
t.index ["column_id"], name: "index_cards_on_column_id"
end
create_table "closers_filters", id: false, force: :cascade do |t|
t.uuid "closer_id", null: false
t.uuid "filter_id", null: false
t.index ["closer_id"], name: "index_closers_filters_on_closer_id"
t.index ["filter_id"], name: "index_closers_filters_on_filter_id"
end
create_table "closures", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.uuid "user_id"
t.index ["account_id"], name: "index_closures_on_account_id"
t.index ["card_id", "created_at"], name: "index_closures_on_card_id_and_created_at"
t.index ["card_id"], name: "index_closures_on_card_id", unique: true
t.index ["user_id"], name: "index_closures_on_user_id"
end
create_table "columns", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "board_id", null: false
t.string "color", null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.integer "position", default: 0, null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_columns_on_account_id"
t.index ["board_id", "position"], name: "index_columns_on_board_id_and_position"
t.index ["board_id"], name: "index_columns_on_board_id"
end
create_table "comments", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.uuid "creator_id", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_comments_on_account_id"
t.index ["card_id"], name: "index_comments_on_card_id"
end
create_table "creators_filters", id: false, force: :cascade do |t|
t.uuid "creator_id", null: false
t.uuid "filter_id", null: false
t.index ["creator_id"], name: "index_creators_filters_on_creator_id"
t.index ["filter_id"], name: "index_creators_filters_on_filter_id"
end
create_table "entropies", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.bigint "auto_postpone_period", default: 2592000, null: false
t.uuid "container_id", null: false
t.string "container_type", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_entropies_on_account_id"
t.index ["container_type", "container_id", "auto_postpone_period"], name: "idx_on_container_type_container_id_auto_postpone_pe_3d79b50517"
t.index ["container_type", "container_id"], name: "index_entropy_configurations_on_container", unique: true
end
create_table "events", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.string "action", null: false
t.uuid "board_id", null: false
t.datetime "created_at", null: false
t.uuid "creator_id", null: false
t.uuid "eventable_id", null: false
t.string "eventable_type", null: false
t.json "particulars", default: -> { "json_object()" }
t.datetime "updated_at", null: false
t.index ["account_id", "action"], name: "index_events_on_account_id_and_action"
t.index ["board_id", "action", "created_at"], name: "index_events_on_board_id_and_action_and_created_at"
t.index ["board_id"], name: "index_events_on_board_id"
t.index ["creator_id"], name: "index_events_on_creator_id"
t.index ["eventable_type", "eventable_id"], name: "index_events_on_eventable"
end
create_table "filters", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.uuid "creator_id", null: false
t.json "fields", default: -> { "json_object()" }, null: false
t.string "params_digest", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_filters_on_account_id"
t.index ["creator_id", "params_digest"], name: "index_filters_on_creator_id_and_params_digest", unique: true
end
create_table "filters_tags", id: false, force: :cascade do |t|
t.uuid "filter_id", null: false
t.uuid "tag_id", null: false
t.index ["filter_id"], name: "index_filters_tags_on_filter_id"
t.index ["tag_id"], name: "index_filters_tags_on_tag_id"
end
create_table "identities", id: :uuid, force: :cascade do |t|
t.datetime "created_at", null: false
t.string "email_address", null: false
t.datetime "updated_at", null: false
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
end
create_table "magic_links", id: :uuid, force: :cascade do |t|
t.string "code", null: false
t.datetime "created_at", null: false
t.datetime "expires_at", null: false
t.uuid "identity_id"
t.datetime "updated_at", null: false
t.index ["code"], name: "index_magic_links_on_code", unique: true
t.index ["expires_at"], name: "index_magic_links_on_expires_at"
t.index ["identity_id"], name: "index_magic_links_on_identity_id"
end
create_table "mentions", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.uuid "mentionee_id", null: false
t.uuid "mentioner_id", null: false
t.uuid "source_id", null: false
t.string "source_type", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_mentions_on_account_id"
t.index ["mentionee_id"], name: "index_mentions_on_mentionee_id"
t.index ["mentioner_id"], name: "index_mentions_on_mentioner_id"
t.index ["source_type", "source_id"], name: "index_mentions_on_source"
end
create_table "notification_bundles", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.datetime "ends_at", null: false
t.datetime "starts_at", null: false
t.integer "status", default: 0, null: false
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.index ["account_id"], name: "index_notification_bundles_on_account_id"
t.index ["ends_at", "status"], name: "index_notification_bundles_on_ends_at_and_status"
t.index ["user_id", "starts_at", "ends_at"], name: "idx_on_user_id_starts_at_ends_at_7eae5d3ac5"
t.index ["user_id", "status"], name: "index_notification_bundles_on_user_id_and_status"
end
create_table "notifications", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.uuid "creator_id"
t.datetime "read_at"
t.uuid "source_id", null: false
t.string "source_type", null: false
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.index ["account_id"], name: "index_notifications_on_account_id"
t.index ["creator_id"], name: "index_notifications_on_creator_id"
t.index ["source_type", "source_id"], name: "index_notifications_on_source"
t.index ["user_id", "read_at", "created_at"], name: "index_notifications_on_user_id_and_read_at_and_created_at", order: { read_at: :desc, created_at: :desc }
t.index ["user_id"], name: "index_notifications_on_user_id"
end
create_table "pins", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.index ["account_id"], name: "index_pins_on_account_id"
t.index ["card_id", "user_id"], name: "index_pins_on_card_id_and_user_id", unique: true
t.index ["card_id"], name: "index_pins_on_card_id"
t.index ["user_id"], name: "index_pins_on_user_id"
end
create_table "push_subscriptions", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.string "auth_key"
t.datetime "created_at", null: false
t.text "endpoint"
t.string "p256dh_key"
t.datetime "updated_at", null: false
t.string "user_agent"
t.uuid "user_id", null: false
t.index ["account_id"], name: "index_push_subscriptions_on_account_id"
t.index ["user_id", "endpoint"], name: "index_push_subscriptions_on_user_id_and_endpoint", unique: true
end
create_table "reactions", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "comment_id", null: false
t.string "content", limit: 16, null: false
t.datetime "created_at", null: false
t.uuid "reacter_id", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_reactions_on_account_id"
t.index ["comment_id"], name: "index_reactions_on_comment_id"
t.index ["reacter_id"], name: "index_reactions_on_reacter_id"
end
create_table "search_queries", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.string "terms", limit: 2000, null: false
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.index ["account_id"], name: "index_search_queries_on_account_id"
t.index ["user_id", "terms"], name: "index_search_queries_on_user_id_and_terms"
t.index ["user_id", "updated_at"], name: "index_search_queries_on_user_id_and_updated_at", unique: true
t.index ["user_id"], name: "index_search_queries_on_user_id"
end
create_table "search_records", force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "board_id", null: false
t.uuid "card_id", null: false
t.text "content"
t.datetime "created_at", null: false
t.uuid "searchable_id", null: false
t.string "searchable_type", null: false
t.string "title"
t.index ["account_id"], name: "index_search_records_on_account_id"
t.index ["searchable_type", "searchable_id"], name: "index_search_records_on_searchable_type_and_searchable_id", unique: true
end
create_table "sessions", id: :uuid, force: :cascade do |t|
t.datetime "created_at", null: false
t.uuid "identity_id", null: false
t.string "ip_address"
t.datetime "updated_at", null: false
t.string "user_agent"
t.index ["identity_id"], name: "index_sessions_on_identity_id"
end
create_table "steps", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.boolean "completed", default: false, null: false
t.text "content", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_steps_on_account_id"
t.index ["card_id", "completed"], name: "index_steps_on_card_id_and_completed"
t.index ["card_id"], name: "index_steps_on_card_id"
end
create_table "taggings", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.uuid "tag_id", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_taggings_on_account_id"
t.index ["card_id", "tag_id"], name: "index_taggings_on_card_id_and_tag_id", unique: true
t.index ["tag_id"], name: "index_taggings_on_tag_id"
end
create_table "tags", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.string "title"
t.datetime "updated_at", null: false
t.index ["account_id", "title"], name: "index_tags_on_account_id_and_title", unique: true
end
create_table "user_settings", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.integer "bundle_email_frequency", default: 0, null: false
t.datetime "created_at", null: false
t.string "timezone_name"
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.index ["account_id"], name: "index_user_settings_on_account_id"
t.index ["user_id", "bundle_email_frequency"], name: "index_user_settings_on_user_id_and_bundle_email_frequency"
t.index ["user_id"], name: "index_user_settings_on_user_id"
end
create_table "users", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.boolean "active", default: true, null: false
t.datetime "created_at", null: false
t.uuid "identity_id"
t.string "name", null: false
t.string "role", default: "member", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "identity_id"], name: "index_users_on_account_id_and_identity_id", unique: true
t.index ["account_id", "role"], name: "index_users_on_account_id_and_role"
t.index ["identity_id"], name: "index_users_on_identity_id"
end
create_table "watches", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.uuid "card_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.boolean "watching", default: true, null: false
t.index ["account_id"], name: "index_watches_on_account_id"
t.index ["card_id"], name: "index_watches_on_card_id"
t.index ["user_id", "card_id"], name: "index_watches_on_user_id_and_card_id"
t.index ["user_id"], name: "index_watches_on_user_id"
end
create_table "webhook_delinquency_trackers", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.integer "consecutive_failures_count", default: 0
t.datetime "created_at", null: false
t.datetime "first_failure_at"
t.datetime "updated_at", null: false
t.uuid "webhook_id", null: false
t.index ["account_id"], name: "index_webhook_delinquency_trackers_on_account_id"
t.index ["webhook_id"], name: "index_webhook_delinquency_trackers_on_webhook_id"
end
create_table "webhook_deliveries", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.uuid "event_id", null: false
t.text "request"
t.text "response"
t.string "state", null: false
t.datetime "updated_at", null: false
t.uuid "webhook_id", null: false
t.index ["account_id"], name: "index_webhook_deliveries_on_account_id"
t.index ["event_id"], name: "index_webhook_deliveries_on_event_id"
t.index ["webhook_id"], name: "index_webhook_deliveries_on_webhook_id"
end
create_table "webhooks", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.boolean "active", default: true, null: false
t.uuid "board_id", null: false
t.datetime "created_at", null: false
t.string "name"
t.string "signing_secret", null: false
t.text "subscribed_actions"
t.datetime "updated_at", null: false
t.text "url", null: false
t.index ["account_id"], name: "index_webhooks_on_account_id"
t.index ["board_id", "subscribed_actions"], name: "index_webhooks_on_board_id_and_subscribed_actions"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "board_publications", "boards"
add_foreign_key "card_activity_spikes", "cards"
add_foreign_key "card_goldnesses", "cards"
add_foreign_key "card_not_nows", "cards"
add_foreign_key "card_not_nows", "users"
add_foreign_key "cards", "columns"
add_foreign_key "closures", "cards"
add_foreign_key "closures", "users"
add_foreign_key "columns", "boards"
add_foreign_key "comments", "cards"
add_foreign_key "events", "boards"
add_foreign_key "magic_links", "identities"
add_foreign_key "mentions", "users", column: "mentionee_id"
add_foreign_key "mentions", "users", column: "mentioner_id"
add_foreign_key "notification_bundles", "users"
add_foreign_key "notifications", "users"
add_foreign_key "notifications", "users", column: "creator_id"
add_foreign_key "pins", "cards"
add_foreign_key "pins", "users"
add_foreign_key "push_subscriptions", "users"
add_foreign_key "search_queries", "users"
add_foreign_key "sessions", "identities"
add_foreign_key "steps", "cards"
add_foreign_key "taggings", "cards"
add_foreign_key "taggings", "tags"
add_foreign_key "user_settings", "users"
add_foreign_key "users", "identities"
add_foreign_key "watches", "cards"
add_foreign_key "watches", "users"
add_foreign_key "webhook_delinquency_trackers", "webhooks"
add_foreign_key "webhook_deliveries", "events"
add_foreign_key "webhook_deliveries", "webhooks"
add_foreign_key "webhooks", "boards"
execute "CREATE VIRTUAL TABLE search_records_fts USING fts5(\n title,\n content,\n tokenize='porter'\n )"
end
+5
View File
@@ -28,8 +28,13 @@ module SearchTestHelper
private
def clear_search_records
if Search::Record.sqlite?
ActiveRecord::Base.connection.execute("DELETE FROM search_records")
ActiveRecord::Base.connection.execute("DELETE FROM search_records_fts")
else
Search::Record::SHARD_COUNT.times do |shard_id|
ActiveRecord::Base.connection.execute("DELETE FROM search_records_#{shard_id}")
end
end
end
end