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,379 @@
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 specificlanguage 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 'iotaz/WorkSet'
24
+ require 'iotaz/FirebirdInterface'
25
+ require 'thread'
26
+
27
+ module Iotaz
28
+ #
29
+ # This class represents a session of interaction with the persistence
30
+ # mechanism.
31
+ #
32
+ class Session
33
+ #
34
+ # This is the constructor for the Session class.
35
+ #
36
+ # ==== Parameters
37
+ # interface:: A reference to the database interface object that the
38
+ # session will be linked to.
39
+ # listener:: A reference to an object that is interested in knowing
40
+ # if the session is terminated. This is used to allow the
41
+ # SessionFactory class to monitor available sessions. The
42
+ # object specified must implement a terminating method that
43
+ # will be invoked when the session is terminated. This
44
+ # defaults to nil to indicate no interested object.
45
+ #
46
+ def initialize(interface, listener=nil)
47
+ @interface = interface
48
+ @listener = listener
49
+ @atom = nil
50
+ end
51
+
52
+
53
+ #
54
+ # This method initializes a transaction within a session. The method
55
+ # accepts a block that delimits the lifetime of the transaction.
56
+ #
57
+ def start_transaction
58
+ @atom = Atom.new
59
+ if block_given?
60
+ begin
61
+ yield
62
+ commit
63
+ rescue
64
+ rollback
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ #
71
+ # This method invalidates a session, releasing it resources. After a call
72
+ # to this method a Session object should no longer be used.
73
+ #
74
+ def terminate
75
+ @listener.terminating(self) if @listener != nil
76
+ @interface = @atom = nil
77
+ end
78
+
79
+
80
+ #
81
+ # This method attempts to commit any outstanding transaction.
82
+ #
83
+ # ==== Exceptions
84
+ # IotazError:: Generated whenever a problem occurs committing the work
85
+ # for the transaction.
86
+ #
87
+ def commit
88
+ @atom.commit(@interface)
89
+ @atom = nil
90
+ end
91
+
92
+
93
+ #
94
+ # This method attempts to roll back any outstanding transaction.
95
+ #
96
+ # ==== Exceptions
97
+ # IotazError:: Generated whenever a problem occurs rolling back the work
98
+ # for the transaction.
99
+ #
100
+ def rollback
101
+ @atom = nil
102
+ end
103
+
104
+
105
+ #
106
+ # This method saves the details for an object to persistent store. If
107
+ # a transaction is active the actual push to the database is differed
108
+ # until a commit.
109
+ #
110
+ # ==== Parameters
111
+ # object:: A reference to the object to be saved.
112
+ #
113
+ def save(object)
114
+ metadata = object.class.iotaz_meta_data
115
+ saved = true
116
+
117
+ # Check if the object class key is nil.
118
+ metadata.keys.each do |key|
119
+ attribute = metadata.get_attribute(key)
120
+ if attribute.is_generated?
121
+ saved = false if attribute.get(object) == nil
122
+ end
123
+ break if saved == false
124
+ end
125
+
126
+ # Create the action for the save.
127
+ action = callbacks = nil
128
+ if saved
129
+ sql, callbacks, parameters = @interface.get_update_sql(object)
130
+ else
131
+ # Generate the action to save the object.
132
+ sql, callbacks, parameters = @interface.get_insert_sql(object)
133
+ end
134
+ action = Action.new(sql)
135
+ action.add_parameters(*parameters)
136
+
137
+ # Check if a transaction is active.
138
+ if @atom == nil
139
+ execute_immediate(action, *callbacks)
140
+ else
141
+ @atom.add(action, *callbacks)
142
+ end
143
+ end
144
+
145
+
146
+ #
147
+ # This method is used to load an object from the database based on its
148
+ # keys values.
149
+ #
150
+ # ==== Parameters
151
+ # klass:: The class of the object that will be loaded. This class
152
+ # must provide an iotaz_load method the creates an instance of
153
+ # the class from an data set loaded from the database.
154
+ # parameters:: An array of the parameters that for the key values to be
155
+ # used to access the specified record.
156
+ #
157
+ # ==== Exceptions
158
+ # IotazError:: Generated whenever insufficient parameters are provided,
159
+ # the select fetches more than one row or a problem occurs
160
+ # accessing the database.
161
+ #
162
+ def load(klass, *parameters)
163
+ sql = @interface.get_fetch_sql(klass)
164
+ transaction = @interface.start_transaction
165
+ begin
166
+ # Fetch the record data.
167
+ rows = @interface.execute_select(sql, parameters, transaction)
168
+ if rows.size > 1
169
+ raise IotazError.new("Error loading a '{0}' class instance. Key "\
170
+ "matches more than one row in the database.",
171
+ klass.name)
172
+ elsif rows.size == 0
173
+ raise IotazError.new("Error loading a '{0}' class instance. "\
174
+ "Object not found in database.",
175
+ klass.name)
176
+ end
177
+ @interface.commit(transaction)
178
+ transaction = nil
179
+
180
+ # Create and populate the object instance.
181
+ klass.iotaz_meta_data.populate(klass.allocate, rows[0])
182
+ ensure
183
+ @interface.rollback(transaction) if transaction != nil
184
+ end
185
+ end
186
+
187
+
188
+ #
189
+ # This method deletes the database record for an object from the database.
190
+ #
191
+ # ==== Parameters
192
+ # object:: A reference to the object to be deleted.
193
+ #
194
+ def delete(object)
195
+ metadata = object.class.iotaz_meta_data
196
+ action = Action.new(@interface.get_delete_sql(object))
197
+ action.add_parameters(*metadata.get_key_values(object))
198
+
199
+ if @atom == nil
200
+ execute_immediate(action)
201
+ else
202
+ @atom.add(action)
203
+ end
204
+ end
205
+
206
+
207
+ #
208
+ # This method executes a Query object using a Session object. The method
209
+ # returns an array of QueryRow objects.
210
+ #
211
+ # ==== Parameters
212
+ # query:: A reference to the Query object that will be executed.
213
+ # maximum:: An integer specifying the maximum number of rows to be
214
+ # retrieved. Defaults to -1 to indicate a fetch of all rows.
215
+ #
216
+ # ==== Exceptions
217
+ # IotazError:: Generated whenever a problem occurs executing the query.
218
+ #
219
+ def execute(query, maximum=-1)
220
+ rows = nil
221
+ transaction = @interface.start_transaction
222
+
223
+ begin
224
+ rows = @interface.execute_query(query, transaction, maximum)
225
+ @interface.commit(transaction)
226
+ rescue IotazError => error
227
+ @interface.rollback(transaction)
228
+ raise error
229
+ end
230
+
231
+ rows
232
+ end
233
+
234
+
235
+ #
236
+ # This method processes a single action and it's related callbacks at
237
+ # once, as opposed to making them part of a transaction. This is almost
238
+ # like using SQL with an auto-commit function.
239
+ #
240
+ # ==== Parameters
241
+ # action:: A reference to the action to be executed.
242
+ # callbacks:: An array of the callback values to be executed to.
243
+ #
244
+ def execute_immediate(action, *callbacks)
245
+ transaction = @interface.start_transaction
246
+ begin
247
+ @interface.execute_action(action, transaction)
248
+ callbacks.each do |entry|
249
+ data = @interface.execute_callback(entry, transaction)
250
+ entry.assign(data)
251
+ end
252
+ @interface.commit(transaction)
253
+ transaction = nil
254
+ rescue IotazError => error
255
+ @interface.rollback(transaction)
256
+ raise error
257
+ end
258
+ end
259
+
260
+
261
+ # Method access alterations.
262
+ private :execute_immediate
263
+ end # End of the Session class.
264
+
265
+
266
+ #
267
+ # This class represents a means of generating Session objects. The class
268
+ # helps abstract away from the concept of a database and allows for later
269
+ # expansion of the library to support other databases. The constructor for
270
+ # the class takes a hash containing configuration data as a parameter. Most
271
+ # of this data is for the database interface that will sit behind the factory
272
+ # object but an entry for the 'iotaz.database' key is used by the factory class
273
+ # to determine which database is to be used and must, therefore, be provided.
274
+ #
275
+ class SessionFactory
276
+ #
277
+ # This is the constructor for the SessionFactory class. From the details
278
+ # of the configuration data passed to it this method constructs a factory
279
+ # class instance geared to a particular database. The only database that
280
+ # is currently supported is Firebird but this may change in the future.
281
+ #
282
+ # ==== Parameters
283
+ # configuration:: A hash containing the configuration elements to be
284
+ # used by the session factory. As Firebird is the only
285
+ # currently supported RDBMS refer to the
286
+ # FirebirdInterface class for the full set of valid
287
+ # configuration parameters.
288
+ #
289
+ # ==== Exceptions
290
+ # IotazError:: Generated whenever an unknown database is specified or
291
+ # the configuration specified is incomplete.
292
+ #
293
+ def initialize(configuration)
294
+ # Create the interface class instance.
295
+ @interface = get_interface_class(configuration).new(configuration)
296
+ @sessions = []
297
+ @mutex = Mutex.new
298
+
299
+ # Clear out sessions on exit.
300
+ at_exit {@sessions.each {|session| session.terminate}}
301
+ end
302
+
303
+
304
+ #
305
+ # This method is used to have the factory object create a new Session.
306
+ #
307
+ def start
308
+ session = Session.new(@interface, self)
309
+ @mutex.synchronize {@sessions.push(session)}
310
+ session
311
+ end
312
+
313
+
314
+ #
315
+ # This method is used to terminate a session factory whenever it is no
316
+ # longer needed. This should be called to release the resources for a
317
+ # factory.
318
+ #
319
+ def shutdown
320
+ @interface.shutdown
321
+ end
322
+
323
+
324
+ #
325
+ # This method is used by the sessions produced by the factory to inform
326
+ # the factory that the session is being terminated. This method should
327
+ # not be used except in this context.
328
+ #
329
+ # ==== Parameters
330
+ # session:: A reference to the Session object being terminated.
331
+ #
332
+ def terminating(session)
333
+ @mutex.synchronize {@sessions.delete(session)}
334
+ end
335
+
336
+
337
+ #
338
+ # This method retrieves the database interface class associated with a
339
+ # given configuration.
340
+ #
341
+ # ==== Parameters
342
+ # configuration:: The configuration that will be used to determine the
343
+ # database interface class to use. This hash of values
344
+ # will be searched for an entry with the 'iotaz.database'
345
+ # key and the value of this entry used to determine the
346
+ # interface class to use.
347
+ #
348
+ # ==== Exception
349
+ # IotazError:: Generated whenever the required configuration entry is not
350
+ # present or contains a value for an unknown database.
351
+ #
352
+ def get_interface_class(configuration)
353
+ klass = nil
354
+
355
+ if configuration.key?('iotaz.database') == false
356
+ raise IotazError.new("Invalid session factory configuration. The "\
357
+ "specified configuration is missing an entry "\
358
+ "for '{0}'.", 'iotaz.database')
359
+ end
360
+
361
+ case configuration['iotaz.database'].upcase
362
+ when 'FIREBIRD' :
363
+ klass = FirebirdInterface
364
+
365
+ else
366
+ raise IotazError.new("Unknown or unsupported database specified "\
367
+ "for session factory. '{0}' is not a "\
368
+ "recognised or supported database.",
369
+ configuration['iotaz.database'])
370
+ end
371
+
372
+ klass
373
+ end
374
+
375
+
376
+ # Method access alterations.
377
+ private :get_interface_class
378
+ end # End of the SessionFactory class.
379
+ end # End of the Iotaz module.
@@ -0,0 +1,295 @@
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 specificlanguage 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 'iotaz/IotazError'
24
+
25
+ module Iotaz
26
+ #
27
+ # This class represents a SQL statement and multiple potential parameter
28
+ # data sets for the execution of the statement. The cache attribute is
29
+ # intended for use by the database functionality to allow the Action object
30
+ # to be re-used as much as possible.
31
+ #
32
+ class Action
33
+ #
34
+ # This is the constructor for the Action class.
35
+ #
36
+ # ==== Parameters
37
+ # sql:: A string containing the SQL to be executed for the action.
38
+ #
39
+ def initialize(sql)
40
+ @sql = sql
41
+ @index = 0
42
+ @data = Array.new
43
+ @cache = nil
44
+ end
45
+
46
+
47
+ #
48
+ # This method fetches the next set of parameters from an Action object.
49
+ #
50
+ # ==== Exceptions
51
+ # IotazError:: Generated whenever the method is called on an action that
52
+ # has exhausted it's available parameter sets.
53
+ #
54
+ def get_parameters
55
+ if @index == @data.size
56
+ raise IotazError.new('Parameter set requested from exhausted action.')
57
+ end
58
+ @index += 1
59
+ @data[@index - 1]
60
+ end
61
+
62
+
63
+ #
64
+ # This method adds a new set of parameters to an Action object.
65
+ #
66
+ # ==== Parameters
67
+ # parameters:: An array containing the new parameter set.
68
+ #
69
+ def add_parameters(*parameters)
70
+ @data.push(parameters)
71
+ end
72
+
73
+
74
+ # Class attribute accessor.
75
+ attr_reader :sql, :data, :cache
76
+
77
+ # Class attribute mutator.
78
+ attr_writer :cache
79
+ end # End of the Action class.
80
+
81
+
82
+ #
83
+ # This class represents a value that must be fetched back from a database
84
+ # for a generated column. This consists of the SQL statement to fetch the
85
+ # value plus a reference to the object that the value goes into and details
86
+ # of how to get it into the object. To allow for optimizations an instance
87
+ # of the ValueCallback object may refer to more than a single object. In
88
+ # this case care should be taken with using the object. A ValueCallback that
89
+ # contains more than one object behaves differently in it's use of the
90
+ # parameters and assign methods. A call to the parameters method returns a
91
+ # parameter set for the current object and then advances it to the next
92
+ # available object. A call to assign will assign a value to the last object
93
+ # used to provide a parameter set.
94
+ #
95
+ class ValueCallback
96
+ #
97
+ # This is the constructor for the ValueCallback class.
98
+ #
99
+ # ==== Parameters
100
+ # object:: A reference to the object that the value will go back
101
+ # into.
102
+ # attribute:: The name of the attribute that the callback will be used
103
+ # to provide a value for.
104
+ # sql:: A string containing the SQL statement to fetch the object
105
+ # value.
106
+ #
107
+ def initialize(object, attribute, sql)
108
+ @objects = [object]
109
+ @current = 1
110
+ @attribute = attribute
111
+ @sql = sql
112
+ @cache = nil
113
+ end
114
+
115
+
116
+ #
117
+ # This method fetches the current object for a ValueCallback instance.
118
+ # The method does not advance the object reference.
119
+ #
120
+ def object
121
+ @objects[@current - 1]
122
+ end
123
+
124
+
125
+ #
126
+ # This method adds another object that will be covered by the callback
127
+ #
128
+ # ==== Parameters
129
+ # object:: A reference to the object that will be added to the callback.
130
+ #
131
+ def add_object(object)
132
+ @objects.push(object)
133
+ end
134
+
135
+
136
+ #
137
+ # This method assigns the generated value into the object.
138
+ #
139
+ # ==== Parameters
140
+ # value:: A reference to the generated value for the object.
141
+ #
142
+ def assign(value)
143
+ object = @objects[@current - 2]
144
+ metadata = object.class.iotaz_meta_data
145
+ metadata.get_attribute(@attribute).set(object, value)
146
+ end
147
+
148
+
149
+ #
150
+ # This method fetches the data set required to execute the SQL statement
151
+ # associated with a callback.
152
+ #
153
+ # ==== Exceptions
154
+ # IotazError:: Generated whenever any of the key attribute values for the
155
+ # object are nil.
156
+ #
157
+ def parameters
158
+ parameters = []
159
+ object = @objects[@current - 1]
160
+ @curent = @current + 1
161
+ metadata = object.class.iotaz_meta_data
162
+
163
+ metadata.keys.each do |key|
164
+ value = metadata.get_attribute(key).get(object)
165
+ if value == nil
166
+ raise IotazError.new("Value callback parameter key value is nil.")
167
+ end
168
+ parameters.push(value)
169
+ end
170
+ parameters
171
+ end
172
+
173
+
174
+ # Class attribute accessor.
175
+ attr_reader :objects, :attribute, :sql, :cache
176
+
177
+ # Class attribute mutator.
178
+ attr_writer :cache
179
+ end # End of the ValueCallback class.
180
+
181
+
182
+ #
183
+ # This class represents a series of SQL statements making up an atomic
184
+ # set of work.
185
+ #
186
+ class Atom
187
+ #
188
+ # This is the constructor for the Atom class.
189
+ #
190
+ def initialize
191
+ @actions = []
192
+ @callbacks = []
193
+ end
194
+
195
+
196
+ #
197
+ # This method adds a new action and call back set to the atom.
198
+ #
199
+ # ==== Parameters
200
+ # action:: An action reflecting the task that will be executed as
201
+ # as part of the Atom.
202
+ # callbacks:: An array of the callback that go along with the action
203
+ # being added.
204
+ #
205
+ def add(action, *callbacks)
206
+ # Store the action.
207
+ match = @actions.find {|entry| entry.sql == action.sql}
208
+ if match != nil
209
+ match.add_parameters(*action.get_parameters)
210
+ @actions.push(match)
211
+ else
212
+ @actions.push(action)
213
+ end
214
+
215
+ # Store the callbacks.
216
+ callbacks.each do |entry|
217
+ match = @callbacks.find {|callback| callback.sql == entry.sql}
218
+ if match != nil
219
+ match.add_object(entry.object)
220
+ @callbacks.push(match)
221
+ else
222
+ @callbacks.push(entry)
223
+ end
224
+ end
225
+ end
226
+
227
+
228
+ #
229
+ # This method fetches a count of the total number of actions currently
230
+ # stored within an Atom object.
231
+ #
232
+ def action_count
233
+ @actions.size
234
+ end
235
+
236
+
237
+ #
238
+ # This method fetches a count of the total number of value callbacks
239
+ # currently stored within an Atom object.
240
+ #
241
+ def callback_count
242
+ @callbacks.size
243
+ end
244
+
245
+
246
+ #
247
+ # This method attempts to commit the work within an Atom object to the
248
+ # database.
249
+ #
250
+ # ==== Parameters
251
+ # interface:: A reference to the database interface object to be used
252
+ # to execute the work.
253
+ #
254
+ # ==== Exceptions
255
+ # IotazError:: Generated whenever a problem occurs executing the actions
256
+ # or callbacks for the Atom object.
257
+ #
258
+ def commit(interface)
259
+ transaction = nil
260
+ begin
261
+ # Start a transaction and execute the actions.
262
+ transaction = interface.start_transaction
263
+ @actions.each do |entry|
264
+ interface.execute_action(entry, transaction)
265
+ end
266
+
267
+ # Execute the value callbacks to update objects.
268
+ @callbacks.each do |entry|
269
+ entry.assign(interface.execute_callback(entry, transaction))
270
+ end
271
+
272
+ # Clean up cached items.
273
+ items = []
274
+ @actions.each do |entry|
275
+ items.push(entry.cache) if entry.cache != nil
276
+ end
277
+ @callbacks.each do |entry|
278
+ items.push(entry.cache) if entry.cache != nil
279
+ end
280
+ interface.clean_cache(items)
281
+
282
+ # Commit the transaction.
283
+ interface.commit(transaction)
284
+ transaction = nil
285
+ rescue IotazError => error
286
+ raise error
287
+ rescue Exception => error
288
+ raise IotazError.new("Error committing work atom, Cause: {0}",
289
+ error.message)
290
+ ensure
291
+ interface.rollback(transaction) if transaction != nil
292
+ end
293
+ end
294
+ end # End of the Atom class.
295
+ end # End of the Iotaz module.