SqlStatement 1.0.2 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
data/doc/ChangeLog CHANGED
@@ -1,4 +1,18 @@
1
1
  =Change Log
2
+ ==2.0
3
+ * Vastly redesign the DSL. The methods for adding components to an
4
+ SQL statement have all been renamed, and internal state is no longer
5
+ directly exposed to the outside. The interface is now much more intutive and
6
+ consistent. Please read the documentation to find out how this version of
7
+ the library works.
8
+ * Left joins have been added
9
+ * There's no more SelectParts class --
10
+ the corresponding semantics have been built directly into the Select class.
11
+ * <tt>Select.new do |s| ... end</tt> syntax has been added
12
+ * Select statements now remember the order of fields and tables. This makes
13
+ the next two changes feasible.
14
+ * Support for Mysql's STRAIGHT_JOIN modifier has been added
15
+ * Unit tests have been added
2
16
  ==1.0.2
3
17
  * Make the methods in SQLHelpers available as module functions too, so
4
18
  not only can you call "include SQLHelpers; sql_func", you can call
data/lib/sql/statement.rb CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  # Everything in Ruby is a descendant of the Object class. Practically
2
3
  # speaking, this means that any object that hasn't otherwise defined
3
4
  # the +placeheld+ and <tt>to_sqlpart</tt> methods will be treated as a piece
@@ -12,7 +13,7 @@ class Object
12
13
  # then the values to be bound to them are returned here, in the proper order.
13
14
  # This allows SQL statements to be called in a manner similar to
14
15
  # s=Select.new
15
- # s << SelectParts.new {...}
16
+ # s << Select.new {...}
16
17
  # dbh.execute(s.to_s, *s.placeheld)
17
18
  # When there are no unbound variables, and empty array is returned.
18
19
  def placeheld; [self]; end
@@ -49,6 +50,17 @@ class NilClass
49
50
  end
50
51
  end
51
52
 
53
+ module SQLHelpers
54
+
55
+ #This helper method takes a string, and encapsulates it as a proper
56
+ #SQL expression, so that it won't get passed to the database as
57
+ #though it were a (potentially hostile) string value.
58
+ def string_func(str,placeheld=[])
59
+ SQLStatement::Function.new(str,placeheld)
60
+ end
61
+ extend self
62
+ end
63
+
52
64
  module SQLStatement
53
65
 
54
66
  # This class is used to represent complex SQL expressions, so that
@@ -56,7 +68,7 @@ module SQLStatement
56
68
  # conditions in a WHERE clause. There are no utility classes for
57
69
  # generating these (even though they'll probably include field names
58
70
  # and other kinds of values), but for convenience, they can be generated
59
- # with <tt>Kernel#SQLFunc</tt>.
71
+ # with <tt>SQLHelpers#string_func</tt>.
60
72
  class Function < String
61
73
 
62
74
  def initialize(val="",placeheld=[])
@@ -73,6 +85,32 @@ class Function < String
73
85
  end
74
86
  end
75
87
 
88
+ #This class is used to represent table names and field names. It's like a
89
+ #+Symbol+ in this regard, but it can be garbage collected.
90
+ class Identifier < String
91
+ alias_method :_ken_sqlstatement_old_idx, :[]
92
+ def self.[] param
93
+ self.new param
94
+ end
95
+ def [] *params
96
+ if params.length==1 and params[0].is_a?(Symbol)
97
+ SQL_Field.new(self,params[0])
98
+ elsif params.length==1 and params[0].is_a?(Identifier)
99
+ SQL_Field.new(self,params[0])
100
+ else
101
+ _ken_sqlstatement_old_idx *params
102
+ end
103
+ end
104
+ def to_sqlpart
105
+ "`#{self}`"
106
+ end
107
+ def placeheld
108
+ []
109
+ end
110
+ def dbid
111
+ self
112
+ end
113
+ end
76
114
 
77
115
  # +SQL_Field+ is used for representing field names qualified by table names.
78
116
  # Their text expansion looks like <tt>`tablename`.`fieldname`</tt>.
@@ -99,211 +137,148 @@ class SQL_Field
99
137
  def placeheld; []; end
100
138
  end
101
139
 
102
-
103
- # This class is used to represent a (possibly coherent) set of parts to add to a SQL statement.
104
- # If we wanted to generate SQL code similar to
140
+ # This class is used to represent a SELECT statement, or an incomplete set of
141
+ # parts to add to a SELECT statement. If we wanted to generate SQL code
142
+ # similar to
105
143
  # SELECT `value1`,`value2`
106
144
  # FROM `jointable`,`dictionary1`,`dictionary2`
107
145
  # WHERE `jointable`.`id1`=`dictionary1`.`id` AND `jointable`.`id2`=`dictionary2`.`id`
108
146
  # then it may make sense for your application to generate
109
147
  # the +dictionary1+ code and the +dictionary2+ code separately,
110
- # in different +SelectParts+ objects, then combine them into one +Select+.
148
+ # in different +Select+ objects, then combine them into one +Select+.
149
+ # def foo(x)
150
+ # Select.new do |s|
151
+ # s.field :"value#{x}"
152
+ # s.table :"dictionary#{x}"
153
+ # s.condition string_func("`jointable`.`id#{x}`=`dictionary#{x}`.`id`")
154
+ # end
155
+ # end
156
+ #
111
157
  # stmt=Select.new
112
- # stmt.tablesarray << :jointable
158
+ # stmt.table :jointable
113
159
  # (1..2).each do |x|
114
- # s=SelectParts.new
115
- # s.add_fields [:"value#{x}"]
116
- # s.add_tables [:"dictionary#{x}"]
117
- # s.conditions << SQLFunc("`jointable`.`id#{x}`=`dictionary#{x}`.`id`")
118
- # stmt << s
160
+ # stmt << foo(x)
119
161
  # end
