iotaz 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,566 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ #--
4
+ # Copyright � Peter Wood, 2005
5
+ #
6
+ # The contents of this file are subject to the Mozilla Public License Version
7
+ # 1.1 (the "License"); you may not use this file except in compliance with the
8
+ # License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.mozilla.org/MPL/
11
+ #
12
+ # Software distributed under the License is distributed on an "AS IS" basis,
13
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
14
+ # the specific language governing rights and limitations under the License.
15
+ #
16
+ # The Original Code is the FireRuby extension for the Ruby language.
17
+ #
18
+ # The Initial Developer of the Original Code is Peter Wood. All Rights
19
+ # Reserved.
20
+ #++
21
+ #
22
+
23
+ require 'stringio'
24
+ require 'iotaz/IotazError'
25
+
26
+ module Iotaz
27
+ #
28
+ # This class represents a field that constitutes a component within a Query
29
+ # object.
30
+ #
31
+ class QueryField
32
+ #
33
+ # This is a constructor for the QueryField class.
34
+ #
35
+ # ==== Parameters
36
+ # name:: A string containing the name of the attribute to be made
37
+ # into a QueryField
38
+ # metadata:: A reference to the class IotazMetaData object that contains
39
+ # the attribute details.
40
+ # fetched:: A boolean flag to indicate whether the field should be
41
+ # part of the list of values fetched. This defaults to true.
42
+ #
43
+ def initialize(name, metadata, fetched=true)
44
+ @name = name
45
+ @metadata = metadata
46
+ @fetched = fetched
47
+ @comparison = nil
48
+ @value = nil
49
+ if @metadata.get_attribute(name) == nil
50
+ raise IotazError.new("'{0}' is not an attribute of the {1} class "\
51
+ "and therefore cannot be used to create "\
52
+ "query field.", name, metadata.name)
53
+ end
54
+ end
55
+
56
+
57
+ #
58
+ # Attribute accessor that fetches the column name for a QueryField.
59
+ #
60
+ def column
61
+ @metadata.get_attribute(@name).column
62
+ end
63
+
64
+
65
+ #
66
+ # Attribute accessor that fetches the column table name for a QueryField.
67
+ #
68
+ def table
69
+ @metadata.table
70
+ end
71
+
72
+
73
+ #
74
+ # This method attempts to perform a field name match for a QueryField
75
+ # object. A name matches a QueryField name if the name provided...
76
+ #
77
+ # - Matches the name of the underlying attribute.
78
+ #
79
+ # - Matches the name of the underlying attribute qualified by its class
80
+ # name.
81
+ #
82
+ # - Matches the name fo the underlying attributes column name.
83
+ #
84
+ # - Matches the name for the underlying attributes column name qualified
85
+ # with it's table name.
86
+ #
87
+ def name_match?(name)
88
+ attribute = @metadata.get_attribute(@name)
89
+ @name == name or
90
+ "#{@metadata.name}.#{@name}" == name or
91
+ "#{attribute.column}" == name or
92
+ "#{@metadata.table}.#{attribute.column}" == name
93
+ end
94
+
95
+
96
+ #
97
+ # This method sets a query field for a straight equivalence comaparison
98
+ # to another value. If the value passed to this method is nil then this
99
+ # changes the comparison to an is null check.
100
+ #
101
+ # ==== Parameters
102
+ # value:: The value to compare the field to.
103
+ #
104
+ def equal_to(value)
105
+ @comparison = value == nil ? 'is null' : '= ?'
106
+ @value = value
107
+ end
108
+
109
+
110
+ #
111
+ # This method sets a query field for a straight non-equivalence
112
+ # comaparison to another value. If the value passed to this method is nil
113
+ # then this changes the comparison to an is not null check.
114
+ #
115
+ # ==== Parameters
116
+ # value:: The value to compare the field to.
117
+ #
118
+ def not_equal_to(value)
119
+ @comparison = value == nil ? 'is not null' : '!= ?'
120
+ @value = value
121
+ end
122
+
123
+
124
+ #
125
+ # This method sets a query feld for a greater than comparison to another
126
+ # value.
127
+ #
128
+ # ==== Parameters
129
+ # value:: The value to perform the comparison against. This must not be
130
+ # nil.
131
+ #
132
+ # ==== Exceptions
133
+ # IotazError:: Generated whenever a nil value is specified to the method.
134
+ #
135
+ def greater_than(value)
136
+ if value == nil
137
+ raise IotazError.new("Nil value specified for greater than "\
138
+ "comparison on the {0} query field.",
139
+ @attribute.name)
140
+ end
141
+ @comparison = '> ?'
142
+ @value = value
143
+ end
144
+
145
+
146
+ #
147
+ # This method sets a query feld for a less than comparison to another
148
+ # value.
149
+ #
150
+ # ==== Parameters
151
+ # value:: The value to perform the comparison against. This must not be
152
+ # nil.
153
+ #
154
+ # ==== Exceptions
155
+ # IotazError:: Generated whenever a nil value is specified to the method.
156
+ #
157
+ def less_than(value)
158
+ if value == nil
159
+ raise IotazError.new("Nil value specified for less than "\
160
+ "comparison on the {0} query field.",
161
+ @attribute.name)
162
+ end
163
+ @comparison = '< ?'
164
+ @value = value
165
+ end
166
+
167
+
168
+ #
169
+ # This method sets a query feld for a greater than comparison to another
170
+ # value.
171
+ #
172
+ # ==== Parameters
173
+ # value:: The value to perform the comparison against. This must not be
174
+ # nil.
175
+ #
176
+ # ==== Exceptions
177
+ # IotazError:: Generated whenever a nil value is specified to the method.
178
+ #
179
+ def greater_than_or_equal_to(value)
180
+ if value == nil
181
+ raise IotazError.new("Nil value specified for greater than or "\
182
+ "equal to comparison on the {0} query field.",
183
+ @attribute.name)
184
+ end
185
+ @comparison = '>= ?'
186
+ @value = value
187
+ end
188
+
189
+
190
+ #
191
+ # This method sets a query feld for a less than comparison to another
192
+ # value.
193
+ #
194
+ # ==== Parameters
195
+ # value:: The value to perform the comparison against. This must not be
196
+ # nil.
197
+ #
198
+ # ==== Exceptions
199
+ # IotazError:: Generated whenever a nil value is specified to the method.
200
+ #
201
+ def less_than_or_equal_to(value)
202
+ if value == nil
203
+ raise IotazError.new("Nil value specified for less than or equal"\
204
+ "to comparison on the {0} query field.",
205
+ @attribute.name)
206
+ end
207
+ @comparison = '<= ?'
208
+ @value = value
209
+ end
210
+
211
+
212
+ #
213
+ # This method sets a query for a between comparison to two other values.
214
+ #
215
+ # ==== Parameters
216
+ # lower:: A reference to the value that represents the upper edge of the
217
+ # comparison. This value must not be nil.
218
+ # upper:: A reference to the value that represents the upper edge of the
219
+ # comparison. This value must not be nil.
220
+ #
221
+ def between(lower, upper)
222
+ if lower == nil or upper == nil
223
+ raise IotazError.new("Nil value specified for between comparison "\
224
+ "on the {0} query field.", @attribute.name)
225
+ end
226
+ @comparison = 'between ? and ?'
227
+ @value = [lower, upper]
228
+ end
229
+
230
+
231
+ #
232
+ # This method sets a query for a not between comparison to two other
233
+ # values.
234
+ #
235
+ # ==== Parameters
236
+ # lower:: A reference to the value that represents the upper edge of the
237
+ # comparison. This value must not be nil.
238
+ # upper:: A reference to the value that represents the upper edge of the
239
+ # comparison. This value must not be nil.
240
+ #
241
+ def not_between(lower, upper)
242
+ if lower == nil or upper == nil
243
+ raise IotazError.new("Nil value specified for not between "\
244
+ "comparison on the {0} query field.",
245
+ @attribute.name)
246
+ end
247
+ @comparison = 'not between ? and ?'
248
+ @value = [lower, upper]
249
+ end
250
+
251
+
252
+ #
253
+ # This method sets a query for an in comparison to a set of values.
254
+ #
255
+ # ==== Parameters
256
+ # values:: An array containing the full set of values to be compared
257
+ # with. This may not be empty.
258
+ #
259
+ def in(*values)
260
+ if values.size == 0
261
+ raise IotazError.new("Empty values set specified for in "\
262
+ "comparison on the {0} query field.",
263
+ @attribute.name)
264
+ end
265
+
266
+ list = []
267
+ values.each do |entry|
268
+ text = entry.class == String ? "'#{entry}'" : entry
269
+ list.push(text)
270
+ end
271
+
272
+ @comparison = "in (#{list.join(', ')})"
273
+ @value = nil
274
+ end
275
+
276
+
277
+ #
278
+ # This method sets a query for a not in comparison to a set of values.
279
+ #
280
+ # ==== Parameters
281
+ # values:: An array containing the full set of values to be compared
282
+ # with. This may not be empty.
283
+ #
284
+ def not_in(*values)
285
+ if values.size == 0
286
+ raise IotazError.new("Empty values set specified for not in "\
287
+ "comparison on the {0} query field.",
288
+ @attribute.name)
289
+ end
290
+
291
+ list = []
292
+ values.each do |entry|
293
+ text = entry.class == String ? "'#{entry}'" : entry
294
+ list.push(text)
295
+ end
296
+
297
+ @comparison = "not in (#{list.join(', ')})"
298
+ @value = nil
299
+ end
300
+
301
+
302
+ # Attribute accessor.
303
+ attr_reader :name, :fetched, :comparison, :value
304
+
305
+ # Attribute mutator.
306
+ attr_writer :fetched
307
+
308
+ # Method aliases.
309
+ alias :fetched? :fetched
310
+ end # Endo of the QueryField class.
311
+
312
+
313
+ #
314
+ # This class models a SQL query against one or more database tables.
315
+ #
316
+ class Query
317
+ #
318
+ # This is the constructor for the Query class.
319
+ #
320
+ # ==== Parameters
321
+ # klass:: A reference to a Class object representing the first table
322
+ # element of the query.
323
+ # fields:: A array of the name of class attributes that are to be
324
+ # included in the results for the query. If this is empty
325
+ # then all fields are marked as fetched.
326
+ #
327
+ def initialize(klass, *fields)
328
+ # Initialise instances values.
329
+ @definitions = []
330
+ @fields = []
331
+
332
+ # Process the class attributes as fields.
333
+ @definitions.push(klass.iotaz_meta_data)
334
+ @definitions[0].each do |attribute|
335
+ flag = fields.size > 0 ? fields.include?(attribute.name) : true
336
+ @fields.push(QueryField.new(attribute.name, @definitions[0], flag))
337
+ end
338
+ end
339
+
340
+
341
+ #
342
+ # This method retrieves a reference to a QueryField object within a
343
+ # Query instance. Fields my be referenced in a number of ways. They can
344
+ # be referenced by their attribute name, they can be referenced to their
345
+ # attribute name qualified by a class name, they can be referenced by
346
+ # their column name or they can be referenced by their column name
347
+ # qualified by their table name. The method returns nil if the specified
348
+ # field does not exist.
349
+ #
350
+ # ==== Parameters
351
+ # name:: The name of the field to be retrieved.
352
+ #
353
+ # ==== Exceptions
354
+ # IotazError:: Generated whenever the field name specified is ambiguous
355
+ # and can potential refer to multiple fields.
356
+ #
357
+ def field(name)
358
+ field = get_fields(name)
359
+ if field.class == Array
360
+ raise IotazError.new("Ambiguous field name. The name '{0}' cannot "\
361
+ "be resolved to a unique field.", name)
362
+ end
363
+ field
364
+ end
365
+
366
+
367
+ #
368
+ # This method generates the SQL statement for a Query object and assembles
369
+ # the list of parameters that will be needed to execute it. The method
370
+ # returns two elements, a string containing the SQL and an array
371
+ # containing the parameters.
372
+ #
373
+ def to_sql
374
+ parameters = []
375
+ select = StringIO.new
376
+ from = StringIO.new
377
+ where = StringIO.new
378
+
379
+ fields = 0
380
+ tables = []
381
+ clauses = 0
382
+
383
+ select << 'select '
384
+ from << ' from '
385
+ where << ' where '
386
+
387
+ @fields.each do |field|
388
+ # Check if the field is retrieved.
389
+ if field.fetched?
390
+ select << ', ' if fields > 0
391
+ select << field.column
392
+ fields += 1
393
+ end
394
+
395
+ # Check if the fields table has already been added,
396
+ pattern = Regexp.new(field.table)
397
+ if tables.include?(field.table) == false
398
+ from << ", " if tables.size > 0
399
+ from << field.table
400
+ tables.push(field.table)
401
+ end
402
+
403
+ # Check if the field has a comparison.
404
+ if field.comparison != nil
405
+ where << ' and ' if clauses > 0
406
+ where << field.column << ' ' << field.comparison
407
+ if field.value.class == Array
408
+ parameters = parameters.concat(field.value)
409
+ else
410
+ parameters.push(field.value) if field.value != nil
411
+ end
412
+ clauses += 1
413
+ end
414
+ end
415
+
416
+ # Generate the SQL statement.
417
+ sql = select.string + from.string
418
+ sql += where.string if clauses > 0
419
+
420
+ [sql, parameters]
421
+ end
422
+
423
+
424
+ #
425
+ # This method is used for field name matching. The method will return
426
+ # one of three possible values. If a specified name uniquely matches a
427
+ # field within the Query object then this will be returned. If it matches
428
+ # more than one field then an array of fields will be returned. If it
429
+ # matches not fields then nil will be returned.
430
+ #
431
+ # ==== Parameters
432
+ # name:: A reference to the name of the field to be retrieved. See the
433
+ # description for the field method for details of the values that
434
+ # are accepted for this parameter.
435
+ #
436
+ def get_fields(name)
437
+ result = nil
438
+
439
+ matches = @fields.collect do |field|
440
+ field.name_match?(name) ? field : nil
441
+ end
442
+ matches.compact!
443
+
444
+ if matches.size > 0
445
+ result = matches.size == 1 ? matches[0] : matches
446
+ end
447
+
448
+ result
449
+ end
450
+
451
+
452
+ # Method access alterations.
453
+ private :get_fields
454
+ end # End of the Query class.
455
+
456
+
457
+ #
458
+ # This class models a row of data returned from the execution of a query.
459
+ # The class provides a mapping between the (case insensitive) column names
460
+ # and the column values for the row.
461
+ #
462
+ class QueryRow
463
+ #
464
+ # This is the constructor for the QueryRow class.
465
+ #
466
+ def initialize
467
+ @entries = Hash.new
468
+ end
469
+
470
+ #
471
+ # This method fetches a count of the number of columns within a QueryRow
472
+ # object.
473
+ #
474
+ def size
475
+ @entries.size
476
+ end
477
+
478
+
479
+ #
480
+ # This method test whether a given column name exists within a QueryRow
481
+ # object.
482
+ #
483
+ # ==== Parameters
484
+ # name:: The name of the column to check for.
485
+ #
486
+ def exists?(name)
487
+ @entries.key?(name.upcase)
488
+ end
489
+
490
+
491
+ #
492
+ # This method fetches an array of the column names for a QueryRow object.
493
+ #
494
+ def columns
495
+ @entries.keys
496
+ end
497
+
498
+
499
+ #
500
+ # This method fetches an array of the column values for a QueryRow object.
501
+ #
502
+ def values
503
+ @entries.values
504
+ end
505
+
506
+
507
+ #
508
+ # This method provides an iterator for the contents of a QueryRow object.
509
+ # The block passed to this method should take two parameters. The first
510
+ # will be the column name and the second will be the column value.
511
+ #
512
+ def each
513
+ if block_given?
514
+ @entries.each {|name, value| yield(name, value)}
515
+ end
516
+ end
517
+
518
+
519
+ #
520
+ # This method adds or updates a new column value to a QueryRow object.
521
+ # Note that the column names used by this class are case insensitive so
522
+ # 'COLUMN' is the same as 'Column', 'column' or 'cOLumn'.
523
+ #
524
+ # ==== Parameters
525
+ # name:: A reference to the column name to be stored or updated.
526
+ # value:: A reference to the column value to be stored.
527
+ #
528
+ def []=(name, value)
529
+ @entries[name.upcase] = value
530
+ end
531
+
532
+
533
+ #
534
+ # This method fetches the value for a specified column. If the specified
535
+ # column does not exist then an exception is thrown.
536
+ #
537
+ # ==== Parameters
538
+ # name:: A reference to a string containing the name of the column to
539
+ # fetch the value for.
540
+ #
541
+ # ==== Exception
542
+ # IotazError:: Generated whenever the column name specified does not
543
+ # exist within the QueryRow object.
544
+ #
545
+ def [](name)
546
+ if @entries.key?(name.upcase) == false
547
+ raise IotazError.new("Column not found. A '{0}' column does not "\
548
+ "exist in the query row data.", name)
549
+ end
550
+ @entries[name.upcase]
551
+ end
552
+
553
+
554
+ #
555
+ # This method provides a string description for a QueryRow object.
556
+ #
557
+ def to_s
558
+ out = StringIO.new
559
+ @entries.each do |column, value|
560
+ out << ', ' if out.string.length > 0
561
+ out << "#{column} = #{value}"
562
+ end
563
+ out.string
564
+ end
565
+ end # End of the QueryRow class.
566
+ end # End of the Iotaz module.