summaryrefslogtreecommitdiff
path: root/app/models/concerns/where_composite.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/concerns/where_composite.rb')
-rw-r--r--app/models/concerns/where_composite.rb81
1 files changed, 81 insertions, 0 deletions
diff --git a/app/models/concerns/where_composite.rb b/app/models/concerns/where_composite.rb
new file mode 100644
index 00000000000..3b66efc1c77
--- /dev/null
+++ b/app/models/concerns/where_composite.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module WhereComposite
+ extend ActiveSupport::Concern
+
+ class TooManyIds < ArgumentError
+ LIMIT = 100
+
+ def initialize(no_of_ids)
+ super(<<~MSG)
+ At most #{LIMIT} identifier sets at a time please! Got #{no_of_ids}.
+ Have you considered splitting your request into batches?
+ MSG
+ end
+
+ def self.guard(collection)
+ n = collection.size
+ return collection if n <= LIMIT
+
+ raise self, n
+ end
+ end
+
+ class_methods do
+ # Apply a set of constraints that function as composite IDs.
+ #
+ # This is the plural form of the standard ActiveRecord idiom:
+ # `where(foo: x, bar: y)`, except it allows multiple pairs of `x` and
+ # `y` to be specified, with the semantics that translate to:
+ #
+ # ```sql
+ # WHERE
+ # (foo = x_0 AND bar = y_0)
+ # OR (foo = x_1 AND bar = y_1)
+ # OR ...
+ # ```
+ #
+ # or the equivalent:
+ #
+ # ```sql
+ # WHERE
+ # (foo, bar) IN ((x_0, y_0), (x_1, y_1), ...)
+ # ```
+ #
+ # @param permitted_keys [Array<Symbol>] The keys each hash must have. There
+ # must be at least one key (but really,
+ # it ought to be at least two)
+ # @param hashes [Array<#to_h>|#to_h] The constraints. Each parameter must have a
+ # value for the keys named in `permitted_keys`
+ #
+ # e.g.:
+ # ```
+ # where_composite(%i[foo bar], [{foo: 1, bar: 2}, {foo: 1, bar: 3}])
+ # ```
+ #
+ def where_composite(permitted_keys, hashes)
+ raise ArgumentError, 'no permitted_keys' unless permitted_keys.present?
+
+ # accept any hash-like thing, such as Structs
+ hashes = TooManyIds.guard(Array.wrap(hashes)).map(&:to_h)
+
+ return none if hashes.empty?
+
+ case permitted_keys.size
+ when 1
+ key = permitted_keys.first
+ where(key => hashes.map { |hash| hash.fetch(key) })
+ else
+ clauses = hashes.map do |hash|
+ permitted_keys.map do |key|
+ arel_table[key].eq(hash.fetch(key))
+ end.reduce(:and)
+ end
+
+ where(clauses.reduce(:or))
+ end
+ rescue KeyError
+ raise ArgumentError, "all arguments must contain #{permitted_keys}"
+ end
+ end
+end