Rubernate 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,444 @@
1
+ # Contains users methods for building queries
2
+ module Rubernate
3
+ module Queries
4
+ # Log instance for Queries
5
+ Log = Log4r::Logger.new self.name
6
+
7
+ # Re-Expr express prefix that starts all queries on RQL.
8
+ RQL_PREFIX_REGEXP = /^\s*Select\s+:/
9
+
10
+ # Contains operations definitions
11
+ module Operations
12
+ # Declares +And+ sql clause.
13
+ def And expr1, expr2
14
+ @factory.bin_op expr1, expr2, 'and'
15
+ end
16
+
17
+ # Declares +Or+ sql clause.
18
+ def Or expr1, expr2
19
+ @factory.bin_op expr1, expr2, 'or', true
20
+ end
21
+
22
+ # Declares +=+ sql clause.
23
+ def Eq expr1, expr2
24
+ @factory.bin_op expr1, expr2, '='
25
+ end
26
+
27
+ # Declares +Not+ sql clause.
28
+ def Not expr
29
+ @factory.un_op expr, 'not', true
30
+ end
31
+
32
+ # Declares +is null+ sql clause
33
+ def IsNil expr
34
+ @factory.bin_op expr, @factory.expr(nil), 'is'
35
+ end
36
+
37
+ # Declares +is not null+ sql clause
38
+ def IsNotNil expr
39
+ @factory.bin_op expr, @factory.expr(nil), 'is not'
40
+ end
41
+
42
+ # Declares +in+ sql clause
43
+ def In expr, list
44
+ @factory.bin_op expr, @factory.list(list), 'in'
45
+ end
46
+ end
47
+
48
+ # Factory method, creates queries based on Generic factory.
49
+ def self.query q_text=nil, &q_block
50
+ if block_given?
51
+ @@generic_factory.query(&q_block)
52
+ else
53
+ @@generic_factory.query q_text
54
+ end
55
+ end
56
+
57
+ # Defines query elements factory. By default it creates elements defined
58
+ # in module +Generic+, but this befavior can be changed by seting of appropriate class.
59
+ # Following example changes implementation of BinOpConst
60
+ #
61
+ # :call-seq:
62
+ # f = Factory.new
63
+ # f.bin_op = MyBinOpImpl
64
+ # f.bin_op expr1, expr2, '=' -> instance of MyBinOpImpl properly initialized
65
+ #
66
+ # New implementations of elements MUST accept factory as it's first parameter.
67
+ class Factory
68
+ # Initalizes default implementations
69
+ def initialize
70
+ @expr = Generic::Expr
71
+ @un_op = Generic::UnOpConstr
72
+ @bin_op = Generic::BinOpConstr
73
+ @field = Generic::FieldExpr
74
+ @key_ref = Generic::KeyRefExpr
75
+ @list = Generic::ExprsList
76
+ @r_param = Generic::RParam
77
+ @r_object = Generic::RObject
78
+ @query = Generic::Query
79
+ end
80
+ def query query=nil, &block
81
+ @query.new self, query, &block
82
+ end
83
+ private
84
+ # Defines factory method that will instantiate element instance
85
+ # and pass +self+ as element factory.
86
+ def self.def_factory name
87
+ module_eval %{
88
+ attr_writer :#{name}
89
+ def #{name} *params
90
+ @#{name}.new self, *params
91
+ end
92
+ }
93
+ end
94
+ # The following factory methdos are available.
95
+ for p in %w{expr un_op bin_op field key_ref list r_param r_object}
96
+ def_factory p
97
+ end
98
+ end
99
+
100
+ # Contains classes for standart ANSY SQL.
101
+ module Generic
102
+ # Represent abstract expression
103
+ class Expr
104
+ include Operations
105
+ attr_reader :r_params, :markers
106
+
107
+ def initialize factory, value = nil
108
+ @factory, @value, @r_params, @markers = factory, value, [], []
109
+ @markers << @value if @value.is_a? Symbol
110
+ end
111
+
112
+ # Generates SQL for expression
113
+ def to_sql
114
+ case @value
115
+ when Symbol: '?'
116
+ when Integer: @value
117
+ when nil: 'null'
118
+ else "'#{@value.to_s}'"
119
+ end
120
+ end
121
+ private
122
+ def fit_type expr
123
+ case expr
124
+ when RObject: expr.pk
125
+ when RParam: expr.ref
126
+ when Expr: expr
127
+ else @factory.expr expr
128
+ end
129
+ end
130
+ def self.def_bin_op ruby_op, sql_op
131
+ module_eval %{
132
+ def #{ruby_op} (other) @factory.bin_op self, other, '#{sql_op}'; end
133
+ }
134
+ end
135
+ for op in [['==', '='], ['=~', '<>'], '<', '>', '<=', '>=']
136
+ if op.is_a? Array
137
+ def_bin_op(*op)
138
+ else
139
+ def_bin_op op, op
140
+ end
141
+ end
142
+ end
143
+
144
+ # Represents constraint for one columns.
145
+ class UnOpConstr < Expr
146
+ def initialize factory, expr, op, braces = false
147
+ @factory, @op, @braces = factory, op, braces
148
+ @expr = fit_type expr
149
+ end
150
+
151
+ def r_params
152
+ @expr.r_params
153
+ end
154
+
155
+ def markers
156
+ @expr.markers
157
+ end
158
+
159
+ def to_sql
160
+ if @braces
161
+ "#{@op} (#{@expr.to_sql})"
162
+ else
163
+ "#{@op} #{@expr.to_sql}"
164
+ end
165
+ end
166
+ end
167
+
168
+ # Represent constraint that applied on two columns.
169
+ class BinOpConstr < Expr
170
+ def initialize factory, expr1, expr2, sign, braces = false
171
+ @factory, @sign, @braces = factory, sign, braces
172
+ @expr1, @expr2 = fit_type(expr1), fit_type(expr2)
173
+ end
174
+
175
+ # Returns r_params used in both expressions
176
+ def r_params
177
+ @expr1.r_params + @expr2.r_params
178
+ end
179
+
180
+ # Returns markers used in both expressions
181
+ def markers
182
+ @expr1.markers + @expr2.markers
183
+ end
184
+
185
+ # Generates SQL for constraint
186
+ def to_sql
187
+ if @braces
188
+ "(#{@expr1.to_sql} #{@sign} #{@expr2.to_sql})"
189
+ else
190
+ "#{@expr1.to_sql} #{@sign} #{@expr2.to_sql}"
191
+ end
192
+ end
193
+ end
194
+
195
+ # Represent expression with table's field
196
+ class FieldExpr < Expr
197
+ def initialize factory, table, field
198
+ @factory, @table, @field, @markers = factory, table, field, []
199
+ @r_params = table.is_a?(RParam) ? [table] : []
200
+ end
201
+ # Creates constraint that check if this field is nil
202
+ def is_nil
203
+ IsNil self
204
+ end
205
+ # Creates constraint that check if this field is not nil
206
+ def is_not_nil
207
+ IsNotNil self
208
+ end
209
+
210
+ def to_sql
211
+ @table.to_sql + '.' + @field
212
+ end
213
+ end
214
+
215
+ # Represent +r_params+ for hashes and arrays constrained by key
216
+ class KeyRefExpr < BinOpConstr
217
+ def initialize factory, r_param, key_field, key_value
218
+ @factory, @r_param, @key_field, @key_value = factory, r_param, key_field, key_value
219
+ super factory, key_field, key_value, '='
220
+ end
221
+ def == expr
222
+ And self, Eq(@r_param.ref, expr)
223
+ end
224
+ end
225
+
226
+ # Reresents List of expressions
227
+ class ExprsList < Expr
228
+ def initialize factory, list
229
+ @factory, @exprs = factory, list.collect{|expr| Expr === expr ? expr : factory.expr(expr)}
230
+ end
231
+ def to_sql
232
+ '(' + @exprs.collect{|expr| expr.to_sql}.join(', ') + ')'
233
+ end
234
+ def markers
235
+ @exprs.inject([]) {|res, expr| res.concat expr.markers}
236
+ end
237
+ def r_params
238
+ @exprs.inject([]) {|res, expr| res.concat expr.r_params}
239
+ end
240
+ end
241
+
242
+ # Represents r_params table
243
+ class RParam < Expr
244
+ include Rubernate::DBI
245
+
246
+ attr_reader :r_object, :name
247
+
248
+ # Init Param accepts table and name of param
249
+ def initialize factory, r_object, name
250
+ @factory, @r_object, @name, @r_params = factory, r_object, name, [self]
251
+ end
252
+
253
+ # Returns full param table name with object tables prefix
254
+ def to_sql
255
+ @r_object.to_sql + @name
256
+ end
257
+
258
+ # Fields accessors.
259
+ def pk () f_expr 'object_pk'; end
260
+ def int () f_expr 'int_value'; end
261
+ def str () f_expr 'str_value'; end
262
+ def time () f_expr 'dat_value'; end
263
+ def date () f_expr 'dat_value'; end # TODO: make proper convertation
264
+ def ref () f_expr 'ref_value'; end
265
+ def flags() f_expr 'flags'; end
266
+
267
+ # The following methods checks +r_param.flags+ value. (r_param.flags)
268
+ def is_int () Eq flags, PARAM_FLAG_INT; end
269
+ def is_str () Eq flags, PARAM_FLAG_STRING; end
270
+ def is_time() Eq flags, PARAM_FLAG_TIME; end
271
+ def is_ref () Eq flags, PARAM_FLAG_REF; end
272
+
273
+ # Shortcut for +Eq+ method
274
+ def == expr
275
+ expr.is_a?(RObject) ?
276
+ @factory.bin_op(ref, expr.pk, '=') :
277
+ @factory.bin_op(ref, expr, '=')
278
+ end
279
+
280
+ # Shortcut for arrays and hashes
281
+ def [] key
282
+ case key
283
+ when Integer: key_ref int, key
284
+ when String: key_ref str, key
285
+ when Time: key_ref dat, key
286
+ when Symbol: key_ref int, key
287
+ else raise "invalid key value #{key}"
288
+ end
289
+ end
290
+ private
291
+ # Creates +FieldExpr+ witch field of this (r_param) table
292
+ def f_expr field
293
+ @factory.field self, field
294
+ end
295
+ def key_ref key_field, key_value
296
+ @factory.key_ref self, key_field, key_value
297
+ end
298
+ end
299
+
300
+ # Represent r_objects table
301
+ class RObject < Expr
302
+ # Init Table accepts query and name of table
303
+ def initialize factory, name
304
+ @factory, @to_sql, @r_params = factory, name.to_s + '_', {}
305
+ @pk, @klass = @factory.field(self, 'object_pk'), @factory.field(self, 'object_class')
306
+ end
307
+
308
+ # Fiends access expressions
309
+ attr_reader :pk, :klass, :to_sql
310
+
311
+ # Creates subclasses constraint
312
+ def derived klass
313
+ if klass.subclasses and not klass.subclasses.empty?
314
+ In self.klass, [klass].concat(klass.subclasses)
315
+ else
316
+ Eq self.klass, klass
317
+ end
318
+ end
319
+
320
+ # Tracks missing methods and creates accessor for params.
321
+ def method_missing name, *params
322
+ return super if params.size != 0
323
+ def_param name
324
+ end
325
+
326
+ private
327
+ # Defines accessor for new param. (joins r_param to r_objects)
328
+ def def_param name
329
+ instance_eval "def #{name}() @r_params[:#{name}]; end"
330
+ @r_params[name] = @factory.r_param self, name.to_s
331
+ end
332
+ end
333
+
334
+ # Represents context in which query building executes
335
+ # Holds constraints tables and so on.
336
+ class Query < Expr
337
+ include Operations
338
+
339
+ # Accepts query as string or as block and executes it.
340
+ def initialize factory, query = nil, &block
341
+ @factory, @r_objects, @exprs, @order, @query = factory, {}, [], [], query
342
+ @query = block if block_given?
343
+ end
344
+
345
+ # Next section contains methods available during query construction.
346
+ # Declares objects for selection. The first argument will be result object.
347
+ def Select main, *tables
348
+ for t in [main, *tables].flatten
349
+ @r_objects[t] = @factory.r_object t.to_s
350
+ instance_eval "def #{t}() @r_objects[:#{t}]; end"
351
+ end
352
+ @main = @r_objects[main]
353
+ end
354
+
355
+ # Declares +Where+ clause.
356
+ def Where *exprs
357
+ @exprs = exprs
358
+ end
359
+
360
+ # Declares +Order By+ clause.
361
+ def OrderBy expr, *exprs
362
+ @order << expr
363
+ @order.concat exprs
364
+ end
365
+
366
+ # Returns markers used in query in valid order
367
+ def markers
368
+ @exprs.inject([]){|result, expr| result.concat expr.markers}
369
+ end
370
+
371
+ # Arranges map: +values+ {marker=>value} to ordered array of values
372
+ # accroding to markers in query.
373
+ def params values
374
+ markers.inject([]){|r, m| r << values[m]}
375
+ end
376
+
377
+ # Generates SQL for entire query.
378
+ def to_sql
379
+ eval_query
380
+ sql = "select #{@main.to_sql}.* from #{tables_sql}"
381
+ sql+= "\n\twhere #{where_sql}"
382
+ sql+= "\n\torder by #{order_by_sql}" unless @order.empty?
383
+ dbg_query sql
384
+ sql
385
+ end
386
+
387
+ private
388
+ # Prints debug message
389
+ def dbg_query sql
390
+ return unless Log.debug?
391
+ query = @query.is_a?(Proc) ? 'query given as block' : @query
392
+ Log.debug "Translate: <<#{query}>> to sql: <<#{sql}>>"
393
+ end
394
+
395
+ # Evaluates query withing the object context.
396
+ def eval_query
397
+ return if @evaluated
398
+ if @query.is_a? Proc
399
+ instance_eval(&@query)
400
+ else
401
+ instance_eval @query
402
+ end
403
+ @evaluated = true
404
+ end
405
+
406
+ # Generates +where+ clause for each expression joined by +and+ clause
407
+ def where_sql
408
+ r_params.collect{|rp| rp.to_sql + '.name = \'' + rp.name + '\''}.concat(
409
+ @exprs.collect{|ex| ex.to_sql}).join(" and\n\t\t")
410
+ end
411
+
412
+ # Generates +order by+ sql clause
413
+ def order_by_sql
414
+ @order.collect{|ord| ord.to_sql}.join(', ')
415
+ end
416
+
417
+ # Generates left outer join for each +r_params+ used in query
418
+ def tables_sql
419
+ r_objects.collect{|r_object| 'r_objects ' + r_object.to_sql}.join(', ') +
420
+ r_params.collect{|r_param|
421
+ "\n\tleft outer join r_params " + r_param.to_sql +
422
+ ' on (' + r_param.r_object.to_sql + '.object_pk = ' + r_param.to_sql + '.object_pk)'
423
+ }.join()
424
+ end
425
+
426
+ # Retruns +r_params+ used in query. List of +RParam+.
427
+ def r_params
428
+ res = @exprs.inject([]) {|res, exp| res.concat exp.r_params}
429
+ @order.inject(res) {|res, ord| res.concat ord.r_params}
430
+ res.uniq!
431
+ res
432
+ end
433
+
434
+ # Retruns +r_objects+ used in query. List of +RObject+.
435
+ def r_objects
436
+ res = (@r_objects.values + r_params.inject([]){|res, rp| res << rp.r_object})
437
+ res.uniq!
438
+ res
439
+ end
440
+ end
441
+ end
442
+ @@generic_factory = Factory.new
443
+ end
444
+ end
@@ -0,0 +1,215 @@
1
+ module Rubernate
2
+ # Base class for all "Runtime+ implementations.
3
+ # Most of these methods should be overriden by subclasses.
4
+ class Runtime
5
+ include Callbacks::Runtime
6
+
7
+ # Log for Runtime events
8
+ Log = Log4r::Logger.new self.name
9
+
10
+ def initialize
11
+ @pool = {} # Contains objects loaded during the session
12
+ @factory = Queries::Factory.new
13
+ end
14
+
15
+ # Finds object by primary key,
16
+ # raises ObjectNotFoundException if object is not found
17
+ def find_by_pk pk, load = false
18
+ result = @pool[pk]
19
+ unless result
20
+ result = load_by_pk pk
21
+ unless result
22
+ Log.debug {"Find by pk: #{pk} - NOT found"}
23
+ raise ObjectNotFoundException.new(pk) unless result
24
+ end
25
+ @pool[pk] = result
26
+ end
27
+ load_by_pk pk if load
28
+ Log.debug {"Find by pk: #{pk} - found"}
29
+ result
30
+ end
31
+
32
+ # Finds objects by query. Returns ordered list of objects.
33
+ # If +params+ if Array the query will be treated as native sql
34
+ # and won't be processed.
35
+ def find_by_query query, params={} #TODO: improve working with paramters
36
+ flush_modified
37
+ params = case params
38
+ when Hash: params
39
+ when Array: params
40
+ else [params]
41
+ end
42
+ if query =~ Queries::RQL_PREFIX_REGEXP
43
+ objs = load_by_query(*native_sql(query, params))
44
+ else
45
+ objs = load_by_query query, params.collect{|p| native_param p}
46
+ end
47
+ Log.debug {"Find by query: <<#{query}>>, params: <<#{params}>>,- #{objs.size} objects found,"}
48
+ objs
49
+ rescue Exception => e
50
+ Log.error "Find by query: <<#{query}>>, params: <<#{params}>>, failed: #{e}"
51
+ raise
52
+ end
53
+
54
+ # Attaches object to session. Causes error if object with equal
55
+ # primary key already loaded in session.
56
+ def attach object
57
+ raise "Can't attach object: #{object.primary_key}" if @pool.has_key? object.primary_key
58
+ object.peer = Peer.new unless object.peer
59
+ create object
60
+ @pool[object.primary_key] = object
61
+ Log.debug {"Attach #{object.class.name}: #{object.primary_key} to session"}
62
+ object.on_create
63
+ end
64
+
65
+ # Removes object
66
+ def remove object
67
+ object.on_remove
68
+ @pool.delete object.primary_key
69
+ Log.debug {"Remove object: #{object.primary_key}"}
70
+ delete object
71
+ def object.primary_key
72
+ raise "object #{@primary_key} has already been deleted"
73
+ end
74
+ rescue Exception => ex
75
+ Log.error "Object remove fails due to error #{ex}"
76
+ raise
77
+ end
78
+
79
+ # Begins session
80
+ def begin
81
+ Log.debug {"Begin session #{self}"}
82
+ on_begin
83
+ end
84
+
85
+ # Persist all changes and Commit transction.
86
+ def commit
87
+ before_commit # callback
88
+ flush_modified
89
+ close
90
+ after_commit # callback
91
+ Log.debug {"Commit session #{self}"}
92
+ rescue Exception => ex
93
+ rollback ex
94
+ raise
95
+ end
96
+
97
+ # Discard all changes and Rollback transaction.
98
+ def rollback ex='not specified'
99
+ on_rollback # callback
100
+ Log.warn {"Rollback session #{self} due to error: #{ex}"}
101
+ failed
102
+ rescue Exception => e
103
+ Log.error "Rolling back session: #{self} failed: #{e}"
104
+ raise
105
+ end
106
+
107
+ # Flushes modified objects. Invokes on_store callback before storing.
108
+ def flush_modified
109
+ objects = modified
110
+ before_flush modified # callback
111
+ return if objects.empty?
112
+ Log.debug {"Flush #{objects.size} modified objects"}
113
+ objects.each {|object| object.on_save}
114
+ save objects
115
+ after_flush # callback
116
+ end
117
+
118
+ private
119
+ # Creates new object instance or returns pooled one
120
+ def instantiate pk, klass, peer = nil
121
+ object = @pool[pk]
122
+ if object and object.class != klass
123
+ raise "invalid class #{klass} of object pk: #{pk}: #{object.class}"
124
+ end
125
+ unless object
126
+ object, object.primary_key = klass.allocate, pk
127
+ @pool[pk] = object
128
+ end
129
+ object.peer = peer
130
+ object
131
+ end
132
+
133
+ # Returns modified objects.
134
+ def modified
135
+ @pool.values.find_all {|object| object.peer and object.dirty?}
136
+ end
137
+
138
+ # Callback functions invoked when session successfully closed
139
+ # Should be overriden by subclasses to perform implement closing logic
140
+ def close
141
+ end
142
+
143
+ # Callback functions invoked when session fails
144
+ # Should be overriden by subclasses to cleanup resources
145
+ def failed
146
+ end
147
+
148
+ # The following methods must be overriden by subclasses.
149
+ # Loads object by primary key
150
+ def load_by_pk pk
151
+ raise "method MUST be overiden in subclass"
152
+ end
153
+
154
+ # Loads objects by query
155
+ def load_by_query query, params=[]
156
+ raise "method MUST be overiden in subclass"
157
+ end
158
+
159
+ # Saves object or object if parameter is array
160
+ def save object
161
+ raise "method MUST be overiden in subclass"
162
+ end
163
+
164
+ # Creates object. Paramter can be class of object.
165
+ # Implementation should sets dirty flag to +true+ or +false+.
166
+ # Returns primary_key.
167
+ def create object
168
+ raise "method MUST be overiden in subclass"
169
+ end
170
+
171
+ # Deletes object
172
+ def delete object
173
+ raise "method MUST be overiden in subclass"
174
+ end
175
+
176
+ def post_load object
177
+ for value in object.peer.values
178
+ value.compact! if value.is_a? Array
179
+ end
180
+ object.peer.dirty = false
181
+ object.on_load
182
+ end
183
+
184
+ def native_sql query, params
185
+ query = @factory.query(query)
186
+ [query.to_sql, query.params(params).map!{|p| native_param p}]
187
+ end
188
+
189
+ def native_param p
190
+ case p
191
+ when Rubernate::Entity: p.primary_key
192
+ when Class: p.name
193
+ else p
194
+ end
195
+ end
196
+
197
+ # TODO: continue from here!!!
198
+ class FakeEntity
199
+ persistent
200
+ def method_not_defined
201
+ raise 'object #{primary_key} has undefined class'
202
+ end
203
+ end
204
+
205
+ @@classes = {}
206
+ # Returns class with specified name or throw error if there is on one.
207
+ def class_by_name name
208
+ klass = @@classes[name]
209
+ return klass if klass
210
+ klass = Module.find_class name
211
+ @@classes[name] = klass if klass
212
+ klass
213
+ end
214
+ end
215
+ end