basic indexes fixed, migration script

This commit is contained in:
Dylan Knutson
2025-02-04 19:41:30 +00:00
parent cf5c4d28b6
commit b62f7094f4
77 changed files with 20839 additions and 61 deletions

View File

@@ -92,3 +92,13 @@ task :reverse_csv do
in_csv.reverse_each { |row| out_csv << row.map(&:second) }
out_csv.close
end
task migrate_domain: :environment do
Domain::MigrateToDomain.new.migrate_e621_users
Domain::MigrateToDomain.new.migrate_e621_posts
Domain::MigrateToDomain.new.migrate_fa_users
Domain::MigrateToDomain.new.migrate_fa_posts
Domain::MigrateToDomain.new.migrate_e621_users_favs
Domain::MigrateToDomain.new.migrate_fa_users_favs
Domain::MigrateToDomain.new.migrate_fa_users_followers
end

View File

@@ -0,0 +1,281 @@
# typed: strict
class Domain::MigrateToDomain
extend T::Sig
include HasColorLogger
sig { void }
def migrate_e621_users
Domain::E621::User
.joins(
"LEFT JOIN domain_users ON domain_e621_users.e621_user_id =
(domain_users.json_attributes->>'e621_id')::integer
AND domain_users.type = 'Domain::User::E621User'",
)
.where("domain_users.id IS NULL")
.find_in_batches do |batch|
ReduxApplicationRecord.transaction do
batch.each { |user| migrate_e621_user(user) }
end
end
end
sig { void }
def migrate_e621_posts
Domain::E621::Post
.joins(
"LEFT JOIN domain_posts ON domain_e621_posts.e621_id =
(domain_posts.json_attributes->>'e621_id')::integer
AND domain_posts.type = 'Domain::Post::E621Post'",
)
.where("domain_posts.id IS NULL")
.find_in_batches do |batch|
ReduxApplicationRecord.transaction do
batch.each { |post| migrate_e621_post(post) }
end
end
end
sig { void }
def migrate_fa_users
Domain::Fa::User
.joins(
"LEFT JOIN domain_users ON domain_fa_users.url_name =
domain_users.json_attributes->>'url_name'
AND domain_users.type = 'Domain::User::FaUser'",
)
.where("domain_users.id IS NULL")
.find_in_batches do |batch|
ReduxApplicationRecord.transaction do
batch.each { |user| migrate_fa_user(user) }
end
end
end
sig { void }
def migrate_fa_posts
Domain::Fa::Post
.joins(
"LEFT JOIN domain_posts ON domain_fa_posts.fa_id =
(domain_posts.json_attributes->>'fa_id')::integer
AND domain_posts.type = 'Domain::Post::FaPost'",
)
.where("domain_posts.id IS NULL")
.includes(:creator, :file)
.find_in_batches do |batch|
ReduxApplicationRecord.transaction do
batch.each { |post| migrate_fa_post(post) }
end
end
end
sig { params(user: Domain::Fa::User).void }
def migrate_fa_user(user)
user =
Domain::User::FaUser.find_or_initialize_by(
url_name: user.url_name,
) { |new_user| new_user.name = user.name }
if user.new_record?
logger.info("migrated fa user #{user.url_name}")
user.save!
end
end
sig { params(post: Domain::Fa::Post).void }
def migrate_fa_post(post)
post =
Domain::Post::FaPost.find_or_initialize_by(
fa_id: post.fa_id,
) do |new_post|
new_post.state = post.state
new_post.title = post.title
new_post.fa_id = post.fa_id
new_post.category = post.category
new_post.theme = post.theme
new_post.species = post.species
new_post.gender = post.gender
new_post.description = post.description
new_post.keywords = post.keywords
new_post.num_favorites = post.num_favorites
new_post.num_comments = post.num_comments
new_post.num_views = post.num_views
new_post.posted_at = post.posted_at
new_post.scanned_at = post.scanned_at
new_post.scan_file_error = post.scan_file_error
new_post.last_user_page_id = post.last_user_page_id
new_post.last_submission_page_id = post.last_submission_page_id
new_post.first_browse_page_id = post.first_browse_page_id
new_post.first_gallery_page_id = post.first_gallery_page_id
new_post.first_seen_entry_id = post.first_seen_entry_id
new_post.created_at = post.created_at
if post.creator.present?
new_post.creator =
Domain::User::FaUser.find_by!(url_name: post.creator&.url_name)
end
if post.file.present?
new_post.file =
Domain::PostFile.find_or_create_by(
log_entry: post.file,
) do |new_file|
new_file.log_entry = post.file
new_file.url_str = post.file_url_str
new_file.state = post.state
end
end
end
if post.new_record?
logger.info("migrated fa post #{post.fa_id}")
post.save!
end
end
sig { params(user: Domain::E621::User).void }
def migrate_e621_user(user)
user =
Domain::User::E621User.find_or_initialize_by(
e621_id: user.e621_user_id,
) do |new_user|
new_user.name = user.name
new_user.favs_are_hidden = user.favs_are_hidden
new_user.num_other_favs_cached = user.num_other_favs_cached
new_user.scanned_favs_status = user.scanned_favs_status
new_user.scanned_favs_at = user.scanned_favs_at
end
if user.new_record?
logger.info("migrated e621 user #{user.name}")
user.save!
end
end
sig { params(post: Domain::E621::Post).void }
def migrate_e621_post(post)
post =
Domain::Post::E621Post.find_or_initialize_by(
e621_id: post.e621_id,
) do |new_post|
new_post.state = post.state
new_post.e621_id = post.e621_id
new_post.scanned_post_favs_at = post.scanned_post_favs_at
new_post.rating = post.rating
new_post.tags_array = post.tags_array
new_post.flags_array = post.flags_array
new_post.pools_array = post.pools_array
new_post.sources_array = post.sources_array
new_post.artists_array = post.artists_array
new_post.e621_updated_at = post.e621_updated_at
new_post.last_index_page_id = post.last_index_page_id
new_post.caused_by_entry_id = post.caused_by_entry_id
new_post.scan_log_entry_id = post.scan_log_entry_id
new_post.index_page_ids = post.index_page_ids
new_post.prev_md5s = post.prev_md5s
new_post.scan_error = post.scan_error
new_post.file_error = post.file_error
new_post.created_at = post.created_at
# TODO - migrate parent posts
end
if post.new_record?
logger.info("migrated e621 post #{post.e621_id}")
post.save!
end
end
sig { void }
def migrate_e621_users_favs
Domain::User::E621User
.where_migrated_user_favs_at("is null")
.find_each { |user| migrate_e621_user_favs(user) }
end
sig { params(user: Domain::User::E621User).void }
def migrate_e621_user_favs(user)
user_e621_id = user.e621_id
old_user = Domain::E621::User.find_by!(e621_user_id: user_e621_id)
old_post_e621_ids = old_user.faved_posts.pluck(:e621_id)
new_post_ids =
Domain::Post::E621Post.where(e621_id: old_post_e621_ids).pluck(:id)
Domain::UserPostFav.upsert_all(
new_post_ids.map { |post_id| { user_id: user.id, post_id: } },
unique_by: %i[user_id post_id],
)
if user.faved_posts.count != old_user.faved_posts.count
logger.error(
"favs mismatch for #{user.name}: (#{user.faved_posts.count} != #{old_user.faved_posts.count})",
)
else
user.migrated_user_favs_at = Time.current
user.save!
logger.info("migrated e621 user favs #{user.name} (#{new_post_ids.size})")
end
end
sig { void }
def migrate_fa_users_favs
Domain::User::FaUser
.where_migrated_user_favs_at("is null")
.find_each { |user| migrate_fa_user_favs(user) }
end
sig { params(user: Domain::User::FaUser).void }
def migrate_fa_user_favs(user)
user_url_name = user.url_name
old_user = Domain::Fa::User.find_by!(url_name: user_url_name)
old_post_fa_ids = old_user.fav_posts.pluck(:fa_id)
new_post_ids = Domain::Post::FaPost.where(fa_id: old_post_fa_ids).pluck(:id)
Domain::UserPostFav.upsert_all(
new_post_ids.map { |post_id| { user_id: user.id, post_id: } },
unique_by: %i[user_id post_id],
)
if user.faved_posts.count != old_user.fav_posts.count
logger.error(
"favs mismatch for #{user.name}: (#{user.faved_posts.count} != #{old_user.fav_posts.count})",
)
else
user.migrated_user_favs_at = Time.current
user.save!
logger.info("migrated fa user favs #{user.name} (#{new_post_ids.size})")
end
end
sig { void }
def migrate_fa_users_followers
Domain::User::FaUser
.where_migrated_followers_at("is null")
.find_each { |user| migrate_fa_user_followers(user) }
end
sig { params(user: Domain::User::FaUser).void }
def migrate_fa_user_followers(user)
user_url_name = user.url_name
old_user = Domain::Fa::User.find_by!(url_name: user_url_name)
followed_user_url_names = old_user.follows.pluck(:url_name)
new_user_ids =
Domain::User::FaUser.where(url_name: followed_user_url_names).pluck(:id)
Domain::UserUserFollow.upsert_all(
new_user_ids.map { |user_id| { from_id: user.id, to_id: user_id } },
unique_by: %i[from_id to_id],
)
if user.following_users.count != old_user.follows.count
logger.error(
"followers mismatch for #{user.name}: (#{user.following_users.count} != #{old_user.follows.count})",
)
else
user.migrated_followers_at = Time.current
user.save!
logger.info(
"migrated fa user followers #{user.name} (#{new_user_ids.size})",
)
end
end
end

