From 65a751e375b397dea736c7afabf0e446919bcc37 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 15:15:39 +0100 Subject: [PATCH 01/50] name is a Raw --- lib/rebel/sql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index e495073..e73f2cb 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -165,7 +165,7 @@ module Rebel::SQL return name if name.is_a?(Raw) return raw('*') if name == '*' - name.to_s.split('.').map { |e| "\"#{e}\"" }.join('.') + raw(name.to_s.split('.').map { |e| "\"#{e}\"" }.join('.')) end def names(*names) From 814aa465927844522ca0133799c18af6266970ad Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 15:17:16 +0100 Subject: [PATCH 02/50] operators --- lib/rebel/sql.rb | 52 +++++++++++++++++++++++++++++++++++++++++++++ test/test_raw.rb | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 test/test_raw.rb diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index e73f2cb..99fb0c4 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -66,6 +66,58 @@ module Rebel::SQL def on?(clause) clause ? on(clause) : self end + + def and(clause) + Raw.new("#{self} AND #{Rebel::SQL.and_clause(clause)}") + end + + def or(clause) + Raw.new("#{self} OR #{Rebel::SQL.and_clause(clause)}") + end + + def eq(n) + case n + when nil + Raw.new("#{self} IS NULL") + else + Raw.new("#{self} = #{Rebel::SQL.name_or_value(n)}") + end + end + alias == eq + + def ne(n) + case n + when nil + Raw.new("#{self} IS NOT NULL") + else + Raw.new("#{self} != #{Rebel::SQL.name_or_value(n)}") + end + end + alias != ne + + def lt(n) + Raw.new("#{self} < #{Rebel::SQL.name_or_value(n)}") + end + alias < lt + + def gt(n) + Raw.new("#{self} > #{Rebel::SQL.name_or_value(n)}") + end + alias > gt + + def le(n) + Raw.new("#{self} <= #{Rebel::SQL.name_or_value(n)}") + end + alias <= le + + def ge(n) + Raw.new("#{self} >= #{Rebel::SQL.name_or_value(n)}") + end + alias >= ge + + def in(*v) + Raw.new("#{self} IN (#{Rebel::SQL.values(*v)})") + end end class << self diff --git a/test/test_raw.rb b/test/test_raw.rb new file mode 100644 index 0000000..4a0e0f2 --- /dev/null +++ b/test/test_raw.rb @@ -0,0 +1,55 @@ +require 'minitest/autorun' +require 'helper' +require 'rebel' + +class TestRaw < Minitest::Test + def assert_str_equal(expected, actual) + assert_equal(expected.to_s, actual.to_s) + end + + def test_and + assert_str_equal(Rebel::SQL.name(:foo).eq(1).and(Rebel::SQL.name(:bar).eq(2)), '"foo" = 1 AND "bar" = 2') + end + + def test_or + assert_str_equal(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), '"foo" = 1 OR "bar" = 2') + end + + def test_eq + assert_str_equal(Rebel::SQL.name(:foo).eq(nil), '"foo" IS NULL') + assert_str_equal(Rebel::SQL.name(:foo) == nil, '"foo" IS NULL') + assert_str_equal(Rebel::SQL.name(:foo).eq(Rebel::SQL.name(:bar)), '"foo" = "bar"') + assert_str_equal(Rebel::SQL.name(:foo) == Rebel::SQL.name(:bar), '"foo" = "bar"') + end + + def test_ne + assert_str_equal(Rebel::SQL.name(:foo).ne(Rebel::SQL.name(:bar)), '"foo" != "bar"') + assert_str_equal(Rebel::SQL.name(:foo) != Rebel::SQL.name(:bar), '"foo" != "bar"') + assert_str_equal(Rebel::SQL.name(:foo).ne(nil), '"foo" IS NOT NULL') + assert_str_equal(Rebel::SQL.name(:foo) != nil, '"foo" IS NOT NULL') + end + + def test_lt + assert_str_equal(Rebel::SQL.name(:foo).lt(Rebel::SQL.name(:bar)), '"foo" < "bar"') + assert_str_equal(Rebel::SQL.name(:foo) < Rebel::SQL.name(:bar), '"foo" < "bar"') + end + + def test_gt + assert_str_equal(Rebel::SQL.name(:foo).gt(Rebel::SQL.name(:bar)), '"foo" > "bar"') + assert_str_equal(Rebel::SQL.name(:foo) > Rebel::SQL.name(:bar), '"foo" > "bar"') + end + + def test_le + assert_str_equal(Rebel::SQL.name(:foo).le(Rebel::SQL.name(:bar)), '"foo" <= "bar"') + assert_str_equal(Rebel::SQL.name(:foo) <= Rebel::SQL.name(:bar), '"foo" <= "bar"') + end + + def test_ge + assert_str_equal(Rebel::SQL.name(:foo).ge(Rebel::SQL.name(:bar)), '"foo" >= "bar"') + assert_str_equal(Rebel::SQL.name(:foo) >= Rebel::SQL.name(:bar), '"foo" >= "bar"') + end + + def test_in + assert_str_equal(Rebel::SQL.name(:foo).in(1, 2, 3), '"foo" IN (1, 2, 3)') + end +end From e9ee35f68ed70daef3573bba9c4855297e88c44b Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 15:55:38 +0100 Subject: [PATCH 03/50] parens handling for where --- lib/rebel/sql.rb | 33 ++++++++++++++++++++++----------- test/test_raw.rb | 5 +++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 99fb0c4..a5426fd 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -51,6 +51,20 @@ module Rebel::SQL end class Raw < String + def wants_parens! + @wants_parens = true + self + end + + def wants_parens? + @wants_parens = false unless instance_variable_defined?(:@wants_parens) + @wants_parens + end + + def parens + Raw.new("(#{self})") + end + def as(n) Raw.new(self + " AS #{Rebel::SQL.name(n)}") end @@ -72,7 +86,7 @@ module Rebel::SQL end def or(clause) - Raw.new("#{self} OR #{Rebel::SQL.and_clause(clause)}") + Raw.new("#{self} OR #{Rebel::SQL.and_clause(clause)}").wants_parens! end def eq(n) @@ -257,28 +271,25 @@ module Rebel::SQL def clause_term(left, right) case right when Array - "#{name(left)} IN (#{values(*right)})" + name(left).in(*right) else - "#{name(left)} = #{name_or_value(right)}" + name(left).eq(name_or_value(right)) end end - def and_clause(clause) - return clause if clause.is_a?(Raw) || clause.is_a?(String) - + def and_clause(*clause) clause.map do |e| case e when Array then clause_term(e[0], e[1]) - when Raw, String then e + when Raw then e.wants_parens? && clause.count > 1 ? "(#{e})" : e + when String then e else raise NotImplementedError, e.class end end.join(' AND ') end - def where?(clause) - return "WHERE #{clause}" if clause.is_a?(Raw) || clause.is_a?(String) - - clause && clause.any? ? "WHERE #{Rebel::SQL.and_clause(clause)}" : nil + def where?(*clause) + clause.any? ? "WHERE #{Rebel::SQL.and_clause(*clause)}" : nil end def inner?(join) diff --git a/test/test_raw.rb b/test/test_raw.rb index 4a0e0f2..fbc8552 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -52,4 +52,9 @@ class TestRaw < Minitest::Test def test_in assert_str_equal(Rebel::SQL.name(:foo).in(1, 2, 3), '"foo" IN (1, 2, 3)') end + + def test_where + assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), Rebel::SQL.name(:baz).eq(3)), 'WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') + assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), 'WHERE "foo" = 1 OR "bar" = 2') + end end From d538a9b58465b2cf3caf89df1467e628876a3246 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 16:09:26 +0100 Subject: [PATCH 04/50] stupid settings --- lib/rebel/sql.rb | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index a5426fd..4f07869 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -134,7 +134,23 @@ module Rebel::SQL end end + @identifier_quote = '"' + @string_quote = "'" + @escaped_string_quote = "''" + class << self + def identifier_quote=(str) + @identifier_quote = str + end + + def string_quote=(str) + @string_quote = str + end + + def escaped_string_quote=(str) + @escaped_string_quote = str + end + def raw(str) Raw.new(str) end @@ -231,7 +247,7 @@ module Rebel::SQL return name if name.is_a?(Raw) return raw('*') if name == '*' - raw(name.to_s.split('.').map { |e| "\"#{e}\"" }.join('.')) + raw(name.to_s.split('.').map { |e| "#{@identifier_quote}#{e}#{@identifier_quote}" }.join('.')) end def names(*names) @@ -245,7 +261,7 @@ module Rebel::SQL def value(v) case v when Raw then v - when String then raw "'#{v.tr("'", "''")}'" + when String then raw "'#{v.tr(@string_quote, @escaped_string_quote)}'" when Integer then raw v.to_s when nil then raw 'NULL' else raise NotImplementedError, v.inspect From 8a9b5804fc91fbf2e656e5ed036664d70f307c1e Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 16:48:09 +0100 Subject: [PATCH 05/50] fix hash clause handling --- lib/rebel/sql.rb | 1 + test/test_raw.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 4f07869..587996b 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -296,6 +296,7 @@ module Rebel::SQL def and_clause(*clause) clause.map do |e| case e + when Hash then and_clause(*e.to_a) when Array then clause_term(e[0], e[1]) when Raw then e.wants_parens? && clause.count > 1 ? "(#{e})" : e when String then e diff --git a/test/test_raw.rb b/test/test_raw.rb index fbc8552..a7d702b 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -54,6 +54,7 @@ class TestRaw < Minitest::Test end def test_where + assert_str_equal(Rebel::SQL.where?(foo: 1, bar: 2, baz: 3), 'WHERE "foo" = 1 AND "bar" = 2 AND "baz" = 3') assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), Rebel::SQL.name(:baz).eq(3)), 'WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), 'WHERE "foo" = 1 OR "bar" = 2') end From 421ae6d32e66a15946344db5970eb25438952aa3 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 16:51:07 +0100 Subject: [PATCH 06/50] extract dummy escape --- lib/rebel/sql.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 587996b..90833e0 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -258,10 +258,14 @@ module Rebel::SQL items.join(', ') end + def escape_str(str) + str.tr(@string_quote, @escaped_string_quote) + end + def value(v) case v when Raw then v - when String then raw "'#{v.tr(@string_quote, @escaped_string_quote)}'" + when String then raw "'#{escape_str(v)}'" when Integer then raw v.to_s when nil then raw 'NULL' else raise NotImplementedError, v.inspect From 2c727a38cbb66c04b792678f87ae2713c056555a Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 16:52:43 +0100 Subject: [PATCH 07/50] fix clause passing --- lib/rebel/sql.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 90833e0..e2bdeaf 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -73,20 +73,20 @@ module Rebel::SQL n ? as(n) : self end - def on(clause) - Raw.new(self + " ON #{Rebel::SQL.and_clause(clause)}") + def on(*clause) + Raw.new(self + " ON #{Rebel::SQL.and_clause(*clause)}") end - def on?(clause) + def on?(*clause) clause ? on(clause) : self end - def and(clause) - Raw.new("#{self} AND #{Rebel::SQL.and_clause(clause)}") + def and(*clause) + Raw.new("#{self} AND #{Rebel::SQL.and_clause(*clause)}") end - def or(clause) - Raw.new("#{self} OR #{Rebel::SQL.and_clause(clause)}").wants_parens! + def or(*clause) + Raw.new("#{self} OR #{Rebel::SQL.and_clause(*clause)}").wants_parens! end def eq(n) From d3ba98ab1b561349f20f474369de70eda2952b9d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 17:02:19 +0100 Subject: [PATCH 08/50] fix join without clause --- lib/rebel/sql.rb | 2 +- test/test_raw.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index e2bdeaf..b4cb7d6 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -78,7 +78,7 @@ module Rebel::SQL end def on?(*clause) - clause ? on(clause) : self + clause.any? ? on(clause) : self end def and(*clause) diff --git a/test/test_raw.rb b/test/test_raw.rb index a7d702b..9d61aca 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -58,4 +58,8 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), Rebel::SQL.name(:baz).eq(3)), 'WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), 'WHERE "foo" = 1 OR "bar" = 2') end + + def test_join + assert_str_equal(Rebel::SQL.join(:foo), 'JOIN "foo"') + end end From 3d18261e05ac1ce7dc6e3a3316450cf0459d73c5 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 1 Mar 2017 17:07:19 +0100 Subject: [PATCH 09/50] boolean value type --- lib/rebel/sql.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index b4cb7d6..0605cdf 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -267,6 +267,7 @@ module Rebel::SQL when Raw then v when String then raw "'#{escape_str(v)}'" when Integer then raw v.to_s + when TrueClass, FalseClass then raw(v ? 'TRUE' : 'FALSE') when nil then raw 'NULL' else raise NotImplementedError, v.inspect end From 892b21eaf8fda55814eb416cc3c36595f0668dcd Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Fri, 3 Mar 2017 14:52:54 +0100 Subject: [PATCH 10/50] Raw#is --- lib/rebel/sql.rb | 1 + test/test_raw.rb | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 0605cdf..252dd5a 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -98,6 +98,7 @@ module Rebel::SQL end end alias == eq + alias is eq def ne(n) case n diff --git a/test/test_raw.rb b/test/test_raw.rb index 9d61aca..9d5b707 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -15,6 +15,12 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), '"foo" = 1 OR "bar" = 2') end + def test_is + assert_str_equal(Rebel::SQL.name(:foo).is(nil), '"foo" IS NULL') + assert_str_equal(Rebel::SQL.name(:foo).is(42), '"foo" = 42') + assert_str_equal(Rebel::SQL.name(:foo).is(Rebel::SQL.name(:bar)), '"foo" = "bar"') + end + def test_eq assert_str_equal(Rebel::SQL.name(:foo).eq(nil), '"foo" IS NULL') assert_str_equal(Rebel::SQL.name(:foo) == nil, '"foo" IS NULL') From 448a342d5ff4de1844c8cde4730b61af36b9dc7c Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Fri, 3 Mar 2017 14:53:28 +0100 Subject: [PATCH 11/50] arbitrary functions --- lib/rebel/sql.rb | 9 +++++++++ test/test_raw.rb | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 252dd5a..d36b537 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -218,6 +218,11 @@ module Rebel::SQL ## Functions + def function(name, *args) + raw("#{name}(#{names_or_values(*args)})") + end + alias fn function + def count(*n) raw("COUNT(#{names(*n)})") end @@ -282,6 +287,10 @@ module Rebel::SQL item.is_a?(Symbol) ? name(item) : value(item) end + def names_or_values(*items) + list(items.map { |v| name_or_value(v) }) + end + def equal(l, r) "#{name_or_value(l)} = #{name_or_value(r)}" end diff --git a/test/test_raw.rb b/test/test_raw.rb index 9d5b707..cf84d20 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -68,4 +68,8 @@ class TestRaw < Minitest::Test def test_join assert_str_equal(Rebel::SQL.join(:foo), 'JOIN "foo"') end + + def test_function + assert_str_equal(Rebel::SQL.function('COALESCE', :foo, 0), 'COALESCE("foo", 0)') + end end From ee34b9c600e4bdfe7b5dd4731239a6e92a55c8ef Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Fri, 3 Mar 2017 15:06:16 +0100 Subject: [PATCH 12/50] remove useless absolute qualifiers --- lib/rebel/sql.rb | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index d36b537..7e1871a 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -158,32 +158,32 @@ module Rebel::SQL def create_table(table_name, desc) <<-SQL - CREATE TABLE #{Rebel::SQL.name(table_name)} ( - #{Rebel::SQL.list(desc.map { |k, v| "#{Rebel::SQL.name(k)} #{v}" })} + CREATE TABLE #{name(table_name)} ( + #{list(desc.map { |k, v| "#{name(k)} #{v}" })} ); SQL end def drop_table(table_name) <<-SQL - DROP TABLE #{Rebel::SQL.name(table_name)}; + DROP TABLE #{name(table_name)}; SQL end def select(*fields, from: nil, where: nil, inner: nil, left: nil, right: nil) <<-SQL SELECT #{names(*fields)} FROM #{name(from)} - #{Rebel::SQL.inner?(inner)} - #{Rebel::SQL.left?(left)} - #{Rebel::SQL.right?(right)} - #{Rebel::SQL.where?(where)}; + #{inner?(inner)} + #{left?(left)} + #{right?(right)} + #{where?(where)}; SQL end def insert_into(table_name, *rows) <<-SQL - INSERT INTO #{Rebel::SQL.name(table_name)} (#{Rebel::SQL.names(*rows.first.keys)}) - VALUES #{Rebel::SQL.list(rows.map { |r| "(#{Rebel::SQL.values(*r.values)})" })}; + INSERT INTO #{name(table_name)} (#{names(*rows.first.keys)}) + VALUES #{list(rows.map { |r| "(#{values(*r.values)})" })}; SQL end @@ -191,28 +191,28 @@ module Rebel::SQL raise ArgumentError if set.nil? <<-SQL - UPDATE #{Rebel::SQL.name(table_name)} - SET #{Rebel::SQL.assign_clause(set)} - #{Rebel::SQL.inner?(inner)} - #{Rebel::SQL.left?(left)} - #{Rebel::SQL.right?(right)} - #{Rebel::SQL.where?(where)}; + UPDATE #{name(table_name)} + SET #{assign_clause(set)} + #{inner?(inner)} + #{left?(left)} + #{right?(right)} + #{where?(where)}; SQL end def delete_from(table_name, where: nil, inner: nil, left: nil, right: nil) <<-SQL - DELETE FROM #{Rebel::SQL.name(table_name)} - #{Rebel::SQL.inner?(inner)} - #{Rebel::SQL.left?(left)} - #{Rebel::SQL.right?(right)} - #{Rebel::SQL.where?(where)}; + DELETE FROM #{name(table_name)} + #{inner?(inner)} + #{left?(left)} + #{right?(right)} + #{where?(where)}; SQL end def truncate(table_name) <<-SQL - TRUNCATE #{Rebel::SQL.name(table_name)}; + TRUNCATE #{name(table_name)}; SQL end @@ -321,7 +321,7 @@ module Rebel::SQL end def where?(*clause) - clause.any? ? "WHERE #{Rebel::SQL.and_clause(*clause)}" : nil + clause.any? ? "WHERE #{and_clause(*clause)}" : nil end def inner?(join) From 075957be04aa4ec37abde19f2b6048c97a468879 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Fri, 3 Mar 2017 16:13:06 +0100 Subject: [PATCH 13/50] wrap AND first member w/ parens --- lib/rebel/sql.rb | 8 ++++++-- test/test_raw.rb | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 7e1871a..2a09898 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -65,6 +65,10 @@ module Rebel::SQL Raw.new("(#{self})") end + def parens? + wants_parens? ? parens : self + end + def as(n) Raw.new(self + " AS #{Rebel::SQL.name(n)}") end @@ -82,7 +86,7 @@ module Rebel::SQL end def and(*clause) - Raw.new("#{self} AND #{Rebel::SQL.and_clause(*clause)}") + Raw.new("#{self.parens?} AND #{Rebel::SQL.and_clause(*clause)}") end def or(*clause) @@ -313,7 +317,7 @@ module Rebel::SQL case e when Hash then and_clause(*e.to_a) when Array then clause_term(e[0], e[1]) - when Raw then e.wants_parens? && clause.count > 1 ? "(#{e})" : e + when Raw then e.parens? when String then e else raise NotImplementedError, e.class end diff --git a/test/test_raw.rb b/test/test_raw.rb index cf84d20..3d2b930 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -15,6 +15,14 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), '"foo" = 1 OR "bar" = 2') end + def test_and_or + assert_str_equal(Rebel::SQL.name(:foo).eq(0).and(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), '"foo" = 0 AND ("foo" = 1 OR "bar" = 2)') + end + + def test_or_and_or + assert_str_equal(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)).and(Rebel::SQL.name(:foo).eq(3).or(Rebel::SQL.name(:bar).eq(4))), '("foo" = 1 OR "bar" = 2) AND ("foo" = 3 OR "bar" = 4)') + end + def test_is assert_str_equal(Rebel::SQL.name(:foo).is(nil), '"foo" IS NULL') assert_str_equal(Rebel::SQL.name(:foo).is(42), '"foo" = 42') @@ -62,7 +70,7 @@ class TestRaw < Minitest::Test def test_where assert_str_equal(Rebel::SQL.where?(foo: 1, bar: 2, baz: 3), 'WHERE "foo" = 1 AND "bar" = 2 AND "baz" = 3') assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), Rebel::SQL.name(:baz).eq(3)), 'WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') - assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), 'WHERE "foo" = 1 OR "bar" = 2') + assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), 'WHERE ("foo" = 1 OR "bar" = 2)') end def test_join From 9b6e871b1b74e118e6cc4e30a9b09891cbac6e0d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Fri, 3 Mar 2017 16:13:46 +0100 Subject: [PATCH 14/50] date, time, datetime values support --- lib/rebel/sql.rb | 1 + test/test_raw.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 2a09898..eb9854a 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -278,6 +278,7 @@ module Rebel::SQL when String then raw "'#{escape_str(v)}'" when Integer then raw v.to_s when TrueClass, FalseClass then raw(v ? 'TRUE' : 'FALSE') + when Date, Time, DateTime then value(v.iso8601) when nil then raw 'NULL' else raise NotImplementedError, v.inspect end diff --git a/test/test_raw.rb b/test/test_raw.rb index 3d2b930..8956874 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -80,4 +80,16 @@ class TestRaw < Minitest::Test def test_function assert_str_equal(Rebel::SQL.function('COALESCE', :foo, 0), 'COALESCE("foo", 0)') end + + def test_value + assert_str_equal(Rebel::SQL.value(Rebel::SQL.raw("'FOO'")), "'FOO'") + assert_str_equal(Rebel::SQL.value('FOO'), "'FOO'") + assert_str_equal(Rebel::SQL.value(1), '1') + assert_str_equal(Rebel::SQL.value(true), 'TRUE') + assert_str_equal(Rebel::SQL.value(false), 'FALSE') + assert_str_equal(Rebel::SQL.value(Date.new(2016, 12, 31)), "'2016-12-31'") + assert_str_equal(Rebel::SQL.value(Time.utc(2016, 12, 31, 23, 59, 59)), "'2016-12-31T23:59:59Z'") + assert_str_equal(Rebel::SQL.value(DateTime.new(2016, 12, 31, 23, 59, 59)), "'2016-12-31T23:59:59+00:00'") + assert_str_equal(Rebel::SQL.value(nil), 'NULL') + end end From 3c2d41788cfe464d937216c8afcedacb63d5cb3a Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 6 Mar 2017 15:37:05 +0100 Subject: [PATCH 15/50] release 0.3.0 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index aa5b00e..bb0a08e 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.2.0' + s.version = '0.3.0' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From 06ff4227af473c320c89c78f3e1bb031b420fc02 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 20 Mar 2017 15:56:36 +0100 Subject: [PATCH 16/50] add LIKE --- lib/rebel/sql.rb | 4 ++++ test/test_raw.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index eb9854a..d3d980e 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -137,6 +137,10 @@ module Rebel::SQL def in(*v) Raw.new("#{self} IN (#{Rebel::SQL.values(*v)})") end + + def like(n) + Raw.new("#{self} LIKE #{Rebel::SQL.value(n)}") + end end @identifier_quote = '"' diff --git a/test/test_raw.rb b/test/test_raw.rb index 8956874..d28f7a9 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -67,6 +67,10 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.name(:foo).in(1, 2, 3), '"foo" IN (1, 2, 3)') end + def test_like + assert_str_equal(Rebel::SQL.name(:foo).like('%bar%'), %("foo" LIKE '%bar%')) + end + def test_where assert_str_equal(Rebel::SQL.where?(foo: 1, bar: 2, baz: 3), 'WHERE "foo" = 1 AND "bar" = 2 AND "baz" = 3') assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), Rebel::SQL.name(:baz).eq(3)), 'WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') From 179cba86b6c0cba7443dcb590d583767c7e5b63f Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 20 Mar 2017 15:57:10 +0100 Subject: [PATCH 17/50] release 0.3.1 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index bb0a08e..02f3e5c 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.3.0' + s.version = '0.3.1' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From e37d9034b4ed207ace4cc2960ccdedaf83a7224b Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 17 Jul 2017 16:41:47 +0200 Subject: [PATCH 18/50] don't track Gemfile.lock --- Gemfile.lock | 45 --------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 94ab8ae..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,45 +0,0 @@ -PATH - remote: . - specs: - rebel (0.2.0) - -GEM - remote: https://rubygems.org/ - specs: - ast (2.3.0) - coderay (1.1.1) - method_source (0.8.2) - minitest (5.10.1) - parser (2.4.0.0) - ast (~> 2.2) - powerpack (0.1.1) - pry (0.10.4) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - rainbow (2.2.1) - rake (12.0.0) - rubocop (0.47.1) - parser (>= 2.3.3.1, < 3.0) - powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) - ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.8.1) - slop (3.6.0) - sqlite3 (1.3.13) - unicode-display_width (1.1.3) - -PLATFORMS - ruby - -DEPENDENCIES - minitest - pry - rake - rebel! - rubocop - sqlite3 - -BUNDLED WITH - 1.14.5 From 18569b2b2fdc23fc6cbf3ba5822dbb86e09a44f0 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 17 Jul 2017 16:42:55 +0200 Subject: [PATCH 19/50] queries are raw --- lib/rebel/sql.rb | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index d3d980e..2581de7 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -165,62 +165,62 @@ module Rebel::SQL end def create_table(table_name, desc) - <<-SQL + raw <<-SQL CREATE TABLE #{name(table_name)} ( #{list(desc.map { |k, v| "#{name(k)} #{v}" })} - ); + ) SQL end def drop_table(table_name) - <<-SQL - DROP TABLE #{name(table_name)}; + raw <<-SQL + DROP TABLE #{name(table_name)} SQL end def select(*fields, from: nil, where: nil, inner: nil, left: nil, right: nil) - <<-SQL + raw <<-SQL SELECT #{names(*fields)} FROM #{name(from)} #{inner?(inner)} #{left?(left)} #{right?(right)} - #{where?(where)}; + #{where?(where)} SQL end def insert_into(table_name, *rows) - <<-SQL + raw <<-SQL INSERT INTO #{name(table_name)} (#{names(*rows.first.keys)}) - VALUES #{list(rows.map { |r| "(#{values(*r.values)})" })}; + VALUES #{list(rows.map { |r| "(#{values(*r.values)})" })} SQL end def update(table_name, set: nil, where: nil, inner: nil, left: nil, right: nil) raise ArgumentError if set.nil? - <<-SQL + raw <<-SQL UPDATE #{name(table_name)} SET #{assign_clause(set)} #{inner?(inner)} #{left?(left)} #{right?(right)} - #{where?(where)}; + #{where?(where)} SQL end def delete_from(table_name, where: nil, inner: nil, left: nil, right: nil) - <<-SQL + raw <<-SQL DELETE FROM #{name(table_name)} #{inner?(inner)} #{left?(left)} #{right?(right)} - #{where?(where)}; + #{where?(where)} SQL end def truncate(table_name) - <<-SQL - TRUNCATE #{name(table_name)}; + raw <<-SQL + TRUNCATE #{name(table_name)} SQL end From a010f314033a23614eed16f8378096321257f4f1 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 17 Jul 2017 16:44:53 +0200 Subject: [PATCH 20/50] add a few tests --- test/test_raw.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_raw.rb b/test/test_raw.rb index d28f7a9..26ed190 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -85,6 +85,10 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.function('COALESCE', :foo, 0), 'COALESCE("foo", 0)') end + def test_where_function + assert_str_equal(Rebel::SQL.where?(Rebel::SQL.function('COALESCE', :foo, 0).eq 42), 'WHERE COALESCE("foo", 0) = 42') + end + def test_value assert_str_equal(Rebel::SQL.value(Rebel::SQL.raw("'FOO'")), "'FOO'") assert_str_equal(Rebel::SQL.value('FOO'), "'FOO'") @@ -96,4 +100,12 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.value(DateTime.new(2016, 12, 31, 23, 59, 59)), "'2016-12-31T23:59:59+00:00'") assert_str_equal(Rebel::SQL.value(nil), 'NULL') end + + def test_select + assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo)).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo"') + end + + def test_nested_select + assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo), where: Rebel::SQL.name(:bar).in(Rebel::SQL.select(Rebel::SQL.name(:bar), from: Rebel::SQL.name(:foo)))).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo" WHERE "bar" IN ( SELECT "bar" FROM "foo" )') + end end From 5811131d237befb0596a4b47602f7e9b987531b0 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 17 Jul 2017 16:45:15 +0200 Subject: [PATCH 21/50] release 0.3.2 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index 02f3e5c..e50bfc3 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.3.1' + s.version = '0.3.2' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From 56ad34b79e69be018375329bb2410cd64d851a9d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 27 Sep 2017 10:31:11 +0200 Subject: [PATCH 22/50] Pass argument down on? correctly Fixes an extra `= NULL` appearing at times. --- lib/rebel/sql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 2581de7..3eb3b60 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -82,7 +82,7 @@ module Rebel::SQL end def on?(*clause) - clause.any? ? on(clause) : self + clause.any? ? on(*clause) : self end def and(*clause) From 978dca51bfe894595710ee3cd64058a4c9a65482 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 27 Sep 2017 10:32:04 +0200 Subject: [PATCH 23/50] Release 0.3.3 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index e50bfc3..fb81a99 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.3.2' + s.version = '0.3.3' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From 818da9047a2f1b666c70b1e20a4a5c9b89743b07 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 11:12:54 +0100 Subject: [PATCH 24/50] Improve feedback for unsupported value type --- lib/rebel/sql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 3eb3b60..cda9ce9 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -284,7 +284,7 @@ module Rebel::SQL when TrueClass, FalseClass then raw(v ? 'TRUE' : 'FALSE') when Date, Time, DateTime then value(v.iso8601) when nil then raw 'NULL' - else raise NotImplementedError, v.inspect + else raise NotImplementedError, "#{v.class}: #{v.inspect}" end end From a3712ceefd603ac12c1e4a6bb79e3f4cb37276e4 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 11:14:45 +0100 Subject: [PATCH 25/50] Add support for SELECT without FROM --- lib/rebel/sql.rb | 7 ++++++- test/test_raw.rb | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index cda9ce9..9379fdc 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -180,7 +180,8 @@ module Rebel::SQL def select(*fields, from: nil, where: nil, inner: nil, left: nil, right: nil) raw <<-SQL - SELECT #{names(*fields)} FROM #{name(from)} + SELECT #{names(*fields)} + #{from?(from)} #{inner?(inner)} #{left?(left)} #{right?(right)} @@ -329,6 +330,10 @@ module Rebel::SQL end.join(' AND ') end + def from?(from) + from ? "FROM #{name(from)}" : nil + end + def where?(*clause) clause.any? ? "WHERE #{and_clause(*clause)}" : nil end diff --git a/test/test_raw.rb b/test/test_raw.rb index 26ed190..b19aee1 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -105,6 +105,10 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo)).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo"') end + def test_select_without_from + assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('1')).strip, 'SELECT 1') + end + def test_nested_select assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo), where: Rebel::SQL.name(:bar).in(Rebel::SQL.select(Rebel::SQL.name(:bar), from: Rebel::SQL.name(:foo)))).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo" WHERE "bar" IN ( SELECT "bar" FROM "foo" )') end From adfa38baea125012784877045a292d18488638ac Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 11:17:29 +0100 Subject: [PATCH 26/50] Add support for DISTINCT --- lib/rebel/sql.rb | 7 ++++--- test/test_raw.rb | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 9379fdc..2e2f51d 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -13,8 +13,9 @@ module Rebel::SQL exec(Rebel::SQL.drop_table(table_name)) end - def select(*fields, from: nil, where: nil, inner: nil, left: nil, right: nil) + def select(*fields, distinct: distinct, from: nil, where: nil, inner: nil, left: nil, right: nil) exec(Rebel::SQL.select(*fields, + distinct: distinct, from: from, where: where, inner: inner, @@ -178,9 +179,9 @@ module Rebel::SQL SQL end - def select(*fields, from: nil, where: nil, inner: nil, left: nil, right: nil) + def select(*fields, distinct: nil, from: nil, where: nil, inner: nil, left: nil, right: nil) raw <<-SQL - SELECT #{names(*fields)} + SELECT #{distinct ? "DISTINCT #{names(*distinct)}" : names(*fields)} #{from?(from)} #{inner?(inner)} #{left?(left)} diff --git a/test/test_raw.rb b/test/test_raw.rb index b19aee1..e938772 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -109,6 +109,14 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('1')).strip, 'SELECT 1') end + def test_select_distinct + assert_str_equal(Rebel::SQL.select(distinct: :bar, from: :foo).gsub(/\s+/, ' ').strip, 'SELECT DISTINCT "bar" FROM "foo"') + end + + def test_select_distinct_multiple + assert_str_equal(Rebel::SQL.select(distinct: [:bar, :baz], from: :foo).gsub(/\s+/, ' ').strip, 'SELECT DISTINCT "bar", "baz" FROM "foo"') + end + def test_nested_select assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo), where: Rebel::SQL.name(:bar).in(Rebel::SQL.select(Rebel::SQL.name(:bar), from: Rebel::SQL.name(:foo)))).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo" WHERE "bar" IN ( SELECT "bar" FROM "foo" )') end From 6c4b435ae656f6649e976fcbc566e92a8507e853 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 11:19:48 +0100 Subject: [PATCH 27/50] Add support for LIMIT and OFFSET --- lib/rebel/sql.rb | 13 ++++++++++--- test/test_raw.rb | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 2e2f51d..0f59724 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -13,14 +13,16 @@ module Rebel::SQL exec(Rebel::SQL.drop_table(table_name)) end - def select(*fields, distinct: distinct, from: nil, where: nil, inner: nil, left: nil, right: nil) + def select(*fields, distinct: distinct, from: nil, where: nil, inner: nil, left: nil, right: nil, limit: nil, offset: nil) exec(Rebel::SQL.select(*fields, distinct: distinct, from: from, where: where, inner: inner, left: left, - right: right)) + right: right, + limit: limit, + offset: offset)) end def insert_into(table_name, *rows) @@ -179,7 +181,7 @@ module Rebel::SQL SQL end - def select(*fields, distinct: nil, from: nil, where: nil, inner: nil, left: nil, right: nil) + def select(*fields, distinct: nil, from: nil, where: nil, inner: nil, left: nil, right: nil, limit: nil, offset: nil) raw <<-SQL SELECT #{distinct ? "DISTINCT #{names(*distinct)}" : names(*fields)} #{from?(from)} @@ -187,6 +189,7 @@ module Rebel::SQL #{left?(left)} #{right?(right)} #{where?(where)} + #{limit?(limit, offset)} SQL end @@ -350,5 +353,9 @@ module Rebel::SQL def right?(join) join ? "RIGHT #{join}" : nil end + + def limit?(limit, offset) + limit ? "LIMIT #{value(limit)}" << (offset ? " OFFSET #{offset}" : "") : nil + end end end diff --git a/test/test_raw.rb b/test/test_raw.rb index e938772..2b9f3c9 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -117,6 +117,14 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.select(distinct: [:bar, :baz], from: :foo).gsub(/\s+/, ' ').strip, 'SELECT DISTINCT "bar", "baz" FROM "foo"') end + def test_select_limit + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, limit: 10).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" LIMIT 10') + end + + def test_select_offset + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, limit: 10, offset: 20).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" LIMIT 10 OFFSET 20') + end + def test_nested_select assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo), where: Rebel::SQL.name(:bar).in(Rebel::SQL.select(Rebel::SQL.name(:bar), from: Rebel::SQL.name(:foo)))).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo" WHERE "bar" IN ( SELECT "bar" FROM "foo" )') end From 56ad166c1e4470b4fdb62c86f67dcfe2ca44e795 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 11:20:45 +0100 Subject: [PATCH 28/50] Add support for GROUP, ORDER, BY and HAVING --- lib/rebel/sql.rb | 32 ++++++++++++++++++++++++++++++-- test/test_raw.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 0f59724..3deaccd 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -13,7 +13,7 @@ module Rebel::SQL exec(Rebel::SQL.drop_table(table_name)) end - def select(*fields, distinct: distinct, from: nil, where: nil, inner: nil, left: nil, right: nil, limit: nil, offset: nil) + def select(*fields, distinct: distinct, from: nil, where: nil, inner: nil, left: nil, right: nil, group: nil, order: nil, limit: nil, offset: nil) exec(Rebel::SQL.select(*fields, distinct: distinct, from: from, @@ -21,6 +21,8 @@ module Rebel::SQL inner: inner, left: left, right: right, + group: group, + order: order, limit: limit, offset: offset)) end @@ -88,6 +90,18 @@ module Rebel::SQL clause.any? ? on(*clause) : self end + def having(*clause) + Raw.new(self + " HAVING #{Rebel::SQL.and_clause(*clause)}") + end + + def asc + Raw.new(self + " ASC") + end + + def desc + Raw.new(self + " DESC") + end + def and(*clause) Raw.new("#{self.parens?} AND #{Rebel::SQL.and_clause(*clause)}") end @@ -181,7 +195,7 @@ module Rebel::SQL SQL end - def select(*fields, distinct: nil, from: nil, where: nil, inner: nil, left: nil, right: nil, limit: nil, offset: nil) + def select(*fields, distinct: nil, from: nil, where: nil, inner: nil, left: nil, right: nil, group: nil, order: nil, limit: nil, offset: nil) raw <<-SQL SELECT #{distinct ? "DISTINCT #{names(*distinct)}" : names(*fields)} #{from?(from)} @@ -189,6 +203,8 @@ module Rebel::SQL #{left?(left)} #{right?(right)} #{where?(where)} + #{group?(group)} + #{order?(order)} #{limit?(limit, offset)} SQL end @@ -236,6 +252,10 @@ module Rebel::SQL end alias fn function + def by(*n) + raw("BY #{names(*n)}") + end + def count(*n) raw("COUNT(#{names(*n)})") end @@ -354,6 +374,14 @@ module Rebel::SQL join ? "RIGHT #{join}" : nil end + def group?(group) + group ? "GROUP #{name(group)}" : nil + end + + def order?(order) + order ? "ORDER #{name(order)}" : nil + end + def limit?(limit, offset) limit ? "LIMIT #{value(limit)}" << (offset ? " OFFSET #{offset}" : "") : nil end diff --git a/test/test_raw.rb b/test/test_raw.rb index 2b9f3c9..b159468 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -117,6 +117,34 @@ class TestRaw < Minitest::Test assert_str_equal(Rebel::SQL.select(distinct: [:bar, :baz], from: :foo).gsub(/\s+/, ' ').strip, 'SELECT DISTINCT "bar", "baz" FROM "foo"') end + def test_select_group_by + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, group: Rebel::SQL.by(:baz)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" GROUP BY "baz"') + end + + def test_select_group_by_having + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, group: Rebel::SQL.by(:baz).having(Rebel::SQL.count(:qux).gt(5))).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" GROUP BY "baz" HAVING COUNT("qux") > 5') + end + + def test_select_order_by + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz"') + end + + def test_select_order_by_asc + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz).asc).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz" ASC') + end + + def test_select_order_by_desc + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz).desc).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz" DESC') + end + + def test_select_multiple_order_by + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz, :qux)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz", "qux"') + end + + def test_select_multiple_order_by_opposing + assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(Rebel::SQL.name(:baz).asc, Rebel::SQL.name(:qux).desc)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz" ASC, "qux" DESC') + end + def test_select_limit assert_str_equal(Rebel::SQL.select(:bar, from: :foo, limit: 10).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" LIMIT 10') end From 96d2f9991a96cdd4ae85765bf2c7f71fb82b627a Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 11:22:38 +0100 Subject: [PATCH 29/50] Release 0.4.0 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index fb81a99..6ce6db6 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.3.3' + s.version = '0.4.0' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From 9c4c031db925ecb43409090503e070d3a250ace7 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 13:56:37 +0100 Subject: [PATCH 30/50] Rewrite tests for readability --- test/test_raw.rb | 112 +++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/test/test_raw.rb b/test/test_raw.rb index b159468..438adf2 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -3,157 +3,157 @@ require 'helper' require 'rebel' class TestRaw < Minitest::Test - def assert_str_equal(expected, actual) - assert_equal(expected.to_s, actual.to_s) + def assert_sql(expected, &actual) + assert_equal(expected.to_s, Rebel::SQL.instance_eval(&actual).to_s) end def test_and - assert_str_equal(Rebel::SQL.name(:foo).eq(1).and(Rebel::SQL.name(:bar).eq(2)), '"foo" = 1 AND "bar" = 2') + assert_sql('"foo" = 1 AND "bar" = 2') { name(:foo).eq(1).and(name(:bar).eq(2)) } end def test_or - assert_str_equal(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), '"foo" = 1 OR "bar" = 2') + assert_sql('"foo" = 1 OR "bar" = 2') { name(:foo).eq(1).or(name(:bar).eq(2)) } end def test_and_or - assert_str_equal(Rebel::SQL.name(:foo).eq(0).and(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), '"foo" = 0 AND ("foo" = 1 OR "bar" = 2)') + assert_sql('"foo" = 0 AND ("foo" = 1 OR "bar" = 2)') { name(:foo).eq(0).and(name(:foo).eq(1).or(name(:bar).eq(2))) } end def test_or_and_or - assert_str_equal(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)).and(Rebel::SQL.name(:foo).eq(3).or(Rebel::SQL.name(:bar).eq(4))), '("foo" = 1 OR "bar" = 2) AND ("foo" = 3 OR "bar" = 4)') + assert_sql('("foo" = 1 OR "bar" = 2) AND ("foo" = 3 OR "bar" = 4)') { name(:foo).eq(1).or(name(:bar).eq(2)).and(name(:foo).eq(3).or(name(:bar).eq(4))) } end def test_is - assert_str_equal(Rebel::SQL.name(:foo).is(nil), '"foo" IS NULL') - assert_str_equal(Rebel::SQL.name(:foo).is(42), '"foo" = 42') - assert_str_equal(Rebel::SQL.name(:foo).is(Rebel::SQL.name(:bar)), '"foo" = "bar"') + assert_sql('"foo" IS NULL') { name(:foo).is(nil) } + assert_sql('"foo" = 42') { name(:foo).is(42) } + assert_sql('"foo" = "bar"') { name(:foo).is(name(:bar)) } end def test_eq - assert_str_equal(Rebel::SQL.name(:foo).eq(nil), '"foo" IS NULL') - assert_str_equal(Rebel::SQL.name(:foo) == nil, '"foo" IS NULL') - assert_str_equal(Rebel::SQL.name(:foo).eq(Rebel::SQL.name(:bar)), '"foo" = "bar"') - assert_str_equal(Rebel::SQL.name(:foo) == Rebel::SQL.name(:bar), '"foo" = "bar"') + assert_sql('"foo" IS NULL') { name(:foo).eq(nil) } + assert_sql('"foo" IS NULL') { name(:foo) == nil } + assert_sql('"foo" = "bar"') { name(:foo).eq(name(:bar)) } + assert_sql('"foo" = "bar"') { name(:foo) == name(:bar) } end def test_ne - assert_str_equal(Rebel::SQL.name(:foo).ne(Rebel::SQL.name(:bar)), '"foo" != "bar"') - assert_str_equal(Rebel::SQL.name(:foo) != Rebel::SQL.name(:bar), '"foo" != "bar"') - assert_str_equal(Rebel::SQL.name(:foo).ne(nil), '"foo" IS NOT NULL') - assert_str_equal(Rebel::SQL.name(:foo) != nil, '"foo" IS NOT NULL') + assert_sql('"foo" != "bar"') { name(:foo).ne(name(:bar)) } + assert_sql('"foo" != "bar"') { name(:foo) != name(:bar) } + assert_sql('"foo" IS NOT NULL') { name(:foo).ne(nil) } + assert_sql('"foo" IS NOT NULL') { name(:foo) != nil } end def test_lt - assert_str_equal(Rebel::SQL.name(:foo).lt(Rebel::SQL.name(:bar)), '"foo" < "bar"') - assert_str_equal(Rebel::SQL.name(:foo) < Rebel::SQL.name(:bar), '"foo" < "bar"') + assert_sql('"foo" < "bar"') { name(:foo).lt(name(:bar)) } + assert_sql('"foo" < "bar"') { name(:foo) < name(:bar) } end def test_gt - assert_str_equal(Rebel::SQL.name(:foo).gt(Rebel::SQL.name(:bar)), '"foo" > "bar"') - assert_str_equal(Rebel::SQL.name(:foo) > Rebel::SQL.name(:bar), '"foo" > "bar"') + assert_sql('"foo" > "bar"') { name(:foo).gt(name(:bar)) } + assert_sql('"foo" > "bar"') { name(:foo) > name(:bar) } end def test_le - assert_str_equal(Rebel::SQL.name(:foo).le(Rebel::SQL.name(:bar)), '"foo" <= "bar"') - assert_str_equal(Rebel::SQL.name(:foo) <= Rebel::SQL.name(:bar), '"foo" <= "bar"') + assert_sql('"foo" <= "bar"') { name(:foo).le(name(:bar)) } + assert_sql('"foo" <= "bar"') { name(:foo) <= name(:bar) } end def test_ge - assert_str_equal(Rebel::SQL.name(:foo).ge(Rebel::SQL.name(:bar)), '"foo" >= "bar"') - assert_str_equal(Rebel::SQL.name(:foo) >= Rebel::SQL.name(:bar), '"foo" >= "bar"') + assert_sql('"foo" >= "bar"') { name(:foo).ge(name(:bar)) } + assert_sql('"foo" >= "bar"') { name(:foo) >= name(:bar) } end def test_in - assert_str_equal(Rebel::SQL.name(:foo).in(1, 2, 3), '"foo" IN (1, 2, 3)') + assert_sql('"foo" IN (1, 2, 3)') { name(:foo).in(1, 2, 3) } end def test_like - assert_str_equal(Rebel::SQL.name(:foo).like('%bar%'), %("foo" LIKE '%bar%')) + assert_sql(%("foo" LIKE '%bar%')) { name(:foo).like('%bar%') } end def test_where - assert_str_equal(Rebel::SQL.where?(foo: 1, bar: 2, baz: 3), 'WHERE "foo" = 1 AND "bar" = 2 AND "baz" = 3') - assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2)), Rebel::SQL.name(:baz).eq(3)), 'WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') - assert_str_equal(Rebel::SQL.where?(Rebel::SQL.name(:foo).eq(1).or(Rebel::SQL.name(:bar).eq(2))), 'WHERE ("foo" = 1 OR "bar" = 2)') + assert_sql('WHERE "foo" = 1 AND "bar" = 2 AND "baz" = 3') { where?(foo: 1, bar: 2, baz: 3) } + assert_sql('WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') { where?(name(:foo).eq(1).or(name(:bar).eq(2)), name(:baz).eq(3)) } + assert_sql('WHERE ("foo" = 1 OR "bar" = 2)') { where?(name(:foo).eq(1).or(name(:bar).eq(2))) } end def test_join - assert_str_equal(Rebel::SQL.join(:foo), 'JOIN "foo"') + assert_sql('JOIN "foo"') { join(:foo) } end def test_function - assert_str_equal(Rebel::SQL.function('COALESCE', :foo, 0), 'COALESCE("foo", 0)') + assert_sql('COALESCE("foo", 0)') { function('COALESCE', :foo, 0) } end def test_where_function - assert_str_equal(Rebel::SQL.where?(Rebel::SQL.function('COALESCE', :foo, 0).eq 42), 'WHERE COALESCE("foo", 0) = 42') + assert_sql('WHERE COALESCE("foo", 0) = 42') { where?(function('COALESCE', :foo, 0).eq 42) } end def test_value - assert_str_equal(Rebel::SQL.value(Rebel::SQL.raw("'FOO'")), "'FOO'") - assert_str_equal(Rebel::SQL.value('FOO'), "'FOO'") - assert_str_equal(Rebel::SQL.value(1), '1') - assert_str_equal(Rebel::SQL.value(true), 'TRUE') - assert_str_equal(Rebel::SQL.value(false), 'FALSE') - assert_str_equal(Rebel::SQL.value(Date.new(2016, 12, 31)), "'2016-12-31'") - assert_str_equal(Rebel::SQL.value(Time.utc(2016, 12, 31, 23, 59, 59)), "'2016-12-31T23:59:59Z'") - assert_str_equal(Rebel::SQL.value(DateTime.new(2016, 12, 31, 23, 59, 59)), "'2016-12-31T23:59:59+00:00'") - assert_str_equal(Rebel::SQL.value(nil), 'NULL') + assert_sql("'FOO'") { value(raw("'FOO'")) } + assert_sql("'FOO'") { value('FOO') } + assert_sql('1') { value(1) } + assert_sql('TRUE') { value(true) } + assert_sql('FALSE') { value(false) } + assert_sql("'2016-12-31'") { value(Date.new(2016, 12, 31)) } + assert_sql("'2016-12-31T23:59:59Z'") { value(Time.utc(2016, 12, 31, 23, 59, 59)) } + assert_sql("'2016-12-31T23:59:59+00:00'") { value(DateTime.new(2016, 12, 31, 23, 59, 59)) } + assert_sql('NULL') { value(nil) } end def test_select - assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo)).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo"') + assert_sql('SELECT * FROM "foo"') { select(raw('*'), from: name(:foo)).gsub(/\s+/, ' ').strip } end def test_select_without_from - assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('1')).strip, 'SELECT 1') + assert_sql('SELECT 1') { select(raw('1')).strip } end def test_select_distinct - assert_str_equal(Rebel::SQL.select(distinct: :bar, from: :foo).gsub(/\s+/, ' ').strip, 'SELECT DISTINCT "bar" FROM "foo"') + assert_sql('SELECT DISTINCT "bar" FROM "foo"') { select(distinct: :bar, from: :foo).gsub(/\s+/, ' ').strip } end def test_select_distinct_multiple - assert_str_equal(Rebel::SQL.select(distinct: [:bar, :baz], from: :foo).gsub(/\s+/, ' ').strip, 'SELECT DISTINCT "bar", "baz" FROM "foo"') + assert_sql('SELECT DISTINCT "bar", "baz" FROM "foo"') { select(distinct: [:bar, :baz], from: :foo).gsub(/\s+/, ' ').strip } end def test_select_group_by - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, group: Rebel::SQL.by(:baz)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" GROUP BY "baz"') + assert_sql('SELECT "bar" FROM "foo" GROUP BY "baz"') { select(:bar, from: :foo, group: by(:baz)).gsub(/\s+/, ' ').strip } end def test_select_group_by_having - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, group: Rebel::SQL.by(:baz).having(Rebel::SQL.count(:qux).gt(5))).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" GROUP BY "baz" HAVING COUNT("qux") > 5') + assert_sql('SELECT "bar" FROM "foo" GROUP BY "baz" HAVING COUNT("qux") > 5') { select(:bar, from: :foo, group: by(:baz).having(count(:qux).gt(5))).gsub(/\s+/, ' ').strip } end def test_select_order_by - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz"') + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz"') { select(:bar, from: :foo, order: by(:baz)).gsub(/\s+/, ' ').strip } end def test_select_order_by_asc - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz).asc).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz" ASC') + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" ASC') { select(:bar, from: :foo, order: by(:baz).asc).gsub(/\s+/, ' ').strip } end def test_select_order_by_desc - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz).desc).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz" DESC') + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" DESC') { select(:bar, from: :foo, order: by(:baz).desc).gsub(/\s+/, ' ').strip } end def test_select_multiple_order_by - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(:baz, :qux)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz", "qux"') + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz", "qux"') { select(:bar, from: :foo, order: by(:baz, :qux)).gsub(/\s+/, ' ').strip } end def test_select_multiple_order_by_opposing - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, order: Rebel::SQL.by(Rebel::SQL.name(:baz).asc, Rebel::SQL.name(:qux).desc)).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" ORDER BY "baz" ASC, "qux" DESC') + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" ASC, "qux" DESC') { select(:bar, from: :foo, order: by(name(:baz).asc, name(:qux).desc)).gsub(/\s+/, ' ').strip } end def test_select_limit - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, limit: 10).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" LIMIT 10') + assert_sql('SELECT "bar" FROM "foo" LIMIT 10') { select(:bar, from: :foo, limit: 10).gsub(/\s+/, ' ').strip } end def test_select_offset - assert_str_equal(Rebel::SQL.select(:bar, from: :foo, limit: 10, offset: 20).gsub(/\s+/, ' ').strip, 'SELECT "bar" FROM "foo" LIMIT 10 OFFSET 20') + assert_sql('SELECT "bar" FROM "foo" LIMIT 10 OFFSET 20') { select(:bar, from: :foo, limit: 10, offset: 20).gsub(/\s+/, ' ').strip } end def test_nested_select - assert_str_equal(Rebel::SQL.select(Rebel::SQL.raw('*'), from: Rebel::SQL.name(:foo), where: Rebel::SQL.name(:bar).in(Rebel::SQL.select(Rebel::SQL.name(:bar), from: Rebel::SQL.name(:foo)))).gsub(/\s+/, ' ').strip, 'SELECT * FROM "foo" WHERE "bar" IN ( SELECT "bar" FROM "foo" )') + assert_sql('SELECT * FROM "foo" WHERE "bar" IN ( SELECT "bar" FROM "foo" )') { select(raw('*'), from: name(:foo), where: name(:bar).in(select(name(:bar), from: name(:foo)))).gsub(/\s+/, ' ').strip } end end From 000e7f2ae26e5aeac9d8ce988e18db2331af655a Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 13:58:29 +0100 Subject: [PATCH 31/50] Map (binary) logic operators to AND and OR --- lib/rebel/sql.rb | 2 ++ test/test_raw.rb | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 3deaccd..a64fc3b 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -105,10 +105,12 @@ module Rebel::SQL def and(*clause) Raw.new("#{self.parens?} AND #{Rebel::SQL.and_clause(*clause)}") end + alias & and def or(*clause) Raw.new("#{self} OR #{Rebel::SQL.and_clause(*clause)}").wants_parens! end + alias | or def eq(n) case n diff --git a/test/test_raw.rb b/test/test_raw.rb index 438adf2..1909f79 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -9,18 +9,29 @@ class TestRaw < Minitest::Test def test_and assert_sql('"foo" = 1 AND "bar" = 2') { name(:foo).eq(1).and(name(:bar).eq(2)) } + assert_sql('"foo" = 1 AND "bar" = 2') { name(:foo).eq(1) & name(:bar).eq(2) } + assert_sql('"foo" = 1 AND "bar" = 2') { (name(:foo) == 1) & (name(:bar) == 2) } end def test_or assert_sql('"foo" = 1 OR "bar" = 2') { name(:foo).eq(1).or(name(:bar).eq(2)) } + assert_sql('"foo" = 1 OR "bar" = 2') { name(:foo).eq(1) | name(:bar).eq(2) } + assert_sql('"foo" = 1 OR "bar" = 2') { (name(:foo) == 1) | (name(:bar) == 2) } end def test_and_or assert_sql('"foo" = 0 AND ("foo" = 1 OR "bar" = 2)') { name(:foo).eq(0).and(name(:foo).eq(1).or(name(:bar).eq(2))) } + assert_sql('"foo" = 0 AND ("foo" = 1 OR "bar" = 2)') { name(:foo).eq(0) & (name(:foo).eq(1) | name(:bar).eq(2)) } end def test_or_and_or assert_sql('("foo" = 1 OR "bar" = 2) AND ("foo" = 3 OR "bar" = 4)') { name(:foo).eq(1).or(name(:bar).eq(2)).and(name(:foo).eq(3).or(name(:bar).eq(4))) } + assert_sql('("foo" = 1 OR "bar" = 2) AND ("foo" = 3 OR "bar" = 4)') { (name(:foo).eq(1) | name(:bar).eq(2)) & (name(:foo).eq(3) | name(:bar).eq(4)) } + end + + def test_and_or_and + assert_sql('"foo" = 1 AND "bar" = 2 OR "foo" = 3 AND "bar" = 4') { name(:foo).eq(1).and(name(:bar).eq(2)).or(name(:foo).eq(3).and(name(:bar).eq(4))) } + assert_sql('"foo" = 1 AND "bar" = 2 OR "foo" = 3 AND "bar" = 4') { name(:foo).eq(1) & name(:bar).eq(2) | name(:foo).eq(3) & name(:bar).eq(4) } end def test_is From 422defeecd98512a018ba0893985279530bd77c9 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 13:59:01 +0100 Subject: [PATCH 32/50] Add IS NOT, NOT IN, NOT LIKE --- lib/rebel/sql.rb | 9 +++++++++ test/test_raw.rb | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index a64fc3b..9d39164 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -132,6 +132,7 @@ module Rebel::SQL end end alias != ne + alias is_not ne def lt(n) Raw.new("#{self} < #{Rebel::SQL.name_or_value(n)}") @@ -157,9 +158,17 @@ module Rebel::SQL Raw.new("#{self} IN (#{Rebel::SQL.values(*v)})") end + def not_in(*v) + Raw.new("#{self} NOT IN (#{Rebel::SQL.values(*v)})") + end + def like(n) Raw.new("#{self} LIKE #{Rebel::SQL.value(n)}") end + + def not_like(n) + Raw.new("#{self} NOT LIKE #{Rebel::SQL.value(n)}") + end end @identifier_quote = '"' diff --git a/test/test_raw.rb b/test/test_raw.rb index 1909f79..0906735 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -40,6 +40,12 @@ class TestRaw < Minitest::Test assert_sql('"foo" = "bar"') { name(:foo).is(name(:bar)) } end + def test_is_not + assert_sql('"foo" IS NOT NULL') { name(:foo).is_not(nil) } + assert_sql('"foo" != 42') { name(:foo).is_not(42) } + assert_sql('"foo" != "bar"') { name(:foo).is_not(name(:bar)) } + end + def test_eq assert_sql('"foo" IS NULL') { name(:foo).eq(nil) } assert_sql('"foo" IS NULL') { name(:foo) == nil } @@ -78,10 +84,18 @@ class TestRaw < Minitest::Test assert_sql('"foo" IN (1, 2, 3)') { name(:foo).in(1, 2, 3) } end + def test_not_in + assert_sql('"foo" NOT IN (1, 2, 3)') { name(:foo).not_in(1, 2, 3) } + end + def test_like assert_sql(%("foo" LIKE '%bar%')) { name(:foo).like('%bar%') } end + def test_not_like + assert_sql(%("foo" NOT LIKE '%bar%')) { name(:foo).not_like('%bar%') } + end + def test_where assert_sql('WHERE "foo" = 1 AND "bar" = 2 AND "baz" = 3') { where?(foo: 1, bar: 2, baz: 3) } assert_sql('WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') { where?(name(:foo).eq(1).or(name(:bar).eq(2)), name(:baz).eq(3)) } From 97f5214da7fa06d6ef756e86e98c4bf01d0f033c Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 14:08:13 +0100 Subject: [PATCH 33/50] Fix circular argument reference --- lib/rebel/sql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 9d39164..e89ab67 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -13,7 +13,7 @@ module Rebel::SQL exec(Rebel::SQL.drop_table(table_name)) end - def select(*fields, distinct: distinct, from: nil, where: nil, inner: nil, left: nil, right: nil, group: nil, order: nil, limit: nil, offset: nil) + def select(*fields, distinct: nil, from: nil, where: nil, inner: nil, left: nil, right: nil, group: nil, order: nil, limit: nil, offset: nil) exec(Rebel::SQL.select(*fields, distinct: distinct, from: from, From ddef06756f891b83a21571768e45441f5b7167b6 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 14:08:36 +0100 Subject: [PATCH 34/50] Release 0.5.0 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index 6ce6db6..ff59fce 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.4.0' + s.version = '0.5.0' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From 351e7e9645da0910403caba60224b01cb68cafa5 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 16:35:15 +0100 Subject: [PATCH 35/50] Make Rebel::SQL properly configurable to database peculiarities --- lib/rebel/sql.rb | 97 +++++++++++++++++++++++++++--------------------- test/test_raw.rb | 52 +++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 43 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index e89ab67..88cda4b 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -1,4 +1,4 @@ -module Rebel::SQL +module Rebel::SQLQ attr_reader :conn def exec(query) @@ -54,7 +54,9 @@ module Rebel::SQL def outer_join(table, on: nil) Rebel::SQL.outer_join(table, on: on) end +end +module Rebel class Raw < String def wants_parens! @wants_parens = true @@ -67,7 +69,7 @@ module Rebel::SQL end def parens - Raw.new("(#{self})") + sql.raw("(#{self})") end def parens? @@ -75,7 +77,7 @@ module Rebel::SQL end def as(n) - Raw.new(self + " AS #{Rebel::SQL.name(n)}") + sql.raw(self + " AS #{sql.name(n)}") end def as?(n) @@ -83,7 +85,7 @@ module Rebel::SQL end def on(*clause) - Raw.new(self + " ON #{Rebel::SQL.and_clause(*clause)}") + sql.raw(self + " ON #{sql.and_clause(*clause)}") end def on?(*clause) @@ -91,33 +93,33 @@ module Rebel::SQL end def having(*clause) - Raw.new(self + " HAVING #{Rebel::SQL.and_clause(*clause)}") + sql.raw(self + " HAVING #{sql.and_clause(*clause)}") end def asc - Raw.new(self + " ASC") + sql.raw(self + " ASC") end def desc - Raw.new(self + " DESC") + sql.raw(self + " DESC") end def and(*clause) - Raw.new("#{self.parens?} AND #{Rebel::SQL.and_clause(*clause)}") + sql.raw("#{self.parens?} AND #{sql.and_clause(*clause)}") end alias & and def or(*clause) - Raw.new("#{self} OR #{Rebel::SQL.and_clause(*clause)}").wants_parens! + sql.raw("#{self} OR #{sql.and_clause(*clause)}").wants_parens! end alias | or def eq(n) case n when nil - Raw.new("#{self} IS NULL") + sql.raw("#{self} IS NULL") else - Raw.new("#{self} = #{Rebel::SQL.name_or_value(n)}") + sql.raw("#{self} = #{sql.name_or_value(n)}") end end alias == eq @@ -126,70 +128,60 @@ module Rebel::SQL def ne(n) case n when nil - Raw.new("#{self} IS NOT NULL") + sql.raw("#{self} IS NOT NULL") else - Raw.new("#{self} != #{Rebel::SQL.name_or_value(n)}") + sql.raw("#{self} != #{sql.name_or_value(n)}") end end alias != ne alias is_not ne def lt(n) - Raw.new("#{self} < #{Rebel::SQL.name_or_value(n)}") + sql.raw("#{self} < #{sql.name_or_value(n)}") end alias < lt def gt(n) - Raw.new("#{self} > #{Rebel::SQL.name_or_value(n)}") + sql.raw("#{self} > #{sql.name_or_value(n)}") end alias > gt def le(n) - Raw.new("#{self} <= #{Rebel::SQL.name_or_value(n)}") + sql.raw("#{self} <= #{sql.name_or_value(n)}") end alias <= le def ge(n) - Raw.new("#{self} >= #{Rebel::SQL.name_or_value(n)}") + sql.raw("#{self} >= #{sql.name_or_value(n)}") end alias >= ge def in(*v) - Raw.new("#{self} IN (#{Rebel::SQL.values(*v)})") + sql.raw("#{self} IN (#{sql.values(*v)})") end def not_in(*v) - Raw.new("#{self} NOT IN (#{Rebel::SQL.values(*v)})") + sql.raw("#{self} NOT IN (#{sql.values(*v)})") end def like(n) - Raw.new("#{self} LIKE #{Rebel::SQL.value(n)}") + sql.raw("#{self} LIKE #{sql.value(n)}") end def not_like(n) - Raw.new("#{self} NOT LIKE #{Rebel::SQL.value(n)}") + sql.raw("#{self} NOT LIKE #{sql.value(n)}") + end + + private + + def sql + @sql ||= Rebel::SQLQ end end - @identifier_quote = '"' - @string_quote = "'" - @escaped_string_quote = "''" - - class << self - def identifier_quote=(str) - @identifier_quote = str - end - - def string_quote=(str) - @string_quote = str - end - - def escaped_string_quote=(str) - @escaped_string_quote = str - end - + module SQLB def raw(str) - Raw.new(str) + Raw.new(str).tap { |r| r.instance_variable_set(:@sql, self) } end def create_table(table_name, desc) @@ -309,15 +301,15 @@ module Rebel::SQL end def escape_str(str) - str.tr(@string_quote, @escaped_string_quote) + str.gsub(@string_quote, @escaped_string_quote) end def value(v) case v when Raw then v - when String then raw "'#{escape_str(v)}'" + when String then raw "#{@string_quote}#{escape_str(v)}#{@string_quote}" when Integer then raw v.to_s - when TrueClass, FalseClass then raw(v ? 'TRUE' : 'FALSE') + when TrueClass, FalseClass then raw(v ? @true_literal : @false_literal) when Date, Time, DateTime then value(v.iso8601) when nil then raw 'NULL' else raise NotImplementedError, "#{v.class}: #{v.inspect}" @@ -398,3 +390,24 @@ module Rebel::SQL end end end + +module Rebel + def self.SQL(options = {}, &block) + sql = const_defined?(:SQL) && options.empty? ? SQL : Module.new do + @identifier_quote = options[:identifier_quote] || '"' + @string_quote = options[:string_quote] || "'" + @escaped_string_quote = options[:escaped_string_quote] || "''" + @true_literal = options[:true_literal] || 'TRUE' + @false_literal = options[:false_literal] || 'FALSE' + + extend Rebel::SQLB + include Rebel::SQLQ + end + + return sql.instance_eval(&block) unless block.nil? + + sql + end + + SQL = SQL() +end diff --git a/test/test_raw.rb b/test/test_raw.rb index 0906735..76dd28c 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -4,7 +4,19 @@ require 'rebel' class TestRaw < Minitest::Test def assert_sql(expected, &actual) - assert_equal(expected.to_s, Rebel::SQL.instance_eval(&actual).to_s) + assert_equal(expected.to_s, Rebel::SQL(&actual).to_s) + end + + def assert_mysql(expected, &actual) + assert_equal(expected.to_s, Rebel::SQL(identifier_quote: '`', string_quote: '"', escaped_string_quote: '""', &actual).to_s) + end + + def assert_sqlite(expected, &actual) + assert_equal(expected.to_s, Rebel::SQL(true_literal: '1', false_literal: '0', &actual).to_s) + end + + def assert_postgresql(expected, &actual) + assert_equal(expected.to_s, Rebel::SQL(&actual).to_s) end def test_and @@ -114,6 +126,44 @@ class TestRaw < Minitest::Test assert_sql('WHERE COALESCE("foo", 0) = 42') { where?(function('COALESCE', :foo, 0).eq 42) } end + def test_name + assert_sql('"foo"') { name(:foo) } + assert_mysql('`foo`') { name(:foo) } + assert_postgresql('"foo"') { name(:foo) } + assert_sqlite('"foo"') { name(:foo) } + end + + def test_string + assert_sql("'FOO'") { value('FOO') } + assert_mysql('"FOO"') { value('FOO') } + assert_postgresql("'FOO'") { value('FOO') } + assert_sqlite("'FOO'") { value('FOO') } + end + + def test_escaped_string + assert_sql("'FOO''BAR'") { value("FOO'BAR") } + assert_mysql('"FOO\'BAR"') { value("FOO'BAR") } + assert_postgresql("'FOO''BAR'") { value("FOO'BAR") } + assert_sqlite("'FOO''BAR'") { value("FOO'BAR") } + + assert_sql("'FOO\"BAR'") { value('FOO"BAR') } + assert_mysql('"FOO""BAR"') { value('FOO"BAR') } + assert_postgresql("'FOO\"BAR'") { value('FOO"BAR') } + assert_sqlite("'FOO\"BAR'") { value('FOO"BAR') } + end + + def test_boolean_literal + assert_sql("TRUE") { value(true) } + assert_mysql("TRUE") { value(true) } + assert_postgresql("TRUE") { value(true) } + assert_sqlite("1") { value(true) } + + assert_sql("FALSE") { value(false) } + assert_mysql("FALSE") { value(false) } + assert_postgresql("FALSE") { value(false) } + assert_sqlite("0") { value(false) } + end + def test_value assert_sql("'FOO'") { value(raw("'FOO'")) } assert_sql("'FOO'") { value('FOO') } From c746cf18aa093ba6532c5d7a222c3819f58de980 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 16:35:35 +0100 Subject: [PATCH 36/50] Release 0.6.0 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index ff59fce..d868531 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.5.0' + s.version = '0.6.0' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From f3156797138b239d45fbb9e5936b6ae1e3940106 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 21 Nov 2017 17:24:20 +0100 Subject: [PATCH 37/50] Fix introspection when extending with Rebel::SQLB --- lib/rebel/sql.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 88cda4b..d0680b8 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -285,7 +285,8 @@ module Rebel ## Support - def name(name) + def name(name = nil) + super() if name.nil? # workaround for pry and introspection return name if name.is_a?(Raw) return raw('*') if name == '*' @@ -402,6 +403,15 @@ module Rebel extend Rebel::SQLB include Rebel::SQLQ + + def self.name(name = nil) + return "Rebel::SQL" if name.nil? + super + end + + def self.inspect + "#" + end end return sql.instance_eval(&block) unless block.nil? From cfe0851f04596d0e01c847840ba679987fb95844 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 22 Nov 2017 15:12:35 +0100 Subject: [PATCH 38/50] Adjust prose --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 2949548..e5d7e1a 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,11 @@ No bullshit No layers No wrappers No smarty-pants -No weird stance No sexy +No nonsense No AST No lazy loading No crazy mapping -No pretense -No nonsense What you write is what you get: readable and obvious What you write is what you meant: tasty and delicious From 02043ec2339fb1dec4fa1911fc7908a07db941e4 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 22 Nov 2017 15:13:20 +0100 Subject: [PATCH 39/50] Replace '*' by :* --- lib/rebel/sql.rb | 2 +- test/test_exec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index d0680b8..6ea99f7 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -288,7 +288,7 @@ module Rebel def name(name = nil) super() if name.nil? # workaround for pry and introspection return name if name.is_a?(Raw) - return raw('*') if name == '*' + return raw('*') if name == :* raw(name.to_s.split('.').map { |e| "#{@identifier_quote}#{e}#{@identifier_quote}" }.join('.')) end diff --git a/test/test_exec.rb b/test/test_exec.rb index cf7c18c..f9cb146 100644 --- a/test/test_exec.rb +++ b/test/test_exec.rb @@ -44,6 +44,6 @@ class TestExec < Minitest::Test def test_select create_table :foo, id: 'INT', col: 'VARCHAR(255)' insert_into :foo, id: 1, col: 'whatevs' - assert_equal(select('*', from: :foo), [[1, 'whatevs']]) + assert_equal(select(:*, from: :foo), [[1, 'whatevs']]) end end From 93592329dea8bf309e7bab0df66feecb3c3d0d56 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 22 Nov 2017 16:03:30 +0100 Subject: [PATCH 40/50] Improve README with design, examples, FAQ --- README.md | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) diff --git a/README.md b/README.md index e5d7e1a..f4a9c33 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,260 @@ Wait, it doesn't execute!? Just use your fave client gem, isn't that cute? ``` +## Motivation + +There are many a time where you end up knowing exactly what SQL query you want, +yet have to wrap your head around for the ORM to produce it, which is when the +point of such a layer is entirely defeated. Concatenating and interpolating +only goes so far. + +As ActiveRecord grows, a significant decision has been taken in the Rails team +to turn ARel into a library purely internal to ActiveRecord: the whole of it is +basically considered internal and private, and only ActiveRecord's public +interface should be used. Unfortunately, some highly dynamic, complex queries +simply cannot be built using ActiveRecord, and concatenating strings to build +SQL fragments and clauses simply does not cut it. + +## Philosophy + +- It must be readable as being SQL +- Yet it must be as much Ruby syntax and types as possible +- It must be able to produce fragments for others to use +- And somehow be composable enough +- It must not rely on metaprogramming magic +- Nor need monkeypatching core types + +## Design + +There are two goals to this library: + +- query building +- query execution + +Query building is about assembling a string containing a partial or complete +query that you will later pass on to be executed by an executor. + +Query execution is about writing a query that will be executed on the spot. + +There are also non-goals to this library: + +- be any sort of ORM +- or any sort of abstraction layer +- or any sort of query optimiser + +## Usage + +`Rebel::SQL` is a module that contains building and execution features, and +output ANSI-style SQL. + +`Rebel::SQL()` is a function that produces a customised module enabling support +for alternative dialects, and when passed a block, allows you to write things +more literally. + +Ruby types are best-effort mapped to SQL entities in a simple, regular way: + +- Symbols map to quoted SQL names such as tables, columns, aliases. +- Strings map to strings. Always. (Quote style can be configured). +- Integers and floats map to, well, integers and floats. +- Date, Time and DateTime map to their ISO 8601 string representation +- Booleans map to their respective ANSI literals (unless overriden by + configuration). +- `nil` maps to `NULL` and is expected to have the same "unknown" semantic + +Variable arguments are generally used. Hashes, depending on context, map to: + +- `=` equality or `IN` operators joined by `AND` +- `=` assignment operator joined by commas + +## Examples + +### Query building + +```ruby +require 'rebel' + +# Here's a typical query +Rebel::SQL.select :id, from: :customers, where: { :first_name => 'John', :last_name => 'Doe' } +=> SELECT "id" FROM "customers" WHERE "first_name" = 'John' AND "last_name" = 'Doe' + +# More args give more columns +Rebel::SQL.select :first_name, :last_name, from: :customers, where: { :id => [1, 2, 3] } +=> SELECT "first_name", "last_name" FROM "customers" WHERE "id" IN (1, 2, 3) + +# * is special-cased for names +Rebel::SQL.select :*, from: :customers, where: { :id => [1, 2, 3] } +=> SELECT * FROM "customers" WHERE "id" IN (1, 2, 3) + +# You can emit fragments to produce clauses +puts Rebel::SQL.and_clause :id => [1, 2, 3], :country => 'GB' +=> "id" IN (1, 2, 3) AND "country" = 'GB' +Rebel::SQL.where? :id => [1, 2, 3], :country => 'GB' +=> WHERE "id" IN (1, 2, 3) AND "country" = 'GB' + +# Here the question mark means where? swallows nil arguments: maybe it's a Maybe monad +Rebel::SQL.where?(nil) +=> nil + +# Let's emit join clauses +Rebel::SQL.join(:contracts, on: :customer_id => :id) +#=> JOIN "contracts" ON "customer_id" = "id" +Rebel::SQL.join(:contracts).on(:customer_id => :id) +#=> JOIN "contracts" ON "customer_id" = "id" + +# :contracts might have an :id too, so we can disambiguate those columns +Rebel::SQL.join(:contracts).on(:'contracts.customer_id' => :'customers.id') +#=> JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" + +# Other types of join are obviously available +Rebel::SQL.inner_join(:contracts).on(:'contracts.customer_id' => :'customers.id') +#=> INNER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" +Rebel::SQL.outer_join(:contracts).on(:'contracts.customer_id' => :'customers.id') +#=> OUTER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" +Rebel::SQL.left_outer_join(:contracts).on(:'contracts.customer_id' => :'customers.id') +#=> LEFT OUTER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" +Rebel::SQL.right_outer_join(:contracts).on(:'contracts.customer_id' => :'customers.id') +#=> RIGHT OUTER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" + +# The type of join can be split off. Again, note the question mark. +Rebel::SQL.inner? Rebel::SQL.join(:contracts).on(:'contracts.customer_id' => :'customers.id') +#=> INNER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" +Rebel::SQL.left? Rebel::SQL.outer_join(:contracts).on(:'contracts.customer_id' => :'customers.id') +#=> LEFT OUTER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" + +# And in a full query +Rebel::SQL.select :'customers.id', :'contracts.id', + from: :customers, + where: { :first_name => 'John', :last_name => 'Doe' }, + inner: Rebel::SQL.join(:contracts).on(:'contracts.customer_id' => :'customers.id'), + order: Rebel::SQL.by(:'customer.age').asc +#=> SELECT "customers"."id", "contracts"."id" +# FROM "customers" +# INNER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" +# WHERE "first_name" = 'John' AND "last_name" = 'Doe' +# ORDER BY "customer"."age" ASC + +# All those Rebel::SQL can get unwieldy, so let's reduce the noise +Rebel::SQL() do + select :'customers.id', :'contracts.id', + from: :customers, + where: { :first_name => 'John', :last_name => 'Doe' }, + inner: join(:contracts).on(:'contracts.customer_id' => :'customers.id'), + order: by(:'customer.age').asc +end +#=> SELECT "customers"."id", "contracts"."id" +# FROM "customers" +# INNER JOIN "contracts" ON "contracts"."customer_id" = "customers"."id" +# WHERE "first_name" = 'John' AND "last_name" = 'Doe' +# ORDER BY "customer"."age" ASC + +# Now, that function can be used to make things different +Rebel::SQL.name(:foo) +#=> "foo" +Rebel::SQL(identifier_quote: '`').name(:foo) +#=> `foo` +Rebel::SQL.value(true) +#=> TRUE +Rebel::SQL(true_literal: '1').value(true) +#=> 1 +Rebel::SQL(true_literal: '1') { select value(true) } +#=> SELECT 1 + +# While we're at it, let's call arbitrary functions +Rebel::SQL() { select function('NOW') } +#=> SELECT NOW() +Rebel::SQL() { select function('LENGTH', "a string") } +#=> SELECT LENGTH('a string') +Rebel::SQL() { select function('COUNT', :id), from: :customers, where: { :age => 42 } } +#=> SELECT COUNT("id") FROM "customers" WHERE "age" = 42 +Rebel::SQL() { select count(:id), from: :customers, where: { :age => 42 } } +#=> SELECT COUNT("id") FROM "customers" WHERE "age" = 42 + +# And throw in some aliases +Rebel::SQL() { select function('LENGTH', "a string").as(:length) } +#=> SELECT LENGTH('a string') AS "length" +Rebel::SQL() { select name(:id).as(:customer_id), from: :customers } +#=> SELECT "id" AS "customer_id" FROM "customers" +Rebel::SQL() { select count(:id).as(:count), from: :customers, where: { :age => 42 } } +#=> SELECT COUNT("id") FROM "customers" WHERE "age" = 42 + +# While we're counting things, let's group results +Rebel::SQL() { select count(:id).as(:count), :country, from: :customers, group: by(:country).having(count(:customer_id) => 5) } +#=> SELECT COUNT("id") AS "count", "country" FROM "customers" GROUP BY "country" HAVING COUNT("customer_id") = 5 +``` + +### Query execution + +If you provide Rebel::SQL an environment within which a query executor is +available, queries can be executed directly. + +```ruby +class CreateTableCustomers + include Rebel::SQL + + # provide a connection that responds to exec(query) + def conn + @conn ||= PG.connect( dbname: 'sales' ) + end + + # remember that SQL() returns a module! + include Rebel::SQL(true_literal: '1', false_literal: '0') + + # alternatively, redefine the provided exec (which calls conn.exec) + def exec(query) + @db ||= SQLite3::Database.new "test.db" + @db.execute(query) + end + + def up + create_table :customers, { + id: 'SERIAL', + name: 'VARCHAR(255)', + address: 'VARCHAR(255)', + city: 'VARCHAR(255)', + zip: 'VARCHAR(255)', + country: 'VARCHAR(255)', + } + + insert_into :customers, + { name: 'Lewis Caroll', address: '1, Alice St.', city: 'Oxford', zip: '1865', country: 'Wonderland' }, + { name: 'Neal Stephenson', address: '2, Hiro Blvd.', city: 'Los Angeles', zip: '1992', country: 'Metaverse' } + + results = select :name, :country, from: :customers + + update :customers, set: { city: 'FooTown' }, where: { zip: 1234 } + + delete_from :customers, where: { zip: 1234 } + + truncate :customers + end + + def down + drop_table :customers + end +end +``` + +## FAQ + +### X is missing/database specific, how do I write it? + +You can use `Rebel::SQL.raw("whatever")` and drop it in. + +### Why the weird syntax like `inner: join` instead of `inner_join`? + +This allows for a more uniform interface as well as not monkeypatching core types. + +### Can I write nonsensical SQL with this? + +Yes. Just as you can write nonsensical SQL in SQL. + +### Your query builder is not using an AST. + +That's not a question. You're welcome to implement one that does though, and if +it leverages the visitor pattern, allocates a trajillion objects along the way +and manages to produce invalid SQL in some corner cases, well congratulations +for reimplementing ARel. + ## License MIT From a1084539afda9119f25314fbb687715f10717fbf Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 22 Nov 2017 17:20:26 +0100 Subject: [PATCH 41/50] Fix Arel name --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f4a9c33..0b3489b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ You've been fighting yet another abstraction... Aren't you fed up with object-relation magic? But wait, here comes a humongous migration. Is ActiveRecord making you sick? -To hell with that monstrous ARel expression! +To hell with that monstrous Arel expression! Tell the truth, you were just wishing That it was as simple as a here-string. But could it keep some Ruby notation @@ -42,7 +42,7 @@ point of such a layer is entirely defeated. Concatenating and interpolating only goes so far. As ActiveRecord grows, a significant decision has been taken in the Rails team -to turn ARel into a library purely internal to ActiveRecord: the whole of it is +to turn Arel into a library purely internal to ActiveRecord: the whole of it is basically considered internal and private, and only ActiveRecord's public interface should be used. Unfortunately, some highly dynamic, complex queries simply cannot be built using ActiveRecord, and concatenating strings to build @@ -286,7 +286,7 @@ Yes. Just as you can write nonsensical SQL in SQL. That's not a question. You're welcome to implement one that does though, and if it leverages the visitor pattern, allocates a trajillion objects along the way and manages to produce invalid SQL in some corner cases, well congratulations -for reimplementing ARel. +for reimplementing Arel. ## License From 354f4a6860c88d070715e78dac50e3d159f00a9a Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 22 Nov 2017 17:20:57 +0100 Subject: [PATCH 42/50] Require "date" since we use `Date` --- lib/rebel/sql.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 6ea99f7..3a5132a 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -1,3 +1,5 @@ +require 'date' + module Rebel::SQLQ attr_reader :conn From 6ffa4e88f0425c967ddee9e1417178234ec768d9 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 22 Nov 2017 17:22:27 +0100 Subject: [PATCH 43/50] Normalize best-effort vs strict operator usage --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/rebel/sql.rb | 26 +++++++++++------------ test/test_raw.rb | 18 +++++++++------- 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0b3489b..e6fb495 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,60 @@ Rebel::SQL() { select count(:id).as(:count), from: :customers, where: { :age => # While we're counting things, let's group results Rebel::SQL() { select count(:id).as(:count), :country, from: :customers, group: by(:country).having(count(:customer_id) => 5) } #=> SELECT COUNT("id") AS "count", "country" FROM "customers" GROUP BY "country" HAVING COUNT("customer_id") = 5 + +# Passing a hash does a best effort to map Ruby to SQL +Rebel::SQL() { select :id, from: :customers, where: { :age => 42 } } +#=> SELECT "id" FROM "customers" WHERE "age" = 42 +Rebel::SQL() { select :id, from: :customers, where: { :age => [20, 21, 22] } } +#=> SELECT "id" FROM "customers" WHERE "age" IN (20, 21, 22) +Rebel::SQL() { select :id, from: :customers, where: { :age => nil } } +#=> SELECT "id" FROM "customers" WHERE "age" IS NULL + +# Using operators ensures the expected SQL operator is used +Rebel::SQL() { select :id, from: :customers, where: name(:age).eq(42) } +#=> SELECT "id" FROM "customers" WHERE "age" = 42 +Rebel::SQL() { select :id, from: :customers, where: name(:age).eq(nil) } +#=> SELECT "id" FROM "customers" WHERE "age" = NULL +Rebel::SQL() { select :id, from: :customers, where: name(:age).ne(42) } +#=> SELECT "id" FROM "customers" WHERE "age" != NULL +Rebel::SQL() { select :id, from: :customers, where: name(:age).is(42) } +#=> SELECT "id" FROM "customers" WHERE "age" IS 42 +Rebel::SQL() { select :id, from: :customers, where: name(:age).is(nil) } +#=> SELECT "id" FROM "customers" WHERE "age" IS NULL +Rebel::SQL() { select :id, from: :customers, where: name(:age).is_not(nil) } +#=> SELECT "id" FROM "customers" WHERE "age" IS NOT NULL + +# Other operators are available +Rebel::SQL() { select :id, from: :customers, where: name(:age).ge(42) } +#=> SELECT "id" FROM "customers" WHERE "age" >= 42 +Rebel::SQL() { select :id, from: :customers, where: name(:age).in(21, 22, 23) } +#=> SELECT "id" FROM "customers" WHERE "age" IN (21, 22, 23) +Rebel::SQL() { select :id, from: :customers, where: name(:first_name).like("J%") } +#=> SELECT "id" FROM "customers" WHERE "first_name" LIKE "J%" + +# Aliases to overload operators are available +Rebel::SQL() { select :id, from: :customers, where: name(:age) == 42 } +#=> SELECT "id" FROM "customers" WHERE "age" = 42 +Rebel::SQL() { select :id, from: :customers, where: name(:age) < 42 } +#=> SELECT "id" FROM "customers" WHERE "age" < 42 +Rebel::SQL() { select :id, from: :customers, where: name(:age) >= 42 } +#=> SELECT "id" FROM "customers" WHERE "age" >= 42 +Rebel::SQL() { select :id, from: :customers, where: name(:age) != 42 } +#=> SELECT "id" FROM "customers" WHERE "age" != 42 + +# Conditions can be combined +Rebel::SQL() { select :id, from: :customers, where: name(:age).gt(42).or(name(:age).lt(21)) } +#=> SELECT "id" FROM "customers" WHERE ("age" > 42 OR "age" < 21) +Rebel::SQL() { select :id, from: :customers, where: (name(:age) < 42).and(name(:age) > 21) } +#=> SELECT "id" FROM "customers" WHERE ("age" < 42 AND "age" > 21) + +# Binary-wise operators can be used to tie conditions +# WARNING: Usefulness if this hack is still debated. It might be removed in the future. +Rebel::SQL() { select :id, from: :customers, where: ((name(:age) > 42) | (name(:age) < 21)) } +#=> SELECT "id" FROM "customers" WHERE ("age" > 42 OR "age" < 21) +Rebel::SQL() { select :id, from: :customers, where: name(:age).lt(42) & (name(:age).gt(21)) } +#=> SELECT "id" FROM "customers" WHERE ("age" < 42 AND "age" > 21) + ``` ### Query execution diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 3a5132a..b79d56d 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -117,26 +117,22 @@ module Rebel alias | or def eq(n) - case n - when nil - sql.raw("#{self} IS NULL") - else - sql.raw("#{self} = #{sql.name_or_value(n)}") - end + sql.raw("#{self} = #{sql.name_or_value(n)}") end alias == eq - alias is eq + + def is(n) + sql.raw("#{self} IS #{sql.name_or_value(n)}") + end def ne(n) - case n - when nil - sql.raw("#{self} IS NOT NULL") - else - sql.raw("#{self} != #{sql.name_or_value(n)}") - end + sql.raw("#{self} != #{sql.name_or_value(n)}") end alias != ne - alias is_not ne + + def is_not(n) + sql.raw("#{self} IS NOT #{sql.name_or_value(n)}") + end def lt(n) sql.raw("#{self} < #{sql.name_or_value(n)}") @@ -343,6 +339,8 @@ module Rebel case right when Array name(left).in(*right) + when nil + name(left).is(name_or_value(right)) else name(left).eq(name_or_value(right)) end diff --git a/test/test_raw.rb b/test/test_raw.rb index 76dd28c..185f84c 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -48,19 +48,19 @@ class TestRaw < Minitest::Test def test_is assert_sql('"foo" IS NULL') { name(:foo).is(nil) } - assert_sql('"foo" = 42') { name(:foo).is(42) } - assert_sql('"foo" = "bar"') { name(:foo).is(name(:bar)) } + assert_sql('"foo" IS 42') { name(:foo).is(42) } + assert_sql('"foo" IS "bar"') { name(:foo).is(name(:bar)) } end def test_is_not assert_sql('"foo" IS NOT NULL') { name(:foo).is_not(nil) } - assert_sql('"foo" != 42') { name(:foo).is_not(42) } - assert_sql('"foo" != "bar"') { name(:foo).is_not(name(:bar)) } + assert_sql('"foo" IS NOT 42') { name(:foo).is_not(42) } + assert_sql('"foo" IS NOT "bar"') { name(:foo).is_not(name(:bar)) } end def test_eq - assert_sql('"foo" IS NULL') { name(:foo).eq(nil) } - assert_sql('"foo" IS NULL') { name(:foo) == nil } + assert_sql('"foo" = NULL') { name(:foo).eq(nil) } + assert_sql('"foo" = NULL') { name(:foo) == nil } assert_sql('"foo" = "bar"') { name(:foo).eq(name(:bar)) } assert_sql('"foo" = "bar"') { name(:foo) == name(:bar) } end @@ -68,8 +68,8 @@ class TestRaw < Minitest::Test def test_ne assert_sql('"foo" != "bar"') { name(:foo).ne(name(:bar)) } assert_sql('"foo" != "bar"') { name(:foo) != name(:bar) } - assert_sql('"foo" IS NOT NULL') { name(:foo).ne(nil) } - assert_sql('"foo" IS NOT NULL') { name(:foo) != nil } + assert_sql('"foo" != NULL') { name(:foo).ne(nil) } + assert_sql('"foo" != NULL') { name(:foo) != nil } end def test_lt @@ -112,6 +112,8 @@ class TestRaw < Minitest::Test assert_sql('WHERE "foo" = 1 AND "bar" = 2 AND "baz" = 3') { where?(foo: 1, bar: 2, baz: 3) } assert_sql('WHERE ("foo" = 1 OR "bar" = 2) AND "baz" = 3') { where?(name(:foo).eq(1).or(name(:bar).eq(2)), name(:baz).eq(3)) } assert_sql('WHERE ("foo" = 1 OR "bar" = 2)') { where?(name(:foo).eq(1).or(name(:bar).eq(2))) } + assert_sql('WHERE "foo" IS NULL') { where?(foo: nil) } + assert_sql('WHERE "foo" IN (1, 2, 3)') { where?(foo: [1, 2, 3]) } end def test_join From 31045461ab10d0e54e5b8610df416829658b65ad Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 22 Nov 2017 17:22:46 +0100 Subject: [PATCH 44/50] Release 0.7.0 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index d868531..eb211cb 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.6.0' + s.version = '0.7.0' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From 3bb41b81f0cccc9950faf204c410923bbedb2cce Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 23 Nov 2017 10:39:59 +0100 Subject: [PATCH 45/50] Generate less whitespace --- lib/rebel/sql.rb | 74 +++++++++++++++++++++--------------------------- test/test_raw.rb | 26 ++++++++--------- 2 files changed, 46 insertions(+), 54 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index b79d56d..8a91797 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -183,67 +183,59 @@ module Rebel end def create_table(table_name, desc) - raw <<-SQL - CREATE TABLE #{name(table_name)} ( - #{list(desc.map { |k, v| "#{name(k)} #{v}" })} - ) - SQL + raw %[CREATE TABLE #{name(table_name)} (#{list(desc.map { |k, v| "#{name(k)} #{v}" })})] end def drop_table(table_name) - raw <<-SQL - DROP TABLE #{name(table_name)} - SQL + raw "DROP TABLE #{name(table_name)}" end def select(*fields, distinct: nil, from: nil, where: nil, inner: nil, left: nil, right: nil, group: nil, order: nil, limit: nil, offset: nil) - raw <<-SQL - SELECT #{distinct ? "DISTINCT #{names(*distinct)}" : names(*fields)} - #{from?(from)} - #{inner?(inner)} - #{left?(left)} - #{right?(right)} - #{where?(where)} - #{group?(group)} - #{order?(order)} - #{limit?(limit, offset)} - SQL + raw [ + "SELECT #{distinct ? "DISTINCT #{names(*distinct)}" : names(*fields)}", + from?(from), + inner?(inner), + left?(left), + right?(right), + where?(where), + group?(group), + order?(order), + limit?(limit, offset), + ].compact.join(' ') end def insert_into(table_name, *rows) - raw <<-SQL - INSERT INTO #{name(table_name)} (#{names(*rows.first.keys)}) - VALUES #{list(rows.map { |r| "(#{values(*r.values)})" })} - SQL + raw [ + "INSERT INTO #{name(table_name)} (#{names(*rows.first.keys)})", + "VALUES #{list(rows.map { |r| "(#{values(*r.values)})" })}", + ].join(' ') end def update(table_name, set: nil, where: nil, inner: nil, left: nil, right: nil) raise ArgumentError if set.nil? - raw <<-SQL - UPDATE #{name(table_name)} - SET #{assign_clause(set)} - #{inner?(inner)} - #{left?(left)} - #{right?(right)} - #{where?(where)} - SQL + raw [ + "UPDATE #{name(table_name)}", + "SET #{assign_clause(set)}", + inner?(inner), + left?(left), + right?(right), + where?(where), + ].compact.join(' ') end def delete_from(table_name, where: nil, inner: nil, left: nil, right: nil) - raw <<-SQL - DELETE FROM #{name(table_name)} - #{inner?(inner)} - #{left?(left)} - #{right?(right)} - #{where?(where)} - SQL + raw [ + "DELETE FROM #{name(table_name)}", + inner?(inner), + left?(left), + right?(right), + where?(where), + ].join(' ') end def truncate(table_name) - raw <<-SQL - TRUNCATE #{name(table_name)} - SQL + raw "TRUNCATE #{name(table_name)}" end ## Functions diff --git a/test/test_raw.rb b/test/test_raw.rb index 185f84c..c5c246c 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -179,7 +179,7 @@ class TestRaw < Minitest::Test end def test_select - assert_sql('SELECT * FROM "foo"') { select(raw('*'), from: name(:foo)).gsub(/\s+/, ' ').strip } + assert_sql('SELECT * FROM "foo"') { select(raw('*'), from: name(:foo)) } end def test_select_without_from @@ -187,50 +187,50 @@ class TestRaw < Minitest::Test end def test_select_distinct - assert_sql('SELECT DISTINCT "bar" FROM "foo"') { select(distinct: :bar, from: :foo).gsub(/\s+/, ' ').strip } + assert_sql('SELECT DISTINCT "bar" FROM "foo"') { select(distinct: :bar, from: :foo) } end def test_select_distinct_multiple - assert_sql('SELECT DISTINCT "bar", "baz" FROM "foo"') { select(distinct: [:bar, :baz], from: :foo).gsub(/\s+/, ' ').strip } + assert_sql('SELECT DISTINCT "bar", "baz" FROM "foo"') { select(distinct: [:bar, :baz], from: :foo) } end def test_select_group_by - assert_sql('SELECT "bar" FROM "foo" GROUP BY "baz"') { select(:bar, from: :foo, group: by(:baz)).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" GROUP BY "baz"') { select(:bar, from: :foo, group: by(:baz)) } end def test_select_group_by_having - assert_sql('SELECT "bar" FROM "foo" GROUP BY "baz" HAVING COUNT("qux") > 5') { select(:bar, from: :foo, group: by(:baz).having(count(:qux).gt(5))).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" GROUP BY "baz" HAVING COUNT("qux") > 5') { select(:bar, from: :foo, group: by(:baz).having(count(:qux).gt(5))) } end def test_select_order_by - assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz"') { select(:bar, from: :foo, order: by(:baz)).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz"') { select(:bar, from: :foo, order: by(:baz)) } end def test_select_order_by_asc - assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" ASC') { select(:bar, from: :foo, order: by(:baz).asc).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" ASC') { select(:bar, from: :foo, order: by(:baz).asc) } end def test_select_order_by_desc - assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" DESC') { select(:bar, from: :foo, order: by(:baz).desc).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" DESC') { select(:bar, from: :foo, order: by(:baz).desc) } end def test_select_multiple_order_by - assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz", "qux"') { select(:bar, from: :foo, order: by(:baz, :qux)).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz", "qux"') { select(:bar, from: :foo, order: by(:baz, :qux)) } end def test_select_multiple_order_by_opposing - assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" ASC, "qux" DESC') { select(:bar, from: :foo, order: by(name(:baz).asc, name(:qux).desc)).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" ORDER BY "baz" ASC, "qux" DESC') { select(:bar, from: :foo, order: by(name(:baz).asc, name(:qux).desc)) } end def test_select_limit - assert_sql('SELECT "bar" FROM "foo" LIMIT 10') { select(:bar, from: :foo, limit: 10).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" LIMIT 10') { select(:bar, from: :foo, limit: 10) } end def test_select_offset - assert_sql('SELECT "bar" FROM "foo" LIMIT 10 OFFSET 20') { select(:bar, from: :foo, limit: 10, offset: 20).gsub(/\s+/, ' ').strip } + assert_sql('SELECT "bar" FROM "foo" LIMIT 10 OFFSET 20') { select(:bar, from: :foo, limit: 10, offset: 20) } end def test_nested_select - assert_sql('SELECT * FROM "foo" WHERE "bar" IN ( SELECT "bar" FROM "foo" )') { select(raw('*'), from: name(:foo), where: name(:bar).in(select(name(:bar), from: name(:foo)))).gsub(/\s+/, ' ').strip } + assert_sql('SELECT * FROM "foo" WHERE "bar" IN (SELECT "bar" FROM "foo")') { select(raw('*'), from: name(:foo), where: name(:bar).in(select(name(:bar), from: name(:foo)))) } end end From a0f115340756f93c17b726752b032cfe02076f91 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 23 Nov 2017 10:40:12 +0100 Subject: [PATCH 46/50] Adjust quote style --- test/test_raw.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/test_raw.rb b/test/test_raw.rb index c5c246c..458a29a 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -155,15 +155,15 @@ class TestRaw < Minitest::Test end def test_boolean_literal - assert_sql("TRUE") { value(true) } - assert_mysql("TRUE") { value(true) } - assert_postgresql("TRUE") { value(true) } - assert_sqlite("1") { value(true) } + assert_sql('TRUE') { value(true) } + assert_mysql('TRUE') { value(true) } + assert_postgresql('TRUE') { value(true) } + assert_sqlite('1') { value(true) } - assert_sql("FALSE") { value(false) } - assert_mysql("FALSE") { value(false) } - assert_postgresql("FALSE") { value(false) } - assert_sqlite("0") { value(false) } + assert_sql('FALSE') { value(false) } + assert_mysql('FALSE') { value(false) } + assert_postgresql('FALSE') { value(false) } + assert_sqlite('0') { value(false) } end def test_value From 86ce2b65ff18c96ff1239a65e1702676b5d56b49 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 23 Nov 2017 16:33:01 +0100 Subject: [PATCH 47/50] Properly handle backslash escaping --- lib/rebel/sql.rb | 6 +++++- test/test_raw.rb | 30 ++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/rebel/sql.rb b/lib/rebel/sql.rb index 8a91797..b5216e8 100644 --- a/lib/rebel/sql.rb +++ b/lib/rebel/sql.rb @@ -292,7 +292,10 @@ module Rebel end def escape_str(str) - str.gsub(@string_quote, @escaped_string_quote) + str.dup.tap do |s| + s.gsub!('\\') { @escaped_string_backslash } if @escaped_string_backslash + s.gsub!(@string_quote) { @escaped_string_quote } + end end def value(v) @@ -390,6 +393,7 @@ module Rebel @identifier_quote = options[:identifier_quote] || '"' @string_quote = options[:string_quote] || "'" @escaped_string_quote = options[:escaped_string_quote] || "''" + @escaped_string_backslash = options[:escaped_string_backslash] @true_literal = options[:true_literal] || 'TRUE' @false_literal = options[:false_literal] || 'FALSE' diff --git a/test/test_raw.rb b/test/test_raw.rb index 458a29a..e3cee69 100644 --- a/test/test_raw.rb +++ b/test/test_raw.rb @@ -8,7 +8,7 @@ class TestRaw < Minitest::Test end def assert_mysql(expected, &actual) - assert_equal(expected.to_s, Rebel::SQL(identifier_quote: '`', string_quote: '"', escaped_string_quote: '""', &actual).to_s) + assert_equal(expected.to_s, Rebel::SQL(identifier_quote: '`', escaped_string_quote: "\\'", escaped_string_backslash: '\\', &actual).to_s) end def assert_sqlite(expected, &actual) @@ -137,21 +137,31 @@ class TestRaw < Minitest::Test def test_string assert_sql("'FOO'") { value('FOO') } - assert_mysql('"FOO"') { value('FOO') } + assert_mysql("'FOO'") { value('FOO') } assert_postgresql("'FOO'") { value('FOO') } assert_sqlite("'FOO'") { value('FOO') } end def test_escaped_string - assert_sql("'FOO''BAR'") { value("FOO'BAR") } - assert_mysql('"FOO\'BAR"') { value("FOO'BAR") } - assert_postgresql("'FOO''BAR'") { value("FOO'BAR") } - assert_sqlite("'FOO''BAR'") { value("FOO'BAR") } + assert_sql (%q('FOO''BAR')) { value(%q(FOO'BAR)) } + assert_postgresql (%q('FOO''BAR')) { value(%q(FOO'BAR)) } + assert_sqlite (%q('FOO''BAR')) { value(%q(FOO'BAR)) } + assert_mysql (%q('FOO\'BAR')) { value(%q(FOO'BAR)) } - assert_sql("'FOO\"BAR'") { value('FOO"BAR') } - assert_mysql('"FOO""BAR"') { value('FOO"BAR') } - assert_postgresql("'FOO\"BAR'") { value('FOO"BAR') } - assert_sqlite("'FOO\"BAR'") { value('FOO"BAR') } + assert_sql (%q('FOO"BAR')) { value(%q(FOO"BAR)) } + assert_postgresql (%q('FOO"BAR')) { value(%q(FOO"BAR)) } + assert_sqlite (%q('FOO"BAR')) { value(%q(FOO"BAR)) } + assert_mysql (%q('FOO"BAR')) { value(%q(FOO"BAR)) } + + assert_sql (%q('FOO\BAR')) { value(%q(FOO\BAR)) } + assert_postgresql (%q('FOO\BAR')) { value(%q(FOO\BAR)) } + assert_sqlite (%q('FOO\BAR')) { value(%q(FOO\BAR)) } + assert_mysql (%q('FOO\\BAR')) { value(%q(FOO\BAR)) } + + assert_sql (%q('FOO\\''BAR')) { value(%q(FOO\'BAR)) } + assert_postgresql (%q('FOO\\''BAR')) { value(%q(FOO\'BAR)) } + assert_sqlite (%q('FOO\\''BAR')) { value(%q(FOO\'BAR)) } + assert_mysql (%q('FOO\\\'BAR')) { value(%q(FOO\'BAR)) } end def test_boolean_literal From 071932e4bea8fde6aaac130c36ca478174587b5b Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 23 Nov 2017 16:33:26 +0100 Subject: [PATCH 48/50] Release 0.7.1 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index eb211cb..4b5e6a4 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.7.0' + s.version = '0.7.1' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?' From 95ecac4946f5466c5402a3a03d4187448eaf97b2 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Fri, 14 Sep 2018 09:52:55 +0200 Subject: [PATCH 49/50] Update gem homepage --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index 4b5e6a4..3642a4b 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -7,5 +7,5 @@ Gem::Specification.new do |s| s.authors = ['Loic Nageleisen'] s.email = 'loic.nageleisen@gmail.com' s.files = Dir['lib/**/*.rb'] - s.homepage = 'https://github.com/lloeki/rebel.git' + s.homepage = 'https://gitlab.com/lloeki/rebel.git' end From 0e82d96755ba2a201e9890b79a596716554a26ab Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Fri, 14 Sep 2018 09:53:26 +0200 Subject: [PATCH 50/50] Release 0.7.2 --- rebel.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebel.gemspec b/rebel.gemspec index 3642a4b..75a3680 100644 --- a/rebel.gemspec +++ b/rebel.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'rebel' - s.version = '0.7.1' + s.version = '0.7.2' s.licenses = ['MIT'] s.summary = 'Fight against the Object tyranny' s.description = 'SQL-flavoured Ruby, or is it the other way around?'