Support SQLite searching
This commit is contained in:
@@ -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) ],
|
||||
update_only: [ :card_id, :board_id, :title, :content, :created_at ]
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+127
-9
@@ -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,8 +23,29 @@ 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)
|
||||
SHARD_CLASSES[shard_id_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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
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
|
||||
# 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
|
||||
|
||||
# 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -28,8 +28,13 @@ module SearchTestHelper
|
||||
|
||||
private
|
||||
def clear_search_records
|
||||
Search::Record::SHARD_COUNT.times do |shard_id|
|
||||
ActiveRecord::Base.connection.execute("DELETE FROM search_records_#{shard_id}")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user