Skip to content

Commit

Permalink
Merge pull request #2 from GeorgeKaraszi/feature/either_anyof
Browse files Browse the repository at this point in the history
Add .either_join and .either_order querying methods
  • Loading branch information
GeorgeKaraszi authored May 5, 2018
2 parents d2824cd + b387455 commit f3cb199
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 6 deletions.
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
active_record_extended (0.1.0)
activerecord (>= 5.1, < 6.0)
ar_outer_joins (~> 0.2)
pg (~> 0.18)

GEM
Expand All @@ -19,6 +20,8 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
ar_outer_joins (0.2.0)
activerecord (>= 3.2)
arel (9.0.0)
ast (2.4.0)
byebug (10.0.2)
Expand Down
12 changes: 11 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,17 @@ namespace :db do
end

create_table :tags, force: true do |t|
t.integer "person_id"
t.belongs_to :person
end

create_table :profile_ls, force: true do |t|
t.belongs_to :person
t.integer :likes
end

create_table :profile_rs, force: true do |t|
t.belongs_to :person
t.integer :dislikes
end
end

Expand Down
1 change: 1 addition & 0 deletions active_record_extended.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_dependency "activerecord", ">= 5.1", "< 6.0"
spec.add_dependency "ar_outer_joins", "~> 0.2"
spec.add_dependency "pg", "~> 0.18"

spec.add_development_dependency "bundler", "~> 1.16"
Expand Down
3 changes: 2 additions & 1 deletion lib/active_record_extended/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

require "active_record"

require "active_record_extended/query_methods_decorator"
require "active_record_extended/predicate_builder/array_handler_decorator"
require "active_record_extended/query_methods/where_chain"
require "active_record_extended/query_methods/either"

if ActiveRecord::VERSION::MAJOR >= 5
if ActiveRecord::VERSION::MINOR >= 2
Expand Down
60 changes: 60 additions & 0 deletions lib/active_record_extended/query_methods/either.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require "ar_outer_joins"

module ActiveRecordExtended
module QueryMethods
module Either
XOR_FIELD_SQL = "(CASE WHEN %<t1>s.%<c1>s IS NULL THEN %<t2>s.%<c2>s ELSE %<t1>s.%<c1>s END) "
XOR_FIELD_KEYS = %i[t1 c1 t2 c2].freeze

def either_join(initial_association, fallback_association)
associations = [initial_association, fallback_association]
association_options = xor_field_options_for_associations(associations)
condition__query = xor_field_sql(association_options) + "= #{table_name}.#{primary_key}"
outer_joins(associations).where(Arel.sql(condition__query))
end

def either_order(direction, **associations_and_columns)
reflected_columns = map_columns_to_tables(associations_and_columns)
conditional_query = xor_field_sql(reflected_columns) + sort_order_sql(direction)
outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
end

private

def xor_field_sql(options)
XOR_FIELD_SQL % Hash[xor_field_options(options)]
end

def sort_order_sql(dir)
%w[asc desc].include?(dir.to_s) ? dir.to_s : "asc"
end

def xor_field_options(options)
str_args = options.flatten.take(XOR_FIELD_KEYS.size).map(&:to_s)
Hash[XOR_FIELD_KEYS.zip(str_args)]
end

def map_columns_to_tables(associations_and_columns)
if associations_and_columns.respond_to?(:transform_keys)
associations_and_columns.transform_keys { |assc| reflect_on_association(assc).table_name }
else
associations_and_columns.each_with_object({}) do |(assc, value), key_table|
reflect_table = reflect_on_association(assc).table_name
key_table[reflect_table] = value
end
end
end

def xor_field_options_for_associations(associations)
associations.each_with_object({}) do |association_name, options|
reflection = reflect_on_association(association_name)
options[reflection.table_name] = reflection.foreign_key
end
end
end
end
end

ActiveRecord::Base.extend(ActiveRecordExtended::QueryMethods::Either)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module ActiveRecordExtended
module QueryMethodsDecorator
module WhereChain
def overlap(opts, *rest)
substitute_comparisons(opts, rest, Arel::Nodes::Overlap, "overlap")
end
Expand Down Expand Up @@ -92,7 +92,7 @@ def substitute_comparisons(opts, rest, arel_node_class, method)
module ActiveRecord
module QueryMethods
class WhereChain
prepend ActiveRecordExtended::QueryMethodsDecorator
prepend ActiveRecordExtended::WhereChain

