Skip to content

Commit 2cef3ac

Browse files
authored
Merge pull request rails#47729 from Shopify/pm/cpk-where-syntax
Introduce query-by-tuple syntax
2 parents 047a76e + 350b397 commit 2cef3ac

File tree

7 files changed

+97
-13
lines changed

7 files changed

+97
-13
lines changed

activerecord/CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
* Allow specifying where clauses with column-tuple syntax.
2+
3+
Querying through `#where` now accepts a new tuple-syntax which accepts, as
4+
a key, an array of columns and, as a value, an array of corresponding tuples.
5+
The key specifies a list of columns, while the value is an array of
6+
ordered-tuples that conform to the column list.
7+
8+
For instance:
9+
10+
```ruby
11+
# Cpk::Book => Cpk::Book(author_id: integer, number: integer, title: string, revision: integer)
12+
# Cpk::Book.primary_key => ["author_id", "number"]
13+
14+
book = Cpk::Book.create!(author_id: 1, number: 1)
15+
Cpk::Book.where(Cpk::Book.primary_key => [[1, 2]]) # => [book]
16+
17+
# Topic => Topic(id: integer, title: string, author_name: string...)
18+
19+
Topic.where([:title, :author_name] => [["The Alchemist", "Paul Coelho"], ["Harry Potter", "J.K Rowling"]])
20+
```
21+
22+
*Paarth Madan*
23+
124
* Allow warning codes to be ignore when reporting SQL warnings.
225

326
Active Record config that can ignore warning codes

activerecord/lib/active_record/destroy_association_async_job.rb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,8 @@ def perform(
2323
raise DestroyAssociationAsyncError, "owner record not destroyed"
2424
end
2525

26-
if association_model.query_constraints_list
27-
association_ids
28-
.map { |assoc_ids| association_model.where(association_primary_key_column.zip(assoc_ids).to_h) }
29-
.inject(&:or)
30-
.find_each { |r| r.destroy }
31-
else
32-
association_model.where(association_primary_key_column => association_ids).find_each do |r|
33-
r.destroy
34-
end
26+
association_model.where(association_primary_key_column => association_ids).find_each do |r|
27+
r.destroy
3528
end
3629
end
3730

activerecord/lib/active_record/encryption/extended_deterministic_queries.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def process_arguments(owner, args, check_for_additional_values)
4444
return args if owner.deterministic_encrypted_attributes&.empty?
4545

4646
if args.is_a?(Array) && (options = args.first).is_a?(Hash)
47-
options = options.stringify_keys
47+
options = options.transform_keys do |key|
48+
if key.is_a?(Array)
49+
key.map(&:to_s)
50+
else
51+
key.to_s
52+
end
53+
end
4854
args[0] = options
4955

5056
owner.deterministic_encrypted_attributes&.each do |attribute_name|

activerecord/lib/active_record/relation/predicate_builder.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ def expand_from_hash(attributes, &block)
7878
return ["1=0"] if attributes.empty?
7979

8080
attributes.flat_map do |key, value|
81-
if value.is_a?(Hash) && !table.has_column?(key)
81+
if key.is_a?(Array)
82+
queries = Array(value).map do |ids_set|
83+
raise ArgumentError, "Expected corresponding value for #{key} to be an Array" unless ids_set.is_a?(Array)
84+
expand_from_hash(key.zip(ids_set).to_h)
85+
end
86+
grouping_queries(queries)
87+
elsif value.is_a?(Hash) && !table.has_column?(key)
8288
table.associated_table(key, &block)
8389
.predicate_builder.expand_from_hash(value.stringify_keys)
8490
elsif table.associated_with?(key)

activerecord/lib/active_record/relation/query_methods.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,8 +1476,12 @@ def build_where_clause(opts, rest = []) # :nodoc:
14761476
parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])]
14771477
when Hash
14781478
opts = opts.transform_keys do |key|
1479-
key = key.to_s
1480-
klass.attribute_aliases[key] || key
1479+
if key.is_a?(Array)
1480+
key.map { |k| klass.attribute_aliases[k.to_s] || k.to_s }
1481+
else
1482+
key = key.to_s
1483+
klass.attribute_aliases[key] || key
1484+
end
14811485
end
14821486
references = PredicateBuilder.references(opts)
14831487
self.references_values |= references unless references.empty?

activerecord/test/cases/primary_keys_test.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,10 @@ def test_assigning_a_composite_primary_key
386386
book.save!
387387

388388
assert_equal [1, 2], book.id
389+
assert_equal 1, book.author_id
390+
assert_equal 2, book.number
391+
ensure
392+
Cpk::Book.delete_all
389393
end
390394

391395
def test_assigning_a_non_array_value_to_model_with_composite_primary_key_raises

activerecord/test/cases/relation/where_test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require "models/topic"
1717
require "models/treasure"
1818
require "models/vertex"
19+
require "models/cpk"
1920
require "support/stubs/strong_parameters"
2021

2122
module ActiveRecord
@@ -95,6 +96,53 @@ def test_rewhere_on_root
9596
assert_equal posts(:welcome), Post.rewhere(title: "Welcome to the weblog").first
9697
end
9798

99+
def test_where_with_tuple_syntax
100+
first_topic = topics(:first)
101+
third_topic = topics(:third)
102+
103+
key = [:title, :author_name]
104+
105+
conditions = [
106+
[first_topic.title, first_topic.author_name],
107+
[third_topic.title, third_topic.author_name],
108+
]
109+
110+
assert_equal [first_topic], Topic.where([:id] => [[first_topic.id]])
111+
assert_equal [first_topic, third_topic].sort, Topic.where(key => conditions).sort
112+
end
113+
114+
def test_where_with_tuple_syntax_on_composite_models
115+
book_one = Cpk::Book.create!(author_id: 1, number: 2)
116+
book_two = Cpk::Book.create!(author_id: 3, number: 4)
117+
118+
assert_equal [book_one], Cpk::Book.where([:author_id, :number] => [[1, 2]])
119+
assert_equal [book_one, book_two].sort, Cpk::Book.where(Cpk::Book.primary_key => [[1, 2], [3, 4]]).sort
120+
assert_empty Cpk::Book.where([:author_id, :number] => [[1, 4], [3, 2]])
121+
end
122+
123+
def test_where_with_tuple_syntax_with_incorrect_arity
124+
error = assert_raise ArgumentError do
125+
Cpk::Book.where([:one, :two, :three] => [1, 2, 3])
126+
end
127+
128+
assert_match(/Expected corresponding value for.*to be an Array/, error.message)
129+
130+
error = assert_raise ArgumentError do
131+
Cpk::Book.where([:one] => 1)
132+
end
133+
134+
assert_match(/Expected corresponding value for.*to be an Array/, error.message)
135+
end
136+
137+
def test_where_with_tuple_syntax_and_regular_syntax_combined
138+
book_one = Cpk::Book.create!(author_id: 1, number: 2, title: "The Alchemist")
139+
book_two = Cpk::Book.create!(author_id: 3, number: 4, title: "The Alchemist")
140+
141+
assert_equal [book_one, book_two].sort, Cpk::Book.where(title: "The Alchemist").sort
142+
assert_equal [book_one, book_two].sort, Cpk::Book.where(title: "The Alchemist", [:author_id, :number] => [[1, 2], [3, 4]]).sort
143+
assert_equal [book_two], Cpk::Book.where(title: "The Alchemist", [:author_id, :number] => [[3, 4]])
144+
end
145+
98146
def test_belongs_to_shallow_where
99147
author = Author.new
100148
author.id = 1

0 commit comments

Comments
 (0)