120
- # dbh.execute(stmt.to_s,*stmt.placeheld)
121
- class SelectParts
122
-
123
- def expectarray (arg,hash)
124
- v=hash[arg]
125
- if v==nil
126
- []
127
- elsif Array===v
128
- v
129
- else
130
- [v]
131
- end
132
- end
133
-
134
- def expecthash (arg,hash,*options)
135
- v=hash[arg]
136
- if v==nil
137
- v={}
138
- elsif Array===v and not options.include?(:PreserveArray)
139
- v=Hash[*([v,v].transpose.flatten)]
140
- elsif Array===v
141
- v
142
- elsif Hash===v
143
- v
144
- else
145
- [v]
146
- end
147
- end
148
-
149
- protected :expectarray,:expecthash
150
-
162
+ # dbh.execute(stmt)
163
+ class Select
151
164
  #Initializes the various pieces of the SQL statement to values specified in the hash.
152
165
  #Use Symbols corresponding to the attribute names to set an attribute here, otherwise a default
153
166
  #empty array or hash will be used.
154
- def initialize(hash={})
155
- @fields=expecthash :fields,hash,:PreserveArray
156
- @tables=expecthash :tables,hash,:PreserveArray
157
- @conditions=expectarray :conditions,hash
158
- @groupby=expectarray :groupby,hash
159
- @orderby=expectarray :orderby,hash
160
- end
161
-
162
- #See the documentation for SelectStatement which describes these.
163
- attr_accessor :conditions, :groupby, :orderby, :fields, :tables
164
-
165
- #Adds an array of unaliased fields, or a hash of aliased fields to
166
- #this SQL statement. See documentation at SelectStatement
167
- def add_fields (newfields)
168
- if newfields.is_a?(Hash)
169
- @fields.merge! newfields
170
- else
171
- newfields.each { |x| @fields[x]=x }
172
- end
173
- nil
174
- end
175
-
176
- #Adds an array of unaliased tables or a hash of aliased tables to
177
- #this SQL statement. See documentation for SelectStatement
178
- def add_tables (newtables)
179
- if newtables.is_a?(Hash)
180
- @tables.merge! newtables
181
- else
182
- newtables.each { |x| @tables[x]=x }
183
- end
184
- end
185
-
186
- #Merge several SelectParts objects into one.
187
- def +(rhs)
188
- b=self.dup
189
- b.fields.merge!(rhs.fields)
190
- b.tables.merge!(rhs.tables)
191
- b.conditions+=rhs.conditions
192
- b.groupby+=rhs.groupby
193
- b.orderby+=rhs.orderby
194
- b
195
- end
196
-
197
- class << self
198
- #Use this to define additional array attributes in descendant classes
199
- #with all of the necessary addition, initialization and merging (<<)
200
- #semantics. <b>You can only call this function once per class, as
201
- #it redefines methods each time it is called.</b>
202
- def newlists *lists
203
- attr_accessor *lists
204
-
205
- lines=lists.collect do |listname|
206
- ["@#{listname}=expectarray :#{listname},hash",
207
- "b.#{listname}+=rhs.#{listname}"]
208
- end.transpose
209
- class_eval <<-"end;"
210
- def initialize hash={}
211
- super
212
- #{lines[0].join("\n")}
213
- end
214
- def + (rhs)
215
- b=super
216
- #{lines[1].join("\n")}
217
- b
218
- end
219
- end;
220
- end
221
- protected :newlists
222
- end
223
-
224
- end
225
-
226
- # Creates a SELECT statement
227
- class Select
167
+ #
168
+ #The block is a convenient way to scpe the creation of the whole statement into one block.
228
169
  def initialize
229
- @fields={}
230
- @tables={}
231
- @conditions=[]
232
- @groupby=[]
233
- @orderby=[]
234
- end
235
-
236
- #This is the list of fields or instructions to be retrieved. In a
170
+ @field_names=[]
171
+ @field_aliases=[]
172
+ @table_names=[]
173
+ @table_aliases=[]
174
+ @conditions=[]
175
+ @groupby_fields=[]
176
+ @orderby_fields=[]
177
+ @leftjoin_alias=[]
178
+ @leftjoin_table=[]
179
+ @leftjoin_condition=[]
180
+ yield self if block_given?
181
+ end
182
+
183
+ #Add a condition to the WHERE clause of the SQL statment. Conditions will be
184
+ #joined by AND. This should either be an array representing an s-expression
185
+ #for the condition, or it should be a SQLStatement::Function containing a
186
+ #string for the condition.
187
+ def condition c
188
+ @conditions << c
189
+ self
190
+ end
191
+ def groupby c
192
+ @groupby_fields << c
193
+ self
194
+ end
195
+ def orderby c
196
+ @orderby_fields << c
197
+ self
198
+ end
199
+
200
+
201
+
202
+ #Add a field or expression to be retrieved in the expression. In a
237
203
  #+SELECT+ statement, this appears immediately after the +SELECT+ keyword.
238
204
  #In an +UPDATE+ statement, this appears after the +SET+ keyword. (And in other
239
- #places for other kinds of queries #In an +UPDATE+ statement, this appears after the +SET+ keyword. (And in other
240
205
  #places for other kinds of queries. The SQL inventors were nothing if not consistent.)
241
206
  #
242
- #This is a hash of fields to be used in the SQL statmenet. Entries are in the
243
- #form <tt>alias => underlying_expression</tt>. If you are not
244
- #aliasing a field name, use the form <tt>fieldname => fieldname</tt>.
245
- attr_accessor :fields
246
-
247
- #Adds an array of unaliased fields, or a hash of aliased fields to
248
- #this SQL statement.
249
- def add_fields (newfields)
250
- if newfields.is_a?(Hash)
251
- @fields.merge! newfields
252
- else
253
- newfields.each { |x| @fields[x]=x }
254
- end
255
- nil
207
+ # +thefield+ is a field or expression to put in the result of this Select statement
208
+ # +thealias+ is a name to use for the column, and can be omitted to use the default
209
+ def field thefield, thealias=nil
210
+ @field_names << thefield
211
+ @field_aliases << thealias
256
212
  end
