diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index f1372c85034eae337427e7f31310f0c03181a88f..ce56586eb34201c65a34cd968f4cb20f4697dc43 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -185,12 +185,16 @@ def column_name_with_order_matcher # :nodoc: private def type_casted_binds(binds) case binds.first - when ActiveModel::Attribute - binds.map { |attr| type_cast(attr.value_for_database) } when Array binds.map { |column, value| type_cast(value, column) } else - binds.map { |value| type_cast(value) } + binds.map do |value| + if ActiveModel::Attribute === value + type_cast(value.value_for_database) + else + type_cast(value) + end + end end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 5dca75c539db1a32a992ba62e775eac3e624e194..7fe04106abc9f9acde24e4d6af59c6d282cabbb6 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -37,13 +37,18 @@ def str.inspect private def render_bind(attr) - value = if attr.type.binary? && attr.value - "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + if ActiveModel::Attribute === attr + value = if attr.type.binary? && attr.value + "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + else + connection.type_cast(attr.value_for_database) + end else - connection.type_cast(attr.value_for_database) + value = connection.type_cast(attr) + attr = nil end - [attr.name, value] + [attr&.name, value] end end end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index f8047163a34c2edc9a2a364f2273aff90928349d..186079a2d53c45ea490a34294d48ff1e225285a4 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -50,8 +50,13 @@ def initialize(values) def sql_for(binds, connection) val = @values.dup - casted_binds = binds.map(&:value_for_database) - @indexes.each { |i| val[i] = connection.quote(casted_binds.shift) } + @indexes.each do |i| + value = binds.shift + if ActiveModel::Attribute === value + value = value.value_for_database + end + val[i] = connection.quote(value) + end val.join end end @@ -75,6 +80,15 @@ def add_bind(obj) self end + def add_binds(binds) + @binds.concat binds + binds.size.times do |i| + @parts << ", " unless i == 0 + @parts << Substitute.new + end + self + end + def value [@parts, @binds] end @@ -102,7 +116,7 @@ def initialize(bound_attributes) @bound_attributes = bound_attributes bound_attributes.each_with_index do |attr, i| - if Substitute === attr.value + if ActiveModel::Attribute === attr && Substitute === attr.value @indexes << i end end diff --git a/activerecord/lib/arel/collectors/bind.rb b/activerecord/lib/arel/collectors/bind.rb index 6f8912575dce8cec9de779138c2871c682eddbbe..2abe7f6cdd753b14cd0002c41c6ba7b61473b221 100644 --- a/activerecord/lib/arel/collectors/bind.rb +++ b/activerecord/lib/arel/collectors/bind.rb @@ -16,6 +16,11 @@ def add_bind(bind) self end + def add_binds(binds) + @binds.concat binds + self + end + def value @binds end diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb index 0df073164e99bae837ea45dd073faf35f9438d86..ee5c0bb21ca3315fd08ec7ec73e8743a4eba4660 100644 --- a/activerecord/lib/arel/collectors/composite.rb +++ b/activerecord/lib/arel/collectors/composite.rb @@ -22,6 +22,12 @@ def add_bind(bind, &block) self end + def add_binds(binds, &block) + left.add_binds(binds, &block) + right.add_binds(binds, &block) + self + end + def value [left.value, right.value] end diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb index 6c52335b4590fe4b67e2c5502c65d6c498f096a3..b67d1194b4b226290e9a2eeb69c3624e3e96a77d 100644 --- a/activerecord/lib/arel/collectors/sql_string.rb +++ b/activerecord/lib/arel/collectors/sql_string.rb @@ -17,6 +17,11 @@ def add_bind(bind) @bind_index += 1 self end + + def add_binds(binds, &block) + self << (@bind_index...@bind_index += binds.size).map(&block).join(", ") + self + end end end end diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb index ea77db0215d0fc18e4c92816278b5b9d52fba651..24270a6d77196917fb58c82dec77fb056e83ffc8 100644 --- a/activerecord/lib/arel/collectors/substitute_binds.rb +++ b/activerecord/lib/arel/collectors/substitute_binds.rb @@ -20,6 +20,10 @@ def add_bind(bind) self << quoter.quote(bind) end + def add_binds(binds) + self << binds.map { |bind| quoter.quote(bind) }.join(", ") + end + def value delegate.value end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb index 65eceeef7424a10a39a2a95b397cb5e452b9f729..0afd81352820d130f9e03284416c9f375226a0d6 100644 --- a/activerecord/lib/arel/visitors/postgresql.rb +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -41,10 +41,6 @@ def visit_Arel_Nodes_DistinctOn(o, collector) visit(o.expr, collector) << " )" end - def visit_Arel_Nodes_BindParam(o, collector) - collector.add_bind(o.value) { |i| "$#{i}" } - end - def visit_Arel_Nodes_GroupingElement(o, collector) collector << "( " visit(o.expr, collector) << " )" @@ -92,6 +88,11 @@ def visit_Arel_Nodes_NullsLast(o, collector) collector << " NULLS LAST" end + BIND_BLOCK = proc { |i| "$#{i}" } + private_constant :BIND_BLOCK + + def bind_block; BIND_BLOCK; end + # Used by Lateral visitor to enclose select queries in parentheses def grouping_parentheses(o, collector) if o.expr.is_a? Nodes::SelectStatement diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 81706549719603f40b816d1b6e1e7a5b0fc9d266..fac8c921daeefe9d5fb7c79f4ae7d563139a5d53 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -332,15 +332,14 @@ def visit_Arel_Nodes_HomogeneousIn(o, collector) collector << " NOT IN (" end - values = o.casted_values.map { |v| @connection.quote(v) } + values = o.casted_values - expr = if values.empty? - @connection.quote(nil) + if values.empty? + collector << @connection.quote(nil) else - values.join(",") + collector.add_binds(values, &bind_block) end - collector << expr collector << ")" collector end @@ -691,8 +690,13 @@ def visit_Arel_Attributes_Attribute(o, collector) collector << quote_table_name(join_name) << "." << quote_column_name(o.name) end + BIND_BLOCK = proc { "?" } + private_constant :BIND_BLOCK + + def bind_block; BIND_BLOCK; end + def visit_Arel_Nodes_BindParam(o, collector) - collector.add_bind(o.value) { "?" } + collector.add_bind(o.value, &bind_block) end def visit_Arel_Nodes_SqlLiteral(o, collector) diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 7ff21dad575f65522f3981bd0ffc975a9d1d5ad1..c9ed2647aa0bb365a6e19fd76786a88e6b22f41d 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -164,7 +164,57 @@ def test_logs_legacy_binds_after_type_cast end end + def test_bind_params_to_sql_with_prepared_statements + assert_bind_params_to_sql + end + + def test_bind_params_to_sql_with_unprepared_statements + @connection.unprepared_statement do + assert_bind_params_to_sql + end + end + private + def assert_bind_params_to_sql + table = Author.quoted_table_name + pk = "#{table}.#{Author.quoted_primary_key}" + + # prepared_statements: true + # + # SELECT `authors`.* FROM `authors` WHERE (`authors`.`id` IN (?, ?, ?) OR `authors`.`id` IS NULL) + # + # prepared_statements: false + # + # SELECT `authors`.* FROM `authors` WHERE (`authors`.`id` IN (1, 2, 3) OR `authors`.`id` IS NULL) + # + sql = "SELECT #{table}.* FROM #{table} WHERE (#{pk} IN (#{bind_params(1..3)}) OR #{pk} IS NULL)" + + authors = Author.where(id: [1, 2, 3, nil]) + assert_equal sql, @connection.to_sql(authors.arel) + assert_sql(sql) { assert_equal 3, authors.length } + + # prepared_statements: true + # + # SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (?, ?, ?) + # + # prepared_statements: false + # + # SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (1, 2, 3) + # + sql = "SELECT #{table}.* FROM #{table} WHERE #{pk} IN (#{bind_params(1..3)})" + + authors = Author.where(id: [1, 2, 3, 9223372036854775808]) + assert_equal sql, @connection.to_sql(authors.arel) + assert_sql(sql) { assert_equal 3, authors.length } + end + + def bind_params(ids) + collector = @connection.send(:collector) + bind_params = ids.map { |i| Arel::Nodes::BindParam.new(i) } + sql, _ = @connection.visitor.compile(bind_params, collector) + sql + end + def to_sql_key(arel) sql = @connection.to_sql(arel) @connection.respond_to?(:sql_key, true) ? @connection.send(:sql_key, sql) : sql