def build_where_chain(opts, rest, &block)
where_clause = @scope.send(:where_clause_factory).build(opts, rest)
Expand Down
36 changes: 36 additions & 0 deletions spec/query_methods/either_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe "Active Record Either Methods" do
let!(:one) { Person.create! }
let!(:two) { Person.create! }
let!(:three) { Person.create! }
let!(:profile_l) { ProfileL.create!(person_id: one.id, likes: 100) }
let!(:profile_r) { ProfileR.create!(person_id: two.id, dislikes: 50) }

describe ".either_join/2" do
it "Should only only return records that belong to profile L or profile R" do
query = Person.either_join(:profile_l, :profile_r)
expect(query).to include(one, two)
expect(query).to_not include(three)
end
end

describe ".either_order/2" do
it "Should not exclude anyone who does not have a relationship" do
query = Person.either_order(:asc, profile_l: :likes, profile_r: :dislikes)
expect(query).to include(one, two, three)
end

it "Should order people based on their likes and dislikes in ascended order" do
query = Person.either_order(:asc, profile_l: :likes, profile_r: :dislikes).where(id: [one.id, two.id])
expect(query).to match_array([two, one])
end

it "Should order people based on their likes and dislikes in descending order" do
query = Person.either_order(:desc, profile_l: :likes, profile_r: :dislikes).where(id: [one.id, two.id])
expect(query).to match_array([one, two])
end
end
end
4 changes: 2 additions & 2 deletions spec/query_methods/hash_query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
let!(:three) { Person.create!(data: { nickname: "georgey" }) }

describe "#contains" do
context "HStore Column " do
context "HStore Column Type" do
it "returns records that contain hash elements in joined tables" do
tag_one = Tag.create!(person_id: one.id)
tag_two = Tag.create!(person_id: two.id)
Expand All @@ -25,7 +25,7 @@
end
end

context "JSONB Column" do
context "JSONB Column Type" do
it "returns records that contains a json hashed value" do
query = Person.where.contains(jsonb_data: { payment: "zip" })
expect(query).to include(one)
Expand Down
55 changes: 55 additions & 0 deletions spec/sql_inspections/either_sql_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe "Either Methods SQL Queries" do
let(:contains_array_regex) { /\"people\"\.\"tag_ids\" @> '\{1,2\}'/ }
let(:profile_l_outer_join) { /LEFT OUTER JOIN \"profile_ls\" ON \"profile_ls\".\"person_id\" = \"people\".\"id\"/ }
let(:profile_r_outer_join) { /LEFT OUTER JOIN \"profile_rs\" ON \"profile_rs\".\"person_id\" = \"people\".\"id\"/ }
let(:where_join_case) do
"WHERE ((CASE WHEN profile_ls.person_id IS NULL"\
" THEN profile_rs.person_id"\
" ELSE profile_ls.person_id END) "\
"= people.id)"
end

let(:order_case) do
"ORDER BY "\
"(CASE WHEN profile_ls.likes IS NULL"\
" THEN profile_rs.dislikes"\
" ELSE profile_ls.likes END)"
end

describe ".either_join/2" do
it "Should contain outer joins on the provided relationships" do
query = Person.either_join(:profile_l, :profile_r).to_sql
expect(query).to match_regex(profile_l_outer_join)
expect(query).to match_regex(profile_r_outer_join)
end

it "Should contain a case statement that will conditionally alternative between tables" do
query = Person.either_join(:profile_l, :profile_r).to_sql
expect(query).to include(where_join_case)
end
end

describe ".either_order/2" do
let(:ascended_order) { Person.either_order(:asc, profile_l: :likes, profile_r: :dislikes).to_sql }
let(:descended_order) { Person.either_order(:desc, profile_l: :likes, profile_r: :dislikes).to_sql }

it "Should contain outer joins on the provided relationships" do
expect(ascended_order).to match_regex(profile_l_outer_join)
expect(ascended_order).to match_regex(profile_r_outer_join)
expect(descended_order).to match_regex(profile_l_outer_join)
expect(descended_order).to match_regex(profile_r_outer_join)
end

it "Should contain a relational ordering case statement for a relations column" do
expect(ascended_order).to include(order_case)
expect(ascended_order).to end_with("asc")

expect(descended_order).to include(order_case)
expect(descended_order).to end_with("desc")
end
end
end
10 changes: 10 additions & 0 deletions spec/support/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@

class Person < ActiveRecord::Base
has_many :hm_tags, class_name: "Tag"
has_one :profile_l, class_name: "ProfileL"
has_one :profile_r, class_name: "ProfileR"
end

class Tag < ActiveRecord::Base
belongs_to :person
end

class ProfileL < ActiveRecord::Base
belongs_to :person
end

class ProfileR < ActiveRecord::Base
belongs_to :person
end

0 comments on commit f3cb199

Please sign in to comment.