257
-
258
- #This is the tables to include in the query (i.e. the +FROM+ clause).
213
+ # Add a table to join into the expression, either as the first table, or by using an inner join
259
214
  #
260
- #This is a hash of tables to be used in the SQL statmenet. Entries are in the
261
- #form <tt>alias => underlying_expression</tt>. If you are not
262
- #aliasing a table name, use the form <tt>tablename => tablename</tt>.
263
- attr_accessor :tables
264
-
265
- #Adds an array of unaliased tables or a hash of aliased tables to
266
- #this SQL statement.
267
- def add_tables (newtables)
268
- if newtables.is_a?(Hash)
269
- @tables.merge! newtables
270
- else
271
- newtables.each { |x| @tables[x]=x }
272
- end
215
+ #+thefield+ is a field or expression to put in the result of this Select statement
216
+ #+thealias+ is a name to use for the column, and can be omitted to use the default
217
+ #
218
+ #If the table/alias pair is already included in the query, it is not included again, but
219
+ #if a table can be included again by using a new alias
220
+ def table thetable, thealias=nil
221
+ return if @table_names.zip(@table_aliases).include? [thetable,thealias]
222
+ @table_names << thetable
223
+ @table_aliases << thealias
224
+ end
225
+ #Adds a left-join with join conditon
226
+ def leftjoin table, condition, the_alias=nil
227
+ return if @leftjoin_table.zip(@leftjoin_alias).include? [table,the_alias]
228
+ @leftjoin_alias << the_alias
229
+ @leftjoin_table << table
230
+ @leftjoin_condition << condition
273
231
  end
274
232
 
275
- #This is an array of Function objects that specify the contents of
276
- #the +WHERE+ clause.
277
- attr_accessor :conditions
278
233
 
279
- #Specifieds fields or expressions to group by, as an array.
280
- attr_accessor :groupby
281
234
 
282
- #Specifieds fields or expressions to sort by, as an array.
283
- attr_accessor :orderby
235
+
236
+ #Merge several SelectParts objects into one.
237
+ def +(rhs)
238
+ b=self.dup
239
+ b << rhs
240
+ end
284
241
 
285
- #If you overriding this class (or any of its subclasses) to constructing other fields
286
- #from other lists, override this function, call +super+, construct the additional fields here,
287
- #and add them to the result of +super+. Then return the resulting hash.
242
+
243
+ #Returns two lists: a list of field names (aliases) in the expression, and a
244
+ #list of field values. If you overriding this class (or any of its
245
+ #subclasses) to constructing other fields from other lists, override this
246
+ #function, call +super+, construct the additional fields here, and add them
247
+ #to the result of +super+. Then return the results.
288
248
  def allfields
289
- @fields.dup
249
+ [@field_aliases.dup, @field_names.dup]
290
250
  end
291
251
 
292
252
  #Select whether this is a SELECT statement or a SELECT DISTINCT
293
253
  #statement. (non-distinct by default)
294
- attr_writer :distinct
254
+ attr_accessor :distinct
255
+
256
+ #Determine whether to use MySQL's STRAIGHT_JOIN modifier to override the query optimizer
257
+ attr_accessor :straight_join
295
258
 
296
- def distinct
297
- @distinct ||= false
259
+ def straight_join
260
+ @straight_join ||= false
298
261
  end
299
262
 
300
263
  #Merges the various parts of a SelectParts into the correct places in this SQL statement.
301
264
  def << (parts)
302
- @fields.merge!(parts.fields)
303
- @tables.merge!(parts.tables)
265
+ parts.table_names.zip(parts.table_aliases).each do |name,thealias|
266
+ next if @table_names.zip(@table_aliases).include? [name,thealias]
267
+ @table_names << name
268
+ @table_aliases << thealias
269
+ end
270
+ parts.leftjoin_alias.zip(parts.leftjoin_table,parts.leftjoin_condition).each \
271
+ do |thealias,thetable,thecondition|
272
+ next if @leftjoin_alias.zip(@leftjoin_table).include? [thealias,thetable]
273
+ @leftjoin_alias << thealias
274
+ @leftjoin_table << thetable
275
+ @leftjoin_condition << thecondition
276
+ end
277
+ @field_names += parts.field_names
278
+ @field_aliases += parts.field_aliases
304
279
  @conditions += parts.conditions
305
- @groupby += parts.groupby
306
- @orderby += parts.orderby
280
+ @groupby_fields += parts.groupby_fields
281
+ @orderby_fields += parts.orderby_fields
307
282
  self
308
283
  end
309
284
 
@@ -312,15 +287,26 @@ class Select
312
287
  #be retrieved with the placeheld method, and used in a manner similar to
313
288
  # dbh.execute(s.to_s,*s.placeheld)
314
289
  def to_s
315
- statement="SELECT #{distinct ? 'DISTINCT' : ''} #{fields_s} FROM #{tables_s}"
316
- v=conditions_s; statement << " WHERE "<< v if v
317
- v=groupby_s; statement << " GROUP BY "<< v if v
318
- v=orderby_s; statement << " ORDER BY "<< v if v
290
+ statement="SELECT"
291
+ statement << " DISTINCT" if distinct
292
+ statement << " STRAIGHT_JOIN" if straight_join
293
+ statement << "\n " << fields_s
294
+ statement << "\nFROM " << tables_s
295
+ v=conditions_s; statement << "\nWHERE "<< v if v
296
+ v=groupby_s; statement << "\nGROUP BY "<< v if v
297
+ v=orderby_s; statement << "\nORDER BY "<< v if v
319
298
  return statement
