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.
@@ -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