diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 71ef69063f6f5..48a69914302f8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -44,6 +44,9 @@ def type_cast(value) when nil, Numeric, String then value when Type::Time::Value then quoted_time(value) when Date, Time then quoted_date(value) + when ActiveSupport::Duration + warn_quote_duration_deprecated + value.to_i else raise TypeError, "can't cast #{value.class.name}" end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index c967b074c7b9c..ba590e23ccf89 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -141,6 +141,11 @@ def type_cast(value) # :nodoc: encode_array(value) when Range encode_range(value) + when Rational + value.to_f + when ActiveSupport::Duration + warn_quote_duration_deprecated + value.to_i else super end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index d28787b5cf5ae..fcca1eeba8561 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -63,7 +63,7 @@ def quote_default_expression(value, column) # :nodoc: def type_cast(value) # :nodoc: case value - when BigDecimal + when BigDecimal, Rational value.to_f when String if value.encoding == Encoding::ASCII_8BIT @@ -71,6 +71,9 @@ def type_cast(value) # :nodoc: else super end + when ActiveSupport::Duration + warn_quote_duration_deprecated + value.to_i else super end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b9dc7392323c1..3c734f0e22d4d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1512,9 +1512,17 @@ def build_subquery(subquery_alias, select_value) # :nodoc: def build_where_clause(opts, rest = []) # :nodoc: opts = sanitize_forbidden_attributes(opts) + if opts.is_a?(Array) + opts, *rest = opts + end + case opts - when String, Array - parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])] + when String + if opts.include?("?") + parts = [build_bound_sql_literal(opts, rest)] + else + parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])] + end when Hash opts = opts.transform_keys do |key| if key.is_a?(Array) @@ -1550,6 +1558,26 @@ def async spawn.async! end + def build_bound_sql_literal(statement, values) + bound_values = values.map do |value| + if ActiveRecord::Relation === value + Arel.sql(value.to_sql) + elsif value.respond_to?(:map) && !value.acts_like?(:string) + values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v } + values.empty? ? nil : values + else + value = value.id_for_database if value.respond_to?(:id_for_database) + value + end + end + + begin + Arel::Nodes::BoundSqlLiteral.new("(#{statement})", bound_values, nil) + rescue Arel::BindError => error + raise ActiveRecord::PreparedStatementInvalid, error.message + end + end + def lookup_table_klass_from_join_dependencies(table_name) each_join_dependencies do |join| return join.base_klass if table_name == join.table_name diff --git a/activerecord/lib/arel/nodes/bound_sql_literal.rb b/activerecord/lib/arel/nodes/bound_sql_literal.rb index d3e266f67052e..52dc7c6e026de 100644 --- a/activerecord/lib/arel/nodes/bound_sql_literal.rb +++ b/activerecord/lib/arel/nodes/bound_sql_literal.rb @@ -6,13 +6,17 @@ class BoundSqlLiteral < NodeExpression attr_reader :sql_with_placeholders, :positional_binds, :named_binds def initialize(sql_with_placeholders, positional_binds, named_binds) - if !positional_binds.empty? && !named_binds.empty? - raise BindError.new("cannot mix positional and named binds", sql_with_placeholders) - elsif !positional_binds.empty? + has_positional = !(positional_binds.nil? || positional_binds.empty?) + has_named = !(named_binds.nil? || named_binds.empty?) + + if has_positional + if has_named + raise BindError.new("cannot mix positional and named binds", sql_with_placeholders) + end if positional_binds.size != (expected = sql_with_placeholders.count("?")) raise BindError.new("wrong number of bind variables (#{positional_binds.size} for #{expected})", sql_with_placeholders) end - elsif !named_binds.empty? + elsif has_named tokens_in_string = sql_with_placeholders.scan(/:(?