320
299
  end
321
300
 
322
301
  def placeheld
323
- (allfields.values+@tables.values+conditions+groupby+orderby).collect{|x| x.placeheld}.flatten
302
+ (
303
+ allfields[1] +
304
+ @table_names +
305
+ @leftjoin_table.zip(@leftjoin_alias,@leftjoin_condition).flatten +
306
+ conditions +
307
+ groupby_fields +
308
+ orderby_fields
309
+ ).collect{|x| x.placeheld}.flatten
324
310
  end
325
311
 
326
312
  #This is useful for writing nested queries.
@@ -329,42 +315,66 @@ class Select
329
315
  end
330
316
  protected
331
317
 
318
+ attr_reader :field_names, :field_aliases, :table_names, :table_aliases,
319
+ :conditions, :groupby_fields, :orderby_fields, :leftjoin_alias,
320
+ :leftjoin_table, :leftjoin_condition
321
+
332
322
  def tables_s
333
- @tables.collect{|key,value| value==key ? key.to_sqlpart : "#{value.to_sqlpart} as #{key.to_sqlpart}"}.join(", ")
323
+ part1=@table_aliases.zip(@table_names).collect do |a,t|
324
+ if a
325
+ "#{t.to_sqlpart} as #{a.to_sqlpart}"
326
+ else
327
+ t.to_sqlpart
328
+ end
329
+ end.join("\n INNER JOIN ")
330
+ part2=@leftjoin_alias.zip(@leftjoin_table, @leftjoin_condition).collect do |a,t,c|
331
+ if a==nil
332
+ "\n LEFT JOIN #{t.to_sqlpart} ON #{c.to_sqlpart}"
333
+ else
334
+ "\n LEFT JOIN #{t.to_sqlpart} as #{a.to_sqlpart} ON #{c.to_sqlpart}"
335
+ end
336
+ end.join
337
+ part1+part2
334
338
  end
335
339
 
336
340
  def conditions_s
337
- @conditions==[] ? nil : @conditions.collect{|x| x.to_sqlpart}.join(" AND ")
341
+ @conditions==[] ? nil : @conditions.collect{|x| x.to_sqlpart}.join("\n AND ")
338
342
  end
339
343
 
340
344
  def groupby_s
341
- @groupby==[] ? nil : @groupby.collect{|x| x.to_sqlpart}.join(", ")
345
+ @groupby_fields==[] ? nil : @groupby_fields.collect{|x| x.to_sqlpart}.join(", ")
342
346
  end
343
347
 
344
348
  def orderby_s
345
- @orderby==[] ? nil : @orderby.collect{|x| x.to_sqlpart}.join(", ")
349
+ @orderby_fields==[] ? nil : @orderby_fields.collect{|x| x.to_sqlpart}.join(", ")
346
350
  end
347
351
 
348
352
  def fields_s
349
- allfields.collect{|key,value| value==key ? key.to_sqlpart : "#{value.to_sqlpart} as #{key.to_sqlpart}"}.join(", ")
353
+ allfields.transpose.collect do |a,f|
354
+ if a
355
+ "#{f.to_sqlpart} as #{a.to_sqlpart}"
356
+ else
357
+ f.to_sqlpart
358
+ end
359
+ end.join(", ")
350
360
  end
351
361
 
352
362
  class << self
353
363
  #Use this to define additional array attributes in descendant classes
354
364
  #with all of the necessary addition, initialization and merging (<<)
355
- #semantics. You still need to appropriately define +allfields+ to make
356
- #appropriate use of the lists you have added. <b> You can only call
357
- #this method once, as it redefines methods each time it is called</b>
365
+ #semantics. <b>You can only call this function once per class, as
366
+ #it redefines methods each time it is called.</b>
358
367
  def newlists *lists
359
368
  attr_accessor *lists
360
369
 
361
- lines=lists.collect do |listname|
362
- ["@#{listname}=[]","@#{listname} += parts.#{listname}"]
370
+ lines=lists.collect do |listname|
371
+ ["@#{listname}=[]",
372
+ "@#{listname} += parts.#{listname}"]
363
373
  end.transpose
364
374
  class_eval <<-"end;"
365
- def initialize *args
366
- super
375
+ def initialize &block
367
376
  #{lines[0].join("\n")}
377
+ super &block
368
378
  end
369
379
  def << (parts)
370
380
  super
@@ -389,14 +399,8 @@ class SelectCreate < Select
389
399
  #This may not be supported by all databases.
390
400
  attr_accessor :temporary
391
401
 
392
- def initialize(targettable,temporary=false)
393
- super()
394
- @targettable=targettable
395
- @temporary=temporary
396
- end
397
-
398
402
  def to_s
399
- "CREATE #{ @temporary ? 'TEMPORARY':''} TABLE #{@targettable.to_sqlpart} #{super}"
403
+ "CREATE #{ @temporary ? 'TEMPORARY ':''}TABLE #{@targettable.to_sqlpart} #{super}"
400
404
  end
401
405
  end
402
406
 
@@ -407,26 +411,21 @@ class SelectInsert < Select
407
411
  #The name of the table to insert into.
408
412
  attr_accessor :targettable
409
413
 
410
- def initialize(targettable)
411
- super()
412
- @targettable=targettable
413
- end
414
-
415
414
  def to_s
416
- "INSERT INTO #{@targettable.to_sqlpart} #{names_s} #{super}"
415
+ "INSERT INTO #{@targettable.to_sqlpart} (#{names_s}) #{super}"
417
416
  end
418
417
 
419
418
  protected
420
419
 
421
420
  def names_s
422
- intermediate=allfields.keys.collect do |x|
421
+ intermediate=allfields[0].collect do |x|
423
422
  if x.is_a? SQL_Field
424
423
  x.field.to_sqlpart
