selekt 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 +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 [](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
|