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