425
424
  else
426
425
  x.to_sqlpart
427
426
  end
428
427
  end
429
- "("+intermediate.join(", ")+")"
428
+ intermediate.join(", ")
430
429
  end
431
430
  end
432
431
 
@@ -443,20 +442,28 @@ class Update < Select
443
442
  end
444
443
 
445
444
  def placeheld
446
- (allfields.values+conditions).collect{|x| x.placeheld}.flatten
445
+ (@table_names+allfields[1]+conditions).collect{|x| x.placeheld}.flatten
446
+ end
447
+
448
+ # Set a field to contain a value. Note the order of the parameters matches
449
+ # the order used for the Select statement
450
+ def field expression, targetfield
451
+ super expression, targetfield
447
452
  end
448
453
 
449
454
  protected
450
455
 
451
456
  def updatepart_s
452
- allfields.collect{|key,value| key.to_sqlpart+"="+value.to_sqlpart}.join(", ")
457
+ allfields.transpose.collect do |target,value|
458
+ target.to_sqlpart+"="+value.to_sqlpart
459
+ end.join(", ")
453
460
  end
454
461
  end
455
462
 
456
463
  #Insert values directly into a table. This class does not support the
457
- #use of SelectParts, rather it behaves like a hash of column names (as
458
- #symbols) to values. You can get the "slice" functionality by merging
459
- #hashes into the statement using <tt>Hash.merge</tt>.
464
+ #ability to merge a Select object, rather it behaves like a hash of column
465
+ #names (as symbols) to values. You can get the "slice" functionality by merging
466
+ #hashes into the statement using <tt>Hash#merge!</tt>.
460
467
  class Insert < Hash
461
468
  def initialize table
462
469
  @targettable=table
@@ -482,13 +489,16 @@ end
482
489
 
483
490
  end
484
491
 
485
- module SQLHelpers
492
+ class String
493
+ #Returns a SQLStatement::Identifier for this string
494
+ def dbid
495
+ SQLStatement::Identifier.new(self)
496
+ end
497
+ end
486
498
 
