Created
July 26, 2019 07:51
-
-
Save cyrilchampier/fdb945e8a09f93d50c7e89305c2f53f0 to your computer and use it in GitHub Desktop.
Patch on ActiveRecord to randomize all select without order
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# As PostgreSQL documentation states: | |
# https://www.postgresql.org/docs/9.1/queries-order.html | |
# The actual order in that case will depend on the scan and join plan types and the order on disk, | |
# but it must not be relied on. | |
# | |
# Problem is, in 99% of the CI run, in tests, the order will be the same. | |
# And in 1% of the run, regardless how many retries, the order will be different and the test will fail. | |
# Solution here is to "really" randomize requests so that retries will work, | |
# and test will be marked as flaky instead of breaking the build. | |
# | |
# All these test cases queries should return a different order at each call: | |
# Patient.pluck(:id, :last_name) | |
# Patient.find_by(email: '') | |
# Patient.first.update!(last_name: rand().to_s) | |
# Patient.where(master_patient: MasterPatient.where.not(id: nil)) | |
# Patient.distinct | |
# Patient.all.select('DISTINCT ON (email) id') | |
# Patient.from(Patient.arel_table.create_table_alias(Patient.all.arel.union(Patient.all), :patients)) | |
# | |
# But the use case where we inject to_sql in an union is impossible to handle, and should not be used in the codebase: | |
# "select count(*) from (#{directory_doctors_query.select('1').to_sql} "\ | |
# "union all #{profiles_query.select('1').to_sql}) as doctors" | |
# | |
module DatabaseRandomizer | |
RANDOM_AREL = Arel.sql('random()') | |
# Add a random order on every non ordered select request. | |
module RelationRandomOrder | |
def build_order(arel) | |
orders = order_values.uniq | |
orders.reject!(&:blank?) | |
if orders.empty? && | |
!distinct_value && | |
select_values.none? { |select_value| select_value.to_s.downcase.include?('distinct') } | |
orders = [RANDOM_AREL] | |
end | |
arel.order(*orders) | |
end | |
end | |
ActiveRecord::Relation.prepend(RelationRandomOrder) | |
# But remove that order if we union the select (union do not support order by values). | |
module ArelUnionRemovesRandomOrder | |
def union(operation, other = nil) | |
orders.delete(RANDOM_AREL) | |
operation.arel.orders.delete(RANDOM_AREL) | |
other.arel.orders.delete(RANDOM_AREL) if other.present? | |
super(operation, other) | |
end | |
def intersect(other) | |
orders.delete(RANDOM_AREL) | |
other.orders.delete(RANDOM_AREL) | |
super(other) | |
end | |
def except(other) | |
orders.delete(RANDOM_AREL) | |
other.orders.delete(RANDOM_AREL) | |
super(other) | |
end | |
end | |
Arel::SelectManager.prepend(ArelUnionRemovesRandomOrder) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment