qdsl 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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