Merge pull request #46948 from olefriis/add-arel-node-for-simple-sql-construction

Add Arel functionality for "stitching together" SQL
This commit is contained in:
Matthew Draper 2023-01-19 21:56:08 +10:30 committed by GitHub
commit b73fb939de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 0 deletions

@ -1,3 +1,9 @@
* Multiple `Arel::Nodes::SqlLiteral` nodes can now be added together to
form `Arel::Nodes::Fragments` nodes. This allows joining several pieces
of SQL.
*Matthew Draper*, *Ole Friis*
* `ActiveRecord::Base#signed_id` raises if called on a new record * `ActiveRecord::Base#signed_id` raises if called on a new record
Previously it would return an ID that was not usable, since it was based on `id = nil`. Previously it would return an ID that was not usable, since it was based on `id = nil`.

@ -8,6 +8,7 @@
require "arel/nodes/insert_statement" require "arel/nodes/insert_statement"
require "arel/nodes/update_statement" require "arel/nodes/update_statement"
require "arel/nodes/bind_param" require "arel/nodes/bind_param"
require "arel/nodes/fragments"
# terminal # terminal

@ -0,0 +1,35 @@
# frozen_string_literal: true
module Arel # :nodoc: all
module Nodes
class Fragments < Arel::Nodes::Node
attr_reader :values
def initialize(values = [])
super()
@values = values
end
def initialize_copy(other)
super
@values = @values.clone
end
def hash
[@values].hash
end
def +(other)
raise ArgumentError, "Expected Arel node" unless Arel.arel_node?(other)
self.class.new([*@values, other])
end
def eql?(other)
self.class == other.class &&
self.values == other.values
end
alias :== :eql?
end
end
end

@ -14,6 +14,12 @@ def encode_with(coder)
def fetch_attribute def fetch_attribute
end end
def +(other)
raise ArgumentError, "Expected Arel node" unless Arel.arel_node?(other)
Fragments.new([self, other])
end
end end
end end
end end

@ -791,6 +791,10 @@ def visit_Array(o, collector)
end end
alias :visit_Set :visit_Array alias :visit_Set :visit_Array
def visit_Arel_Nodes_Fragments(o, collector)
inject_join o.values, collector, " "
end
def quote(value) def quote(value)
return value if Arel::Nodes::SqlLiteral === value return value if Arel::Nodes::SqlLiteral === value
@connection.quote value @connection.quote value

@ -0,0 +1,38 @@
# frozen_string_literal: true
require_relative "../helper"
require "yaml"
module Arel
module Nodes
class FragmentsTest < Arel::Spec
describe "equality" do
it "is equal with equal values" do
array = [Fragments.new(["foo", "bar"]), Fragments.new(["foo", "bar"])]
assert_equal 1, array.uniq.size
end
it "is not equal with different values" do
array = [Fragments.new(["foo"]), Fragments.new(["bar"])]
assert_equal 2, array.uniq.size
end
it "can be joined with other nodes" do
fragments = Fragments.new(["foo", "bar"])
sql = Arel.sql("SELECT")
joined_fragments = fragments + sql
assert_equal ["foo", "bar"], fragments.values
assert_equal ["foo", "bar", sql], joined_fragments.values
end
it "fails if joined with something that is not an Arel node" do
fragments = Fragments.new
assert_raises ArgumentError do
fragments + "Not a node"
end
end
end
end
end
end

@ -70,6 +70,23 @@ def compile(node)
assert_equal("foo", YAML.load(yaml_literal)) assert_equal("foo", YAML.load(yaml_literal))
end end
end end
describe "addition" do
it "generates a Fragments node" do
sql1 = Arel.sql "SELECT *"
sql2 = Arel.sql "FROM users"
fragments = sql1 + sql2
_(fragments).must_be_kind_of Arel::Nodes::Fragments
assert_equal([sql1, sql2], fragments.values)
end
it "fails if joined with something that is not an Arel node" do
sql = Arel.sql "SELECT *"
assert_raises ArgumentError do
sql + "Not a node"
end
end
end
end end
end end
end end

@ -768,6 +768,20 @@ def dispatch
} }
end end
end end
describe "Nodes::Fragments" do
it "joins subexpressions" do
sql = Arel.sql("SELECT foo, bar") + Arel.sql(" FROM customers")
_(compile(sql)).must_be_like "SELECT foo, bar FROM customers"
end
it "can be built by adding SQL fragments one at a time" do
sql = Arel.sql("SELECT foo, bar")
sql += Arel.sql("FROM customers")
sql += Arel.sql("GROUP BY foo")
_(compile(sql)).must_be_like "SELECT foo, bar FROM customers GROUP BY foo"
end
end
end end
end end
end end