487
- #This helper method takes a string, and encapsulates it as a proper
488
- #SQL expression, so that it won't get passed to the database as
489
- #though it were a (potentially hostile) string value.
490
- def string_func(str,placeheld=[])
491
- SQLStatement::Function.new(str,placeheld)
492
- end
493
- extend self
499
+ class Symbol
500
+ #Returns a SQLStatement::Identifier for this Symbol
501
+ def dbid
502
+ SQLStatement::Identifier.new(to_s)
503
+ end
494
504
  end
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require '../lib/sql/statement.rb'
4
+
5
+ class TestStatementParts < Test::Unit::TestCase
6
+ include SQLHelpers
7
+
8
+ def test_shiftop
9
+ part=SQLStatement::Select.new do |s|
10
+ s.table :foo
11
+ s.leftjoin :faz, string_func("a=b")
12
+ s.field :bar
13
+ s.field :baz
14
+ end
15
+ targetstmt=SQLStatement::Select.new do |s|
16
+ s.table :foo
17
+ s.leftjoin :faz, string_func("a=b")
18
+ s.field :bar
19
+ s.field :baz
20
+ end
21
+
22
+ targetstmt << part
23
+
24
+ assert_equal "`foo`\n LEFT JOIN `faz` ON a=b", targetstmt.send(:tables_s)
25
+ assert_equal "`bar`, `baz`, `bar`, `baz`", targetstmt.send(:fields_s)
26
+ end
27
+
28
+ def test_plus
29
+ part=SQLStatement::Select.new do |s|
30
+ s.table :foo
31
+ s.leftjoin :faz, string_func("a=b")
32
+ s.field :bar
33
+ s.field :baz
34
+ end
35
+ part2=SQLStatement::Select.new do |s|
36
+ s.table :foo
37
+ s.leftjoin :faz, string_func("a=b")
38
+ s.field :bar
39
+ s.field :baz
40
+ end
41
+
42
+ targetstmt=part+part2
43
+
44
+ #neither of the original parts is modified
45
+ assert_equal "`bar`, `baz`", part.send(:fields_s)
46
+ assert_equal "`bar`, `baz`", part2.send(:fields_s)
47
+
48
+ #but the new one conforms to the semantics of <<
49
+ assert_equal "`foo`\n LEFT JOIN `faz` ON a=b", targetstmt.send(:tables_s)
50
+ assert_equal "`bar`, `baz`, `bar`, `baz`", targetstmt.send(:fields_s)
51
+ end
52
+ end
@@ -0,0 +1,31 @@
1
+ require 'test/unit'
2
+ require '../lib/sql/statement.rb'
3
+
4
+ class Primatives < Test::Unit::TestCase
5
+ def test_construction
6
+ foo=SQLStatement::Identifier.new("foo")
7
+ bar=SQLStatement::Identifier["bar"]
8
+ assert_kind_of SQLStatement::Identifier, foo
9
+ assert_kind_of SQLStatement::Identifier, bar
10
+ end
11
+
12
+ def test_dbid
13
+ assert_kind_of SQLStatement::Identifier, :foo.dbid
14
+ assert_kind_of SQLStatement::Identifier, "foo".dbid
15
+ end
16
+
17
+ def test_indexing
18
+ foo=SQLStatement::Identifier.new("foo")
19
+ bar=SQLStatement::Identifier["bar"]
20
+ assert_kind_of SQLStatement::SQL_Field, foo[bar]
21
+ assert_kind_of SQLStatement::SQL_Field, foo[:bar]
22
+ if RUBY_VERSION=~/^1\.8/
23
+ assert_kind_of Numeric, foo[1]
24
+ elsif RUBY_VERSION=~/^1\.9/
25
+ #I don't guarantee that this library can work with Ruby 1.9 to begin
26
+ #with, but just for good measure, let's get this test right.
27
+ assert_kind_of String, foo[1]
28
+ end
29
+ assert_kind_of String, foo[1,1]
30
+ end
31
+ end
@@ -0,0 +1,127 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require '../lib/sql/statement.rb'
4
+
5
+ class TestStatementParts < Test::Unit::TestCase
6
+ include SQLHelpers
7
+
8
+ def test_table
9
+ s=SQLStatement::Select.new
10
+ s.table :foo
11
+ assert_equal "`foo`", s.send(:tables_s)
12
+ s.table :bar
13
+ assert_equal "`foo`\n INNER JOIN `bar`", s.send(:tables_s)
14
+
15
+ #adding an existing table/alias pair doesn't add anything to the statement
16
+ s.table :foo
17
+ assert_equal "`foo`\n INNER JOIN `bar`", s.send(:tables_s)
18
+
19
+ #but adding the same table with a new alias does
20
+ s.table :foo, :alias
21
+ assert_equal "`foo`\n INNER JOIN `bar`\n INNER JOIN `foo` as `alias`", s.send(:tables_s)
22
+ end
23
+
24
+ def test_leftjoin
25
+ s=SQLStatement::Select.new
26
+ s.table :foo
27
+ assert_equal "`foo`", s.send(:tables_s)
28
+ s.leftjoin :bar, string_func("`foo`.`a`=`bar`.`a`")
29
+ assert_equal "`foo`\n LEFT JOIN `bar` ON `foo`.`a`=`bar`.`a`", s.send(:tables_s)
30
+
31
+ #adding the same table/alias pair adds nothing, and doesn't update the join condition
32
+ s.leftjoin :bar, string_func("`foo`.`b`=`bar`.`b`")
33
+ assert_equal "`foo`\n LEFT JOIN `bar` ON `foo`.`a`=`bar`.`a`", s.send(:tables_s)
34
+
35
+ #but adding the same table with a new alias adds a new join with the new condition
36
+ s.leftjoin :bar, string_func("`foo`.`b`=`baz`.`b`"), :baz
37
+ assert_equal "`foo`\n LEFT JOIN `bar` ON `foo`.`a`=`bar`.`a`
38
+ LEFT JOIN `bar` as `baz` ON `foo`.`b`=`baz`.`b`", s.send(:tables_s)
39
+ end
40
+
41
+ def test_fields
42
+ s=SQLStatement::Select.new
43
+ s.field :foo
44
+ assert_equal "`foo`", s.send(:fields_s)
45
+
46
+ #adding a field/alias pair does add it to the statement a second time
47
+ s.field :foo
48
+ assert_equal "`foo`, `foo`", s.send(:fields_s)
49
+
50
+ s.field :foo, :bar
51
+ assert_equal "`foo`, `foo`, `foo` as `bar`", s.send(:fields_s)
52
+
53
+ s.field string_func("1")
54
+ assert_equal "`foo`, `foo`, `foo` as `bar`, 1", s.send(:fields_s)
55
+ end
56
+
57
+ def test_conditions
58
+ s=SQLStatement::Select.new
59
+ assert_nil s.send(:conditions_s)
60
+ s.condition string_func("a=b")
61
+ assert_equal "a=b",s.send(:conditions_s)
62
+ s.condition string_func("b=c")
63
+ assert_equal "a=b\n AND b=c",s.send(:conditions_s)
64
+ end
65
+
66
+ def test_orderby
67
+ s=SQLStatement::Select.new
68
+ assert_nil s.send(:orderby_s)
69
+ s.orderby :foo
70
+ assert_equal "`foo`", s.send(:orderby_s)
71
+ s.orderby :bar
72
+ assert_equal "`foo`, `bar`", s.send(:orderby_s)
73
+ s.orderby string_func("`baz` desc")
74
+ assert_equal "`foo`, `bar`, `baz` desc", s.send(:orderby_s)
75
+ end
76
+
77
+ def test_groupby
78
+ s=SQLStatement::Select.new
79
+ assert_nil s.send(:groupby_s)
80
+ s.groupby :foo
81
+ assert_equal "`foo`", s.send(:groupby_s)
82
+ s.groupby :bar
83
+ assert_equal "`foo`, `bar`", s.send(:groupby_s)
84
+ end
85
+
86
+ # the names of the fields we are inserting into
87
+ def test_selectinsert_names
88
+ s=SQLStatement::SelectInsert.new
89
+ s.field :foo, :bar
90
+ assert_equal "`bar`",s.send(:names_s)
91
+ s.field :foo2, :baz
92
+ assert_equal "`bar`, `baz`",s.send(:names_s)
93
+ end
94
+
95
+ #the same tests as for Select, but for SelectInsert we are specifically
96
+ #specifying that the aliases show up in both as destination fields and as
97
+ #aliases in the select part
98
+ def test_selectinsert_fields
99
+ s=SQLStatement::SelectInsert.new
100
+ s.field :foo
101
+ assert_equal "`foo`", s.send(:fields_s)
102
+
103
+ #adding a field/alias pair does add it to the statement a second time
104
+ s.field :foo
105
+ assert_equal "`foo`, `foo`", s.send(:fields_s)
106
+
107
+ s.field :foo, :bar
108
+ assert_equal "`foo`, `foo`, `foo` as `bar`", s.send(:fields_s)
109
+
110
+ s.field string_func("1")
111
+ assert_equal "`foo`, `foo`, `foo` as `bar`, 1", s.send(:fields_s)
112
+ end
113
+ def test_updatepart
114
+ s=SQLStatement::Update.new
115
+ assert_raise ArgumentError do
116
+ #an alias is required, because it's going to be the name of the field to update
117
+ s.field :foo
118
+ end
119
+
120
+ s.field :foo, :bar
121
+ assert_equal "`bar`=`foo`", s.send(:updatepart_s)
122
+
123
+ s.field string_func("1"), :baz
124
+ assert_equal "`bar`=`foo`, `baz`=1", s.send(:updatepart_s)
125
+ end
126
+
127
+ end
@@ -0,0 +1,68 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require '../lib/sql/statement.rb'
4
+
5
+ class TestStatements < Test::Unit::TestCase
6
+ include SQLHelpers
7
+
8
+
9
+ def test_createselect
10
+ parts=SQLStatement::SelectCreate.new do |s|
11
+ s.targettable = :targettable
12
+ s.field :a[:b],:targetcol
13
+ s.table :extracted_appraisal_expressions_26, :a
14
+ s.table :extracted_appraisal_26, :d
15
+ s.leftjoin :dictionary_appraisal_attitude, string_func("a.att_appraisal_attitude_left=b.left_key"), :b
16
+ s.leftjoin :dictionary_deixis_deixis, string_func("a.att_deixis_deixis_left=c.left_key"), :c
17
+ s.condition string_func("a>1")
18
+ end
19
+ expected="CREATE TABLE `targettable` SELECT
20
+ `a`.`b` as `targetcol`
21
+ FROM `extracted_appraisal_expressions_26` as `a`
22
+ INNER JOIN `extracted_appraisal_26` as `d`
23
+ LEFT JOIN `dictionary_appraisal_attitude` as `b` ON a.att_appraisal_attitude_left=b.left_key
24
+ LEFT JOIN `dictionary_deixis_deixis` as `c` ON a.att_deixis_deixis_left=c.left_key
25
+ WHERE a>1"
26
+ assert_equal expected, parts.to_s
27
+ end
28
+
29
+ def test_insertselect
30
+ parts=SQLStatement::SelectInsert.new do |s|
31
+ s.targettable = :targettable
32
+ s.field :a[:b],:targetcol
33
+ s.table :extracted_appraisal_expressions_26, :a
34
+ s.table :extracted_appraisal_26, :d
35
+ s.leftjoin :dictionary_appraisal_attitude, string_func("a.att_appraisal_attitude_left=b.left_key"), :b
36
+ s.leftjoin :dictionary_deixis_deixis, string_func("a.att_deixis_deixis_left=c.left_key"), :c
37
+ s.condition string_func("a>1")
38
+ end
39
+ expected="INSERT INTO `targettable` (`targetcol`) SELECT
40
+ `a`.`b` as `targetcol`
41
+ FROM `extracted_appraisal_expressions_26` as `a`
42
+ INNER JOIN `extracted_appraisal_26` as `d`
43
+ LEFT JOIN `dictionary_appraisal_attitude` as `b` ON a.att_appraisal_attitude_left=b.left_key
44
+ LEFT JOIN `dictionary_deixis_deixis` as `c` ON a.att_deixis_deixis_left=c.left_key
45
+ WHERE a>1"
46
+ assert_equal expected, parts.to_s
47
+ end
48
+ def test_select
49
+ parts=SQLStatement::Select.new do |s|
50
+ s.straight_join=true
51
+ s.distinct=true
52
+ s.field :a[:*]
53
+ s.table :extracted_appraisal_expressions_26, :a
54
+ s.table :extracted_appraisal_26, :d
55
+ s.leftjoin :dictionary_appraisal_attitude, string_func("a.att_appraisal_attitude_left=b.left_key"), :b
56
+ s.leftjoin :dictionary_deixis_deixis, string_func("a.att_deixis_deixis_left=c.left_key"), :c
57
+ s.condition string_func("a>1")
58
+ end
59
+ expected="SELECT DISTINCT STRAIGHT_JOIN
60
+ `a`.*
61
+ FROM `extracted_appraisal_expressions_26` as `a`
62
+ INNER JOIN `extracted_appraisal_26` as `d`
63
+ LEFT JOIN `dictionary_appraisal_attitude` as `b` ON a.att_appraisal_attitude_left=b.left_key
64
+ LEFT JOIN `dictionary_deixis_deixis` as `c` ON a.att_deixis_deixis_left=c.left_key
65
+ WHERE a>1"
66
+ assert_equal expected, parts.to_s
67
+ end
68
+ end
metadata CHANGED
@@ -1,63 +1,75 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.0
3
- specification_version: 1
4
2
  name: SqlStatement
