# frozen_string_literal: true # Override ActiveRecord::QueryMethods.build_order to append a final ORDER(RAND()) when necessary require "active_record/connection_adapters/abstract_adapter" module Unreliable module BuildOrder def build_order(arel) super(arel) return unless Unreliable::Config.enabled? return if from_only_internal_metadata?(arel) return if from_one_table_with_ordered_pk?(arel) case Arel::Table.engine.connection.adapter_name when "Mysql2" # https://dev.mysql.com/doc/refman/8.0/en/mathematical-functions.html#function_rand arel.order("RAND()") when "PostgreSQL", "SQLite" # https://www.postgresql.org/docs/13/functions-math.html#FUNCTIONS-MATH-RANDOM-TABLE # https://www.sqlite.org/lang_corefunc.html#random arel.order("RANDOM()") else raise ArgumentError, "unknown Arel::Table.engine" end end def from_only_internal_metadata?(arel) # No need to randomize queries on ar_internal_metadata arel.froms.map(&:name) == [ActiveRecord::Base.internal_metadata_table_name] end def from_one_table_with_ordered_pk?(arel) # This gem isn't (yet) capable of determining if ordering is reliable when two or # more tables are being joined. return false if arel.ast.cores.first.source.is_a?(Arel::Nodes::JoinSource) && arel.ast.cores.first.source.right.present? return false if arel.froms.count > 1 # If the single table's primary key's column(s) are covered by the order columns, # return true and don't randomize the order. (primary_key_columns(arel) - order_columns(arel)).empty? end def primary_key_columns(arel) # primary_keys returns a String if it's one column, an Array if two or more. # Using the SchemaCache minimizes the number of times we have to, e.g. in MySQL, # SELECT column_name FROM information_schema.statistics # (or in Rails < 6, SELECT column_name FROM information_schema.key_column_usage) [ActiveRecord::Base.connection.schema_cache.primary_keys(arel.froms.first.name)].flatten end def order_columns(arel) from_table_name = arel.froms.first.name arel.orders .select { |order| order.is_a? Arel::Nodes::Ordering } # Don't try to parse textual orders .map(&:expr) .select { |expr| expr.relation.name == from_table_name } .map(&:name) .map(&:to_s) # In Rails < 5.2, the order column names are symbols; >= 5.2, strings end end end