SqlStatement 1.0.2 → 2.0

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