Skip to content

Commit

Permalink
Add ability to concatenate Arel.sql fragments
Browse files Browse the repository at this point in the history
  • Loading branch information
Ole Friis authored and olefriis committed Jan 19, 2023
1 parent 847cc9f commit 06ef7a9
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 0 deletions.
6 changes: 6 additions & 0 deletions activerecord/CHANGELOG.md
@@ -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

Previously it would return an ID that was not usable, since it was based on `id = nil`.
Expand Down
1 change: 1 addition & 0 deletions activerecord/lib/arel/nodes.rb
Expand Up @@ -8,6 +8,7 @@
require "arel/nodes/insert_statement"
require "arel/nodes/update_statement"
require "arel/nodes/bind_param"
require "arel/nodes/fragments"

# terminal

Expand Down
35 changes: 35 additions & 0 deletions activerecord/lib/arel/nodes/fragments.rb
@@ -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
6 changes: 6 additions & 0 deletions activerecord/lib/arel/nodes/sql_literal.rb
Expand Up @@ -14,6 +14,12 @@ def encode_with(coder)

def fetch_attribute
end

def +(other)
raise ArgumentError, "Expected Arel node" unless Arel.arel_node?(other)

Fragments.new([self, other])
end
end
end
end
4 changes: 4 additions & 0 deletions activerecord/lib/arel/visitors/to_sql.rb
Expand Up @@ -791,6 +791,10 @@ def visit_Array(o, collector)
end
alias :visit_Set :visit_Array

def visit_Arel_Nodes_Fragments(o, collector)
inject_join o.values, collector, " "
end

def quote(value)
return value if Arel::Nodes::SqlLiteral === value
@connection.quote value
Expand Down
38 changes: 38 additions & 0 deletions activerecord/test/cases/arel/nodes/fragments_test.rb
@@ -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
17 changes: 17 additions & 0 deletions activerecord/test/cases/arel/nodes/sql_literal_test.rb
Expand Up @@ -70,6 +70,23 @@ def compile(node)
assert_equal("foo", YAML.load(yaml_literal))
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
14 changes: 14 additions & 0 deletions activerecord/test/cases/arel/visitors/to_sql_test.rb
Expand Up @@ -768,6 +768,20 @@ def dispatch
}
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

0 comments on commit 06ef7a9

Please sign in to comment.