From 37b221447267b7cb2258c94aff6c3b486bf5d18e Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 11 Dec 2018 18:10:20 -0600 Subject: [PATCH 1/5] post_tags_match: replace joins with subqueries. Refactor various post_tag_match methods to use subqueries instead of joins. This simplifies things inside PostQueryBuilder, since now we can assume we're always dealing with a Post relation, rather than some other table joined with the posts table. --- app/logical/post_query_builder.rb | 11 +++++------ app/models/artist_commentary.rb | 2 +- app/models/comment.rb | 2 +- app/models/note.rb | 2 +- app/models/post.rb | 2 +- app/models/post_appeal.rb | 2 +- app/models/post_approval.rb | 2 +- app/models/post_flag.rb | 2 +- app/models/post_replacement.rb | 2 +- app/models/upload.rb | 2 +- 10 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb index d748a41e0..8d7bd0b8b 100644 --- a/app/logical/post_query_builder.rb +++ b/app/logical/post_query_builder.rb @@ -1,8 +1,9 @@ class PostQueryBuilder - attr_accessor :query_string + attr_accessor :query_string, :read_only - def initialize(query_string) + def initialize(query_string, read_only: false) @query_string = query_string + @read_only = read_only end def add_range_relation(arr, field, relation) @@ -91,14 +92,12 @@ class PostQueryBuilder return CurrentUser.user.hide_deleted_posts? end - def build(relation = nil) + def build unless query_string.is_a?(Hash) q = Tag.parse_query(query_string) end - if relation.nil? - relation = Post.where("true") - end + relation = read_only ? PostReadOnly.all : Post.all if q[:tag_count].to_i > Danbooru.config.tag_query_limit raise ::Post::SearchError.new("You cannot search for more than #{Danbooru.config.tag_query_limit} tags at a time") diff --git a/app/models/artist_commentary.rb b/app/models/artist_commentary.rb index fba316ac3..74d3c29eb 100644 --- a/app/models/artist_commentary.rb +++ b/app/models/artist_commentary.rb @@ -19,7 +19,7 @@ class ArtistCommentary < ApplicationRecord end def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)).reorder("") + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def deleted diff --git a/app/models/comment.rb b/app/models/comment.rb index 5d35857ae..5a9f48786 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -52,7 +52,7 @@ class Comment < ApplicationRecord end def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)).reorder("") + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def for_creator(user_id) diff --git a/app/models/note.rb b/app/models/note.rb index 1c8fdcf81..4955c75c1 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -18,7 +18,7 @@ class Note < ApplicationRecord end def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)).reorder("") + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def for_creator(user_id) diff --git a/app/models/post.rb b/app/models/post.rb index 9aa696214..cd206825a 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1662,7 +1662,7 @@ class Post < ApplicationRecord if read_only begin - PostQueryBuilder.new(query).build(PostReadOnly.where("true")) + PostQueryBuilder.new(query, read_only: true).build rescue PG::ConnectionBad PostQueryBuilder.new(query).build end diff --git a/app/models/post_appeal.rb b/app/models/post_appeal.rb index c04b732e4..f32a8b348 100644 --- a/app/models/post_appeal.rb +++ b/app/models/post_appeal.rb @@ -11,7 +11,7 @@ class PostAppeal < ApplicationRecord module SearchMethods def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)) + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def resolved diff --git a/app/models/post_approval.rb b/app/models/post_approval.rb index 801d03cb3..3300c90e8 100644 --- a/app/models/post_approval.rb +++ b/app/models/post_approval.rb @@ -35,7 +35,7 @@ class PostApproval < ApplicationRecord concerning :SearchMethods do class_methods do def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)) + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def search(params) diff --git a/app/models/post_flag.rb b/app/models/post_flag.rb index 528cd57de..f02dcf9e6 100644 --- a/app/models/post_flag.rb +++ b/app/models/post_flag.rb @@ -33,7 +33,7 @@ class PostFlag < ApplicationRecord end def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)) + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def resolved diff --git a/app/models/post_replacement.rb b/app/models/post_replacement.rb index 79223d0c5..014f4bf87 100644 --- a/app/models/post_replacement.rb +++ b/app/models/post_replacement.rb @@ -21,7 +21,7 @@ class PostReplacement < ApplicationRecord concerning :Search do class_methods do def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)) + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def search(params = {}) diff --git a/app/models/upload.rb b/app/models/upload.rb index 481afc1be..62997c802 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -175,7 +175,7 @@ class Upload < ApplicationRecord end def post_tags_match(query) - PostQueryBuilder.new(query).build(self.joins(:post)).reorder("") + where(post_id: PostQueryBuilder.new(query).build.reorder("")) end def search(params) From 4a1f0523a4a2142cd7a86fb4789248011dc45508 Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 11 Dec 2018 18:10:20 -0600 Subject: [PATCH 2/5] search: add comment_count, note_count metatags (#4004). Add these metatags: * comment_count * deleted_comment_count * active_comment_count * note_count * deleted_note_count * active_note_count * order:comment_count * order:deleted_comment_count * order:active_comment_count * order:note_count * order:deleted_note_count * order:active_note_count --- app/logical/post_query_builder.rb | 32 +++++++++++++++++++++++++++++++ app/models/post.rb | 27 ++++++++++++++++++++++++++ app/models/tag.rb | 9 ++++++++- test/unit/post_test.rb | 15 +++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb index 8d7bd0b8b..f386432b9 100644 --- a/app/logical/post_query_builder.rb +++ b/app/logical/post_query_builder.rb @@ -85,6 +85,28 @@ class PostQueryBuilder relation end + def table_for_metatag(metatag) + if metatag.in?(Tag::COUNT_METATAGS) + metatag[/(?[a-z]+)_count\z/i, :table] + else + nil + end + end + + def tables_for_query(q) + metatags = q.keys + metatags << q[:order].remove(/_(asc|desc)\z/i) if q[:order].present? + + tables = metatags.map { |metatag| table_for_metatag(metatag.to_s) } + tables.compact.uniq + end + + def add_joins(q, relation) + tables = tables_for_query(q) + relation = relation.with_stats(tables) + relation + end + def hide_deleted_posts?(q) return false if CurrentUser.admin_mode? return false if q[:status].in?(%w[deleted active any all]) @@ -107,6 +129,7 @@ class PostQueryBuilder relation = relation.where("posts.rating = 's'") end + relation = add_joins(q, relation) relation = add_range_relation(q[:post_id], "posts.id", relation) relation = add_range_relation(q[:mpixels], "posts.image_width * posts.image_height / 1000000.0", relation) relation = add_range_relation(q[:ratio], "ROUND(1.0 * posts.image_width / GREATEST(1, posts.image_height), 2)", relation) @@ -122,6 +145,10 @@ class PostQueryBuilder end relation = add_range_relation(q[:post_tag_count], "posts.tag_count", relation) + Tag::COUNT_METATAGS.each do |column| + relation = add_range_relation(q[column.to_sym], "posts.#{column}", relation) + end + if q[:md5] relation = relation.where("posts.md5": q[:md5]) end @@ -544,6 +571,11 @@ class PostQueryBuilder when "filesize_asc" relation = relation.order("posts.file_size ASC") + when /\A(?#{Tag::COUNT_METATAGS.join("|")})(_(?asc|desc))?\z/i + column = $~[:column] + direction = $~[:direction] || "desc" + relation = relation.order(column => direction, :id => direction) + when "tagcount", "tagcount_desc" relation = relation.order("posts.tag_count DESC") diff --git a/app/models/post.rb b/app/models/post.rb index cd206825a..c928ac07f 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1609,6 +1609,33 @@ class Post < ApplicationRecord joins("CROSS JOIN unnest(string_to_array(tag_string, ' ')) AS tag") end + def with_comment_stats + relation = left_outer_joins(:comments).group(:id).select("posts.*") + relation = relation.select("COUNT(comments.id) AS comment_count") + relation = relation.select("COUNT(comments.id) FILTER (WHERE comments.is_deleted = TRUE) AS deleted_comment_count") + relation = relation.select("COUNT(comments.id) FILTER (WHERE comments.is_deleted = FALSE) AS active_comment_count") + relation + end + + def with_note_stats + relation = left_outer_joins(:notes).group(:id).select("posts.*") + relation = relation.select("COUNT(notes.id) AS note_count") + relation = relation.select("COUNT(notes.id) FILTER (WHERE notes.is_active = TRUE) AS active_note_count") + relation = relation.select("COUNT(notes.id) FILTER (WHERE notes.is_active = FALSE) AS deleted_note_count") + relation + end + + def with_stats(tables) + return all if tables.empty? + + relation = all + tables.each do |table| + relation = relation.send("with_#{table}_stats") + end + + from(relation.arel.as("posts")) + end + def pending where(is_pending: true) end diff --git a/app/models/tag.rb b/app/models/tag.rb index 26519fff8..1a2176d33 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,5 +1,9 @@ class Tag < ApplicationRecord COSINE_SIMILARITY_RELATED_TAG_THRESHOLD = 300 + COUNT_METATAGS = %w[ + comment_count deleted_comment_count active_comment_count + note_count deleted_note_count active_note_count + ] METATAGS = %w[ -user user -approver approver commenter comm noter noteupdater artcomm -pool pool ordpool -favgroup favgroup -fav fav ordfav md5 -rating rating @@ -7,7 +11,7 @@ class Tag < ApplicationRecord -source id -id date age order limit -status status tagcount parent -parent child pixiv_id pixiv search upvote downvote filetype -filetype flagger -flagger appealer -appealer disapproval -disapproval - ] + TagCategory.short_name_list.map {|x| "#{x}tags"} + ] + TagCategory.short_name_list.map {|x| "#{x}tags"} + COUNT_METATAGS SUBQUERY_METATAGS = %w[commenter comm noter noteupdater artcomm flagger -flagger appealer -appealer] @@ -790,6 +794,9 @@ class Tag < ApplicationRecord q[:downvote] = User.name_to_id(g2) end + when *COUNT_METATAGS + q[g1.to_sym] = parse_helper(g2) + end else diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index 2af27e446..1e67ecf80 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -2103,6 +2103,16 @@ class PostTest < ActiveSupport::TestCase assert_tag_match([posts[1]], "noter:none") end + should "return posts for the note_count: metatag" do + posts = FactoryBot.create_list(:post, 3) + FactoryBot.create(:note, post: posts[0], is_active: true) + FactoryBot.create(:note, post: posts[1], is_active: false) + + assert_tag_match([posts[1], posts[0]], "note_count:1") + assert_tag_match([posts[0]], "active_note_count:1") + assert_tag_match([posts[1]], "deleted_note_count:1") + end + should "return posts for the artcomm: metatag" do users = FactoryBot.create_list(:user, 2) posts = FactoryBot.create_list(:post, 2) @@ -2387,6 +2397,8 @@ class PostTest < ActiveSupport::TestCase p end + FactoryBot.create(:note, post: posts.second) + assert_tag_match(posts.reverse, "order:id_desc") assert_tag_match(posts.reverse, "order:score") assert_tag_match(posts.reverse, "order:favcount") @@ -2404,6 +2416,8 @@ class PostTest < ActiveSupport::TestCase assert_tag_match(posts.reverse, "order:chartags") assert_tag_match(posts.reverse, "order:copytags") assert_tag_match(posts.reverse, "order:rank") + assert_tag_match(posts.reverse, "order:note_count") + assert_tag_match(posts.reverse, "order:note_count_desc") assert_tag_match(posts, "order:id_asc") assert_tag_match(posts, "order:score_asc") @@ -2421,6 +2435,7 @@ class PostTest < ActiveSupport::TestCase assert_tag_match(posts, "order:arttags_asc") assert_tag_match(posts, "order:chartags_asc") assert_tag_match(posts, "order:copytags_asc") + assert_tag_match(posts, "order:note_count_asc") end should "return posts for order:comment_bumped" do From b1335616dd44166bbe94e895f85f50ea28b34eaf Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 11 Dec 2018 18:10:20 -0600 Subject: [PATCH 3/5] search: add {flag,appeal,approval,replacement,child,pool}_count metatags (#4004). --- app/models/post.rb | 46 ++++++++++++++++++++++++++++++++++++++++++++++ app/models/tag.rb | 4 ++++ 2 files changed, 50 insertions(+) diff --git a/app/models/post.rb b/app/models/post.rb index c928ac07f..827ae84c1 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1625,6 +1625,52 @@ class Post < ApplicationRecord relation end + def with_flag_stats + relation = left_outer_joins(:flags).group(:id).select("posts.*") + relation = relation.select("COUNT(post_flags.id) AS flag_count") + relation = relation.select("COUNT(post_flags.id) FILTER (WHERE post_flags.is_resolved = TRUE) AS resolved_flag_count") + relation = relation.select("COUNT(post_flags.id) FILTER (WHERE post_flags.is_resolved = FALSE) AS unresolved_flag_count") + relation + end + + def with_appeal_stats + relation = left_outer_joins(:appeals).group(:id).select("posts.*") + relation = relation.select("COUNT(post_appeals.id) AS appeal_count") + relation + end + + def with_approval_stats + relation = left_outer_joins(:approvals).group(:id).select("posts.*") + relation = relation.select("COUNT(post_approvals.id) AS approval_count") + relation + end + + def with_replacement_stats + relation = left_outer_joins(:replacements).group(:id).select("posts.*") + relation = relation.select("COUNT(post_replacements.id) AS replacement_count") + relation + end + + def with_child_stats + relation = left_outer_joins(:children).group(:id).select("posts.*") + relation = relation.select("COUNT(children_posts.id) AS child_count") + relation = relation.select("COUNT(children_posts.id) FILTER (WHERE children_posts.is_deleted = TRUE) AS deleted_child_count") + relation = relation.select("COUNT(children_posts.id) FILTER (WHERE children_posts.is_deleted = FALSE) AS active_child_count") + relation + end + + def with_pool_stats + pool_posts = Pool.joins("CROSS JOIN unnest(post_ids) AS post_id").select(:id, :is_deleted, :category, "post_id") + relation = joins("LEFT OUTER JOIN (#{pool_posts.to_sql}) pools ON pools.post_id = posts.id").group(:id).select("posts.*") + + relation = relation.select("COUNT(pools.id) AS pool_count") + relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.is_deleted = TRUE) AS deleted_pool_count") + relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.is_deleted = FALSE) AS active_pool_count") + relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.category = 'series') AS series_pool_count") + relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.category = 'collection') AS collection_pool_count") + relation + end + def with_stats(tables) return all if tables.empty? diff --git a/app/models/tag.rb b/app/models/tag.rb index 1a2176d33..a043abb3f 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,6 +3,10 @@ class Tag < ApplicationRecord COUNT_METATAGS = %w[ comment_count deleted_comment_count active_comment_count note_count deleted_note_count active_note_count + flag_count resolved_flag_count unresolved_flag_count + child_count deleted_child_count active_child_count + pool_count deleted_pool_count active_pool_count series_pool_count collection_pool_count + appeal_count approval_count replacement_count ] METATAGS = %w[ -user user -approver approver commenter comm noter noteupdater artcomm From ea9c3576d8eb6b301597c2bdebd420fbc475da77 Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 11 Dec 2018 18:10:20 -0600 Subject: [PATCH 4/5] search: add synonyms for *_count metatags. Allow e.g. `deleted_comments` as a synonym for `deleted_comment_count`. --- app/models/tag.rb | 19 +++++++++++++++++-- test/unit/post_test.rb | 7 +++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index a043abb3f..064b48258 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -8,6 +8,10 @@ class Tag < ApplicationRecord pool_count deleted_pool_count active_pool_count series_pool_count collection_pool_count appeal_count approval_count replacement_count ] + + # allow e.g. `deleted_comments` as a synonym for `deleted_comment_count` + COUNT_METATAG_SYNONYMS = COUNT_METATAGS.map { |str| str.delete_suffix("_count").pluralize } + METATAGS = %w[ -user user -approver approver commenter comm noter noteupdater artcomm -pool pool ordpool -favgroup favgroup -fav fav ordfav md5 -rating rating @@ -15,7 +19,7 @@ class Tag < ApplicationRecord -source id -id date age order limit -status status tagcount parent -parent child pixiv_id pixiv search upvote downvote filetype -filetype flagger -flagger appealer -appealer disapproval -disapproval - ] + TagCategory.short_name_list.map {|x| "#{x}tags"} + COUNT_METATAGS + ] + TagCategory.short_name_list.map {|x| "#{x}tags"} + COUNT_METATAGS + COUNT_METATAG_SYNONYMS SUBQUERY_METATAGS = %w[commenter comm noter noteupdater artcomm flagger -flagger appealer -appealer] @@ -764,7 +768,14 @@ class Tag < ApplicationRecord q[:child] = g2.downcase when "order" - q[:order] = g2.downcase + g2 = g2.downcase + + order, suffix, _ = g2.partition(/_(asc|desc)\z/i) + if order.in?(COUNT_METATAG_SYNONYMS) + g2 = order.singularize + "_count" + suffix + end + + q[:order] = g2 when "limit" # Do nothing. The controller takes care of it. @@ -801,6 +812,10 @@ class Tag < ApplicationRecord when *COUNT_METATAGS q[g1.to_sym] = parse_helper(g2) + when *COUNT_METATAG_SYNONYMS + g1 = "#{g1.singularize}_count" + q[g1.to_sym] = parse_helper(g2) + end else diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index 1e67ecf80..13c5ad6ef 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -2111,6 +2111,10 @@ class PostTest < ActiveSupport::TestCase assert_tag_match([posts[1], posts[0]], "note_count:1") assert_tag_match([posts[0]], "active_note_count:1") assert_tag_match([posts[1]], "deleted_note_count:1") + + assert_tag_match([posts[1], posts[0]], "notes:1") + assert_tag_match([posts[0]], "active_notes:1") + assert_tag_match([posts[1]], "deleted_notes:1") end should "return posts for the artcomm: metatag" do @@ -2418,6 +2422,8 @@ class PostTest < ActiveSupport::TestCase assert_tag_match(posts.reverse, "order:rank") assert_tag_match(posts.reverse, "order:note_count") assert_tag_match(posts.reverse, "order:note_count_desc") + assert_tag_match(posts.reverse, "order:notes") + assert_tag_match(posts.reverse, "order:notes_desc") assert_tag_match(posts, "order:id_asc") assert_tag_match(posts, "order:score_asc") @@ -2436,6 +2442,7 @@ class PostTest < ActiveSupport::TestCase assert_tag_match(posts, "order:chartags_asc") assert_tag_match(posts, "order:copytags_asc") assert_tag_match(posts, "order:note_count_asc") + assert_tag_match(posts, "order:notes_asc") end should "return posts for order:comment_bumped" do From 957d1527d14760dd781cf9a81c72ced38c529653 Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 11 Dec 2018 18:10:20 -0600 Subject: [PATCH 5/5] autocomplete: add order:*_count metatags. --- .../src/javascripts/autocomplete.js.erb | 20 ++--------------- app/models/tag.rb | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb index 963f0fda3..a7fa76055 100644 --- a/app/javascript/src/javascripts/autocomplete.js.erb +++ b/app/javascript/src/javascripts/autocomplete.js.erb @@ -5,6 +5,7 @@ let Autocomplete = {}; Autocomplete.METATAGS = <%= Tag::METATAGS.to_json.html_safe %>; Autocomplete.TAG_CATEGORIES = <%= TagCategory.mapping.to_json.html_safe %>; +Autocomplete.ORDER_METATAGS = <%= Tag::ORDER_METATAGS.to_json.html_safe %>; Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|"); Autocomplete.TAG_PREFIXES_REGEX = new RegExp("^(" + Autocomplete.TAG_PREFIXES + ")(.*)$", "i"); @@ -389,24 +390,7 @@ Autocomplete.render_item = function(list, item) { }; Autocomplete.static_metatags = { - order: [ - "id", "id_desc", - "score", "score_asc", - "favcount", "favcount_asc", - "created_at", "created_at_asc", - "change", "change_asc", - "comment", "comment_asc", - "comment_bumped", "comment_bumped_asc", - "note", "note_asc", - "artcomm", "artcomm_asc", - "mpixels", "mpixels_asc", - "portrait", "landscape", - "filesize", "filesize_asc", - "tagcount", "tagcount_asc", - "rank", - "random", - "custom" - ].concat(<%= TagCategory.short_name_list.map {|category| [category + "tags", category + "tags_asc"]}.flatten %>), + order: Autocomplete.ORDER_METATAGS, status: [ "any", "deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated" ], diff --git a/app/models/tag.rb b/app/models/tag.rb index 064b48258..cb9cc7d68 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -23,6 +23,28 @@ class Tag < ApplicationRecord SUBQUERY_METATAGS = %w[commenter comm noter noteupdater artcomm flagger -flagger appealer -appealer] + ORDER_METATAGS = %w[ + id id_desc + score score_asc + favcount favcount_asc + created_at created_at_asc + change change_asc + comment comment_asc + comment_bumped comment_bumped_asc + note note_asc + artcomm artcomm_asc + mpixels mpixels_asc + portrait landscape + filesize filesize_asc + tagcount tagcount_asc + rank + random + custom + ] + + COUNT_METATAGS + + COUNT_METATAG_SYNONYMS.flat_map { |str| [str, "#{str}_asc"] } + + TagCategory.short_name_list.flat_map { |str| ["#{str}tags", "#{str}tags_asc"] } + has_one :wiki_page, :foreign_key => "title", :primary_key => "name" has_one :artist, :foreign_key => "name", :primary_key => "name" has_one :antecedent_alias, -> {active}, :class_name => "TagAlias", :foreign_key => "antecedent_name", :primary_key => "name"