qdsl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd1c1b61ff92582af4dbb84566ac95128698db36
4
+ data.tar.gz: 746092cbe04dfa4b583dc7f3a0e2e9b43976ee1a
5
+ SHA512:
6
+ metadata.gz: 187f3afba4a996b5f2010c4b67b0645daa9d4912f837d3d7ce28b7ee34fe59fe545b3791403b3bb5098037f2fbb2e085d2beda687405162d65874ab6ce302a8c
7
+ data.tar.gz: 7935cb02ccdecf88385d27d68aca1c2c9faa10823aadbe5032332ec3a4b233485c0eb74e8c13ff5c77044da678bab626c399a1f301c5fe44a2a1510861a8beda
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in qdsl.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Richard Cook
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # qdsl
2
+
3
+ [![Install gem](https://badge.fury.io/rb/qdsl.png)](https://rubygems.org/gems/qdsl)
4
+ [![Build status](https://travis-ci.org/rcook/qdsl.png)](https://travis-ci.org/rcook/qdsl)
5
+
6
+ qdsl is SQL query builder DSL for Ruby: think of it as "hygienic macros for
7
+ SQL". Note that this gem is in its very early development stages and is very far
8
+ from complete. Here are the main talking points:
9
+
10
+ * It's most likely riddled with bugs.
11
+ * It is intended to have minimal external dependencies: it doesn't currently
12
+ depend on any database adapters or anything like that since it is intended to
13
+ generate only text and query parameter bindings.
14
+ * It will generate PostgreSQL-compatible SQL, since that's what I care about.
15
+ * It is intended to allow clean and straightforward composition of SQL queries
16
+ and subqueries.
17
+ * It takes a similar approach to other SQL Ruby-embedded DSLs (e.g.
18
+ [SQLDSL](https://github.com/dydx/SQLDSL)) but differs as follows:
19
+ * It attempts to validate the query: column names are currently supported and
20
+ the intention is to support column types too.
21
+ * It automatically generates table aliases in order to cleanly scope column
22
+ names and to remove some of the confusion around joining tables with identically
23
+ named columns.
24
+
25
+ ## Installation
26
+
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem 'qdsl'
31
+ ```
32
+
33
+ And then execute:
34
+
35
+ ```bash
36
+ $ bundle
37
+ ```
38
+
39
+ Or install it yourself as:
40
+
41
+ ```bash
42
+ $ gem install qdsl
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ qdsl allows us to build SQL queries in a fairly natural way as a series of
48
+ Ruby chained method calls. A few examples are given below.
49
+
50
+ ### Reference the gem
51
+
52
+ ```ruby
53
+ require 'qdsl'
54
+ ```
55
+
56
+ ### Define a table
57
+
58
+ ```ruby
59
+ clients = Qdsl::Table.new(
60
+ :clients,
61
+ :id,
62
+ :first_name,
63
+ :last_name
64
+ )
65
+ ```
66
+
67
+ This does not connect to a database or anything like that. This is just a way
68
+ to define a table name and the list of column names from which we can build
69
+ subsequent queries. Eventually, we might support generating these `Table`
70
+ objects from Active Record models, but until that day comes, this is how we
71
+ define data sources.
72
+
73
+ ### Define a query
74
+
75
+ This is based on the most complex query defined in the gem's test suite:
76
+
77
+ ```ruby
78
+ subquery = Qdsl::select(:id, :first_name, :last_name).
79
+ from(clients)
80
+ query = Qdsl::select { |t0, t1| [t0.first_name, t1.last_name] }.
81
+ from(subquery).
82
+ inner_join(subquery).on { |t0, t1| t0.id.equals(t1.id) }.
83
+ where { |t0, t1| t0.first_name.equals('str0').and(t1.last_name.equals('str1')) }
84
+ ```
85
+
86
+ Calling `render` on the query object `query` yields SQL that looks something
87
+ like the following:
88
+
89
+ ```sql
90
+ SELECT _00.first_name, _01.last_name
91
+ FROM (
92
+ SELECT _02.id, _02.first_name, _02.last_name
93
+ FROM clients AS _02
94
+ ) AS _00
95
+ INNER JOIN (
96
+ SELECT _03.id, _03.first_name, _03.last_name
97
+ FROM clients AS _03
98
+ ) AS _01
99
+ ON _00.id = _01.id
100
+ WHERE (_00.first_name = :_param00) AND (_01.last_name = :_param01)
101
+ ```
102
+
103
+ The `parameters` attribute on the `Context` object populated by `render` will
104
+ define the keys `_param00` and `_param01` with values `str0` and `str1`
105
+ respectively. Notice the following:
106
+
107
+ * The use of code blocks to perform validation of column names
108
+ * Rudimentary support for boolean expressions
109
+ * Replacement of string literals with query parameters
110
+ * Automatic generation of table aliases in order to avoid column name collisions
111
+
112
+
113
+ ## Contributing
114
+
115
+ 1. Fork it from [here](https://github.com/rcook/qdsl/fork)
116
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
117
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
118
+ 4. Push to the branch (`git push origin my-new-feature`)
119
+ 5. Create a new pull request
120
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task default: :test
10
+
data/lib/and.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Qdsl
2
+ class And < Boolean
3
+ def initialize(exprs)
4
+ super 'AND', exprs
5
+ end
6
+
7
+ def and(expr)
8
+ And.new(@exprs + [expr])
9
+ end
10
+
11
+ def or(expr)
12
+ Or.new([self, expr])
13
+ end
14
+ end
15
+ end
16
+
data/lib/boolean.rb ADDED
@@ -0,0 +1,14 @@
1
+ module Qdsl
2
+ class Boolean < Expression
3
+ def initialize(operator, exprs)
4
+ @operator = operator
5
+ @exprs = exprs
6
+ end
7
+
8
+ def render(context, ids)
9
+ expr_results = @exprs.collect { |x| render_operand(context, ids, x) }
10
+ expr_results.collect { |x| "(#{x})" }.join(" #{@operator} ")
11
+ end
12
+ end
13
+ end
14
+
data/lib/column.rb ADDED
@@ -0,0 +1,43 @@
1
+ module Qdsl
2
+ class Column
3
+ attr_reader :name, :alias_name, :source
4
+
5
+ def self.[](obj)
6
+ if obj.is_a?(Column)
7
+ obj
8
+ elsif obj.is_a?(String) || obj.is_a?(Symbol)
9
+ Column.new(obj)
10
+ else
11
+ raise "Cannot convert object of class \"#{obj.class.name}\" to column"
12
+ end
13
+ end
14
+
15
+ def initialize(name, alias_name = nil, source = nil)
16
+ @name = name.to_s
17
+ @alias_name = alias_name.nil? ? nil : alias_name.to_s
18
+ @source = source
19
+ end
20
+
21
+ def with_name(name)
22
+ Column.new(name, @alias_name, @source)
23
+ end
24
+
25
+ def with_alias_name(alias_name)
26
+ Column.new(@name, alias_name, @source)
27
+ end
28
+ alias_method :as, :with_alias_name
29
+
30
+ def with_source(source)
31
+ Column.new(@name, @alias_name, source)
32
+ end
33
+
34
+ def render(context, id)
35
+ @alias_name ? "#{id}.#{@name} AS #{@alias_name}" : "#{id}.#{@name}"
36
+ end
37
+
38
+ def equals(column)
39
+ Equals.new(self, column)
40
+ end
41
+ end
42
+ end
43
+
data/lib/context.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Qdsl
2
+ class Context
3
+ attr_reader :parameters
4
+
5
+ def initialize
6
+ @id_base = 0
7
+ @parameters = {}
8
+ end
9
+
10
+ def id
11
+ result = "_#{@id_base.to_s.rjust(2, '0')}"
12
+ @id_base += 1
13
+ result
14
+ end
15
+
16
+ def add_parameter(value)
17
+ parameter_id = "_param#{@parameters.size.to_s.rjust(2, '0')}"
18
+ @parameters[parameter_id] = value
19
+ parameter_id
20
+ end
21
+ end
22
+ end
23
+
data/lib/equals.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Qdsl
2
+ class Equals < Expression
3
+ def initialize(column0, column1)
4
+ @column0 = column0
5
+ @column1 = column1
6
+ end
7
+
8
+ def and(expr)
9
+ And.new([self, expr])
10
+ end
11
+
12
+ def or(expr)
13
+ Or.new([self, expr])
14
+ end
15
+
16
+ def render(context, ids)
17
+ column0_result = render_operand(context, ids, @column0)
18
+ column1_result = render_operand(context, ids, @column1)
19
+ "#{column0_result} = #{column1_result}"
20
+ end
21
+ end
22
+ end
23
+
data/lib/expression.rb ADDED
@@ -0,0 +1,21 @@
1
+ module Qdsl
2
+ class Expression
3
+ protected
4
+
5
+ def render_operand(context, ids, operand)
6
+ if operand.is_a?(String)
7
+ parameter_id = context.add_parameter(operand)
8
+ ":#{parameter_id}"
9
+ elsif operand.is_a?(TrueClass)
10
+ 'TRUE'
11
+ elsif operand.is_a?(FalseClass)
12
+ 'FALSE'
13
+ elsif operand.is_a?(Expression)
14
+ operand.render(context, ids)
15
+ else
16
+ operand.render(context, ids[operand.source])
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,20 @@
1
+ module Qdsl
2
+ class FunkyProxy
3
+ def initialize(source)
4
+ @source = source
5
+ source.column_names.each do |column|
6
+ (class << self; self; end).class_eval do
7
+ define_method column do
8
+ Column.new(column, nil, source)
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ def [](name)
15
+ raise unless @source.column_names.include?(name.to_s)
16
+ Column.new(name.to_s, nil, @source)
17
+ end
18
+ end
19
+ end
20
+
data/lib/inner_join.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Qdsl
2
+ class InnerJoin
3
+ attr_reader :source, :predicate
4
+
5
+ def initialize(source, predicate)
6
+ @source = source
7
+ @predicate = predicate
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,14 @@
1
+ module Qdsl
2
+ class InnerJoinBuilder
3
+ def initialize(query_builder, source)
4
+ @query_builder = query_builder
5
+ @source = source
6
+ end
7
+
8
+ def on
9
+ predicate = yield(FunkyProxy.new(@query_builder.source), FunkyProxy.new(@source))
10
+ @query_builder.add_inner_join(@source, predicate)
11
+ end
12
+ end
13
+ end
14
+
data/lib/or.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Qdsl
2
+ class Or < Boolean
3
+ def initialize(exprs)
4
+ super 'OR', exprs
5
+ end
6
+
7
+ def and(expr)
8
+ And.new([self, expr])
9
+ end
10
+
11
+ def or(expr)
12
+ Or.new(@exprs + [expr])
13
+ end
14
+ end
15
+ end
16
+
data/lib/qdsl.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'qdsl/version'
2
+
3
+ %w{
4
+ expression
5
+ boolean
6
+ and
7
+ column
8
+ context
9
+ equals
10
+ funky_proxy
11
+ inner_join
12
+ inner_join_builder
13
+ or
14
+ qdsl
15
+ select
16
+ table
17
+ table_query
18
+ }.each do |file_name|
19
+ require_relative file_name
20
+ end
21
+
22
+ module Qdsl
23
+ def self.select(*columns, &block)
24
+ Select.new(columns.collect { |x| Column[x] }, &block)
25
+ end
26
+ end
27
+
@@ -0,0 +1,4 @@
1
+ module Qdsl
2
+ VERSION = '0.0.1'
3
+ end
4
+
data/lib/select.rb ADDED
@@ -0,0 +1,95 @@
1
+ module Qdsl
2
+ class SelectQuery
3
+ attr_reader :column_names
4
+
5
+ def initialize(select, column_names)
6
+ @select = select
7
+ @column_names = column_names
8
+ end
9
+
10
+ def render(context, depth, id)
11
+ select_result = @select.render(context, depth)
12
+ "(\n#{select_result}) AS #{id}"
13
+ end
14
+ end
15
+
16
+ class Select
17
+ attr_reader :source
18
+
19
+ def initialize(columns, &block)
20
+ @columns = columns
21
+ @block = block
22
+ @inner_joins = []
23
+ end
24
+
25
+ def from(source)
26
+ @source = source.create_query
27
+ self
28
+ end
29
+
30
+ def create_query
31
+ # Clone columns?
32
+ column_names = @columns.collect(&:name)
33
+ SelectQuery.new(self, column_names)
34
+ end
35
+
36
+ def where
37
+ sources = [@source] + @inner_joins.collect(&:source)
38
+ proxies = sources.collect { |x| FunkyProxy.new(x) }
39
+ @where = yield(proxies.size == 1 ? proxies.first : proxies)
40
+ self
41
+ end
42
+
43
+ def inner_join(source)
44
+ InnerJoinBuilder.new(self, source.create_query)
45
+ end
46
+
47
+ def add_inner_join(source, predicate)
48
+ @inner_joins << InnerJoin.new(source, predicate)
49
+ self
50
+ end
51
+
52
+ def render(context = nil, depth = 0)
53
+ context ||= Context.new
54
+
55
+ indent = ' ' * depth
56
+
57
+ unknown_columns = @columns.select { |x| !@source.column_names.include?(x.name) }
58
+ raise "One or more unknown columns: #{unknown_columns.collect(&:name).join(', ')}" unless unknown_columns.empty?
59
+
60
+ columns = @columns.collect { |x| x.with_source(@source) }
61
+
62
+ sources = [@source] + @inner_joins.collect(&:source)
63
+
64
+ extra_columns = if @block
65
+ proxies = sources.collect { |x| FunkyProxy.new(x) }
66
+ [*@block.call(proxies.size == 1 ? proxies.first : proxies)]
67
+ else
68
+ []
69
+ end
70
+
71
+ ids = sources.inject({}) { |acc, x| acc[x] = context.id; acc }
72
+
73
+ column_results = (columns + extra_columns).collect { |x| x.render(context, ids[x.source]) }
74
+
75
+ fragments = []
76
+
77
+ fragments << "#{indent}SELECT #{column_results.join(', ')}\n"
78
+ fragments << "#{indent}FROM #{@source.render(context, depth + 1, ids[@source])}\n"
79
+
80
+ @inner_joins.each do |inner_join|
81
+ predicate_result = inner_join.predicate.render(context, ids)
82
+ fragments << "#{indent}INNER JOIN #{inner_join.source.render(context, depth + 1, ids[inner_join.source])}\n"
83
+ fragments << "#{indent}ON #{predicate_result}\n"
84
+ end
85
+
86
+ if @where
87
+ predicate_result = @where.render(context, ids)
88
+ fragments << "#{indent}WHERE #{predicate_result}"
89
+ end
90
+
91
+ fragments.join
92
+ end
93
+ end
94
+ end
95
+
data/lib/table.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Qdsl
2
+ class Table
3
+ attr_reader :column_names
4
+
5
+ def initialize(name, *column_names)
6
+ @name = name
7
+ @column_names = column_names.collect(&:to_s)
8
+ end
9
+
10
+ def create_query
11
+ TableQuery.new(@name, @column_names)
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,15 @@
1
+ module Qdsl
2
+ class TableQuery
3
+ attr_reader :column_names
4
+
5
+ def initialize(name, column_names)
6
+ @name = name
7
+ @column_names = column_names
8
+ end
9
+
10
+ def render(context, depth, id)
11
+ "#{@name} AS #{id}"
12
+ end
13
+ end
14
+ end
15
+
data/qdsl.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'qdsl/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'qdsl'
8
+ spec.version = Qdsl::VERSION
9
+ spec.authors = ['Richard Cook']
10
+ spec.email = ['rcook@rcook.org']
11
+ spec.summary = %q{SQL query builder DSL for Ruby}
12
+ spec.description = %q{Think of it as "hygienic macros for SQL}
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'minitest'
24
+ end
25
+
@@ -0,0 +1,115 @@
1
+ require_relative 'minitest_helper'
2
+
3
+ module Qdsl
4
+ class ColumnTest < Minitest::Test
5
+ def test_convert_from_column
6
+ column = Column.new('name', 'alias_name', 'source')
7
+ result = Column[column]
8
+ assert_equal column, result
9
+ end
10
+
11
+ def test_convert_from_string
12
+ result = Column['name']
13
+ assert_equal 'name', result.name
14
+ assert_nil result.alias_name
15
+ assert_nil result.source
16
+ end
17
+
18
+ def test_convert_from_symbol
19
+ result = Column[:name]
20
+ assert_equal 'name', result.name
21
+ assert_nil result.alias_name
22
+ assert_nil result.source
23
+ end
24
+
25
+ def test_initialize_strings
26
+ result = Column.new('name', 'alias_name', 'source')
27
+ assert_equal 'name', result.name
28
+ assert_equal 'alias_name', result.alias_name
29
+ assert_equal 'source', result.source
30
+ end
31
+
32
+ def test_initialize_symbols
33
+ result = Column.new(:name, :alias_name, :source)
34
+ assert_equal 'name', result.name
35
+ assert_equal 'alias_name', result.alias_name
36
+ assert_equal :source, result.source
37
+ end
38
+
39
+ def test_initialize_name_only_string
40
+ result = Column.new('name')
41
+ assert_equal 'name', result.name
42
+ assert_nil result.alias_name
43
+ assert_nil result.source
44
+ end
45
+
46
+ def test_initialize_name_only_symbol
47
+ result = Column.new(:name)
48
+ assert_equal 'name', result.name
49
+ assert_nil result.alias_name
50
+ assert_nil result.source
51
+ end
52
+
53
+ def test_with_name_string
54
+ result = Column.new('name', 'alias_name', 'source').with_name('new_name')
55
+ assert_equal 'new_name', result.name
56
+ assert_equal 'alias_name', result.alias_name
57
+ assert_equal 'source', result.source
58
+ end
59
+
60
+ def test_with_name_symbol
61
+ result = Column.new('name', 'alias_name', 'source').with_name(:new_name)
62
+ assert_equal 'new_name', result.name
63
+ assert_equal 'alias_name', result.alias_name
64
+ assert_equal 'source', result.source
65
+ end
66
+
67
+ def test_with_alias_name_string
68
+ result = Column.new('name', 'alias_name', 'source').with_alias_name('new_alias_name')
69
+ assert_equal 'name', result.name
70
+ assert_equal 'new_alias_name', result.alias_name
71
+ assert_equal 'source', result.source
72
+ end
73
+
74
+ def test_with_alias_name_symbol
75
+ result = Column.new('name', 'alias_name', 'source').with_alias_name(:new_alias_name)
76
+ assert_equal 'name', result.name
77
+ assert_equal 'new_alias_name', result.alias_name
78
+ assert_equal 'source', result.source
79
+ end
80
+
81
+ def test_with_source
82
+ result = Column.new('name', 'alias_name', 'source').with_source('new_source')
83
+ assert_equal 'name', result.name
84
+ assert_equal 'alias_name', result.alias_name
85
+ assert_equal 'new_source', result.source
86
+ end
87
+
88
+ def test_as_string
89
+ result = Column.new('name', 'alias_name', 'source').as('new_name_alias')
90
+ assert_equal 'name', result.name
91
+ assert_equal 'new_name_alias', result.alias_name
92
+ assert_equal 'source', result.source
93
+ end
94
+
95
+ def test_as_symbol
96
+ result = Column.new('name', 'alias_name', 'source').as(:new_name_alias)
97
+ assert_equal 'name', result.name
98
+ assert_equal 'new_name_alias', result.alias_name
99
+ assert_equal 'source', result.source
100
+ end
101
+
102
+ def test_render_name_only
103
+ column = Column.new('name')
104
+ result = column.render(Context.new, 'id')
105
+ assert_equal 'id.name', result
106
+ end
107
+
108
+ def test_render_alias_name
109
+ column = Column.new('name', 'alias_name')
110
+ result = column.render(Context.new, 'id')
111
+ assert_equal 'id.name AS alias_name', result
112
+ end
113
+ end
114
+ end
115
+
@@ -0,0 +1,19 @@
1
+ require_relative 'minitest_helper'
2
+ require_relative 'query_test'
3
+
4
+ module Qdsl
5
+ class ContextTest < QueryTest
6
+ def test_simple
7
+ query = Qdsl::select(:first_name).from(@clients).where { |t| t.id.equals('Cook') }
8
+ context = Context.new
9
+ result = query.render(context)
10
+
11
+ assert_sql 'SELECT _00.first_name ' +
12
+ 'FROM clients AS _00 ' +
13
+ 'WHERE _00.id = :_param00', result
14
+ assert_equal 1, context.parameters.size
15
+ assert_equal 'Cook', context.parameters['_param00']
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,26 @@
1
+ require_relative 'minitest_helper'
2
+ require_relative 'query_test'
3
+
4
+ module Qdsl
5
+ class EndToEndTest < QueryTest
6
+ def test_aliases
7
+ query = Qdsl::select(
8
+ Column[:id].as(:alias0),
9
+ Column['first_name'].as(:alias1),
10
+ Column.new(:last_name).as(:alias2)
11
+ ).from(@clients)
12
+ result = query.render
13
+
14
+ assert_sql 'SELECT _00.id AS alias0, _00.first_name AS alias1, _00.last_name AS alias2 FROM clients AS _00', result
15
+ end
16
+
17
+ def test_unknown_column
18
+ query = Qdsl::select(:unknown).from(@clients)
19
+
20
+ assert_raises RuntimeError do
21
+ query.render
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,108 @@
1
+ require_relative 'minitest_helper'
2
+
3
+ module Qdsl
4
+ class ExpressionTest < QueryTest
5
+ def test_simple_and
6
+ query = Qdsl::select(:id).
7
+ from(@clients).
8
+ where { |t| t.first_name.equals('Richard').and(t.last_name.equals('Cook')) }
9
+
10
+ context = Context.new
11
+ result = query.render(context)
12
+
13
+ assert_sql 'SELECT _00.id ' +
14
+ 'FROM clients AS _00 ' +
15
+ 'WHERE (_00.first_name = :_param00) AND (_00.last_name = :_param01)', result
16
+ assert_equal 2, context.parameters.size
17
+ assert_equal 'Richard', context.parameters['_param00']
18
+ assert_equal 'Cook', context.parameters['_param01']
19
+ end
20
+
21
+ def test_multiple_and
22
+ query = Qdsl::select(:id).
23
+ from(@clients).
24
+ where { |t| t.first_name.equals('Richard').and(t.last_name.equals('Cook')).and(t.id.equals('ID')) }
25
+
26
+ context = Context.new
27
+ result = query.render(context)
28
+
29
+ assert_sql 'SELECT _00.id ' +
30
+ 'FROM clients AS _00 ' +
31
+ 'WHERE (_00.first_name = :_param00) AND (_00.last_name = :_param01) AND (_00.id = :_param02)', result
32
+ assert_equal 3, context.parameters.size
33
+ assert_equal 'Richard', context.parameters['_param00']
34
+ assert_equal 'Cook', context.parameters['_param01']
35
+ assert_equal 'ID', context.parameters['_param02']
36
+ end
37
+
38
+ def test_simple_or
39
+ query = Qdsl::select(:id).
40
+ from(@clients).
41
+ where { |t| t.first_name.equals('Richard').or(t.last_name.equals('Cook')) }
42
+
43
+ context = Context.new
44
+ result = query.render(context)
45
+
46
+ assert_sql 'SELECT _00.id ' +
47
+ 'FROM clients AS _00 ' +
48
+ 'WHERE (_00.first_name = :_param00) OR (_00.last_name = :_param01)', result
49
+ assert_equal 2, context.parameters.size
50
+ assert_equal 'Richard', context.parameters['_param00']
51
+ assert_equal 'Cook', context.parameters['_param01']
52
+ end
53
+
54
+ def test_multiple_or
55
+ query = Qdsl::select(:id).
56
+ from(@clients).
57
+ where { |t| t.first_name.equals('Richard').or(t.last_name.equals('Cook')).or(t.id.equals('ID')) }
58
+
59
+ context = Context.new
60
+ result = query.render(context)
61
+
62
+ assert_sql 'SELECT _00.id ' +
63
+ 'FROM clients AS _00 ' +
64
+ 'WHERE (_00.first_name = :_param00) OR (_00.last_name = :_param01) OR (_00.id = :_param02)', result
65
+ assert_equal 3, context.parameters.size
66
+ assert_equal 'Richard', context.parameters['_param00']
67
+ assert_equal 'Cook', context.parameters['_param01']
68
+ assert_equal 'ID', context.parameters['_param02']
69
+ end
70
+
71
+ def big_or(ids, t)
72
+ ids.collect { |x| t[x].equals(true) }.inject { |acc, x| acc.or(x) }
73
+ end
74
+
75
+ def test_multiple_or2
76
+ query = Qdsl::select(:id).
77
+ from(@clients).
78
+ where { |t| big_or([:value0, :value1, :value2, :value3], t) }
79
+ result = query.render
80
+
81
+ assert_sql 'SELECT _00.id ' +
82
+ 'FROM clients AS _00 ' +
83
+ 'WHERE (_00.value0 = TRUE) OR (_00.value1 = TRUE) OR (_00.value2 = TRUE) OR (_00.value3 = TRUE)', result
84
+ end
85
+
86
+ def test_boolean
87
+ query = Qdsl::select(:id).
88
+ from(@clients).
89
+ where { |t| t.first_name.equals('Richard').or(t.last_name.equals('Cook')).and(t.id.equals('ID').or(t.id.equals('OTHER_ID'))) }
90
+
91
+ context = Context.new
92
+ result = query.render(context)
93
+
94
+ assert_sql 'SELECT _00.id ' +
95
+ 'FROM clients AS _00 ' +
96
+ 'WHERE ((_00.first_name = :_param00) OR ' +
97
+ '(_00.last_name = :_param01)) ' +
98
+ 'AND ((_00.id = :_param02) OR ' +
99
+ '(_00.id = :_param03))', result
100
+ assert_equal 4, context.parameters.size
101
+ assert_equal 'Richard', context.parameters['_param00']
102
+ assert_equal 'Cook', context.parameters['_param01']
103
+ assert_equal 'ID', context.parameters['_param02']
104
+ assert_equal 'OTHER_ID', context.parameters['_param03']
105
+ end
106
+ end
107
+ end
108
+
@@ -0,0 +1,65 @@
1
+ require_relative 'minitest_helper'
2
+
3
+ module Qdsl
4
+ class InnerJoinTest < QueryTest
5
+ def test_inner_join_simple
6
+ query = Qdsl::select { |t0, t1| [t0.id, t1.member_id] }.
7
+ from(@clients).
8
+ inner_join(@members).on { |f, j| f.id.equals(j.id) }
9
+ result = query.render
10
+
11
+ assert_sql 'SELECT _00.id, _01.member_id ' +
12
+ 'FROM clients AS _00 ' +
13
+ 'INNER JOIN members AS _01 ON _00.id = _01.id', result
14
+ end
15
+
16
+ def test_inner_join
17
+ query = Qdsl::select { |t0, t1| [t0.id, t1.member_id] }.
18
+ from(@clients).
19
+ inner_join(@members).on { |t0, t1| t0.id.equals(t1.first_name) }
20
+ result = query.render
21
+
22
+ assert_sql 'SELECT _00.id, _01.member_id FROM clients AS _00 INNER JOIN members AS _01 ON _00.id = _01.first_name', result
23
+ end
24
+
25
+ def test_multiple_inner_joins
26
+ query = Qdsl::select { |t0, t1, t2| [t0.id, t1.member_id, t2.name] }.
27
+ from(@clients).
28
+ inner_join(@members).on { |f, j| f.id.equals(j.first_name) }.
29
+ inner_join(@colours).on { |f, j| f.first_name.equals(j.name) }
30
+ result = query.render
31
+
32
+ assert_sql 'SELECT _00.id, _01.member_id, _02.name ' +
33
+ 'FROM clients AS _00 ' +
34
+ 'INNER JOIN members AS _01 ON _00.id = _01.first_name ' +
35
+ 'INNER JOIN colours AS _02 ON _00.first_name = _02.name', result
36
+ end
37
+
38
+ def test_multiple_inner_join_same
39
+ query = Qdsl::select { |t0, t1| [t0.id, t1.id] }.
40
+ from(@clients).
41
+ inner_join(@clients).on { |f, j| f.id.equals(j.first_name) }
42
+ result = query.render
43
+
44
+ assert_sql 'SELECT _00.id, _01.id FROM ' +
45
+ 'clients AS _00 ' +
46
+ 'INNER JOIN clients AS _01 ON _00.id = _01.first_name', result
47
+ end
48
+
49
+ def test_multiple_inner_join_same_where
50
+ query = Qdsl::select { |t0, t1, t2| [t0.id, t1.id] }.
51
+ from(@clients).
52
+ inner_join(@clients).on { |f, j| f.id.equals(j.first_name) }.
53
+ inner_join(@clients).on { |f, j| f.id.equals(j.first_name) }.
54
+ where { |t0, t1, t2| t0.id.equals(t1.id) }
55
+ result = query.render
56
+
57
+ assert_sql 'SELECT _00.id, _01.id FROM ' +
58
+ 'clients AS _00 ' +
59
+ 'INNER JOIN clients AS _01 ON _00.id = _01.first_name ' +
60
+ 'INNER JOIN clients AS _02 ON _00.id = _02.first_name ' +
61
+ 'WHERE _00.id = _01.id', result
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'qdsl'
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest/pride'
6
+
@@ -0,0 +1,40 @@
1
+ require_relative 'minitest_helper'
2
+
3
+ module Qdsl
4
+ class QueryTest < Minitest::Test
5
+ def setup
6
+ @clients = Table.new(
7
+ :clients,
8
+ :id,
9
+ :first_name,
10
+ :last_name,
11
+ :value0,
12
+ :value1,
13
+ :value2,
14
+ :value3
15
+ )
16
+ @members = Table.new(
17
+ :members,
18
+ :id,
19
+ :first_name,
20
+ :middle_name,
21
+ :last_name,
22
+ :member_id
23
+ )
24
+ @colours = Table.new(
25
+ :colours,
26
+ :id,
27
+ :name
28
+ )
29
+ end
30
+
31
+ def assert_sql(expected, actual)
32
+ expected.gsub! /\s*\n\s*/, ' '
33
+ expected.strip!
34
+ actual.gsub! /\s*\n\s*/, ' '
35
+ actual.strip!
36
+ assert_equal expected, actual
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,96 @@
1
+ require_relative 'minitest_helper'
2
+
3
+ module Qdsl
4
+ class SelectTest < QueryTest
5
+ def test_single_table_select_single_column_string
6
+ query = Qdsl::select('id').from(@clients)
7
+ result = query.render
8
+ assert_sql 'SELECT _00.id FROM clients AS _00', result
9
+ end
10
+
11
+ def test_single_table_select_single_column_symbol
12
+ query = Qdsl::select(:id).from(@clients)
13
+ result = query.render
14
+ assert_sql 'SELECT _00.id FROM clients AS _00', result
15
+ end
16
+
17
+ def test_single_table_select_single_column_method
18
+ query = Qdsl::select { |t| t.id }.from(@clients)
19
+ result = query.render
20
+ assert_sql 'SELECT _00.id FROM clients AS _00', result
21
+ end
22
+
23
+ def test_single_table_select_single_column_name_string
24
+ query = Qdsl::select { |t| t['id'] }.from(@clients)
25
+ result = query.render
26
+ assert_sql 'SELECT _00.id FROM clients AS _00', result
27
+ end
28
+
29
+ def test_single_table_select_single_column_name_symbol
30
+ query = Qdsl::select { |t| t[:id] }.from(@clients)
31
+ result = query.render
32
+ assert_sql 'SELECT _00.id FROM clients AS _00', result
33
+ end
34
+
35
+ def test_single_table_select_two_columns_string
36
+ query = Qdsl::select('id', 'first_name').from(@clients)
37
+ result = query.render
38
+ assert_sql 'SELECT _00.id, _00.first_name FROM clients AS _00', result
39
+ end
40
+
41
+ def test_single_table_select_two_columns_symbol
42
+ query = Qdsl::select(:id, :first_name).from(@clients)
43
+ result = query.render
44
+ assert_sql 'SELECT _00.id, _00.first_name FROM clients AS _00', result
45
+ end
46
+
47
+ def test_single_table_select_two_columns_method
48
+ query = Qdsl::select { |t| [t.id, t.first_name] }.from(@clients)
49
+ result = query.render
50
+ assert_sql 'SELECT _00.id, _00.first_name FROM clients AS _00', result
51
+ end
52
+
53
+ def test_single_table_select_two_columns_name_string
54
+ query = Qdsl::select { |t| [t['id'], t['first_name']] }.from(@clients)
55
+ result = query.render
56
+ assert_sql 'SELECT _00.id, _00.first_name FROM clients AS _00', result
57
+ end
58
+
59
+ def test_single_table_select_two_columns_name_symbol
60
+ query = Qdsl::select { |t| [t[:id], t[:first_name]] }.from(@clients)
61
+ result = query.render
62
+ assert_sql 'SELECT _00.id, _00.first_name FROM clients AS _00', result
63
+ end
64
+
65
+ def test_two_tables_select_single_column_string
66
+ query = Qdsl::select('id').from(@clients).inner_join(@members).on { |t0, t1| t0.id.equals(t1.id) }
67
+ result = query.render
68
+ assert_sql 'SELECT _00.id FROM clients AS _00 INNER JOIN members AS _01 ON _00.id = _01.id', result
69
+ end
70
+
71
+ def test_two_tables_select_single_column_symbol
72
+ query = Qdsl::select(:id).from(@clients).inner_join(@members).on { |t0, t1| t0.id.equals(t1.id) }
73
+ result = query.render
74
+ assert_sql 'SELECT _00.id FROM clients AS _00 INNER JOIN members AS _01 ON _00.id = _01.id', result
75
+ end
76
+
77
+ def test_two_tables_select_single_column_method
78
+ query = Qdsl::select { |t0, t1| t0.id }.from(@clients).inner_join(@members).on { |t0, t1| t0.id.equals(t1.id) }
79
+ result = query.render
80
+ assert_sql 'SELECT _00.id FROM clients AS _00 INNER JOIN members AS _01 ON _00.id = _01.id', result
81
+ end
82
+
83
+ def test_two_tables_select_single_column_name_string
84
+ query = Qdsl::select { |t0, t1| t0['id'] }.from(@clients).inner_join(@members).on { |t0, t1| t0.id.equals(t1.id) }
85
+ result = query.render
86
+ assert_sql 'SELECT _00.id FROM clients AS _00 INNER JOIN members AS _01 ON _00.id = _01.id', result
87
+ end
88
+
89
+ def test_two_tables_select_single_column_name_symbol
90
+ query = Qdsl::select { |t0, t1| t0[:id] }.from(@clients).inner_join(@members).on { |t0, t1| t0.id.equals(t1.id) }
91
+ result = query.render
92
+ assert_sql 'SELECT _00.id FROM clients AS _00 INNER JOIN members AS _01 ON _00.id = _01.id', result
93
+ end
94
+ end
95
+ end
96
+
@@ -0,0 +1,66 @@
1
+ require_relative 'minitest_helper'
2
+
3
+ module Qdsl
4
+ class SubqueryTest < QueryTest
5
+ def test_simple
6
+ subquery = Qdsl::select(:id, :first_name, :last_name).from(@clients)
7
+ query = Qdsl::select(:first_name).from(subquery)
8
+
9
+ assert_sql <<-EOL, query.render
10
+ SELECT _00.first_name
11
+ FROM (
12
+ SELECT _01.id, _01.first_name, _01.last_name
13
+ FROM clients AS _01
14
+ ) AS _00
15
+ EOL
16
+ end
17
+
18
+ def test_subquery_inner_join
19
+ subquery = Qdsl::select(:id, :first_name, :last_name).from(@clients)
20
+ query = Qdsl::select { |t0, t1| [t0.first_name, t1.last_name] }.
21
+ from(subquery).
22
+ inner_join(subquery).on { |t0, t1| t0.id.equals(t1.id) }
23
+
24
+ assert_sql <<-EOL, query.render
25
+ SELECT _00.first_name, _01.last_name
26
+ FROM (
27
+ SELECT _02.id, _02.first_name, _02.last_name
28
+ FROM clients AS _02
29
+ ) AS _00
30
+ INNER JOIN (
31
+ SELECT _03.id, _03.first_name, _03.last_name
32
+ FROM clients AS _03
33
+ ) AS _01
34
+ ON _00.id = _01.id
35
+ EOL
36
+ end
37
+
38
+ def test_subquery_inner_join_where
39
+ subquery = Qdsl::select(:id, :first_name, :last_name).from(@clients)
40
+ query = Qdsl::select { |t0, t1| [t0.first_name, t1.last_name] }.
41
+ from(subquery).
42
+ inner_join(subquery).on { |t0, t1| t0.id.equals(t1.id) }.
43
+ where { |t0, t1| t0.first_name.equals('str0').and(t1.last_name.equals('str1')) }
44
+
45
+ context = Context.new
46
+ assert_sql <<-EOL, query.render(context)
47
+ SELECT _00.first_name, _01.last_name
48
+ FROM (
49
+ SELECT _02.id, _02.first_name, _02.last_name
50
+ FROM clients AS _02
51
+ ) AS _00
52
+ INNER JOIN (
53
+ SELECT _03.id, _03.first_name, _03.last_name
54
+ FROM clients AS _03
55
+ ) AS _01
56
+ ON _00.id = _01.id
57
+ WHERE (_00.first_name = :_param00) AND (_01.last_name = :_param01)
58
+ EOL
59
+
60
+ assert_equal 2, context.parameters.size
61
+ assert_equal 'str0', context.parameters['_param00']
62
+ assert_equal 'str1', context.parameters['_param01']
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,21 @@
1
+ require_relative 'minitest_helper'
2
+
3
+ module Qdsl
4
+ class WhereTest < QueryTest
5
+ def test_simple_where
6
+ query = Qdsl::select(:first_name).
7
+ from(@clients).
8
+ where { |t| t.id.equals('Cook') }
9
+
10
+ context = Context.new
11
+ result = query.render(context)
12
+
13
+ assert_sql 'SELECT _00.first_name ' +
14
+ 'FROM clients AS _00 ' +
15
+ 'WHERE _00.id = :_param00', result
16
+ assert_equal 1, context.parameters.size
17
+ assert_equal 'Cook', context.parameters['_param00']
18
+ end
19
+ end
20
+ end
21
+
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qdsl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Richard Cook
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Think of it as "hygienic macros for SQL
56
+ email:
57
+ - rcook@rcook.org
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/and.rb
69
+ - lib/boolean.rb
70
+ - lib/column.rb
71
+ - lib/context.rb
72
+ - lib/equals.rb
73
+ - lib/expression.rb
74
+ - lib/funky_proxy.rb
75
+ - lib/inner_join.rb
76
+ - lib/inner_join_builder.rb
77
+ - lib/or.rb
78
+ - lib/qdsl.rb
79
+ - lib/qdsl/version.rb
80
+ - lib/select.rb
81
+ - lib/table.rb
82
+ - lib/table_query.rb
83
+ - qdsl.gemspec
84
+ - test/column_test.rb
85
+ - test/context_test.rb
86
+ - test/end_to_end_test.rb
87
+ - test/expression_test.rb
88
+ - test/inner_join_test.rb
89
+ - test/minitest_helper.rb
90
+ - test/query_test.rb
91
+ - test/select_test.rb
92
+ - test/subquery_test.rb
93
+ - test/where_test.rb
94
+ homepage: ''
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.4.5
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: SQL query builder DSL for Ruby
118
+ test_files:
119
+ - test/column_test.rb
120
+ - test/context_test.rb
121
+ - test/end_to_end_test.rb
122
+ - test/expression_test.rb
123
+ - test/inner_join_test.rb
124
+ - test/minitest_helper.rb
125
+ - test/query_test.rb
126
+ - test/select_test.rb
127
+ - test/subquery_test.rb
128
+ - test/where_test.rb