5
3
  version: !ruby/object:Gem::Version
6
- version: 1.0.2
7
- date: 2006-12-18 00:00:00 -06:00
8
- summary: A library for generating arbitrary SQL statements using convenient Ruby objects.
9
- require_paths:
10
- - lib
11
- email: kbloom@gmail.com
12
- homepage: http://www.rubyforge.org/sqlstatement/
13
- rubyforge_project:
14
- description:
15
- autorequire: sqlstatement
16
- default_executable:
17
- bindir: bin
18
- has_rdoc: true
19
- required_ruby_version: !ruby/object:Gem::Version::Requirement
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: "1.8"
24
- version:
4
+ version: "2.0"
25
5
  platform: ruby
26
- signing_key:
27
- cert_chain:
28
- post_install_message:
29
6
  authors:
30
7
  - Ken Bloom
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-01-20 00:00:00 -06:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rubynode
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ version:
24
+ description:
25
+ email: kbloom@gmail.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - doc/ChangeLog
31
32
  files:
32
- - lib/sql
33
33
  - lib/sqlstatement.rb
34
- - lib/sql/expression.rb
34
+ - lib/sql
35
+ - lib/sql/dbi-support.rb
35
36
  - lib/sql/bathon-sxp.rb
37
+ - lib/sql/expression.rb
36
38
  - lib/sql/statement.rb
37
- - lib/sql/dbi-support.rb
38
- - doc/EXAMPLE
39
+ - test/test_combine_statement.rb
40
+ - test/test_statements.rb
41
+ - test/test_statementparts.rb
42
+ - test/test_primatives.rb
39
43
  - doc/ChangeLog
