selekt 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +13 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +109 -0
- data/Rakefile +9 -0
- data/lib/selekt.rb +50 -0
- data/lib/selekt/query.rb +58 -0
- data/lib/selekt/source_stub.rb +60 -0
- data/lib/selekt/sql.rb +29 -0
- data/lib/selekt/sql.treetop +471 -0
- data/lib/selekt/version.rb +3 -0
- data/selekt.gemspec +24 -0
- data/test/parser_test.rb +164 -0
- data/test/query_test.rb +57 -0
- data/test/source_stub_test.rb +62 -0
- data/test/sql_toolkit_test.rb +10 -0
- data/test/test_helper.rb +9 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9f0c51f50abfadf14590ea0dd7a265758f8137e6
|
4
|
+
data.tar.gz: 326f21878ee0351f692ef97eada92fdd0750c343
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d664f245f8964a696a77a5a47d9b692b3b0126e9fcdc920a24708b238eaa79094eb7eb5cb9046fd3cc87122beb303d9697150cf6bd8490442646d08941536b30
|
7
|
+
data.tar.gz: 84475d3136ae0903eabd60bf1c94f1534bbf217c0ef7692c613b13aba62623815cef0b96e90d9ef307694d1fc3b9edb5da817b4558a6173548f4034e50c973ef
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013, 2014 Willem van Bergen
|
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,109 @@
|
|
1
|
+
# Selekt [![Build Status](https://travis-ci.org/wvanbergen/selekt.png)](https://travis-ci.org/wvanbergen/selekt)
|
2
|
+
|
3
|
+
A toolkit to work with SQL queries, mostly for building test suites around
|
4
|
+
applications that use complex SQL queries. It includes:
|
5
|
+
|
6
|
+
- A SQL syntax parser, with abstract syntax tree manipulation tools.
|
7
|
+
- Stubbing tools to replace tables and subqueries in queries with static data.
|
8
|
+
|
9
|
+
The main use case for this library is building test suites for applications with
|
10
|
+
a complex database, for which it is infeasible to load fixtures data, due to
|
11
|
+
performance or permission issues.
|
12
|
+
|
13
|
+
Personally, I have used it to test complicated view definitions for data
|
14
|
+
modeling purposes. Also, I have used it to speed up tests by replace the parts in
|
15
|
+
a SQL query that would require disk access by a static stub.
|
16
|
+
|
17
|
+
The SQL syntax that is supported by the parser is ANSI SQL, with some support
|
18
|
+
for PostgreSQL and Vertica extensions.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile: `gem 'selekt'`
|
23
|
+
and run `bundle install`.
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Testing a complex query using stubs:
|
28
|
+
|
29
|
+
``` ruby
|
30
|
+
# Say we have this view definition, to get a list of your customers
|
31
|
+
# and whether they had at least one sale last month:
|
32
|
+
view_definition = <<-SQL
|
33
|
+
SELECT c.name, COUNT(s.sale_id) >= 1 AS active
|
34
|
+
FROM customers c
|
35
|
+
LEFT JOIN sales s ON s.customer_id = c.customer_id
|
36
|
+
AND s.timestamp >= NOW() - INTERVAL '1 MONTH'
|
37
|
+
GROUP BY c.customer_id
|
38
|
+
SQL
|
39
|
+
|
40
|
+
# To test this definition for different datasets in the
|
41
|
+
# customers and sales table, we would have to load different
|
42
|
+
# fixture sets, which would be hard and slow. Let's stub them
|
43
|
+
# out instead.
|
44
|
+
|
45
|
+
query = Selekt.parse(view_definition)
|
46
|
+
|
47
|
+
customers = Selekt::SourceStub.new(:customer_id, :name)
|
48
|
+
customers << [1, "Willem"]
|
49
|
+
|
50
|
+
single_sale = Selekt::SourceStub.new(:sale_id, :customer_id, :timestamp)
|
51
|
+
single_sale << [1, 1, Time.now]
|
52
|
+
|
53
|
+
# Replace the c and s source (the customers and sales tables) with our stubs
|
54
|
+
stubbed_query = query.stub('c', customers).stub('s', single_sale)
|
55
|
+
|
56
|
+
# Now, run the resulting query against your test DB to assert the right behavior.
|
57
|
+
result = db.query(stubbed_query.sql)
|
58
|
+
assert_equal 1, result.rows.length
|
59
|
+
assert_equal true, result.rows[0][:active]
|
60
|
+
|
61
|
+
# Now let's try it with a sale that should not be counted.
|
62
|
+
old_sale = Selekt::SourceStub.new(:sale_id, :customer_id, :timestamp)
|
63
|
+
old_sale << [1, 1, Time.now - 2.months]
|
64
|
+
stubbed_query = query.stub('c', customers).stub('s', old_sale)
|
65
|
+
|
66
|
+
result = db.query(stubbed_query.sql)
|
67
|
+
assert_equal 1, result.rows.length
|
68
|
+
assert_equal false, result.rows[0][:active]
|
69
|
+
|
70
|
+
# Finally, let's try it with an unrelated sale
|
71
|
+
no_sale = Selekt::SourceStub.new(:sale_id, :customer_id, :timestamp)
|
72
|
+
no_sale << [1, 2, Time.now] # use a different customer_id
|
73
|
+
stubbed_query = query.stub('c', customers).stub('s', no_sale)
|
74
|
+
|
75
|
+
result = db.query(stubbed_query.sql)
|
76
|
+
assert_equal 1, result.rows.length
|
77
|
+
assert_equal false, result.rows[0][:active] # is this going to pass?
|
78
|
+
```
|
79
|
+
|
80
|
+
This way, you can easily quickly test the behavior of your SQL queries, with
|
81
|
+
different sets of source data, without having to load different sets of
|
82
|
+
fixtures. This is a lot faster and you won't need data loading permissions
|
83
|
+
to run these tests.
|
84
|
+
|
85
|
+
### SourceStub
|
86
|
+
|
87
|
+
You don't have to use a `Selekt::SourceStub` object when calling
|
88
|
+
`query.stub(name, stub)`; any SQL query that the library can parse will be
|
89
|
+
accepted. A source stub will simply generate a SQL query by joining
|
90
|
+
a static SELECT query for every row using UNION ALL:
|
91
|
+
|
92
|
+
``` ruby
|
93
|
+
customers = Selekt::SourceStub.new(:customer_id, :name)
|
94
|
+
customers << [1, "Willem"]
|
95
|
+
customers << [2, "Aaron"]
|
96
|
+
customers.sql
|
97
|
+
|
98
|
+
# SELECT 1 AS customer_id, 'Willem' AS name
|
99
|
+
# UNION ALL
|
100
|
+
# SELECT 2 AS customer_id, 'Aaron' AS name
|
101
|
+
```
|
102
|
+
|
103
|
+
## Contributing
|
104
|
+
|
105
|
+
1. Fork it, and create your feature branch (`git checkout -b my-new-feature`)
|
106
|
+
2. Implement your changes and make sure there is test coverage for them.
|
107
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
108
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
109
|
+
5. Create new pull request, and ping @wvanbergen.
|
data/Rakefile
ADDED
data/lib/selekt.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "treetop"
|
2
|
+
require "date"
|
3
|
+
|
4
|
+
module Selekt
|
5
|
+
extend self
|
6
|
+
|
7
|
+
RESERVED_SQL_KEYWORDS = [
|
8
|
+
'select', 'from', 'where', 'group', 'order', 'having', 'union', 'all',
|
9
|
+
'limit', 'offset', 'as', 'by', 'with', 'distinct',
|
10
|
+
'left', 'right', 'inner', 'full', 'outer', 'join', 'on', 'using', 'natural',
|
11
|
+
'case', 'when', 'then', 'else', 'end', 'interval',
|
12
|
+
'over', 'partition', 'range', 'rows', 'window'
|
13
|
+
]
|
14
|
+
|
15
|
+
class ParseError < StandardError; end
|
16
|
+
class StubError < StandardError; end
|
17
|
+
|
18
|
+
def parser
|
19
|
+
@parser ||= begin
|
20
|
+
Treetop.load(File.expand_path('./selekt/sql.treetop', File.dirname(__FILE__)))
|
21
|
+
Selekt::SQLParser.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse(sql)
|
26
|
+
Selekt::Query.new(sql)
|
27
|
+
end
|
28
|
+
|
29
|
+
def safe_identifier(id)
|
30
|
+
id =~ /\A[a-z][a-z0-9_]*\z/i ? id : '"' + id.gsub('"', '""') + '"'
|
31
|
+
end
|
32
|
+
|
33
|
+
def quote(val)
|
34
|
+
case val
|
35
|
+
when NilClass; 'NULL'
|
36
|
+
when TrueClass; 'TRUE'
|
37
|
+
when FalseClass; 'FALSE'
|
38
|
+
when Numeric; val.to_s
|
39
|
+
when String; "'" + val.gsub("'", "''") + "'"
|
40
|
+
when DateTime, Time; quote(val.strftime('%F %X')) + '::timestamp'
|
41
|
+
when Date; quote(val.strftime('%F')) + '::date'
|
42
|
+
else raise "Don't know how to quote #{val.inspect}!"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
require "selekt/version"
|
48
|
+
require "selekt/sql"
|
49
|
+
require "selekt/query"
|
50
|
+
require "selekt/source_stub"
|
data/lib/selekt/query.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
class Selekt::Query
|
2
|
+
|
3
|
+
class Relation < Struct.new(:schema_name, :table_name)
|
4
|
+
def to_s
|
5
|
+
if schema_name.nil?
|
6
|
+
Selekt.safe_identifier(table_name)
|
7
|
+
else
|
8
|
+
Selekt.safe_identifier(schema_name) + '.' + Selekt.safe_identifier(table_name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :ast
|
14
|
+
|
15
|
+
def initialize(sql)
|
16
|
+
@ast = Selekt.parser.parse(sql) or raise Selekt::ParseError.new("Could not parse SQL query: #{sql}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def relations
|
20
|
+
find_nodes(ast, Selekt::SQL::TableReference).map { |tr| Relation.new(tr.schema_name, tr.table_name) }.uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
def sources
|
24
|
+
find_nodes(ast, Selekt::SQL::Source)
|
25
|
+
end
|
26
|
+
|
27
|
+
def source_names
|
28
|
+
sources.map(&:variable_name).uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
def stub(source_name, source_stub)
|
32
|
+
stub_sql = source_stub.respond_to?(:sql) ? source_stub.sql : source_stub.to_s
|
33
|
+
self.class.new(render_stubbed_sql(ast, source_name.to_s, stub_sql))
|
34
|
+
end
|
35
|
+
|
36
|
+
def sql
|
37
|
+
ast.input
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def render_stubbed_sql(ast, source, stubbed_query)
|
43
|
+
return ast.text_value if ast.elements.nil?
|
44
|
+
return "(#{stubbed_query}) AS #{source}" if ast.respond_to?(:variable_name) && ast.variable_name == source
|
45
|
+
ast.elements.map { |a| render_stubbed_sql(a, source, stubbed_query) }.join('')
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def find_nodes(ast, ext_module)
|
50
|
+
return [] if ast.elements.nil?
|
51
|
+
results = ast.elements.map do |element|
|
52
|
+
find_nodes(element, ext_module)
|
53
|
+
end.flatten
|
54
|
+
|
55
|
+
results.unshift(ast) if ast.extension_modules.include?(ext_module)
|
56
|
+
return results
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class Selekt::SourceStub
|
2
|
+
|
3
|
+
attr_reader :fields, :rows
|
4
|
+
|
5
|
+
def initialize(*fields)
|
6
|
+
@fields = fields.map { |f| f.to_sym }
|
7
|
+
@rows = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_row(row)
|
11
|
+
if row.is_a?(Hash)
|
12
|
+
@rows << fields.map { |f| row[f] }
|
13
|
+
else
|
14
|
+
raise Selekt::StubError, "Row should have #{fields.size} values maximum" if fields.size < row.size
|
15
|
+
@rows << fields.map.with_index { |_, i| row[i] }
|
16
|
+
end
|
17
|
+
return self
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :<<, :add_row
|
21
|
+
alias_method :push, :add_row
|
22
|
+
|
23
|
+
def add_rows(rows)
|
24
|
+
rows.each { |row| add_row(row) }
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :concat, :add_rows
|
29
|
+
|
30
|
+
def sql
|
31
|
+
first_row_sql = [row_sql_with_names(rows[0])]
|
32
|
+
other_row_sql = rows[1..-1].map { |row| row_sql_without_names(row) }
|
33
|
+
[first_row_sql].concat(other_row_sql).join("\nUNION ALL\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
def size
|
37
|
+
@rows.size
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
return false unless other.is_a?(Selekt::SourceStub)
|
42
|
+
fields == other.fields && rows == other.rows
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method :length, :size
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
def row_sql_with_names(row)
|
50
|
+
'SELECT ' + fields.map.with_index do |field, index|
|
51
|
+
"#{Selekt.quote(row[index])} AS #{Selekt.safe_identifier(field.to_s)}"
|
52
|
+
end.join(', ')
|
53
|
+
end
|
54
|
+
|
55
|
+
def row_sql_without_names(row)
|
56
|
+
'SELECT ' + fields.map.with_index do |field, index|
|
57
|
+
Selekt.quote(row[index])
|
58
|
+
end.join(', ')
|
59
|
+
end
|
60
|
+
end
|
data/lib/selekt/sql.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Selekt
|
2
|
+
module SQL
|
3
|
+
module TableReference
|
4
|
+
def variable_name
|
5
|
+
if elements.last.empty?
|
6
|
+
elements.first.text_value
|
7
|
+
else
|
8
|
+
elements.last.text_value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Source
|
14
|
+
def variable_name
|
15
|
+
if elements.last.empty?
|
16
|
+
elements.first.variable_name
|
17
|
+
else
|
18
|
+
elements.last.variable_name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Alias
|
24
|
+
def variable_name
|
25
|
+
elements.last.text_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,471 @@
|
|
1
|
+
module Selekt
|
2
|
+
|
3
|
+
grammar SQL
|
4
|
+
|
5
|
+
rule sql_query
|
6
|
+
space? select_statement (space order_by_clause)? (space limit_clause)? space?
|
7
|
+
end
|
8
|
+
|
9
|
+
rule select_statement
|
10
|
+
select_clause
|
11
|
+
(space from_clause)?
|
12
|
+
(space where_clause)?
|
13
|
+
(space group_by_clause)?
|
14
|
+
(space having_clause)?
|
15
|
+
(space named_window_clause)?
|
16
|
+
(space set_operator space select_statement)*
|
17
|
+
end
|
18
|
+
|
19
|
+
rule select_clause
|
20
|
+
SELECT space (DISTINCT space)? projection (comma projection)*
|
21
|
+
end
|
22
|
+
|
23
|
+
rule from_clause
|
24
|
+
FROM space source_with_joins (comma source_with_joins)*
|
25
|
+
/
|
26
|
+
FROM space source_with_joins (comma source_with_joins)*
|
27
|
+
end
|
28
|
+
|
29
|
+
rule source_with_joins
|
30
|
+
source (space source_join)*
|
31
|
+
/
|
32
|
+
'(' source (space source_join)* ')'
|
33
|
+
end
|
34
|
+
|
35
|
+
rule source
|
36
|
+
((table <TableReference>) (alias <Alias>)? / subquery (alias <Alias>)) <Source>
|
37
|
+
end
|
38
|
+
|
39
|
+
rule source_join
|
40
|
+
regular_join_type space source space join_clause
|
41
|
+
end
|
42
|
+
|
43
|
+
rule regular_join_type
|
44
|
+
((INNER / LEFT / RIGHT / FULL space OUTER) space)? JOIN
|
45
|
+
end
|
46
|
+
|
47
|
+
rule join_clause
|
48
|
+
ON space boolean_expression
|
49
|
+
/
|
50
|
+
USING space? '(' space? identifier (comma identifier)* ')'
|
51
|
+
end
|
52
|
+
|
53
|
+
rule comma
|
54
|
+
space? ',' space?
|
55
|
+
end
|
56
|
+
|
57
|
+
rule subquery
|
58
|
+
'(' space? select_statement space? ')'
|
59
|
+
end
|
60
|
+
|
61
|
+
rule table
|
62
|
+
schema_table / schemaless_table
|
63
|
+
end
|
64
|
+
|
65
|
+
rule schemaless_table
|
66
|
+
identifier {
|
67
|
+
def schema_name
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def table_name
|
72
|
+
elements[0].value
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
rule schema_table
|
78
|
+
schema '.' identifier {
|
79
|
+
def schema_name
|
80
|
+
schema.value
|
81
|
+
end
|
82
|
+
|
83
|
+
def table_name
|
84
|
+
elements.last.value
|
85
|
+
end
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
rule schema
|
90
|
+
identifier
|
91
|
+
end
|
92
|
+
|
93
|
+
rule column
|
94
|
+
table_column / identifier
|
95
|
+
end
|
96
|
+
|
97
|
+
rule table_column
|
98
|
+
identifier '.' identifier
|
99
|
+
end
|
100
|
+
|
101
|
+
rule projection
|
102
|
+
'*' / table '.' '*' / expression alias?
|
103
|
+
end
|
104
|
+
|
105
|
+
rule alias
|
106
|
+
space (AS space)? identifier
|
107
|
+
end
|
108
|
+
|
109
|
+
rule where_clause
|
110
|
+
WHERE space expression
|
111
|
+
end
|
112
|
+
|
113
|
+
rule group_by_clause
|
114
|
+
GROUP space BY space expression (comma expression)*
|
115
|
+
end
|
116
|
+
|
117
|
+
rule having_clause
|
118
|
+
HAVING space expression
|
119
|
+
end
|
120
|
+
|
121
|
+
rule order_by_clause
|
122
|
+
ORDER space BY space order_expression (comma order_expression)*
|
123
|
+
end
|
124
|
+
|
125
|
+
rule limit_clause
|
126
|
+
LIMIT space integer (space OFFSET space integer)?
|
127
|
+
end
|
128
|
+
|
129
|
+
rule order_expression
|
130
|
+
expression (space (ASC / DESC) (space NULLS space (FIRST / LAST))?)?
|
131
|
+
end
|
132
|
+
|
133
|
+
rule expression
|
134
|
+
boolean_expression
|
135
|
+
end
|
136
|
+
|
137
|
+
rule single_expression
|
138
|
+
(interval_expression / case_expression / function_call / column / literal / subquery / '(' space? expression space? ')')
|
139
|
+
(space? '::' space? type)?
|
140
|
+
end
|
141
|
+
|
142
|
+
rule case_expression
|
143
|
+
CASE space
|
144
|
+
(expression space)?
|
145
|
+
(WHEN space expression space THEN space expression space)+
|
146
|
+
(ELSE space expression space)?
|
147
|
+
END
|
148
|
+
end
|
149
|
+
|
150
|
+
rule interval_expression
|
151
|
+
INTERVAL space expression
|
152
|
+
end
|
153
|
+
|
154
|
+
rule type
|
155
|
+
unquoted_identifier
|
156
|
+
end
|
157
|
+
|
158
|
+
rule function_call
|
159
|
+
(
|
160
|
+
COUNT '(' space? (DISTINCT space)? ('*' / expression) space? ')'
|
161
|
+
/
|
162
|
+
unquoted_identifier '(' (space? expression (comma expression)*)? space? ')'
|
163
|
+
)
|
164
|
+
(space OVER (space? window_clause / space identifier))?
|
165
|
+
end
|
166
|
+
|
167
|
+
rule window_clause
|
168
|
+
'(' space?
|
169
|
+
(partition_by_clause space)?
|
170
|
+
order_by_clause
|
171
|
+
space? ')'
|
172
|
+
end
|
173
|
+
|
174
|
+
rule named_window_clause
|
175
|
+
WINDOW space identifier space AS space? window_clause
|
176
|
+
end
|
177
|
+
|
178
|
+
rule partition_by_clause
|
179
|
+
PARTITION space BY space expression (comma expression)*
|
180
|
+
end
|
181
|
+
|
182
|
+
rule arithmetic_expression
|
183
|
+
single_expression (space? arithmetic_operator space? single_expression)*
|
184
|
+
end
|
185
|
+
|
186
|
+
rule arithmetic_operator
|
187
|
+
'+' / '-' / '*' / '/' / '%' / '||'
|
188
|
+
end
|
189
|
+
|
190
|
+
rule comparison_expression
|
191
|
+
EXISTS space? subquery
|
192
|
+
/
|
193
|
+
arithmetic_expression space IS (space NOT)? space boolean
|
194
|
+
/
|
195
|
+
arithmetic_expression space (NOT space)? IN space list_of_values
|
196
|
+
/
|
197
|
+
arithmetic_expression (space? comparison_operator space? arithmetic_expression)?
|
198
|
+
end
|
199
|
+
|
200
|
+
rule list_of_values
|
201
|
+
subquery
|
202
|
+
/
|
203
|
+
'(' space? expression (comma expression)* space? ')'
|
204
|
+
end
|
205
|
+
|
206
|
+
rule comparison_operator
|
207
|
+
'<=' / '>=' / '<>' / '>' / '<' / '!=' / '=' / LIKE / ILIKE
|
208
|
+
end
|
209
|
+
|
210
|
+
rule negatable_expression
|
211
|
+
(NOT space)? comparison_expression
|
212
|
+
end
|
213
|
+
|
214
|
+
rule boolean_expression
|
215
|
+
negatable_expression (space boolean_operator space negatable_expression)*
|
216
|
+
end
|
217
|
+
|
218
|
+
rule boolean_operator
|
219
|
+
AND / OR
|
220
|
+
end
|
221
|
+
|
222
|
+
rule set_operator
|
223
|
+
UNION (space ALL)?
|
224
|
+
end
|
225
|
+
|
226
|
+
rule space
|
227
|
+
[ \t\r\n]+ (comment space?)?
|
228
|
+
end
|
229
|
+
|
230
|
+
rule comment
|
231
|
+
'--' [^\r\n]*
|
232
|
+
end
|
233
|
+
|
234
|
+
rule identifier
|
235
|
+
quoted_identifier / unquoted_identifier !{|seq| seq[0].reserved? } {
|
236
|
+
def value
|
237
|
+
elements.first.value
|
238
|
+
end
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
rule unquoted_identifier
|
243
|
+
[A-Za-z] [_A-Za-z0-9]*
|
244
|
+
{
|
245
|
+
def value
|
246
|
+
text_value
|
247
|
+
end
|
248
|
+
|
249
|
+
def reserved?
|
250
|
+
Selekt::RESERVED_SQL_KEYWORDS.include?(text_value.downcase)
|
251
|
+
end
|
252
|
+
}
|
253
|
+
end
|
254
|
+
|
255
|
+
rule quoted_identifier
|
256
|
+
'"' body:('""' / [^"])* '"' {
|
257
|
+
def value
|
258
|
+
body.text_value.gsub('""', '"')
|
259
|
+
end
|
260
|
+
}
|
261
|
+
end
|
262
|
+
|
263
|
+
rule literal
|
264
|
+
string / float / integer / boolean
|
265
|
+
end
|
266
|
+
|
267
|
+
rule string
|
268
|
+
"'" ("''" / [^'])* "'"
|
269
|
+
end
|
270
|
+
|
271
|
+
rule boolean
|
272
|
+
TRUE / FALSE / NULL
|
273
|
+
end
|
274
|
+
|
275
|
+
rule integer
|
276
|
+
"-"? [0-9]+
|
277
|
+
end
|
278
|
+
|
279
|
+
rule float
|
280
|
+
"-"? [0-9]* '.' [0-9]+
|
281
|
+
end
|
282
|
+
|
283
|
+
rule SELECT
|
284
|
+
[Ss] [Ee] [Ll] [Ee] [Cc] [Tt]
|
285
|
+
end
|
286
|
+
|
287
|
+
rule DISTINCT
|
288
|
+
[Dd] [Ii] [Ss] [Tt] [Ii] [Nn] [Cc] [Tt]
|
289
|
+
end
|
290
|
+
|
291
|
+
rule FROM
|
292
|
+
[Ff] [Rr] [Oo] [Mm]
|
293
|
+
end
|
294
|
+
|
295
|
+
rule HAVING
|
296
|
+
[Hh] [Aa] [Vv] [Ii] [Nn] [Gg]
|
297
|
+
end
|
298
|
+
|
299
|
+
rule ORDER
|
300
|
+
[Oo] [Rr] [Dd] [Ee] [Rr]
|
301
|
+
end
|
302
|
+
|
303
|
+
rule ASC
|
304
|
+
[Aa] [Ss] [Cc]
|
305
|
+
end
|
306
|
+
|
307
|
+
rule DESC
|
308
|
+
[Dd] [Ee] [Ss] [Cc]
|
309
|
+
end
|
310
|
+
|
311
|
+
rule NULLS
|
312
|
+
[Nn] [Uu] [Ll] [Ll] [Ss]
|
313
|
+
end
|
314
|
+
|
315
|
+
rule FIRST
|
316
|
+
[Ff] [Ii] [Rr] [Ss] [Tt]
|
317
|
+
end
|
318
|
+
|
319
|
+
rule LAST
|
320
|
+
[Ll] [Aa] [Ss] [Tt]
|
321
|
+
end
|
322
|
+
|
323
|
+
rule AS
|
324
|
+
[Aa] [Ss]
|
325
|
+
end
|
326
|
+
|
327
|
+
rule JOIN
|
328
|
+
[Jj] [Oo] [Ii] [Nn]
|
329
|
+
end
|
330
|
+
|
331
|
+
rule FULL
|
332
|
+
[Ff] [Uu] [Ll] [Ll]
|
333
|
+
end
|
334
|
+
|
335
|
+
rule OUTER
|
336
|
+
[Oo] [Uu] [Tt] [Ee] [Rr]
|
337
|
+
end
|
338
|
+
|
339
|
+
rule INNER
|
340
|
+
[Ii] [Nn] [Nn] [Ee] [Rr]
|
341
|
+
end
|
342
|
+
|
343
|
+
rule LEFT
|
344
|
+
[Ll] [Ee] [Ff] [Tt]
|
345
|
+
end
|
346
|
+
|
347
|
+
rule RIGHT
|
348
|
+
[Rr] [Ii] [Gg] [Hh] [Tt]
|
349
|
+
end
|
350
|
+
|
351
|
+
rule ON
|
352
|
+
[Oo] [Nn]
|
353
|
+
end
|
354
|
+
|
355
|
+
rule USING
|
356
|
+
[Uu] [Ss] [Ii] [Nn] [Gg]
|
357
|
+
end
|
358
|
+
|
359
|
+
rule WHERE
|
360
|
+
[Ww] [Hh] [Ee] [Rr] [Ee]
|
361
|
+
end
|
362
|
+
|
363
|
+
rule GROUP
|
364
|
+
[Gg] [Rr] [Oo] [Uu] [Pp]
|
365
|
+
end
|
366
|
+
|
367
|
+
rule BY
|
368
|
+
[Bb] [Yy]
|
369
|
+
end
|
370
|
+
|
371
|
+
rule LIMIT
|
372
|
+
[Ll] [Ii] [Mm] [Ii] [Tt]
|
373
|
+
end
|
374
|
+
|
375
|
+
rule OFFSET
|
376
|
+
[Oo] [Ff] [Ff] [Ss] [Ee] [Tt]
|
377
|
+
end
|
378
|
+
|
379
|
+
rule UNION
|
380
|
+
[Uu] [Nn] [Ii] [Oo] [Nn]
|
381
|
+
end
|
382
|
+
|
383
|
+
rule ALL
|
384
|
+
[Aa] [Ll] [Ll]
|
385
|
+
end
|
386
|
+
|
387
|
+
rule COUNT
|
388
|
+
[Cc] [Oo] [Uu] [Nn] [Tt]
|
389
|
+
end
|
390
|
+
|
391
|
+
rule OVER
|
392
|
+
[Oo] [Vv] [Ee] [Rr]
|
393
|
+
end
|
394
|
+
|
395
|
+
rule PARTITION
|
396
|
+
[Pp] [Aa] [Rr] [Tt] [Ii] [Tt] [Ii] [Oo] [Nn]
|
397
|
+
end
|
398
|
+
|
399
|
+
rule WINDOW
|
400
|
+
[Ww] [Ii] [Nn] [Dd] [Oo] [Ww]
|
401
|
+
end
|
402
|
+
|
403
|
+
rule CASE
|
404
|
+
[Cc] [Aa] [Ss] [Ee]
|
405
|
+
end
|
406
|
+
|
407
|
+
rule WHEN
|
408
|
+
[Ww] [Hh] [Ee] [Nn]
|
409
|
+
end
|
410
|
+
|
411
|
+
rule THEN
|
412
|
+
[Tt] [Hh] [Ee] [Nn]
|
413
|
+
end
|
414
|
+
|
415
|
+
rule ELSE
|
416
|
+
[Ee] [Ll] [Ss] [Ee]
|
417
|
+
end
|
418
|
+
|
419
|
+
rule END
|
420
|
+
[Ee] [Nn] [Dd]
|
421
|
+
end
|
422
|
+
|
423
|
+
rule INTERVAL
|
424
|
+
[Ii] [Nn] [Tt] [Ee] [Rr] [Vv] [Aa] [Ll]
|
425
|
+
end
|
426
|
+
|
427
|
+
rule IS
|
428
|
+
[Ii] [Ss]
|
429
|
+
end
|
430
|
+
|
431
|
+
rule IN
|
432
|
+
[Ii] [Nn]
|
433
|
+
end
|
434
|
+
|
435
|
+
rule EXISTS
|
436
|
+
[Ee] [Xx] [Ii] [Ss] [Tt] [Ss]
|
437
|
+
end
|
438
|
+
|
439
|
+
rule NOT
|
440
|
+
[Nn] [Oo] [Tt]
|
441
|
+
end
|
442
|
+
|
443
|
+
rule AND
|
444
|
+
[Aa] [Nn] [Dd]
|
445
|
+
end
|
446
|
+
|
447
|
+
rule OR
|
448
|
+
[Oo] [Rr]
|
449
|
+
end
|
450
|
+
|
451
|
+
rule TRUE
|
452
|
+
[Tt] [Rr] [Uu] [Ee]
|
453
|
+
end
|
454
|
+
|
455
|
+
rule FALSE
|
456
|
+
[Ff] [Aa] [Ll] [Ss] [Ee]
|
457
|
+
end
|
458
|
+
|
459
|
+
rule NULL
|
460
|
+
[Nn] [Uu] [Ll] [Ll]
|
461
|
+
end
|
462
|
+
|
463
|
+
rule LIKE
|
464
|
+
[Ll] [Ii] [Kk] [Ee]
|
465
|
+
end
|
466
|
+
|
467
|
+
rule ILIKE
|
468
|
+
[Ii] [Ll] [Ii] [Kk] [Ee]
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
data/selekt.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'selekt/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "selekt"
|
8
|
+
gem.version = Selekt::VERSION
|
9
|
+
gem.authors = ["Willem van Bergen"]
|
10
|
+
gem.email = ["willem@railsdoctors.com"]
|
11
|
+
gem.description = %q{A toolkit to work with the SQL language. Incluses a SQL parser, tree manipulations, and tools for testing and monitoring}
|
12
|
+
gem.summary = %q{Toolkit to work with SQL queries}
|
13
|
+
gem.homepage = "https://github.com/wvanbergen/selekt"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_development_dependency('rake')
|
21
|
+
gem.add_development_dependency('minitest', '~> 5')
|
22
|
+
|
23
|
+
gem.add_runtime_dependency('treetop')
|
24
|
+
end
|
data/test/parser_test.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ParserTest < Minitest::Test
|
4
|
+
|
5
|
+
def assert_parses(sql)
|
6
|
+
q = Selekt.parse(sql) rescue nil
|
7
|
+
assert_instance_of Selekt::Query, q, "Expected the provided string to parse as valid SQL"
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_basic_syntax_flexibility
|
11
|
+
assert_parses('select c1')
|
12
|
+
assert_parses('SELECT c1')
|
13
|
+
assert_parses('SeLeCT c1')
|
14
|
+
assert_parses(' select c1 , c2, c3,c4 ')
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_literals
|
18
|
+
assert_parses 'select true, false, null'
|
19
|
+
assert_parses "select 'test', 'test''with''quotes'"
|
20
|
+
assert_parses 'select 1, -1, 1.2, .001, -1.2, -.001'
|
21
|
+
assert_parses 'select c1 AS """Cool"" column", "c2"'
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_projections
|
25
|
+
assert_parses('select 1, \'test\', id, "id"')
|
26
|
+
assert_parses('select *')
|
27
|
+
assert_parses('select table.*, other, "fields"')
|
28
|
+
assert_parses('select schema.table.*')
|
29
|
+
assert_parses('select distinct schema.table.*')
|
30
|
+
assert_parses('select min(table)')
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_set_operations
|
34
|
+
assert_parses "select * from t1 union select * from t2"
|
35
|
+
assert_parses "select * from t1 union all select * from t2 union all select * from t3"
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_sources
|
39
|
+
assert_parses('select * from t1')
|
40
|
+
assert_parses('select * from (t1)')
|
41
|
+
assert_parses('select * from table1 "t1"')
|
42
|
+
assert_parses('select * from schema.table1 as t1')
|
43
|
+
assert_parses('select * from table_1 as "first table", table_2 as "second table"')
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_joins
|
47
|
+
assert_parses('select * from table t1 join table t2 on t1.a = t2.a')
|
48
|
+
assert_parses('select * from (table t1 join table t2 on t1.a = t2.a)')
|
49
|
+
assert_parses('select * from t1 full outer join t2 using (country, state)')
|
50
|
+
assert_parses(<<-SQL)
|
51
|
+
SELECT *
|
52
|
+
FROM table1 AS t1
|
53
|
+
JOIN table2 AS t2 on t1.id = t2.id
|
54
|
+
INNER JOIN (
|
55
|
+
SELECT 1 AS id
|
56
|
+
) t3 ON t3.id = t1.id
|
57
|
+
LEFT JOIN table4 t4 on t1.id = t4.id AND NOT t1.fraud
|
58
|
+
SQL
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_subquery
|
62
|
+
assert_parses('select a from (select b) as b_alias')
|
63
|
+
assert_parses('select a from ( select b from (select c) as c_alias ) as b_alias')
|
64
|
+
assert_parses <<-SQL
|
65
|
+
select * from (SELECT 'test' AS field_1, 123 AS field_2
|
66
|
+
UNION ALL
|
67
|
+
SELECT 'test', 456
|
68
|
+
UNION ALL
|
69
|
+
SELECT 'test', 789) AS t1
|
70
|
+
SQL
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_arithmetic_operators
|
74
|
+
assert_parses("select 'a' + 'b'")
|
75
|
+
assert_parses("select 'a' || ('b' || 'c') || 'd'")
|
76
|
+
assert_parses('select 1 + 2 - (3 * 4)::float / 5 % 6')
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_comparison_operators
|
80
|
+
assert_parses('select 1 > 2')
|
81
|
+
assert_parses('select 1 + 2 > 2')
|
82
|
+
assert_parses('select a > b')
|
83
|
+
assert_raises(Selekt::ParseError) { Selekt.parse('select 1 > 2 > 3') }
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_boolean_tests
|
87
|
+
assert_parses('select column IS NOT TRUE')
|
88
|
+
assert_parses('select column IS NULL')
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_function_calls
|
92
|
+
assert_parses('select MIN(column), now(), complicated_stuff(1, 4 + 2)')
|
93
|
+
assert_parses('select count(*)')
|
94
|
+
assert_parses('select count(distinct *)')
|
95
|
+
assert_parses('select count(distinct id)')
|
96
|
+
assert_parses('select count(1)')
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_over_clause
|
100
|
+
assert_parses "SELECT ROW_NUMBER() OVER (ORDER BY a, b DESC)"
|
101
|
+
assert_parses "SELECT ROW_NUMBER() OVER (PARTITION BY id ORDER BY time)"
|
102
|
+
assert_parses "SELECT ROW_NUMBER() OVER (PARTITION BY id1, id2 ORDER BY time, event_id)"
|
103
|
+
assert_parses "SELECT ROW_NUMBER() OVER w AS index WINDOW w AS (ORDER BY timestamp)"
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_in_construct
|
107
|
+
assert_parses('select 1 IN (1,2,3)')
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_exist_construct
|
111
|
+
assert_parses('select exists (select 1)')
|
112
|
+
assert_parses('select not exists (select 1)')
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_case_expression
|
116
|
+
assert_parses 'select CASE column WHEN 1 THEN TRUE WHEN 2 THEN TRUE ELSE FALSE END'
|
117
|
+
assert_parses 'select CASE column WHEN 1 THEN TRUE END'
|
118
|
+
assert_parses 'select CASE WHEN column = 1 THEN TRUE ELSE FALSE END'
|
119
|
+
assert_parses 'select CASE WHEN column <= 10 THEN TRUE WHEN column > 10 THEN FALSE END'
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_interval_expression
|
123
|
+
assert_parses "select NOW() + interval '10 day'"
|
124
|
+
assert_parses "select NOW() + interval column"
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_boolean_operators
|
128
|
+
assert_parses('select (a > b AND b ilike c) OR a IS NULL OR c IS NULL')
|
129
|
+
assert_parses('select a >= 10 and b <= 0')
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_where
|
133
|
+
assert_parses("select * from t1 where a = 'test' and b >= 10")
|
134
|
+
assert_parses('select a where (false)')
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_group_by_and_having
|
138
|
+
assert_parses('select a, b, min(c) min_c group by a, b')
|
139
|
+
assert_parses('select a, b, min(c) min_c group by a, b having a >= 10 and min_c')
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_order_by
|
143
|
+
assert_parses('select * from table order by field > 10')
|
144
|
+
assert_parses('select * from table order by field1, field2')
|
145
|
+
assert_parses('select * from table order by field ASC')
|
146
|
+
assert_parses('select * from table order by field DESC NULLS FIRST')
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_limit_offset
|
150
|
+
assert_parses('select * from table limit 10')
|
151
|
+
assert_parses('select * from table limit 10 offset 50')
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_comments
|
155
|
+
assert_parses("select 1 -- comment\n")
|
156
|
+
assert_parses("select -- comment\n-- more comments \n 1")
|
157
|
+
assert_parses(<<-SQL)
|
158
|
+
select 1,2,3,4 -- ... and so on
|
159
|
+
from my_first_table,
|
160
|
+
my_second_table
|
161
|
+
-- EOQ
|
162
|
+
SQL
|
163
|
+
end
|
164
|
+
end
|
data/test/query_test.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class QueryTest < Minitest::Test
|
4
|
+
|
5
|
+
def test_sql_roundtrip
|
6
|
+
query = 'select * from table'
|
7
|
+
assert_equal query, Selekt.parse(query).sql
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_sources
|
11
|
+
source_names = Selekt.parse(<<-SQL).source_names
|
12
|
+
SELECT *
|
13
|
+
FROM schema.table1
|
14
|
+
LEFT JOIN table2 t2 ON t1.id = t2.id
|
15
|
+
WHERE EXISTS (SELECT 1 FROM table3 WHERE value > t1.value)
|
16
|
+
AND t1.id NOT IN (SELECT table1_id FROM table3 t3)
|
17
|
+
SQL
|
18
|
+
|
19
|
+
assert_equal ['table1', 't2', 'table3', 't3'], source_names
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_stubbing_sources
|
23
|
+
query = Selekt.parse('select * from t1')
|
24
|
+
assert_equal "select * from (select 1) AS t1", query.stub('t1', 'select 1').sql
|
25
|
+
|
26
|
+
t1_stub = Selekt::SourceStub.new(:field_1, :field_2)
|
27
|
+
t1_stub << ['test', 123]
|
28
|
+
t1_stub << ['test', 456]
|
29
|
+
t1_stub << ['test', 789]
|
30
|
+
|
31
|
+
assert_equal query.stub(:t1, t1_stub).sql, "select * from (SELECT 'test' AS field_1, 123 AS field_2\nUNION ALL\nSELECT 'test', 456\nUNION ALL\nSELECT 'test', 789) AS t1"
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_relations
|
35
|
+
assert_equal ['a', 'b'], Selekt.parse('select * from a t1, b t2').relations.map(&:table_name)
|
36
|
+
|
37
|
+
query = Selekt.parse('select * from schema.table t1 INNER JOIN schema.table t2 ON 1=1')
|
38
|
+
assert_equal ["schema"], query.relations.map(&:schema_name)
|
39
|
+
assert_equal ["table"], query.relations.map(&:table_name)
|
40
|
+
|
41
|
+
query = Selekt.parse(<<-SQL)
|
42
|
+
SELECT *
|
43
|
+
FROM schema."table1" t1
|
44
|
+
LEFT JOIN table2 t2 ON t1.id = t2.id
|
45
|
+
AND t1.id NOT IN (SELECT table1_id FROM table3)
|
46
|
+
|
47
|
+
UNION
|
48
|
+
|
49
|
+
SELECT *
|
50
|
+
FROM table5 t5
|
51
|
+
LEFT JOIN (
|
52
|
+
SELECT * FROM schema.table6
|
53
|
+
) AS t6 ON t6.id = t5.id
|
54
|
+
SQL
|
55
|
+
assert_equal ["schema.table1", "table2", "table3", "table5", "schema.table6"], query.relations.map(&:to_s)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SourceStubTest < Minitest::Test
|
4
|
+
|
5
|
+
def test_row_size_check
|
6
|
+
ss = Selekt::SourceStub.new(:a, :b)
|
7
|
+
assert_raises(Selekt::StubError) { ss.add_row [1,2,3] }
|
8
|
+
assert_equal 0, ss.rows.size
|
9
|
+
|
10
|
+
ss.add_row [1,2]
|
11
|
+
assert_equal 1, ss.rows.size
|
12
|
+
|
13
|
+
ss.add_row [1]
|
14
|
+
assert_equal 2, ss.rows.size
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_add_row_as_hash
|
18
|
+
s1 = Selekt::SourceStub.new(:a, :b)
|
19
|
+
s2 = Selekt::SourceStub.new(:a, :b)
|
20
|
+
|
21
|
+
s1.push [1, 2]
|
22
|
+
s2.push a: 1, b: 2
|
23
|
+
|
24
|
+
assert_equal s1, s2
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_add_row_as_hash_with_nil_values
|
28
|
+
s1 = Selekt::SourceStub.new(:a, :b)
|
29
|
+
s2 = Selekt::SourceStub.new(:a, :b)
|
30
|
+
|
31
|
+
s1.push [nil, 1]
|
32
|
+
s2.push b: 1
|
33
|
+
|
34
|
+
assert_equal s1, s2
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_add_rows
|
38
|
+
s1 = Selekt::SourceStub.new(:a, :b)
|
39
|
+
s1.add_rows([
|
40
|
+
[1],
|
41
|
+
{ b: 2 }
|
42
|
+
])
|
43
|
+
|
44
|
+
assert_equal [1, nil], s1.rows[0]
|
45
|
+
assert_equal [nil, 2], s1.rows[1]
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_sql_generation
|
49
|
+
ss = Selekt::SourceStub.new(:a, :b)
|
50
|
+
ss.add_row [nil, 2]
|
51
|
+
assert_equal "SELECT NULL AS a, 2 AS b", ss.sql
|
52
|
+
ss.add_row ['test', 10]
|
53
|
+
ss.add_row ['test2', 123]
|
54
|
+
assert_equal "SELECT NULL AS a, 2 AS b\nUNION ALL\nSELECT 'test', 10\nUNION ALL\nSELECT 'test2', 123", ss.sql
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_value_quoting_for_sql
|
58
|
+
ss = Selekt::SourceStub.new(:a, :b, :c)
|
59
|
+
ss.add_row [DateTime.parse('2012-01-03 12:44:33'), Date.parse('2012-01-01'), "'"]
|
60
|
+
assert_equal "SELECT '2012-01-03 12:44:33'::timestamp AS a, '2012-01-01'::date AS b, '''' AS c", ss.sql
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SelektTest < Minitest::Test
|
4
|
+
|
5
|
+
def test_safe_identifier
|
6
|
+
assert_equal 'test', Selekt.safe_identifier('test')
|
7
|
+
assert_equal %q["test'"], Selekt.safe_identifier(%q[test'])
|
8
|
+
assert_equal %q["""test"""], Selekt.safe_identifier(%q["test"])
|
9
|
+
end
|
10
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: selekt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Willem van Bergen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: treetop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: A toolkit to work with the SQL language. Incluses a SQL parser, tree
|
56
|
+
manipulations, and tools for testing and monitoring
|
57
|
+
email:
|
58
|
+
- willem@railsdoctors.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- .gitignore
|
64
|
+
- .travis.yml
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- lib/selekt.rb
|
70
|
+
- lib/selekt/query.rb
|
71
|
+
- lib/selekt/source_stub.rb
|
72
|
+
- lib/selekt/sql.rb
|
73
|
+
- lib/selekt/sql.treetop
|
74
|
+
- lib/selekt/version.rb
|
75
|
+
- selekt.gemspec
|
76
|
+
- test/parser_test.rb
|
77
|
+
- test/query_test.rb
|
78
|
+
- test/source_stub_test.rb
|
79
|
+
- test/sql_toolkit_test.rb
|
80
|
+
- test/test_helper.rb
|
81
|
+
homepage: https://github.com/wvanbergen/selekt
|
82
|
+
licenses: []
|
83
|
+
metadata: {}
|
84
|
+
post_install_message:
|
85
|
+
rdoc_options: []
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 2.0.3
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: Toolkit to work with SQL queries
|
104
|
+
test_files:
|
105
|
+
- test/parser_test.rb
|
106
|
+
- test/query_test.rb
|
107
|
+
- test/source_stub_test.rb
|
108
|
+
- test/sql_toolkit_test.rb
|
109
|
+
- test/test_helper.rb
|