View File

@@ -3,29 +3,40 @@
require "arel/visitors/to_sql"
class Arel::Visitors::ToSql
extend T::Sig
class UnquotedArelSqlLiteral < Arel::Nodes::SqlLiteral
extend T::Sig
sig { params(name_in_to_s: String, name_in_to_sql: String).void }
def initialize(name_in_to_s, name_in_to_sql)
super(name_in_to_s)
@name_in_to_sql = name_in_to_sql
end
sig { returns(String) }
def in_sql
@name_in_to_sql
end
end
prepend(
Module.new do
extend T::Sig
sig { params(o: T.untyped, collector: T.untyped).returns(T.untyped) }
def visit_Arel_Attributes_Attribute(o, collector)
join_name = o.relation.table_alias || o.relation.name
ar_table = o.relation.instance_variable_get("@klass")
if ar_table && ar_table < AttrJsonRecordAliases
registry =
T.cast(
T.unsafe(ar_table).attr_json_registry,
AttrJson::AttributeDefinition::Registry,
)
attribute_def =
T.cast(
registry[o.name.to_sym],
T.nilable(AttrJson::AttributeDefinition),
)
if attribute_def
attr_type =
T.cast(
attribute_def.type.type,
T.any(Symbol, ActiveModel::Type::Value),
)
attr_type_cast = ar_table.json_attribute_type_cast(attr_type)
column = "json_attributes->>'#{o.name}'"
str =
"((#{quote_table_name(join_name)}.#{column})#{attr_type_cast})"
return collector << str
end
end
sig { params(value: T.untyped).returns(String) }
def quote_column_name(value)
return value.in_sql if UnquotedArelSqlLiteral === value
super(value)
collector << quote_table_name(join_name) << "." <<
quote_column_name(o.name)
end
end,
)
@@ -77,11 +88,42 @@ module AttrJsonRecordAliases
extend T::Helpers
requires_ancestor { T.class_of(ActiveRecord::Base) }
sig { params(attr_name: Symbol).void }
def json_attributes_scope(attr_name)
sig do
params(type: T.any(Symbol, ActiveModel::Type::Value)).returns(String)
end
def json_attribute_type_cast(type)
case type
when :integer
"::integer"
when :string
"::text"
else
""
end
end
sig do
params(
attr_name: Symbol,
type: T.any(Symbol, ActiveModel::Type::Value),
).returns(String)
end
def json_attribute_expression(attr_name, type)
"json_attributes->>'#{attr_name}'"
end
sig do
params(
attr_name: Symbol,
type: T.any(Symbol, ActiveModel::Type::Value),
).void
end
def json_attributes_scope(attr_name, type)
attribute_expression = json_attribute_expression(attr_name, type)
db_type = json_attribute_type_cast(type)
scope :"where_#{attr_name}",
->(expr, *binds) do
where("json_attributes->>'#{attr_name}' #{expr}", binds)
where("(#{attribute_expression}#{db_type}) #{expr}", binds)
end
scope :"order_#{attr_name}",
@@ -89,28 +131,20 @@ module AttrJsonRecordAliases
unless [:asc, :desc, nil].include?(dir)
raise("invalid direction: #{dir}")
end
order(Arel.sql "json_attributes->>'#{attr_name}' #{dir}")
order(Arel.sql("#{attribute_expression} #{dir}"))
end
end
sig do
params(
name: Symbol,
attr_name: Symbol,
type: T.any(Symbol, ActiveModel::Type::Value),
options: T.untyped,
).void
end
def attr_json(name, type, options)
super(name, type, options)
json_attributes_scope(name)
self.attribute_aliases =
self.attribute_aliases.merge(
name.to_s =>
Arel::Visitors::ToSql::UnquotedArelSqlLiteral.new(
name.to_s,
"json_attributes->>'#{name}'",
),
)
def attr_json_scoped(attr_name, type, **options)
T.unsafe(self).attr_json(attr_name, type, **options)
json_attributes_scope(attr_name, type)
end
end

View File

@@ -87,18 +87,6 @@ class Domain::E621::Post < ReduxApplicationRecord
ta.is_a?(Hash) ? ta : { "general" => ta }
end
# sig { returns(T.nilable(HttpLogEntry)) }
# def index_page_http_log_entry
# if state_detail["last_index_page_id"].present?
# HttpLogEntry.find_by(id: state_detail["last_index_page_id"])
# end
# end
# sig { returns(T.nilable(HttpLogEntry)) }
# def index_page_http_log_entry
# HttpLogEntry.find_by(id: last_index_page_id) if last_index_page_id.present?
# end
sig { returns(T.nilable(Addressable::URI)) }
def file_uri
Addressable::URI.parse(self.file_url_str) if self.file_url_str.present?

View File

@@ -4,23 +4,23 @@ class Domain::E621::User < ReduxApplicationRecord
include AttrJsonRecordAliases
include AttrJson::Record::QueryScopes
json_attributes_scope :scanned_favs_at
json_attributes_scope :scanned_favs_status
json_attributes_scope :num_other_favs_cached
validates_inclusion_of :scanned_favs_status,
in: %w[ok error],
if: :scanned_favs_status?
has_many :favs, class_name: "Domain::E621::Fav", inverse_of: :user
attr_json :favs_are_hidden, :boolean
has_many :faved_posts,
class_name: "Domain::E621::Post",
through: :favs,
source: :post
attr_json_scoped :favs_are_hidden, :boolean
# number of favorites that the user has, derived from scraped html
# on /posts/<post_id>/favorites?page=<n>
# Used to find users with a significant number of favorites
attr_json :num_other_favs_cached, :integer
attr_json :scanned_favs_status, :string
attr_json :scanned_favs_at, :datetime
attr_json_scoped :num_other_favs_cached, :integer
attr_json_scoped :scanned_favs_status, :string
attr_json_scoped :scanned_favs_at, :datetime
sig { returns(T.nilable(::String)) }
def url_name

38
app/models/domain/post.rb Normal file
View File

@@ -0,0 +1,38 @@
# typed: strict
class Domain::Post < ReduxApplicationRecord
extend T::Helpers
include AttrJsonRecordAliases
self.table_name = "domain_posts"
abstract!
sig { returns(String) }
def type
super
end
sig { params(value: String).returns(String) }
def type=(value)
super
end
has_many :files,
class_name: "::Domain::PostFile",
inverse_of: :post,
dependent: :destroy
has_many :user_post_creations,
class_name: "::Domain::UserPostCreation",
inverse_of: :post,
dependent: :destroy
has_many :user_post_favs,
class_name: "::Domain::UserPostFav",
inverse_of: :post,
dependent: :destroy
has_many :faving_users, through: :user_post_favs, source: :user
sig { abstract.returns(T.nilable(String)) }
def to_param
end
end