40
- test_files: []
41
-
44
+ has_rdoc: true
45
+ homepage: http://www.rubyforge.org/sqlstatement/
46
+ post_install_message:
42
47
  rdoc_options:
43
48
  - --main
44
49
  - lib/sqlstatement.rb
45
- extra_rdoc_files:
46
- - doc/EXAMPLE
47
- - doc/ChangeLog
48
- executables: []
49
-
50
- extensions: []
51
-
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "1.8"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
52
64
  requirements: []
53
65
 
54
- dependencies:
55
- - !ruby/object:Gem::Dependency
56
- name: rubynode
57
- version_requirement:
58
- version_requirements: !ruby/object:Gem::Version::Requirement
59
- requirements:
60
- - - ">"
61
- - !ruby/object:Gem::Version
62
- version: 0.0.0
63
- version:
66
+ rubyforge_project:
67
+ rubygems_version: 1.0.1
68
+ signing_key:
69
+ specification_version: 2
70
+ summary: A library for generating arbitrary SQL statements using convenient Ruby objects.
71
+ test_files:
72
+ - test/test_combine_statement.rb
73
+ - test/test_statements.rb
74
+ - test/test_statementparts.rb
75
+ - test/test_primatives.rb
data/doc/EXAMPLE DELETED
@@ -1,124 +0,0 @@
1
- #=Example
2
- # # We subclass the standard classes so that we can construct
3
- # # a query with a complex computed field. The newlists method
4
- # # makes this easy, giving us a constructor and combination
5
- # # operators for free.
6
- #
7
- # class ProbabilityParts < SQLStatement::SelectParts
8
- # newlists :multiply
9
- # end
10
- # class ProbabilityStatement < SQLStatement::SelectCreate
11
- # newlists :multiply
12
- #
13
- # # Add our own special fields here, computed
14
- # # from the list we added to this class.
15
- # def allfields
16
- # retval=super
17
- # retval.merge! :probability =>
18
- # SQLFunc(@multiply.collect{|x| x.to_sqlpart}.join('*'))
19
- # retval
20
- # end
21
- # end
22
- #
23
- #
24
- # #In real life, I use a special library for detecting what attributes
25
- # #are available based on the columns in my tables.
26
- # #We'll mimic that functionality here.
27
- # #
28
- # #The first definition of this module mocks the stuff that's in my more
29
- # #complicated library
30
- # module AttributeDetection
31
- # class Attribute
32
- # def initialize(domain,name)
33
- # @domain=domain
34
- # @name=name
35
- # end
36
- # attr_reader :name, :domain
37
- # end
38
- # class Hierarchy < Attribute
39
- # def initialize(leftcolumn)
40
- # ignored1,m_domain,m_name,ignored2=leftcolumn.split(/_/)
41
- # super(m_domain,m_name)
42
- # end
43
- # end
44
- # class DomainlessPerGroup < Attribute
45
- # def initialize (colname)
46
- # name=@colname=colname
47
- # domain="!"
48
- # super(domain,name)
49
- # end
50
- # end
51
- #
52
- # #I have a few other attribute types, none of which
53
- # #are used in this code yet.
54
- #
55
- # end
56
- #
57
- # include SQLHelpers
58
- #
59
- # #The second definition of this module is the code that I actually define
60
- # #in this particular program.
61
- # module AttributeDetection
62
- # class Attribute
63
- # def condname
64
- # :"cond_#{name}"
65
- # end
66
- # def priorname
67
- # :"prior_#{name}"
68
- # end
69
- # #parts that go in to the CREATE TABLE `probabilities` statement
70
- # #which relate to the attribute we are disambiguating
71
- # def probability_disambig
72
- # parts=ProbabilityParts.new
73
- # parts.add_fields [priorname[probkey]]
74
- # parts.add_tables [priorname]
75
- # parts.multiply << priorname[:pt]
76
- # parts
77
- # end
78
- # #parts that go in to the CREATE TABLE `probabilities` statement
79
- # #which relate to the attributes we are using as features
80
- # def probability_features(conditionalon)
81
- # parts=ProbabilityParts.new
82
- # parts.tables={priorname=>priorname,condname=>condname}
83
- # parts.add_fields [priorname[probkey]]
84
- # parts.conditions << sql_func{ condname[probkey]==priorname[probkey]}
85
- # parts.conditions << sql_func{
86
- # condname[conditionalon.probkey]==
87
- # conditionalon.priorname[conditionalon.probkey]
88
- # }
89
- # parts.multiply << sql_func{condname[:pt]/priorname[:pt]}
90
- # parts
91
- # end
92
- # end
93
- # class Hierarchy
94
- # def probkey
95
- # "att_#{domain}_#{name}_left".to_sym
96
- # end
97
- # end
98
- # class DomainlessPerGroup
99
- # def probkey
100
- # @colname.to_sym
101
- # end
102
- # end
103
- # end
104
- #
105
- # include AttributeDetection
106
- # include SQLHelpers
107
- #
108
- # #And this code calls methods to actually build the query.
109
- #
110
- # #First we define the names of the attributes
111
- # att_disambig=Hierarchy.new("att_appraisal_attitude_left")
112
- # att_features=[Hierarchy.new("att_products_appraisedtype_left"),
113
- # DomainlessPerGroup.new("priority")]
114
- #
115
- #
116
- # #Then we construct the statement
117
- # stmt=ProbabilityStatement.new :probabilities,true
118
- #
119
- # stmt << att_disambig.probability_disambig
120
- # att_features.each { |x| stmt<< x.probability_features(att_disambig) }
121
- #
122
- # #Finally, we execute it against the database. Assume dbh is a
123
- # #dbi connection handle.
124
- # dbh.execute(stmt.to_s,*stmt.placeheld)