selekt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .DS_Store
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ script: bundle exec rake
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.1
7
+ - ruby-head
8
+ - rbx
9
+ - jruby-19mode
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: rbx
13
+ - rvm: ruby-head
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ platform :rbx do
5
+ gem 'rubysl'
6
+ end
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.
@@ -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.
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ end
8
+
9
+ task :default => :test
@@ -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"
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Selekt
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'pp'
5
+ require 'minitest/autorun'
6
+ require 'minitest/pride'
7
+ require 'minitest/mock'
8
+
9
+ require 'selekt'
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