View File

@@ -0,0 +1,59 @@
# typed: strict
class Domain::Post::E621Post < Domain::Post
class FileError
include AttrJson::Model
attr_json :retry_count, :integer
attr_json :status_code, :integer
attr_json :log_entry_id, :integer
end
attr_json_scoped :state, :string
attr_json_scoped :e621_id, :integer
# When was the post's /posts/<post_id>/favorites pages scanned?
# Used to identify users with a significant number of favorites, setting
# their `num_other_favs_cached` attribute
attr_json_scoped :scanned_post_favs_at, :datetime
attr_json_scoped :rating, :string
attr_json_scoped :tags_array, ActiveModel::Type::Value.new
attr_json_scoped :flags_array, :string, array: true
attr_json_scoped :pools_array, :string, array: true
attr_json_scoped :sources_array, :string, array: true
attr_json_scoped :artists_array, :string, array: true
attr_json_scoped :e621_updated_at, :datetime
attr_json_scoped :parent_post_id, :integer
attr_json_scoped :last_index_page_id, :integer
attr_json_scoped :caused_by_entry_id, :integer
attr_json_scoped :scan_log_entry_id, :integer
attr_json_scoped :index_page_ids, :integer, array: true
attr_json_scoped :prev_md5s, :string, array: true
attr_json_scoped :scan_error, :string
attr_json_scoped :file_error, FileError.to_type
attr_json_scoped :uploader_user_id, :integer
belongs_to :parent_post, class_name: "Domain::Post::E621Post", optional: true
belongs_to :uploader_user,
class_name: "::Domain::User::E621User",
inverse_of: :uploaded_posts,
optional: true
has_one :file, class_name: "::Domain::PostFile", foreign_key: :post_id
belongs_to :last_index_page, class_name: "HttpLogEntry", optional: true
validates :state, inclusion: { in: %w[ok removed scan_error file_error] }
validates :rating, inclusion: { in: %w[s q e] }
validates :e621_id, presence: true
after_initialize do
self.state ||= "ok"
self.flags_array ||= []
self.pools_array ||= []
self.sources_array ||= []
self.artists_array ||= []
end
sig { override.returns(T.nilable(String)) }
def to_param
"e621/#{self.e621_id}" if self.e621_id.present?
end
end

View File

@@ -0,0 +1,56 @@
# typed: strict
class Domain::Post::FaPost < Domain::Post
include AttrJsonRecordAliases
attr_json_scoped :title, :string
attr_json_scoped :state, :string
attr_json_scoped :fa_id, :integer
attr_json_scoped :category, :string
attr_json_scoped :theme, :string
attr_json_scoped :species, :string
attr_json_scoped :gender, :string
attr_json_scoped :description, :string
attr_json_scoped :keywords, :string, array: true, default: []
attr_json_scoped :num_favorites, :integer
attr_json_scoped :num_comments, :integer
attr_json_scoped :num_views, :integer
attr_json_scoped :posted_at, :datetime
attr_json_scoped :scanned_at, :datetime
attr_json_scoped :scan_file_error, :string
attr_json :last_user_page_id, :integer
attr_json :last_submission_page_id, :integer
attr_json :first_browse_page_id, :integer
attr_json :first_gallery_page_id, :integer
attr_json :first_seen_entry_id, :integer
belongs_to :last_user_page, class_name: "::HttpLogEntry", optional: true
belongs_to :last_submission_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_browse_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_gallery_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_seen_entry, class_name: "::HttpLogEntry", optional: true
has_one :user_post_creation,
class_name: "::Domain::UserPostCreation",
inverse_of: :post,
dependent: :destroy
has_one :creator,
through: :user_post_creation,
source: :user,
class_name: "::Domain::User::FaUser"
has_one :file,
class_name: "::Domain::PostFile",
inverse_of: :post,
dependent: :destroy
after_initialize { self.state ||= "ok" }
validates :state, inclusion: { in: %w[ok removed scan_error file_error] }
validates :fa_id, presence: true
sig { override.returns(T.nilable(String)) }
def to_param
"fa/#{self.fa_id}" if self.fa_id.present?
end
end

View File

@@ -0,0 +1,16 @@
# typed: strict
class Domain::PostFile < ReduxApplicationRecord
include AttrJsonRecordAliases
self.table_name = "domain_post_files"
belongs_to :post, class_name: "::Domain::Post", inverse_of: :files
belongs_to :log_entry, class_name: "::HttpLogEntry"
attr_json_scoped :state, :string
attr_json_scoped :url_str, :string
attr_json_scoped :error, :string
validates :state, inclusion: { in: %w[ok error] }
after_initialize { self.state ||= "ok" }
end

55
app/models/domain/user.rb Normal file
View File

@@ -0,0 +1,55 @@
# typed: strict
class Domain::User < ReduxApplicationRecord
extend T::Helpers
include AttrJsonRecordAliases
self.table_name = "domain_users"
abstract!
sig { returns(String) }
def type
super
end
sig { params(value: String).returns(String) }
def type=(value)
super
end
attr_json_scoped :migrated_user_favs_at, :datetime
has_many :user_post_creations,
class_name: "::Domain::UserPostCreation",
inverse_of: :user,
dependent: :destroy
has_many :user_post_favs,
class_name: "::Domain::UserPostFav",
inverse_of: :user,
dependent: :destroy
has_many :user_user_follows_from,
class_name: "::Domain::UserUserFollow",
foreign_key: :from_id,
inverse_of: :from,
dependent: :destroy
has_many :user_user_follows_to,
class_name: "::Domain::UserUserFollow",
foreign_key: :to_id,
inverse_of: :to,
dependent: :destroy
has_many :faved_posts, through: :user_post_favs, source: :post
has_many :following_users, through: :user_user_follows_to, source: :to
has_many :followed_by_users, through: :user_user_follows_from, source: :from
has_one :avatar,
-> { order(created_at: :desc) },
class_name: "::Domain::UserAvatar",
inverse_of: :user
has_many :avatars,
class_name: "::Domain::UserAvatar",
inverse_of: :user,
dependent: :destroy
end

View File

@@ -0,0 +1,22 @@
# typed: strict
class Domain::User::E621User < Domain::User
attr_json_scoped :e621_id, :integer
attr_json_scoped :name, :string
attr_json_scoped :favs_are_hidden, :boolean
attr_json_scoped :num_other_favs_cached, :integer
attr_json_scoped :scanned_favs_status, :string
attr_json_scoped :scanned_favs_at, :datetime
has_many :uploaded_posts,
class_name: "::Domain::Post::E621Post",
foreign_key: :uploader_user_id,
inverse_of: :uploader_user
validates :scanned_favs_status,
inclusion: {
in: %w[ok error],
if: :scanned_favs_status?,
}
validates :e621_id, presence: true
validates :name, presence: true
end

View File

@@ -0,0 +1,14 @@
# typed: strict
class Domain::User::FaUser < Domain::User
has_many :posts,
class_name: "::Domain::Fa::Post",
through: :user_post_creations,
source: :post
attr_json_scoped :name, :string
attr_json_scoped :url_name, :string
attr_json_scoped :migrated_followers_at, :datetime
validates :name, presence: true
validates :url_name, presence: true
end

View File

@@ -0,0 +1,13 @@
# typed: strict
class Domain::UserAvatar < ReduxApplicationRecord
include AttrJsonRecordAliases
self.table_name = "domain_user_avatars"
belongs_to :user, class_name: "::Domain::User", inverse_of: :avatar
attr_json :url, :string
attr_json :state, :string
attr_json :downloaded_at, :datetime
attr_json :log_entry_id, :integer
belongs_to :log_entry, class_name: "::HttpLogEntry", optional: true
end

View File

@@ -0,0 +1,14 @@
class Domain::UserPostCreation < ReduxApplicationRecord
include AttrJsonRecordAliases
self.table_name = "domain_user_post_creations"
self.primary_key = %i[user_id post_id]
validates :user_id, uniqueness: { scope: :post_id }
belongs_to :post,
class_name: "::Domain::Post",
inverse_of: :user_post_creations
belongs_to :user,
class_name: "::Domain::User",
inverse_of: :user_post_creations
end

