iotaz 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/PERSISTENT +137 -0
- data/doc/README +470 -0
- data/examples/example01.rb +105 -0
- data/examples/example02.rb +105 -0
- data/lib/iotaz.rb +35 -0
- data/lib/iotaz/DatabaseInterfaceFactory.rb +92 -0
- data/lib/iotaz/FirebirdInterface.rb +788 -0
- data/lib/iotaz/IotazError.rb +81 -0
- data/lib/iotaz/MetaData.rb +646 -0
- data/lib/iotaz/ObjectPool.rb +299 -0
- data/lib/iotaz/Persistent.rb +155 -0
- data/lib/iotaz/Query.rb +566 -0
- data/lib/iotaz/Session.rb +379 -0
- data/lib/iotaz/WorkSet.rb +295 -0
- data/test/ActionTest.rb +31 -0
- data/test/AtomTest.rb +90 -0
- data/test/AttributeTest.rb +64 -0
- data/test/DatabaseInterfaceFactoryTest.rb +41 -0
- data/test/FirebirdInterfaceTest.rb +276 -0
- data/test/GeneratedAttributeTest.rb +124 -0
- data/test/IotazErrorTest.rb +23 -0
- data/test/IotazMetaDataTest.rb +87 -0
- data/test/ObjectPoolTest.rb +106 -0
- data/test/PersistentTest.rb +102 -0
- data/test/QueryFieldTest.rb +152 -0
- data/test/QueryRowTest.rb +42 -0
- data/test/QueryTest.rb +59 -0
- data/test/SessionTest.rb +162 -0
- data/test/UnitTest.rb +17 -0
- data/test/ValueCallbackTest.rb +44 -0
- data/test/dbinfo.rb +7 -0
- metadata +75 -0
data/lib/iotaz/Query.rb
ADDED
@@ -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.
|