Skip to content

Instantly share code, notes, and snippets.

@cyrilchampier
Created July 26, 2019 07:51
Show Gist options
  • Save cyrilchampier/fdb945e8a09f93d50c7e89305c2f53f0 to your computer and use it in GitHub Desktop.
Save cyrilchampier/fdb945e8a09f93d50c7e89305c2f53f0 to your computer and use it in GitHub Desktop.
Patch on ActiveRecord to randomize all select without order
# 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