View File

@@ -0,0 +1,7 @@
class Domain::UserPostFav < ReduxApplicationRecord
self.table_name = "domain_user_post_favs"
self.primary_key = %i[user_id post_id]
belongs_to :user, class_name: "Domain::User", inverse_of: :user_post_favs
belongs_to :post, class_name: "Domain::Post", inverse_of: :user_post_favs
end

View File

@@ -0,0 +1,7 @@
class Domain::UserUserFollow < ReduxApplicationRecord
self.table_name = "domain_user_user_follows"
self.primary_key = %i[from_id to_id]
belongs_to :from, class_name: "Domain::User"
belongs_to :to, class_name: "Domain::User"
end

View File

@@ -2,6 +2,7 @@
class ReduxApplicationRecord < ActiveRecord::Base
extend T::Sig
include HasPrometheusClient
include HasColorLogger
# hack to make sorbet recognize the `@after_save_deferred_jobs` instance variable
sig { params(attributes: T.untyped).void }
@@ -11,7 +12,6 @@ class ReduxApplicationRecord < ActiveRecord::Base
end
self.abstract_class = true
logger.level = Logger::ERROR
after_initialize { observe(:initialize) }
after_create { observe(:create) }

View File

@@ -61,6 +61,9 @@ Rails.application.configure do
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.level = :error
# Suppress logger output for asset requests.
config.assets.quiet = true

View File

@@ -0,0 +1,163 @@
# typed: strict
#
class CreateDomainPosts < ActiveRecord::Migration[7.2]
extend T::Sig
sig { void }
def change
reversible do |dir|
dir.up do
execute "CREATE TYPE domain_post_type AS ENUM ('Domain::Post::FaPost', 'Domain::Post::E621Post')"
end
dir.down { execute "DROP TYPE domain_post_type" }
end
create_table :domain_posts do |t|
t.enum :type, null: false, enum_type: "domain_post_type"
t.jsonb :json_attributes, default: {}
t.timestamps
t.index :type
end
reversible do |dir|
dir.up do
execute "CREATE TYPE domain_user_type AS ENUM ('Domain::User::FaUser', 'Domain::User::E621User')"
end
dir.down { execute "DROP TYPE domain_user_type" }
end
create_table :domain_users do |t|
t.enum :type, null: false, enum_type: "domain_user_type"
t.jsonb :json_attributes, default: {}
t.timestamps
t.index :type
end
create_table :domain_user_avatars do |t|
t.string :type, null: false
t.references :user, null: false, foreign_key: { to_table: :domain_users }
t.jsonb :json_attributes, default: {}
t.timestamps
t.index :type
end
create_table :domain_post_files do |t|
t.references :post, null: false, foreign_key: { to_table: :domain_posts }
t.references :log_entry,
null: false,
foreign_key: {
to_table: :http_log_entries,
}
t.jsonb :json_attributes, default: {}
t.timestamps
end
create_table :domain_user_post_creations, id: false do |t|
t.references :user,
null: false,
foreign_key: {
to_table: :domain_users,
},
index: false
t.references :post,
null: false,
foreign_key: {
to_table: :domain_posts,
},
index: false
t.index %i[user_id post_id], unique: true
t.index %i[post_id user_id]
end
create_table :domain_user_post_favs, id: false do |t|
t.references :user,
null: false,
foreign_key: {
to_table: :domain_users,
},
index: false
t.references :post,
null: false,
foreign_key: {
to_table: :domain_posts,
},
index: false
t.boolean :removed, null: false, default: false
t.index %i[user_id post_id], unique: true
t.index %i[post_id user_id]
end
create_table :domain_user_user_follows, id: false do |t|
t.references :from,
null: false,
foreign_key: {
to_table: :domain_users,
},
index: false
t.references :to,
null: false,
foreign_key: {
to_table: :domain_users,
},
index: false
t.index %i[from_id to_id], unique: true
t.index %i[to_id from_id]
end
reversible do |dir|
dir.up do
# timestamps are formatted like `2001-02-03T04:05:06Z`
# aka ISO 8601
execute <<~SQL
CREATE OR REPLACE FUNCTION f_cast_isots(text)
RETURNS timestamptz AS
$$SELECT to_timestamp($1, 'YYYY-MM-DDTHH24:MI:SSZ')$$
LANGUAGE sql IMMUTABLE;
SQL
end
dir.down { execute "DROP FUNCTION f_cast_isots(text)" }
end
add_index :domain_posts,
"(cast(json_attributes->>'fa_id' as integer))",
where: "type = 'Domain::Post::FaPost'",
name: "idx_domain_fa_posts_on_fa_id",
unique: true
add_index :domain_posts,
"((json_attributes->>'e621_id')::integer)",
where: "type = 'Domain::Post::E621Post'",
name: "idx_domain_e621_posts_on_e621_id",
unique: true
add_index :domain_posts,
"((json_attributes->>'uploader_user_id')::integer)",
where: "type = 'Domain::Post::E621Post'",
name: "idx_domain_e621_posts_on_uploader_user_id",
unique: true
add_index :domain_users,
"((json_attributes->>'url_name')::text)",
where: "type = 'Domain::User::FaUser'",
name: "idx_domain_fa_users_on_url_name",
unique: true
add_index :domain_users,
"((json_attributes->>'e621_id')::integer)",
where: "type = 'Domain::User::E621User'",
name: "idx_domain_e621_users_on_e621_id",
unique: true
add_index :domain_users,
"(json_attributes->>'migrated_user_favs_at')",
name: "idx_domain_users_on_migrated_user_favs_at"
end
end

View File

@@ -93,6 +93,26 @@ CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public;
COMMENT ON EXTENSION vector IS 'vector data type and ivfflat access method';
--
-- Name: domain_post_type; Type: TYPE; Schema: public; Owner: -
--
CREATE TYPE public.domain_post_type AS ENUM (
'Domain::Post::FaPost',
'Domain::Post::E621Post'
);
--
-- Name: domain_user_type; Type: TYPE; Schema: public; Owner: -
--
CREATE TYPE public.domain_user_type AS ENUM (
'Domain::User::FaUser',
'Domain::User::E621User'
);
--
-- Name: postable_type; Type: TYPE; Schema: public; Owner: -
--
@@ -105,6 +125,15 @@ CREATE TYPE public.postable_type AS ENUM (
);
--
-- Name: f_cast_isots(text); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.f_cast_isots(text) RETURNS timestamp with time zone
LANGUAGE sql IMMUTABLE
AS $_$SELECT to_timestamp($1, 'YYYY-MM-DDTHH24:MI:SSZ')$_$;
SET default_tablespace = '';
SET default_table_access_method = heap;
@@ -2626,6 +2655,71 @@ CREATE SEQUENCE public.domain_inkbunny_users_id_seq
ALTER SEQUENCE public.domain_inkbunny_users_id_seq OWNED BY public.domain_inkbunny_users.id;
--
-- Name: domain_post_files; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_post_files (
id bigint NOT NULL,
post_id bigint NOT NULL,
log_entry_id bigint NOT NULL,
json_attributes jsonb DEFAULT '{}'::jsonb,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: domain_post_files_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_post_files_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_post_files_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_post_files_id_seq OWNED BY public.domain_post_files.id;
--
-- Name: domain_posts; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_posts (
id bigint NOT NULL,
type public.domain_post_type NOT NULL,
json_attributes jsonb DEFAULT '{}'::jsonb,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: domain_posts_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_posts_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_posts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_posts_id_seq OWNED BY public.domain_posts.id;
SET default_tablespace = mirai;
--
@@ -2759,6 +2853,102 @@ ALTER SEQUENCE public.domain_twitter_users_id_seq OWNED BY public.domain_twitter
SET default_tablespace = '';
--
-- Name: domain_user_avatars; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_user_avatars (
id bigint NOT NULL,
type character varying NOT NULL,
user_id bigint NOT NULL,
json_attributes jsonb DEFAULT '{}'::jsonb,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: domain_user_avatars_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_user_avatars_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_user_avatars_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_user_avatars_id_seq OWNED BY public.domain_user_avatars.id;
--
-- Name: domain_user_post_creations; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_user_post_creations (
user_id bigint NOT NULL,
post_id bigint NOT NULL
);
--
-- Name: domain_user_post_favs; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_user_post_favs (
user_id bigint NOT NULL,
post_id bigint NOT NULL,
removed boolean DEFAULT false NOT NULL
);
--
-- Name: domain_user_user_follows; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_user_user_follows (
from_id bigint NOT NULL,
to_id bigint NOT NULL
);
--
-- Name: domain_users; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_users (
id bigint NOT NULL,
type public.domain_user_type NOT NULL,
json_attributes jsonb DEFAULT '{}'::jsonb,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: domain_users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_users_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_users_id_seq OWNED BY public.domain_users.id;
--
-- Name: flat_sst_entries; Type: TABLE; Schema: public; Owner: -
--
@@ -4195,6 +4385,20 @@ ALTER TABLE ONLY public.domain_inkbunny_tags ALTER COLUMN id SET DEFAULT nextval
ALTER TABLE ONLY public.domain_inkbunny_users ALTER COLUMN id SET DEFAULT nextval('public.domain_inkbunny_users_id_seq'::regclass);
--
-- Name: domain_post_files id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_files ALTER COLUMN id SET DEFAULT nextval('public.domain_post_files_id_seq'::regclass);
--
-- Name: domain_posts id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_posts ALTER COLUMN id SET DEFAULT nextval('public.domain_posts_id_seq'::regclass);
--
-- Name: domain_twitter_tweets id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -4216,6 +4420,20 @@ ALTER TABLE ONLY public.domain_twitter_user_versions ALTER COLUMN id SET DEFAULT
ALTER TABLE ONLY public.domain_twitter_users ALTER COLUMN id SET DEFAULT nextval('public.domain_twitter_users_id_seq'::regclass);
--
-- Name: domain_user_avatars id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_avatars ALTER COLUMN id SET DEFAULT nextval('public.domain_user_avatars_id_seq'::regclass);
--
-- Name: domain_users id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_users ALTER COLUMN id SET DEFAULT nextval('public.domain_users_id_seq'::regclass);
--
-- Name: global_states id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -4928,6 +5146,22 @@ ALTER TABLE ONLY public.domain_inkbunny_users
ADD CONSTRAINT domain_inkbunny_users_pkey PRIMARY KEY (id);
--
-- Name: domain_post_files domain_post_files_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_files
ADD CONSTRAINT domain_post_files_pkey PRIMARY KEY (id);
--
-- Name: domain_posts domain_posts_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_posts
ADD CONSTRAINT domain_posts_pkey PRIMARY KEY (id);
SET default_tablespace = mirai;
--
@@ -4956,6 +5190,22 @@ ALTER TABLE ONLY public.domain_twitter_users
SET default_tablespace = '';
--
-- Name: domain_user_avatars domain_user_avatars_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_avatars
ADD CONSTRAINT domain_user_avatars_pkey PRIMARY KEY (id);
--
-- Name: domain_users domain_users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_users
ADD CONSTRAINT domain_users_pkey PRIMARY KEY (id);
--
-- Name: global_states global_states_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -5098,6 +5348,48 @@ CREATE INDEX domain_fa_users_url_name_idx ON public.domain_fa_users USING gist (
SET default_tablespace = '';
--
-- Name: idx_domain_e621_posts_on_e621_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_domain_e621_posts_on_e621_id ON public.domain_posts USING btree ((((json_attributes ->> 'e621_id'::text))::integer)) WHERE (type = 'Domain::Post::E621Post'::public.domain_post_type);
--
-- Name: idx_domain_e621_posts_on_uploader_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_domain_e621_posts_on_uploader_user_id ON public.domain_posts USING btree ((((json_attributes ->> 'uploader_user_id'::text))::integer)) WHERE (type = 'Domain::Post::E621Post'::public.domain_post_type);
--
-- Name: idx_domain_e621_users_on_e621_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_domain_e621_users_on_e621_id ON public.domain_users USING btree ((((json_attributes ->> 'e621_id'::text))::integer)) WHERE (type = 'Domain::User::E621User'::public.domain_user_type);
--
-- Name: idx_domain_fa_posts_on_fa_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_domain_fa_posts_on_fa_id ON public.domain_posts USING btree ((((json_attributes ->> 'fa_id'::text))::integer)) WHERE (type = 'Domain::Post::FaPost'::public.domain_post_type);
--
-- Name: idx_domain_fa_users_on_url_name; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_domain_fa_users_on_url_name ON public.domain_users USING btree (((json_attributes ->> 'url_name'::text))) WHERE (type = 'Domain::User::FaUser'::public.domain_user_type);
--
-- Name: idx_domain_users_on_migrated_user_favs_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_domain_users_on_migrated_user_favs_at ON public.domain_users USING btree (((json_attributes ->> 'migrated_user_favs_at'::text)));
--
-- Name: idx_on_good_job_execution_id_685ddb5560; Type: INDEX; Schema: public; Owner: -
--
@@ -6375,6 +6667,27 @@ CREATE UNIQUE INDEX index_domain_inkbunny_users_on_ib_user_id ON public.domain_i
CREATE INDEX index_domain_inkbunny_users_on_shallow_update_log_entry_id ON public.domain_inkbunny_users USING btree (shallow_update_log_entry_id);
--
-- Name: index_domain_post_files_on_log_entry_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_post_files_on_log_entry_id ON public.domain_post_files USING btree (log_entry_id);
--
-- Name: index_domain_post_files_on_post_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_post_files_on_post_id ON public.domain_post_files USING btree (post_id);
--
-- Name: index_domain_posts_on_type; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_posts_on_type ON public.domain_posts USING btree (type);
SET default_tablespace = mirai;
--
@@ -6435,6 +6748,69 @@ CREATE UNIQUE INDEX index_domain_twitter_users_on_tw_id ON public.domain_twitter
SET default_tablespace = '';
--
-- Name: index_domain_user_avatars_on_type; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_avatars_on_type ON public.domain_user_avatars USING btree (type);
--
-- Name: index_domain_user_avatars_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_avatars_on_user_id ON public.domain_user_avatars USING btree (user_id);
--
-- Name: index_domain_user_post_creations_on_post_id_and_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_post_creations_on_post_id_and_user_id ON public.domain_user_post_creations USING btree (post_id, user_id);
--
-- Name: index_domain_user_post_creations_on_user_id_and_post_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_domain_user_post_creations_on_user_id_and_post_id ON public.domain_user_post_creations USING btree (user_id, post_id);
--
-- Name: index_domain_user_post_favs_on_post_id_and_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_post_favs_on_post_id_and_user_id ON public.domain_user_post_favs USING btree (post_id, user_id);
--
-- Name: index_domain_user_post_favs_on_user_id_and_post_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_domain_user_post_favs_on_user_id_and_post_id ON public.domain_user_post_favs USING btree (user_id, post_id);
--
-- Name: index_domain_user_user_follows_on_from_id_and_to_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_domain_user_user_follows_on_from_id_and_to_id ON public.domain_user_user_follows USING btree (from_id, to_id);
--
-- Name: index_domain_user_user_follows_on_to_id_and_from_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_user_follows_on_to_id_and_from_id ON public.domain_user_user_follows USING btree (to_id, from_id);
--
-- Name: index_domain_users_on_type; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_users_on_type ON public.domain_users USING btree (type);
--
-- Name: index_e621_posts_on_scanned_post_favs_at; Type: INDEX; Schema: public; Owner: -
--
@@ -7669,6 +8045,14 @@ ALTER TABLE ONLY public.http_log_entries
ADD CONSTRAINT fk_rails_42f35e9da0 FOREIGN KEY (response_headers_id) REFERENCES public.http_log_entry_headers(id);
--
-- Name: domain_user_user_follows fk_rails_4b2ab65400; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_user_follows
ADD CONSTRAINT fk_rails_4b2ab65400 FOREIGN KEY (from_id) REFERENCES public.domain_users(id);
--
-- Name: domain_fa_favs fk_rails_503a610bac; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -7725,6 +8109,14 @@ ALTER TABLE ONLY public.domain_fa_user_avatar_versions
ADD CONSTRAINT fk_rails_77fefb9ac3 FOREIGN KEY (item_id) REFERENCES public.domain_fa_user_avatars(id);
--
-- Name: domain_post_files fk_rails_7eb6ae5fa3; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_files
ADD CONSTRAINT fk_rails_7eb6ae5fa3 FOREIGN KEY (post_id) REFERENCES public.domain_posts(id);
--
-- Name: domain_inkbunny_posts fk_rails_82ffd77f0c; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -7757,6 +8149,22 @@ ALTER TABLE ONLY public.good_job_execution_log_lines_collections
ADD CONSTRAINT fk_rails_98c288034f FOREIGN KEY (good_job_execution_id) REFERENCES public.good_job_executions(id) ON DELETE CASCADE;
--
-- Name: domain_user_post_creations fk_rails_9f4b85bc57; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_post_creations
ADD CONSTRAINT fk_rails_9f4b85bc57 FOREIGN KEY (user_id) REFERENCES public.domain_users(id);
--
-- Name: domain_user_user_follows fk_rails_b45e6e3979; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_user_follows
ADD CONSTRAINT fk_rails_b45e6e3979 FOREIGN KEY (to_id) REFERENCES public.domain_users(id);
--
-- Name: domain_inkbunny_posts fk_rails_c2d9f4b382; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -7781,6 +8189,22 @@ ALTER TABLE ONLY public.http_log_entries
ADD CONSTRAINT fk_rails_c5f7bcff78 FOREIGN KEY (caused_by_id) REFERENCES public.http_log_entries(id);
--
-- Name: domain_user_post_favs fk_rails_c79733f291; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_post_favs
ADD CONSTRAINT fk_rails_c79733f291 FOREIGN KEY (user_id) REFERENCES public.domain_users(id);
--
-- Name: domain_user_post_favs fk_rails_ce892be9a6; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_post_favs
ADD CONSTRAINT fk_rails_ce892be9a6 FOREIGN KEY (post_id) REFERENCES public.domain_posts(id);
--
-- Name: http_log_entries fk_rails_cf47f64c57; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -7789,6 +8213,14 @@ ALTER TABLE ONLY public.http_log_entries
ADD CONSTRAINT fk_rails_cf47f64c57 FOREIGN KEY (request_headers_id) REFERENCES public.http_log_entry_headers(id);
--
-- Name: domain_post_files fk_rails_d059c07f77; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_files
ADD CONSTRAINT fk_rails_d059c07f77 FOREIGN KEY (log_entry_id) REFERENCES public.http_log_entries(id);
--
-- Name: domain_inkbunny_users fk_rails_d6ea75e723; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -7821,6 +8253,14 @@ ALTER TABLE ONLY public.domain_inkbunny_follows
ADD CONSTRAINT fk_rails_ee473fedf4 FOREIGN KEY (follower_id) REFERENCES public.domain_inkbunny_users(id);
--
-- Name: domain_user_post_creations fk_rails_f3c2231511; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_post_creations
ADD CONSTRAINT fk_rails_f3c2231511 FOREIGN KEY (post_id) REFERENCES public.domain_posts(id);
--
-- Name: domain_fa_posts fk_rails_f5d5c17363; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -7829,6 +8269,14 @@ ALTER TABLE ONLY public.domain_fa_posts
ADD CONSTRAINT fk_rails_f5d5c17363 FOREIGN KEY (file_id) REFERENCES public.http_log_entries(id);
--
-- Name: domain_user_avatars fk_rails_f89912a20f; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_user_avatars
ADD CONSTRAINT fk_rails_f89912a20f FOREIGN KEY (user_id) REFERENCES public.domain_users(id);
--
-- Name: domain_twitter_tweets on_author_id; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -7844,6 +8292,7 @@ ALTER TABLE ONLY public.domain_twitter_tweets
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20250203235035'),
('20250131060105'),
('20250131055824'),
('20250129174128'),

View File

@@ -20,7 +20,8 @@ namespace :e621 do
task scan_user_favs: :environment do
while user =
Domain::E621::User
.where(scanned_favs_at: nil)
.where_scanned_favs_at("is null")
.where_num_other_favs_cached("< ?", 200)
.order("RANDOM()")
.take
Domain::E621::Job::ScanUserFavsJob.perform_now(user: user)
@@ -78,7 +79,19 @@ namespace :e621 do
desc "debug sql"
task debug_sql: :environment do
puts Domain::E621::Post.where(last_index_page_id: nil).to_sql
binding.pry
# Domain::Post::E621Post.where(e621_id: 5350363)
# puts Domain::Post::E621Post.where(e621_id: 5_350_363).explain.inspect
# puts Domain::Post::FaPost.where(fa_id: 52_801_830).explain.inspect
# puts Domain::Fa::Post.where(fa_id: 52_801_830).explain.inspect
puts Domain::Fa::Post
.joins(
"
LEFT JOIN domain_posts ON domain_fa_posts.fa_id =
(domain_posts.json_attributes->>'fa_id')::integer
",
)
.where(domain_posts: { id: nil, type: "Domain::Post::FaPost" })
.explain
.inspect
end
end

View File

@@ -11,12 +11,18 @@ class BlobEntry
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class BlobFile
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig { params(attributes: T.untyped, block: T.nilable(T.proc.params(object: ::BlobFile).void)).returns(::BlobFile) }
def new(attributes = nil, &block); end

View File

@@ -11,12 +11,18 @@ class Domain::E621::Fav
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -15,6 +15,9 @@ class Domain::E621::Post
sig { returns(T.nilable(Domain::E621::Post::FileError)) }
def file_error; end
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
@@ -42,6 +45,9 @@ class Domain::E621::Post
sig { returns(T::Array[Symbol]) }
def attr_json_registry; end
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class Domain::E621::Tag
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,6 +12,9 @@ class Domain::E621::Tagging
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
@@ -21,6 +24,9 @@ class Domain::E621::Tagging
sig { returns(T::Hash[T.any(String, Symbol), Integer]) }
def categories; end
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,6 +11,9 @@ class Domain::E621::User
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
@@ -38,6 +41,9 @@ class Domain::E621::User
sig { returns(T::Array[Symbol]) }
def attr_json_registry; end
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,
@@ -427,6 +433,20 @@ class Domain::E621::User
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def fav_ids=(ids); end
sig { returns(T::Array[T.untyped]) }
def faved_post_ids; end
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def faved_post_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::E621::User` class because it declared `has_many :faved_posts, through: :favs`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::E621::Post::PrivateCollectionProxy) }
def faved_posts; end
sig { params(value: T::Enumerable[::Domain::E621::Post]).void }
def faved_posts=(value); end
# This method is created by ActiveRecord on the `Domain::E621::User` class because it declared `has_many :favs`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::E621::Fav::PrivateCollectionProxy) }
@@ -533,6 +553,9 @@ class Domain::E621::User
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def order(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def order_favs_are_hidden(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def order_num_other_favs_cached(*args, &blk); end
@@ -585,6 +608,9 @@ class Domain::E621::User
sig { params(args: T.untyped).returns(PrivateAssociationRelation) }
def where(*args); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def where_favs_are_hidden(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def where_num_other_favs_cached(*args, &blk); end
@@ -1360,6 +1386,9 @@ class Domain::E621::User
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def order(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def order_favs_are_hidden(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def order_num_other_favs_cached(*args, &blk); end
@@ -1412,6 +1441,9 @@ class Domain::E621::User
sig { params(args: T.untyped).returns(PrivateRelation) }
def where(*args); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def where_favs_are_hidden(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def where_num_other_favs_cached(*args, &blk); end

View File

@@ -11,12 +11,18 @@ class Domain::Fa::Fav
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class Domain::Fa::Follow
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,6 +12,9 @@ class Domain::Fa::Post
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
@@ -39,6 +42,9 @@ class Domain::Fa::Post
sig { returns(T::Array[Symbol]) }
def attr_json_registry; end
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class Domain::Fa::PostFactor
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -27,6 +27,9 @@ class Domain::Fa::User
sig { returns(T::Boolean) }
def due_for_page_scan?; end
sig { returns(ColorLogger) }
def logger; end
sig { returns(T.nilable(Time)) }
def scanned_favs_at; end
@@ -78,6 +81,9 @@ class Domain::Fa::User
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,12 +12,18 @@ class Domain::Fa::UserAvatar
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class Domain::Fa::UserFactor
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -10,12 +10,18 @@ class Domain::Inkbunny::Fav
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,12 +12,18 @@ class Domain::Inkbunny::File
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -10,12 +10,18 @@ class Domain::Inkbunny::Follow
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class Domain::Inkbunny::Pool
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class Domain::Inkbunny::PoolJoin
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,12 +12,18 @@ class Domain::Inkbunny::Post
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -10,12 +10,18 @@ class Domain::Inkbunny::Tag
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -10,12 +10,18 @@ class Domain::Inkbunny::Tagging
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,6 +12,9 @@ class Domain::Inkbunny::User
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
@@ -21,6 +24,9 @@ class Domain::Inkbunny::User
sig { returns(T::Hash[T.any(String, Symbol), Integer]) }
def avatar_states; end
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -0,0 +1,16 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Domain::MigrateToDomain`.
# Please instead update this file by running `bin/tapioca dsl Domain::MigrateToDomain`.
class Domain::MigrateToDomain
sig { returns(ColorLogger) }
def logger; end
class << self
sig { returns(ColorLogger) }
def logger; end
end
end

1311
sorbet/rbi/dsl/domain/post.rbi generated Normal file

File diff suppressed because it is too large Load Diff

2779
sorbet/rbi/dsl/domain/post/e621_post.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Domain::Post::E621Post::FileError`.
# Please instead update this file by running `bin/tapioca dsl Domain::Post::E621Post::FileError`.
class Domain::Post::E621Post::FileError
sig { returns(T.nilable(::Integer)) }
def log_entry_id; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def log_entry_id=(value); end
sig { returns(T.nilable(::Integer)) }
def retry_count; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def retry_count=(value); end
sig { returns(T.nilable(::Integer)) }
def status_code; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def status_code=(value); end
end

2881
sorbet/rbi/dsl/domain/post/fa_post.rbi generated Normal file

File diff suppressed because it is too large Load Diff

1573
sorbet/rbi/dsl/domain/post_file.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,18 @@ class Domain::Twitter::Media
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,12 +12,18 @@ class Domain::Twitter::Tweet
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,12 +12,18 @@ class Domain::Twitter::User
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

1467
sorbet/rbi/dsl/domain/user.rbi generated Normal file

File diff suppressed because it is too large Load Diff

1937
sorbet/rbi/dsl/domain/user/e621_user.rbi generated Normal file

File diff suppressed because it is too large Load Diff

1730
sorbet/rbi/dsl/domain/user/fa_user.rbi generated Normal file

File diff suppressed because it is too large Load Diff

1620
sorbet/rbi/dsl/domain/user_avatar.rbi generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1192
sorbet/rbi/dsl/domain/user_post_fav.rbi generated Normal file

File diff suppressed because it is too large Load Diff

1151
sorbet/rbi/dsl/domain/user_user_follow.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,18 @@ class FlatSstEntry
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class GlobalState
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -11,12 +11,18 @@ class GoodJobExecutionLogLinesCollection
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -12,12 +12,18 @@ class HttpLogEntry
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -10,12 +10,18 @@ class HttpLogEntryHeader
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -10,12 +10,18 @@ class IndexedPost
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
private
sig { returns(NilClass) }
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
attributes: T.untyped,

View File

@@ -0,0 +1,16 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `ReduxApplicationRecord`.
# Please instead update this file by running `bin/tapioca dsl ReduxApplicationRecord`.
class ReduxApplicationRecord
sig { returns(ColorLogger) }
def logger; end
class << self
sig { returns(ColorLogger) }
def logger; end
end
end

View File

@@ -11,6 +11,9 @@ class User
extend CommonRelationMethods
extend GeneratedRelationMethods
sig { returns(ColorLogger) }
def logger; end
sig { returns(T.untyped) }
def password_confirmation; end
@@ -23,6 +26,9 @@ class User
def to_ary; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig { params(attributes: T.untyped, block: T.nilable(T.proc.params(object: ::User).void)).returns(::User) }
def new(attributes = nil, &block); end

View File

@@ -0,0 +1,14 @@
# typed: false
FactoryBot.define do
factory :domain_post_e621_post, class: "Domain::Post::E621Post" do
sequence(:e621_id) { |n| n }
state { "ok" }
rating { "s" }
tags_array { {} }
flags_array { [] }
pools_array { [] }
sources_array { [] }
artists_array { [] }
e621_updated_at { Time.current }
end
end

View File

@@ -0,0 +1,52 @@
# typed: false
FactoryBot.define do
factory :domain_post_fa_post, class: "Domain::Post::FaPost" do
sequence(:fa_id) { |n| n }
state { "ok" }
category { "artwork" }
theme { "general" }
species { "unspecified" }
gender { "unspecified" }
description { "Test description" }
keywords { %w[test factory] }
num_favorites { 0 }
num_comments { 0 }
num_views { 0 }
posted_at { Time.current }
scanned_at { Time.current }
trait :with_last_user_page do
association :last_user_page, factory: :http_log_entry
end
trait :with_last_submission_page do
association :last_submission_page, factory: :http_log_entry
end
trait :with_first_browse_page do
association :first_browse_page, factory: :http_log_entry
end
trait :with_first_gallery_page do
association :first_gallery_page, factory: :http_log_entry
end
trait :with_first_seen_entry do
association :first_seen_entry, factory: :http_log_entry
end
trait :removed do
state { "removed" }
end
trait :scan_error do
state { "scan_error" }
scan_file_error { "Failed to scan file" }
end
trait :file_error do
state { "file_error" }
scan_file_error { "Failed to process file" }
end
end
end

View File

@@ -0,0 +1,9 @@
# typed: false
FactoryBot.define do
factory :domain_post_file, class: "Domain::PostFile" do
post { create(:domain_post_fa_post) }
log_entry { create(:http_log_entry) }
url_str { "https://example.com/image.jpg" }
state { "ok" }
end
end

View File

@@ -0,0 +1,11 @@
# typed: false
FactoryBot.define do
factory :domain_user_e621_user, class: "Domain::User::E621User" do
sequence(:e621_id) { |n| n }
sequence(:name) { |n| "user#{n}" }
favs_are_hidden { false }
num_other_favs_cached { 0 }
scanned_favs_status { "ok" }
scanned_favs_at { nil }
end
end

View File

@@ -0,0 +1,7 @@
# typed: false
FactoryBot.define do
factory :domain_user_fa_user, class: "Domain::User::FaUser" do
sequence(:url_name) { |n| "user_#{n}" }
sequence(:name) { |n| "User#{n}" }
end
end

View File

@@ -71,7 +71,7 @@ describe Domain::Fa::Job::UserIncrementalJob do
expect(
SpecUtil.enqueued_jobs(Domain::Fa::Job::UserFollowsJob),
).to be_empty
expect(meesh.scanned_follows_at).to be_within(1.second).of(Time.now)
expect(meesh.scanned_follows_at).to be_within(1.second).of(Time.current)
# 19 newly seen faved posts
expect(SpecUtil.enqueued_jobs(Domain::Fa::Job::ScanPostJob).length).to be(
@@ -79,7 +79,7 @@ describe Domain::Fa::Job::UserIncrementalJob do
)
# No new fav in last position, so don't enqueue scan
expect(SpecUtil.enqueued_jobs(Domain::Fa::Job::FavsJob)).to be_empty
expect(meesh.scanned_favs_at).to be_within(1.second).of(Time.now)
expect(meesh.scanned_favs_at).to be_within(1.second).of(Time.current)
end
end
end

View File

@@ -0,0 +1,139 @@
# typed: false
require "rails_helper"
RSpec.describe Domain::Post::E621Post, type: :model do
describe "validations" do
let(:post) { build(:domain_post_e621_post) }
it "requires e621_id" do
post.e621_id = nil
expect(post).not_to be_valid
expect(post.errors[:e621_id]).to include("can't be blank")
end
it "requires state" do
post.state = nil
expect(post).not_to be_valid
expect(post.errors[:state]).to include("is not included in the list")
end
it "validates state inclusion" do
%w[ok removed scan_error file_error].each do |state|
post.state = state
expect(post).to be_valid
end
post.state = "invalid_state"
expect(post).not_to be_valid
expect(post.errors[:state]).to include("is not included in the list")
end
it "validates rating inclusion" do
%w[s q e].each do |rating|
post.rating = rating
expect(post).to be_valid
end
post.rating = "invalid_rating"
expect(post).not_to be_valid
expect(post.errors[:rating]).to include("is not included in the list")
end
end
describe "initialization" do
let(:post) { described_class.new }
it "sets default values" do
expect(post.state).to eq("ok")
expect(post.flags_array).to eq([])
expect(post.pools_array).to eq([])
expect(post.sources_array).to eq([])
expect(post.artists_array).to eq([])
end
end
describe "attributes" do
it "can be queried using generated scopes" do
post1 = create(:domain_post_e621_post)
post2 = create(:domain_post_e621_post, scan_error: "an error")
post3 = create(:domain_post_e621_post, scan_error: nil)
expect(described_class.where_scan_error("is not null")).to match_array(
[post2],
)
expect(described_class.where_scan_error("is null")).to match_array(
[post1, post3],
)
post2.scan_error = nil
post2.save!
expect(described_class.where_scan_error("is null")).to match_array(
[post1, post2, post3],
)
end
end
describe "associations" do
it "belongs to parent_e621" do
parent = create(:domain_post_e621_post)
post = create(:domain_post_e621_post)
post.parent_post = parent
post.save!
expect(post.parent_post).to eq(parent)
end
it "has one file" do
post = create(:domain_post_e621_post)
file = create(:domain_post_file, post: post)
post.reload
expect(post.file).to eq(file)
end
it "belongs to last_index_page" do
post = create(:domain_post_e621_post)
log_entry = create(:http_log_entry)
post.last_index_page = log_entry
post.save!
expect(post.last_index_page).to eq(log_entry)
end
it "has an uploader" do
user = create(:domain_user_e621_user)
post = create(:domain_post_e621_post, uploader_user: user)
expect(post.uploader_user).to eq(user)
expect(user.uploaded_posts).to eq([post])
end
end
describe "#to_param" do
let(:post) { build(:domain_post_e621_post, e621_id: 12_345) }
it "returns nil when e621_id is not present" do
post.e621_id = nil
expect(post.to_param).to be_nil
end
it "returns formatted string when e621_id is present" do
expect(post.to_param).to eq("e621/12345")
end
end
describe "file error handling" do
let(:post) { create(:domain_post_e621_post) }
it "can store file error details" do
error =
Domain::Post::E621Post::FileError.new(
retry_count: 3,
status_code: 404,
log_entry_id: 123,
)
post.file_error = error
post.save!
post.reload
expect(post.file_error.retry_count).to eq(3)
expect(post.file_error.status_code).to eq(404)
expect(post.file_error.log_entry_id).to eq(123)
end
end
end

View File

@@ -0,0 +1,119 @@
# typed: false
require "rails_helper"
RSpec.describe Domain::Post::FaPost do
describe "validations" do
let(:post) { build(:domain_post_fa_post) }
it "requires fa_id" do
post.fa_id = nil
expect(post).not_to be_valid
expect(post.errors[:fa_id]).to include("can't be blank")
end
it "validates state inclusion" do
post.state = "invalid"
expect(post).not_to be_valid
expect(post.errors[:state]).to include("is not included in the list")
%w[ok removed scan_error file_error].each do |valid_state|
post.state = valid_state
expect(post).to be_valid
end
end
it "defaults state to 'ok'" do
new_post = described_class.new
expect(new_post.state).to eq("ok")
end
end
describe "creators" do
it "works" do
post = create(:domain_post_fa_post)
user = create(:domain_user_fa_user)
post.creator = user
expect(post.creator).to eq(user)
expect(post.user_post_creations.count).to eq(1)
expect(post.user_post_creations.first.user).to eq(user)
end
end
describe "files" do
it "works" do
post = create(:domain_post_fa_post)
file = create(:domain_post_file, post: post)
expect(post.file).to eq(file)
expect(Domain::PostFile.where(post: post).count).to eq(1)
expect(Domain::PostFile.where(post: post).first).to eq(file)
end
end
describe "attributes" do
let(:post) { build(:domain_post_fa_post) }
let(:time) { Time.current }
it "can be initialized by fa_id" do
post = described_class.new(fa_id: 123)
expect(post).to be_valid
expect(post.fa_id).to eq(123)
expect(post.state).to eq("ok")
expect(post.save).to be_truthy
end
it "handles basic attributes" do
post.category = "artwork"
post.theme = "fantasy"
post.species = "dragon"
post.gender = "male"
post.description = "A cool description"
post.num_favorites = 42
post.num_comments = 10
post.num_views = 100
expect(post.category).to eq("artwork")
expect(post.theme).to eq("fantasy")
expect(post.species).to eq("dragon")
expect(post.gender).to eq("male")
expect(post.description).to eq("A cool description")
expect(post.num_favorites).to eq(42)
expect(post.num_comments).to eq(10)
expect(post.num_views).to eq(100)
end
it "handles array attributes" do
keywords = %w[dragon fantasy digital]
post.keywords = keywords
expect(post.keywords).to eq(keywords)
end
it "handles datetime attributes" do
post.posted_at = time
post.scanned_at = time
expect(post.posted_at).to be_within(1.second).of(time)
expect(post.scanned_at).to be_within(1.second).of(time)
end
it "defaults keywords to empty array" do
new_post = described_class.new
expect(new_post.keywords).to eq([])
end
end
describe "factory" do
it "works" do
post = create(:domain_post_fa_post)
expect(post).to be_valid
expect(post.type).to eq("Domain::Post::FaPost")
end
it "works with traits" do
post = create(:domain_post_fa_post, :removed)
expect(post).to be_valid
expect(post.state).to eq("removed")
end
end
end

View File

@@ -0,0 +1,85 @@
# typed: false
require "rails_helper"
RSpec.describe Domain::User::E621User, type: :model do
describe "validations" do
let(:user) { build(:domain_user_e621_user) }
it "is valid with valid attributes" do
expect(user).to be_valid
end
it "requires e621_id" do
user.e621_id = nil
expect(user).not_to be_valid
expect(user.errors[:e621_id]).to include("can't be blank")
end
it "requires name" do
user.name = nil
expect(user).not_to be_valid
expect(user.errors[:name]).to include("can't be blank")
end
it "validates scanned_favs_status inclusion" do
user.scanned_favs_status = nil
expect(user).to be_valid # optional field
user.scanned_favs_status = "ok"
expect(user).to be_valid
user.scanned_favs_status = "error"
expect(user).to be_valid
user.scanned_favs_status = "invalid"
expect(user).not_to be_valid
expect(user.errors[:scanned_favs_status]).to include(
"is not included in the list",
)
end
end
describe "initialization" do
let(:user) { described_class.new }
it "has default values" do
expect(user.favs_are_hidden).to be_nil
expect(user.num_other_favs_cached).to be_nil
expect(user.scanned_favs_status).to be_nil
expect(user.scanned_favs_at).to be_nil
end
end
describe "attributes" do
let(:user) { create(:domain_user_e621_user) }
it "can update favs_are_hidden" do
user.favs_are_hidden = true
expect(user.save).to be true
user.reload
expect(user.favs_are_hidden).to be true
end
it "can update num_other_favs_cached" do
user.num_other_favs_cached = 100
expect(user.save).to be true
user.reload
expect(user.num_other_favs_cached).to eq(100)
end
it "can update scanned_favs_status" do
user.scanned_favs_status = "error"
expect(user.save).to be true
user.reload
expect(user.scanned_favs_status).to eq("error")
end
it "can update scanned_favs_at" do
time = Time.current
user.scanned_favs_at = time
expect(user.save).to be true
user.reload
expect(user.scanned_favs_at).to be_within(1.second).of(time)
end
end
end