lafcadio 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -145,7 +145,8 @@ module Lafcadio
145
145
 
146
146
  def order_clause
147
147
  if @order_by
148
- clause = "order by #{ @order_by } "
148
+ order_by_field = @domain_class.get_field( @order_by )
149
+ clause = "order by #{ order_by_field.db_field_name } "
149
150
  clause += @order_by_order == ASC ? 'asc' : 'desc'
150
151
  clause
151
152
  end
@@ -209,21 +210,7 @@ module Lafcadio
209
210
 
210
211
  def db_field_name; get_field.db_table_and_field_name; end
211
212
 
212
- def get_field
213
- a_domain_class = @domain_class
214
- field = nil
215
- while ( a_domain_class < DomainObject ) && !field
216
- field = a_domain_class.get_class_field( @fieldName )
217
- a_domain_class = a_domain_class.superclass
218
- end
219
- if field
220
- field
221
- else
222
- errStr = "Couldn't find field \"#{ @fieldName }\" in " +
223
- "#{ @domain_class } domain class"
224
- raise( MissingError, errStr, caller )
225
- end
226
- end
213
+ def get_field; @domain_class.get_field( @fieldName ); end
227
214
 
228
215
  def query; Query.new( @domain_class, self ); end
229
216
 
@@ -0,0 +1,583 @@
1
+ # = Overview
2
+ # By passing a block to ObjectStore, you can write complex, ad-hoc queries in
3
+ # Ruby. This involves a few more keystrokes than writing raw SQL, but also makes
4
+ # it easier to change queries at runtime, and these queries can also be fully
5
+ # tested against the MockObjectStore.
6
+ # big_invoices = object_store.getInvoices { |inv| inv.rate.gt( 50 ) }
7
+ # # => "select * from invoices where rate > 50"
8
+ # This a full-fledged block, so you can pass in values from the calling context.
9
+ # date = Date.new( 2004, 1, 1 )
10
+ # recent_invoices = object_store.getInvoices { |inv| inv.date.gt( date ) }
11
+ # # => "select * from invoices where date > '2004-01-01'"
12
+ #
13
+ # = Query operators
14
+ # You can compare fields either to simple values, or to other fields in the same
15
+ # table.
16
+ # paid_immediately = object_store.getInvoices { |inv|
17
+ # inv.date.equals( inv.paid )
18
+ # }
19
+ # # => "select * from invoices where date = paid"
20
+ #
21
+ # == Numerical comparisons: +lt+, +lte+, +gte+, +gt+
22
+ # +lt+, +lte+, +gte+, and +gt+ stand for "less than", "less than or equal",
23
+ # "greater than or equal", and "greater than", respectively.
24
+ # tiny_invoices = object_store.getInvoices { |inv| inv.rate.lte( 25 ) }
25
+ # # => "select * from invoices where rate <= 25"
26
+ # These comparators work on fields that contain numbers, dates, and even
27
+ # references to other domain objects.
28
+ # for_1st_ten_clients = object_store.getInvoices { |inv|
29
+ # inv.client.lte( 10 )
30
+ # }
31
+ # # => "select * from invoices where client <= 10"
32
+ #
33
+ # == Equality: +equals+
34
+ # full_week_invs = object_store.getInvoices { |inv| inv.hours.equals( 40 ) }
35
+ # # => "select * from invoices where hours = 40"
36
+ # If you're comparing to a domain object you should pass in the object itself.
37
+ # client = object_store.getClient( 99 )
38
+ # invoices = object_store.getInvoices { |inv| inv.client.equals( client ) }
39
+ # # => "select * from invoices where client = 99"
40
+ #
41
+ # == Inclusion: +in+
42
+ # first_three_invs = object_store.getInvoices { |inv| inv.pk_id.in( 1, 2, 3 ) }
43
+ # # => "select * from invoices where pk_id in ( 1, 2, 3 )"
44
+ #
45
+ # == Text comparison: +like+
46
+ # fname_starts_with_a = object_store.getUsers { |user|
47
+ # user.fname.like( /^a/ )
48
+ # }
49
+ # # => "select * from users where fname like 'a%'"
50
+ # fname_ends_with_a = object_store.getUsers { |user|
51
+ # user.fname.like( /a$/ )
52
+ # }
53
+ # # => "select * from users where fname like '%a'"
54
+ # fname_contains_a = object_store.getUsers { |user|
55
+ # user.fname.like( /a/ )
56
+ # }
57
+ # # => "select * from users where fname like '%a%'"
58
+ # Please note that although we're using the Regexp operators here, these aren't
59
+ # full-fledged regexps. Only ^ and $ work for this.
60
+ #
61
+ # == Compound conditions: <tt>Query.And</tt> and <tt>Query.Or</tt>
62
+ # invoices = object_store.getInvoices { |inv|
63
+ # Query.And( inv.hours.equals( 40 ), inv.rate.equals( 50 ) )
64
+ # }
65
+ # # => "select * from invoices where (hours = 40 and rate = 50)"
66
+ # client99 = object_store.getClient( 99 )
67
+ # invoices = object_store.getInvoices { |inv|
68
+ # Query.Or( inv.hours.equals( 40 ),
69
+ # inv.rate.equals( 50 ),
70
+ # inv.client.equals( client99 ) )
71
+ # }
72
+ # # => "select * from invoices where (hours = 40 or rate = 50 or client = 99)"
73
+ # Note that both compound operators can take 2 or more arguments. Also, they can
74
+ # be nested:
75
+ # invoices = object_store.getInvoices { |inv|
76
+ # Query.And( inv.hours.equals( 40 ),
77
+ # Query.Or( inv.rate.equals( 50 ),
78
+ # inv.client.equals( client99 ) ) )
79
+ # }
80
+ # # => "select * from invoices where (hours = 40 and
81
+ # # (rate = 50 or client = 99))"
82
+ #
83
+ # == Negation: +not+
84
+ # invoices = object_store.getInvoices { |inv| inv.rate.equals( 50 ).not }
85
+ # # => "select * from invoices where rate != 50"
86
+
87
+ require 'delegate'
88
+
89
+ module Lafcadio
90
+ class Query
91
+ def self.And( *conditions ); CompoundCondition.new( *conditions ); end
92
+
93
+ def self.infer( domain_class, &action )
94
+ inferrer = Query::Inferrer.new( domain_class ) { |obj|
95
+ action.call( obj )
96
+ }
97
+ inferrer.execute
98
+ end
99
+
100
+ def self.Or( *conditions )
101
+ conditions << CompoundCondition::OR
102
+ CompoundCondition.new( *conditions)
103
+ end
104
+
105
+ ASC = 1
106
+ DESC = 2
107
+
108
+ attr_reader :domain_class, :condition
109
+ attr_accessor :order_by, :order_by_order, :limit
110
+
111
+ def initialize(domain_class, pk_idOrCondition = nil)
112
+ @domain_class = domain_class
113
+ ( @condition, @order_by, @limit ) = [ nil, nil, nil ]
114
+ if pk_idOrCondition
115
+ if pk_idOrCondition.class <= Condition
116
+ @condition = pk_idOrCondition
117
+ else
118
+ @condition = Query::Equals.new( 'pk_id', pk_idOrCondition,
119
+ domain_class )
120
+ end
121
+ end
122
+ @order_by_order = ASC
123
+ end
124
+
125
+ def and( &action ); compound( CompoundCondition::AND, action ); end
126
+
127
+ def compound( comp_type, action )
128
+ rquery = Query.infer( @domain_class ) { |dobj| action.call( dobj ) }
129
+ comp_cond = Query::CompoundCondition.new( @condition, rquery.condition,
130
+ comp_type )
131
+ comp_cond.query
132
+ end
133
+
134
+ def eql?( other ); other.class <= Query && other.to_sql == to_sql; end
135
+
136
+ def fields; '*'; end
137
+
138
+ def hash; to_sql.hash; end
139
+
140
+ def limit_clause
141
+ "limit #{ @limit.begin }, #{ @limit.end - @limit.begin + 1 }" if @limit
142
+ end
143
+
144
+ def or( &action ); compound( CompoundCondition::OR, action ); end
145
+
146
+ def order_clause
147
+ if @order_by
148
+ order_by_field = @domain_class.get_field( @order_by )
149
+ clause = "order by #{ order_by_field.db_field_name } "
150
+ clause += @order_by_order == ASC ? 'asc' : 'desc'
151
+ clause
152
+ end
153
+ end
154
+
155
+ def sql_primary_key_field(domain_class)
156
+ "#{ domain_class.table_name }.#{ domain_class.sql_primary_key_name }"
157
+ end
158
+
159
+ def tables
160
+ concrete_classes = domain_class.self_and_concrete_superclasses.reverse
161
+ table_names = concrete_classes.collect { |domain_class|
162
+ domain_class.table_name
163
+ }
164
+ table_names.join( ', ' )
165
+ end
166
+
167
+ def to_sql
168
+ clauses = [ "select #{ fields }", "from #{ tables }" ]
169
+ clauses << where_clause if where_clause
170
+ clauses << order_clause if order_clause
171
+ clauses << limit_clause if limit_clause
172
+ clauses.join ' '
173
+ end
174
+
175
+ def where_clause
176
+ concrete_classes = domain_class.self_and_concrete_superclasses.reverse
177
+ where_clauses = []
178
+ concrete_classes.each_with_index { |domain_class, i|
179
+ if i < concrete_classes.size - 1
180
+ join_clause = sql_primary_key_field( domain_class ) + ' = ' +
181
+ sql_primary_key_field( concrete_classes[i+1] )
182
+ where_clauses << join_clause
183
+ else
184
+ where_clauses << @condition.to_sql if @condition
185
+ end
186
+ }
187
+ where_clauses.size > 0 ? 'where ' + where_clauses.join( ' and ' ) : nil
188
+ end
189
+
190
+ class Condition #:nodoc:
191
+ def Condition.search_term_type
192
+ Object
193
+ end
194
+
195
+ attr_reader :domain_class
196
+
197
+ def initialize(fieldName, searchTerm, domain_class)
198
+ @fieldName = fieldName
199
+ @searchTerm = searchTerm
200
+ unless @searchTerm.class <= self.class.search_term_type
201
+ raise "Incorrect searchTerm type #{ searchTerm.class }"
202
+ end
203
+ @domain_class = domain_class
204
+ if @domain_class
205
+ unless @domain_class <= DomainObject
206
+ raise "Incorrect object type #{ @domain_class.to_s }"
207
+ end
208
+ end
209
+ end
210
+
211
+ def db_field_name; get_field.db_table_and_field_name; end
212
+
213
+ def get_field; @domain_class.get_field( @fieldName ); end
214
+
215
+ def query; Query.new( @domain_class, self ); end
216
+
217
+ def not
218
+ Query::Not.new( self )
219
+ end
220
+
221
+ def primary_key_field?; 'pk_id' == @fieldName; end
222
+
223
+ def to_condition; self; end
224
+ end
225
+
226
+ class Compare < Condition #:nodoc:
227
+ LESS_THAN = 1
228
+ LESS_THAN_OR_EQUAL = 2
229
+ GREATER_THAN_OR_EQUAL = 3
230
+ GREATER_THAN = 4
231
+
232
+ @@comparators = {
233
+ LESS_THAN => '<',
234
+ LESS_THAN_OR_EQUAL => '<=',
235
+ GREATER_THAN_OR_EQUAL => '>=',
236
+ GREATER_THAN => '>'
237
+ }
238
+
239
+ @@mockComparators = {
240
+ LESS_THAN => Proc.new { |d1, d2| d1 < d2 },
241
+ LESS_THAN_OR_EQUAL => Proc.new { |d1, d2| d1 <= d2 },
242
+ GREATER_THAN_OR_EQUAL => Proc.new { |d1, d2| d1 >= d2 },
243
+ GREATER_THAN => Proc.new { |d1, d2| d1 > d2 }
244
+ }
245
+
246
+ def initialize(fieldName, searchTerm, domain_class, compareType)
247
+ super fieldName, searchTerm, domain_class
248
+ @compareType = compareType
249
+ end
250
+
251
+ def to_sql
252
+ if ( get_field.kind_of?( LinkField ) &&
253
+ !@searchTerm.respond_to?( :pk_id ) )
254
+ search_val = @searchTerm.to_s
255
+ else
256
+ search_val = get_field.value_for_sql( @searchTerm ).to_s
257
+ end
258
+ "#{ db_field_name } #{ @@comparators[@compareType] } " + search_val
259
+ end
260
+
261
+ def object_meets(anObj)
262
+ value = anObj.send @fieldName
263
+ value = value.pk_id if value.class <= DomainObject
264
+ if value
265
+ @@mockComparators[@compareType].call(value, @searchTerm)
266
+ else
267
+ false
268
+ end
269
+ end
270
+ end
271
+
272
+ class CompoundCondition < Condition #:nodoc:
273
+ AND = 1
274
+ OR = 2
275
+
276
+ def initialize(*conditions)
277
+ if( [ AND, OR ].index(conditions.last) )
278
+ @compoundType = conditions.last
279
+ conditions.pop
280
+ else
281
+ @compoundType = AND
282
+ end
283
+ @conditions = conditions
284
+ @domain_class = conditions[0].domain_class
285
+ end
286
+
287
+ def object_meets(anObj)
288
+ if @compoundType == AND
289
+ @conditions.inject( true ) { |result, cond|
290
+ result && cond.object_meets( anObj )
291
+ }
292
+ else
293
+ @conditions.inject( false ) { |result, cond|
294
+ result || cond.object_meets( anObj )
295
+ }
296
+ end
297
+ end
298
+
299
+ def to_sql
300
+ booleanString = @compoundType == AND ? 'and' : 'or'
301
+ subSqlStrings = @conditions.collect { |cond| cond.to_sql }
302
+ "(#{ subSqlStrings.join(" #{ booleanString } ") })"
303
+ end
304
+ end
305
+
306
+ class DomainObjectImpostor #:nodoc:
307
+ attr_reader :domain_class
308
+
309
+ def initialize( domain_class )
310
+ @domain_class = domain_class
311
+ end
312
+
313
+ def method_missing( methId, *args )
314
+ fieldName = methId.id2name
315
+ begin
316
+ classField = @domain_class.get_field( fieldName )
317
+ ObjectFieldImpostor.new( self, classField )
318
+ rescue MissingError
319
+ super( methId, *args )
320
+ end
321
+ end
322
+ end
323
+
324
+ class Equals < Condition #:nodoc:
325
+ def r_val_string
326
+ field = get_field
327
+ if @searchTerm.class <= ObjectField
328
+ @searchTerm.db_table_and_field_name
329
+ else
330
+ begin
331
+ field.value_for_sql( @searchTerm ).to_s
332
+ rescue DomainObjectInitError
333
+ raise(
334
+ ArgumentError,
335
+ "Can't query using an uncommitted domain object as a search " +
336
+ "term.",
337
+ caller
338
+ )
339
+ end
340
+ end
341
+ end
342
+
343
+ def object_meets(anObj)
344
+ if @searchTerm.class <= ObjectField
345
+ compare_value = anObj.send( @searchTerm.name )
346
+ else
347
+ compare_value = @searchTerm
348
+ end
349
+ compare_value == anObj.send( @fieldName )
350
+ end
351
+
352
+ def to_sql
353
+ sql = "#{ db_field_name } "
354
+ unless @searchTerm.nil?
355
+ sql += "= " + r_val_string
356
+ else
357
+ sql += "is null"
358
+ end
359
+ sql
360
+ end
361
+ end
362
+
363
+ class In < Condition #:nodoc:
364
+ def self.search_term_type
365
+ Array
366
+ end
367
+
368
+ def object_meets(anObj)
369
+ value = anObj.send @fieldName
370
+ @searchTerm.index(value) != nil
371
+ end
372
+
373
+ def to_sql
374
+ "#{ db_field_name } in (#{ @searchTerm.join(', ') })"
375
+ end
376
+ end
377
+
378
+ class Include < CompoundCondition
379
+ def initialize( field_name, search_term, domain_class )
380
+ begin_cond = Like.new( field_name, search_term + ',', domain_class,
381
+ Like::POST_ONLY )
382
+ mid_cond = Like.new( field_name, ',' + search_term + ',',
383
+ domain_class )
384
+ end_cond = Like.new( field_name, ',' + search_term, domain_class,
385
+ Like::PRE_ONLY )
386
+ only_cond = Equals.new( field_name, search_term, domain_class )
387
+ super( begin_cond, mid_cond, end_cond, only_cond, OR )
388
+ end
389
+ end
390
+
391
+ class Inferrer #:nodoc:
392
+ def initialize( domain_class, &action )
393
+ @domain_class = domain_class; @action = action
394
+ end
395
+
396
+ def execute
397
+ impostor = DomainObjectImpostor.new( @domain_class )
398
+ condition = @action.call( impostor ).to_condition
399
+ query = Query.new( @domain_class, condition )
400
+ end
401
+ end
402
+
403
+ class Like < Condition #:nodoc:
404
+ PRE_AND_POST = 1
405
+ PRE_ONLY = 2
406
+ POST_ONLY = 3
407
+
408
+ def initialize(
409
+ fieldName, searchTerm, domain_class, matchType = PRE_AND_POST)
410
+ super fieldName, searchTerm, domain_class
411
+ @matchType = matchType
412
+ end
413
+
414
+ def get_regexp
415
+ if @matchType == PRE_AND_POST
416
+ Regexp.new(@searchTerm)
417
+ elsif @matchType == PRE_ONLY
418
+ Regexp.new(@searchTerm.to_s + "$")
419
+ elsif @matchType == POST_ONLY
420
+ Regexp.new("^" + @searchTerm)
421
+ end
422
+ end
423
+
424
+ def object_meets(anObj)
425
+ value = anObj.send @fieldName
426
+ if value.class <= DomainObject || value.class == DomainObjectProxy
427
+ value = value.pk_id.to_s
428
+ end
429
+ if value.class <= Array
430
+ (value.index(@searchTerm) != nil)
431
+ else
432
+ get_regexp.match(value) != nil
433
+ end
434
+ end
435
+
436
+ def to_sql
437
+ withWildcards = @searchTerm
438
+ if @matchType == PRE_AND_POST
439
+ withWildcards = "%" + withWildcards + "%"
440
+ elsif @matchType == PRE_ONLY
441
+ withWildcards = "%" + withWildcards
442
+ elsif @matchType == POST_ONLY
443
+ withWildcards += "%"
444
+ end
445
+ "#{ db_field_name } like '#{ withWildcards }'"
446
+ end
447
+ end
448
+
449
+ class Max < Query #:nodoc:
450
+ attr_reader :field_name
451
+
452
+ def initialize( domain_class, field_name = 'pk_id' )
453
+ super( domain_class )
454
+ @field_name = field_name
455
+ end
456
+
457
+ def collect( coll )
458
+ max = coll.inject( nil ) { |max, d_obj|
459
+ a_value = d_obj.send( @field_name )
460
+ ( max.nil? || a_value > max ) ? a_value : max
461
+ }
462
+ [ max ]
463
+ end
464
+
465
+ def fields
466
+ "max(#{ @domain_class.get_field( @field_name ).db_field_name })"
467
+ end
468
+ end
469
+
470
+ class Not < Condition #:nodoc:
471
+ def initialize(unCondition)
472
+ @unCondition = unCondition
473
+ end
474
+
475
+ def object_meets(obj)
476
+ !@unCondition.object_meets(obj)
477
+ end
478
+
479
+ def domain_class; @unCondition.domain_class; end
480
+
481
+ def to_sql
482
+ "!(#{ @unCondition.to_sql })"
483
+ end
484
+ end
485
+
486
+ class ObjectFieldImpostor #:nodoc:
487
+ def self.comparators
488
+ {
489
+ 'lt' => Compare::LESS_THAN, 'lte' => Compare::LESS_THAN_OR_EQUAL,
490
+ 'gte' => Compare::GREATER_THAN_OR_EQUAL,
491
+ 'gt' => Compare::GREATER_THAN
492
+ }
493
+ end
494
+
495
+ attr_reader :class_field
496
+
497
+ def initialize( domainObjectImpostor, class_field_or_name )
498
+ @domainObjectImpostor = domainObjectImpostor
499
+ if class_field_or_name == 'pk_id'
500
+ @field_name = 'pk_id'
501
+ else
502
+ @class_field = class_field_or_name
503
+ @field_name = class_field_or_name.name
504
+ end
505
+ end
506
+
507
+ def method_missing( methId, *args )
508
+ methodName = methId.id2name
509
+ if !ObjectFieldImpostor.comparators.keys.index( methodName ).nil?
510
+ register_compare_condition( methodName, *args )
511
+ else
512
+ super( methId, *args )
513
+ end
514
+ end
515
+
516
+ def register_compare_condition( compareStr, searchTerm)
517
+ compareVal = ObjectFieldImpostor.comparators[compareStr]
518
+ Compare.new( @field_name, searchTerm,
519
+ @domainObjectImpostor.domain_class, compareVal )
520
+ end
521
+
522
+ def equals( searchTerm )
523
+ Equals.new( @field_name, field_or_field_name( searchTerm ),
524
+ @domainObjectImpostor.domain_class )
525
+ end
526
+
527
+ def field_or_field_name( search_term )
528
+ if search_term.class == ObjectFieldImpostor
529
+ search_term.class_field
530
+ else
531
+ search_term
532
+ end
533
+ end
534
+
535
+ def include?( search_term )
536
+ if @class_field.instance_of?( TextListField )
537
+ Include.new( @field_name, search_term,
538
+ @domainObjectImpostor.domain_class )
539
+ else
540
+ raise ArgumentError
541
+ end
542
+ end
543
+
544
+ def like( regexp )
545
+ if regexp.is_a?( Regexp )
546
+ if regexp.source =~ /^\^(.*)/
547
+ searchTerm = $1
548
+ matchType = Query::Like::POST_ONLY
549
+ elsif regexp.source =~ /(.*)\$$/
550
+ searchTerm = $1
551
+ matchType = Query::Like::PRE_ONLY
552
+ else
553
+ searchTerm = regexp.source
554
+ matchType = Query::Like::PRE_AND_POST
555
+ end
556
+ Query::Like.new( @field_name, searchTerm,
557
+ @domainObjectImpostor.domain_class, matchType )
558
+ else
559
+ raise(
560
+ ArgumentError, "#{ @field_name }#like needs to receive a Regexp",
561
+ caller
562
+ )
563
+ end
564
+ end
565
+
566
+ def in( *searchTerms )
567
+ Query::In.new( @field_name, searchTerms,
568
+ @domainObjectImpostor.domain_class )
569
+ end
570
+
571
+ def to_condition
572
+ if @class_field.instance_of?( BooleanField )
573
+ Query::Equals.new( @field_name, true,
574
+ @domainObjectImpostor.domain_class )
575
+ else
576
+ raise
577
+ end
578
+ end
579
+
580
+ def not; to_condition.not; end
581
+ end
582
+ end
583
+ end