sql_munger 0.0.7

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,47 @@
1
+ require 'treetop'
2
+ require 'sql_munger/nodes.rb'
3
+
4
+ Treetop.load "#{File.dirname(__FILE__)}/sql.tt"
5
+
6
+ class SqlParser
7
+ unless instance_methods.include? 'case_sensitive_parse'
8
+ alias_method :case_sensitive_parse, :parse
9
+ end
10
+
11
+ # Allow for case-insensitive parsing. Convert
12
+ # to downcase, then parse, then replace the string
13
+ # with the original version. The numeric indices
14
+ # will still point to the right places.
15
+ def parse( st, *args )
16
+ ds = st.downcase
17
+ begin
18
+ root = case_sensitive_parse( ds, *args )
19
+ ensure
20
+ ds.replace st
21
+ end
22
+
23
+ # raise exception on failure
24
+ # or return the root of the parse tree on success
25
+ if root.nil?
26
+ raise "SQL error at #{failure_line}:#{failure_column}. #{failure_reason}"
27
+ else
28
+ root
29
+ end
30
+ end
31
+
32
+ def dump( sql )
33
+ root = parse( sql )
34
+ root.statements.map do |table|
35
+ if table.respond_to? :name
36
+ puts table.name
37
+ columns = %w{name sql_type length precision scale}
38
+ table.fields.each do |field|
39
+ puts " " + columns.map{|x| field.send(x) }.join(', ')
40
+ end
41
+ else
42
+ puts table.text_value
43
+ end
44
+ end
45
+ puts
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ require 'sql_munger/sql_parser.rb'
2
+ require 'sql_munger/quoter.rb'
3
+
4
+ module SqlMunger
5
+
6
+ # Essentially a qualified table name that's parsed
7
+ # and makes the parts accessible.
8
+ # TODO reduce dependency on Treetop and parser.
9
+ class TableName
10
+ include Quoter
11
+
12
+ def initialize( qualified_identifier, quoter = nil )
13
+ @quoter = quoter
14
+ @qualified_identifier = qualified_identifier
15
+ @tree = self.class.sql_parser.parse( @qualified_identifier )
16
+ end
17
+
18
+ attr_reader :tree
19
+
20
+ def self.sql_parser
21
+ if @sql_parser.nil?
22
+ @sql_parser = SqlParser.new
23
+ @sql_parser.root = :qualified_identifier
24
+ end
25
+ @sql_parser
26
+ end
27
+
28
+ def name
29
+ @tree.parts.last
30
+ end
31
+
32
+ def parts
33
+ @tree.parts
34
+ end
35
+
36
+ def quote
37
+ parts.map do |part|
38
+ quoter.quote_ident( part )
39
+ end.join('.')
40
+ end
41
+
42
+ def to_s
43
+ @tree.parts.join( '.' )
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,30 @@
1
+ require 'date'
2
+
3
+ module SqlMunger
4
+
5
+ # Quote a value in default SQL standard style. Probably
6
+ # won't work for many backends, so the quote
7
+ # method from, for example, DBI should be used.
8
+ module ValueQuoter
9
+ def quote( value )
10
+ case value
11
+ when NilClass
12
+ "null"
13
+
14
+ when Numeric
15
+ value
16
+
17
+ when DateTime
18
+ # 2004-10-19 10:23:54
19
+ "'#{value.strftime "%Y-%m-%d %H:%M:%S"}'"
20
+
21
+ when Date, Time
22
+ "'#{value}'"
23
+
24
+ else
25
+ "'#{escape( value )}'"
26
+ end
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,16 @@
1
+
2
+ require File.expand_path(
3
+ File.join(File.dirname(__FILE__), %w[.. lib sql_munger]))
4
+
5
+ Spec::Runner.configure do |config|
6
+ # == Mock Framework
7
+ #
8
+ # RSpec uses it's own mocking framework by default. If you prefer to
9
+ # use mocha, flexmock or RR, uncomment the appropriate line:
10
+ #
11
+ # config.mock_with :mocha
12
+ # config.mock_with :flexmock
13
+ # config.mock_with :rr
14
+ end
15
+
16
+ # EOF
@@ -0,0 +1,7 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe SqlMunger do
5
+ end
6
+
7
+ # EOF
@@ -0,0 +1,2 @@
1
+ class MockField < Struct.new( :name, :sql_type, :null, :default )
2
+ end
@@ -0,0 +1,462 @@
1
+ require File.dirname(__FILE__) + '/test_sql_munger.rb'
2
+ require File.dirname(__FILE__) + '/mock_field.rb'
3
+
4
+ class TestFieldSet < Test::Unit::TestCase
5
+ def setup
6
+ @fields = [
7
+ MockField.new( :id, 'int', false, 'nextval'),
8
+ MockField.new( :name, 'varchar(20)' ),
9
+ MockField.new( :key, 'varchar(50)' ),
10
+ MockField.new( :value, 'varchar(200)' ),
11
+ ]
12
+
13
+ @hash_values = {
14
+ :key => 'cut',
15
+ :value => 15,
16
+ :name => 'Egoyan',
17
+ :id => '700822',
18
+ :extra => 'movie',
19
+ }
20
+
21
+ @name_block = lambda{|x| x.name}
22
+ @field_name_objects = @fields.map{|x| FieldSet::Field.new( @name_block.call(x) ) }
23
+ @field_names = @fields.map( &@name_block )
24
+
25
+ @values = @field_names.map{|x| @hash_values[x] }
26
+
27
+ @quoted_values = @values.dup.map do |value|
28
+ case value
29
+ when Numeric
30
+ value
31
+ else
32
+ %Q{'#{value}'}
33
+ end
34
+ end
35
+
36
+ @fs = FieldSet.new( @fields, &@name_block )
37
+ end
38
+
39
+ def self.should_have_empty_names
40
+ should "have no field_names" do
41
+ assert_nil @fs.instance_variable_get( '@field_names' )
42
+ end
43
+ end
44
+
45
+ def self.should_build_field_names
46
+ should_have_empty_names
47
+
48
+ should "have a field transform" do
49
+ block = @fs.instance_variable_get( '@field_name_block' )
50
+ assert_kind_of Proc, block
51
+
52
+ # check that both blocks return the name from a field object
53
+ the_name = Struct.new( :name ).new( 'my name' )
54
+ assert_equal @name_block.call( the_name ), block.call( the_name )
55
+ end
56
+
57
+ should "create field names" do
58
+ names = @fs.send :sanity_check_fields
59
+ assert_equal @field_names, names
60
+ end
61
+
62
+ should "return field names" do
63
+ assert_equal @field_names, @fs.field_names
64
+ end
65
+ end
66
+
67
+ def self.should_fields_as_names
68
+ should "have the same fields" do
69
+ assert_equal @field_name_objects, @fs.fields
70
+ assert_equal @field_names, @fs.field_names
71
+ end
72
+ end
73
+
74
+ def self.should_have_field_objects
75
+ should "have field objects" do
76
+ @fs.fields.each { |e| assert_instance_of MockField, e }
77
+ assert_equal @fields, @fs.fields
78
+ end
79
+ end
80
+
81
+ should "have symbols for field names" do
82
+ assert @field_names.all?{|x| x.is_a? Symbol}, "#{@field_names} are not all symbols"
83
+ end
84
+
85
+ context "Field" do
86
+ setup do
87
+ @name = 'some_name'
88
+ @field = FieldSet::Field.new( @name )
89
+ @other_field = FieldSet::Field.new( @name )
90
+ end
91
+
92
+ should "be equal" do
93
+ assert_equal @name.to_sym, @field.name
94
+ assert_equal @name.to_sym, @other_field.name
95
+ assert_equal @field, @other_field
96
+ end
97
+ end
98
+
99
+ context "Construct" do
100
+ context "FieldSet.new( Array<Field> ) {|x| x.name}" do
101
+ setup do
102
+ @fs = FieldSet.new( @fields, &@name_block )
103
+ end
104
+
105
+ should_build_field_names
106
+ should_have_field_objects
107
+ end
108
+
109
+ context "FieldSet.new( Array<Field> ) with no block" do
110
+ setup do
111
+ @fs = FieldSet.new( @fields )
112
+ end
113
+
114
+ should_build_field_names
115
+ should_have_field_objects
116
+ end
117
+
118
+ context "FieldSet.new( Array<String> )" do
119
+ setup do
120
+ @fs = FieldSet.new @field_names
121
+ end
122
+
123
+ should_fields_as_names
124
+ end
125
+
126
+ context "FieldSet.new( Array<Symbol> )" do
127
+ setup do
128
+ @fs = FieldSet.new @field_names.map( &:to_sym )
129
+ end
130
+
131
+ should_fields_as_names
132
+ end
133
+
134
+ context "name block" do
135
+ setup do
136
+ @blah_fs = FieldSet.new( @field_names ) {|x| x.blah_name}
137
+ @fs = FieldSet.new( @field_names ) {|x| x.name}
138
+ end
139
+
140
+ should "fail" do
141
+ assert_raise( NoMethodError ) { @blah_fs.field_names }
142
+ end
143
+
144
+ should "succeed" do
145
+ assert_nothing_raised( Exception ) { @fs.field_names }
146
+ end
147
+ end
148
+
149
+ context "FieldSet[Array<FieldObject>]" do
150
+ setup do
151
+ assert_raise RuntimeError { @fs = FieldSet[@fields] }
152
+ end
153
+ end
154
+
155
+ context "FieldSet[Array<String>]" do
156
+ setup do
157
+ @fs = FieldSet[@field_names]
158
+ end
159
+
160
+ should_fields_as_names
161
+ end
162
+ end
163
+
164
+ context "reconstruct" do
165
+ setup do
166
+ @recon = @fs.reconstruct do |fields|
167
+ fields.select{|x| [:key, :value].include?( x.name )}
168
+ end
169
+ end
170
+
171
+ should "have the correct fields" do
172
+ assert_equal [:key, :value], @recon.field_names
173
+ end
174
+ end
175
+
176
+ context "assign fields" do
177
+ setup do
178
+ # create new field objects
179
+ @new_field_names = %w{religion member holiday}
180
+ @new_fields = @new_field_names.map{|x| MockField.new(x)}
181
+
182
+ # now assign the fields, which should clear out @field_names
183
+ @fs.fields = @new_fields
184
+ end
185
+
186
+ should_have_empty_names
187
+
188
+ should "have new names" do
189
+ assert_equal @new_field_names, @fs.field_names
190
+ end
191
+
192
+ should "be different to old names" do
193
+ assert_not_equal @fields, @fs.fields
194
+ assert_not_equal @field_names, @fs.field_names
195
+ end
196
+ end
197
+
198
+ context "qualify field names" do
199
+ setup do
200
+ @qualifier = 'table_name'
201
+ @fs.qualifier = @qualifier
202
+ end
203
+
204
+ should "qualify" do
205
+ assert_equal %Q{"table_name"."id"}, @fs.qualify( 'id' )
206
+ assert_equal %Q{"scheman"."id"}, @fs.qualify( 'scheman', 'id' )
207
+ assert_equal %Q{"scheman"."tbk"."id"}, @fs.qualify( ['scheman', 'tbk'], 'id' )
208
+ assert_equal %Q{"scheman"."tbk"."id"}, @fs.qualify( %w{scheman tbk id} )
209
+ assert_equal %Q{"scheman"."tbk"."id"}, @fs.qualify( 'scheman', 'tbk', 'id' )
210
+ end
211
+
212
+ context "TableName" do
213
+ setup do
214
+ @qualifier = TableName.new( 'more.than.one' )
215
+ @fs.qualifier = @qualifier
216
+ end
217
+
218
+ should "qualify" do
219
+ assert_equal %Q{"more"."than"."one"."id"}, @fs.qualify( 'id' )
220
+ end
221
+ end
222
+
223
+ should "no qualifier" do
224
+ assert_equal @field_names.map{|x| %Q{"#{x}"}}.join(', '), @fs.list
225
+ end
226
+
227
+ should "have qualifier" do
228
+ assert_equal [ @qualifier ], @fs.qualifier
229
+ end
230
+
231
+ should "with builtin qualifier" do
232
+ assert_equal @field_names.map{|x| %Q{"#{@qualifier}"."#{x}"}}.join(', '), @fs.qualified_list
233
+ end
234
+
235
+ should "with other qualifier" do
236
+ qualifier = 'hello'
237
+ assert_equal @field_names.map{|x| %Q{"#{qualifier}"."#{x}"}}.join(', '), @fs.list( qualifier )
238
+ end
239
+
240
+ context "quoter" do
241
+ should_eventually "test alternative quoter"
242
+ should_eventually "test with DBI quoter"
243
+ end
244
+
245
+ context "array qualifier" do
246
+ setup do
247
+ @qualifier = %w{namespace user}
248
+ @fs.qualifier = @qualifier
249
+ end
250
+
251
+ should "list" do
252
+ assert_equal @field_names.map{|x| %Q{"namespace"."user"."#{x}"}}.join(', '), @fs.qualified_list
253
+ end
254
+ end
255
+ end
256
+
257
+ context "Collections" do
258
+ should "be enumerable" do
259
+ assert_kind_of Enumerable, @fs
260
+ end
261
+
262
+ should "enumerate" do
263
+ @fs.each do |field|
264
+ assert_instance_of Symbol, field
265
+ assert @field_names.include?( field )
266
+ end
267
+ end
268
+
269
+ context "values" do
270
+ should "fetch values only" do
271
+ assert_equal( (@values - [ 'movie' ]), @fs.values( @hash_values ) )
272
+ end
273
+
274
+ should "fetch values transformed" do
275
+ assert_equal( (@values - [ 'movie' ]), @fs.values( @hash_values ){|x| x} )
276
+ end
277
+
278
+ should "fetch quoted values only" do
279
+ assert_equal( (@quoted_values - [ "'movie'" ]), @fs.quoted_values( @hash_values ) )
280
+ end
281
+
282
+ should "fetch hash" do
283
+ assert_equal @hash_values.reject{ |k,v| v == 'movie' }, @fs.hash_values( @hash_values )
284
+ end
285
+
286
+ should "fetch hash transformed" do
287
+ assert_equal @hash_values.reject{ |k,v| v == 'movie' }, @fs.hash_values( @hash_values ){|x| x}
288
+ end
289
+ end
290
+ end
291
+
292
+ context "express" do
293
+ should "fail on array size mismatch" do
294
+ assert_raise( RuntimeError ) { @fs.express( 1,2,3 ) }
295
+ end
296
+
297
+ should_eventually "test with Array"
298
+ should_eventually "test with Hash"
299
+ should_eventually "test with to_hash"
300
+ end
301
+
302
+ context "generate SQL fragments" do
303
+ context "comparison" do
304
+ setup do
305
+ @fs.qualifier = "table"
306
+
307
+ @result = <<EOF.chomp
308
+ ( "table"."id" = "other"."id" and "table"."name" = "other"."name" and "table"."key" = "other"."key" and "table"."value" = "other"."value" )
309
+ EOF
310
+
311
+ @value_result = <<EOF.chomp
312
+ ( "table"."id" = '700822' and "table"."name" = 'Egoyan' and "table"."key" = 'cut' and "table"."value" = 15 )
313
+ EOF
314
+ end
315
+
316
+ context "no local qualifier" do
317
+ setup do
318
+ @fs.qualifier = nil
319
+ end
320
+
321
+ should "raise exception" do
322
+ assert_nil @fs.qualifier
323
+ assert_raise( RuntimeError ) { @fs.comparison( 'other' ) }
324
+ end
325
+ end
326
+
327
+ should "other table" do
328
+ assert_not_nil @fs.qualifier
329
+ assert_equal @result, @fs.comparison( 'other' )
330
+ assert_equal @result.gsub( '=', '<' ), @fs.comparison( 'other', '<' )
331
+ end
332
+
333
+ should "values" do
334
+ assert_equal @value_result, @fs.comparison( @values )
335
+ assert_equal @value_result, @fs.comparison( @hash_values )
336
+ end
337
+ end
338
+
339
+ context "update" do
340
+ setup do
341
+ @table_result = <<EOF.chomp
342
+ "id" = "oops"."id", "name" = "oops"."name", "key" = "oops"."key", "value" = "oops"."value"
343
+ EOF
344
+
345
+ @value_result = <<EOF.chomp
346
+ "id" = '700822', "name" = 'Egoyan', "key" = 'cut', "value" = 15
347
+ EOF
348
+ end
349
+
350
+ should "produce update fragment" do
351
+ assert_equal @value_result, @fs.update( @hash_values )
352
+ assert_equal @value_result, @fs.update( @values )
353
+ assert_equal @table_result, @fs.update( 'oops' )
354
+ end
355
+ end
356
+
357
+ context "definition" do
358
+ should "create table" do
359
+ result = "id int, name varchar(20), key varchar(50), value varchar(200)"
360
+ assert_equal result, @fs.definitions
361
+ assert_equal result.gsub(', ', ",\n"), @fs.definitions( :joiner => ",\n" )
362
+ end
363
+ end
364
+ end
365
+
366
+ context "Set operations" do
367
+
368
+ context "difference" do
369
+ setup do
370
+ @other = FieldSet.new( @fields[1..-1] )
371
+ @difference = @fs - @other
372
+ end
373
+
374
+ should "have only id" do
375
+ assert_not_nil @difference
376
+ # Field objects
377
+ assert_equal [ @fields.first ], @difference.fields
378
+
379
+ # strings
380
+ assert_equal [ @fields.first ], ( @fs - ( @field_names - [:id] ) ).fields
381
+
382
+ # symbols
383
+ assert_equal [ @fields.first ], ( @fs - ( @field_names.map{|x| x.to_sym} - [:id] ) ).fields
384
+ end
385
+
386
+ should "copy qualifier" do
387
+ assert_equal @fs.qualifier, @difference.qualifier
388
+ end
389
+
390
+ should "copy quoter" do
391
+ assert_equal @fs.quoter, @difference.quoter
392
+ end
393
+ end
394
+
395
+ context "append" do
396
+ setup do
397
+ @timestamp_field = MockField.new( 'date', 'timestamp' )
398
+ @sum = @fs + FieldSet.new( [@timestamp_field] )
399
+ end
400
+
401
+ should "have timestamp as well" do
402
+ assert_not_nil @sum
403
+ assert_equal @fields + [@timestamp_field], @sum.fields
404
+ end
405
+
406
+ should "not fail on String extension" do
407
+ assert_nothing_raised( RuntimeError ) { @fs + [ 'date' ] }
408
+ end
409
+ end
410
+
411
+ context "union" do
412
+ setup do
413
+ @timestamp_field = MockField.new( :date, 'timestamp' )
414
+ @other_id_field = MockField.new( :id, 'string' )
415
+ @union = @fs | FieldSet.new( [@timestamp_field, @other_id_field] )
416
+ @string_union = @fs | %w{date id}.map( &:to_sym )
417
+ end
418
+
419
+ should "have only one extra field" do
420
+ assert_equal @fs.fields.size+1, @union.fields.size
421
+ assert_equal @fs.fields.size+1, @string_union.fields.size
422
+ end
423
+
424
+ should "have the right field names" do
425
+ assert_equal @field_names + [:date], @union.field_names
426
+ assert_equal @field_names + [:date], @string_union.field_names
427
+ end
428
+
429
+ should "have timestamp" do
430
+ assert_not_nil @union
431
+ assert_equal @fields + [@timestamp_field], @union.fields
432
+ assert_equal @field_names + [:date], @string_union.field_names
433
+ end
434
+
435
+ should "have original id only" do
436
+ assert_not_nil @union
437
+ assert_equal @fields + [@timestamp_field], @union.fields
438
+ assert_not_equal @fs.find_field('id'), @other_id_field
439
+ end
440
+
441
+ should "not fail on String extension" do
442
+ assert_nothing_raised( RuntimeError ) { @fs | [ 'date' ] }
443
+ end
444
+ end
445
+
446
+ context "intersection" do
447
+ setup do
448
+ @id_only = @fs & FieldSet.new( ['id'] )
449
+ end
450
+
451
+ should "accept String subset" do
452
+ assert_nothing_raised( Exception ) { @fs & [ 'date' ] }
453
+ end
454
+
455
+ should "should have only id" do
456
+ #~ puts "id_only.field_names: #{@id_only.field_names.inspect}"
457
+ assert_equal [:id], @id_only.field_names
458
+ end
459
+ end
460
+
461
+ end
462
+ end