Rubernate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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