lafcadio 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -128,7 +128,11 @@ module Lafcadio
128
128
  rquery = Query.infer( @domain_class ) { |dobj| action.call( dobj ) }
129
129
  comp_cond = Query::CompoundCondition.new( @condition, rquery.condition,
130
130
  comp_type )
131
- comp_cond.query
131
+ q = comp_cond.query
132
+ [ :order_by, :order_by_order, :limit ].each do |attr|
133
+ q.send( attr.to_s + '=', self.send( attr ) )
134
+ end
135
+ q
132
136
  end
133
137
 
134
138
  def eql?( other ); other.class <= Query && other.to_sql == to_sql; end
@@ -0,0 +1,654 @@
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 object_meets( dobj ); @condition.object_meets( dobj ); end
145
+
146
+ def or( &action ); compound( CompoundCondition::OR, action ); end
147
+
148
+ def order_clause
149
+ if @order_by
150
+ order_by_field = @domain_class.get_field( @order_by )
151
+ clause = "order by #{ order_by_field.db_field_name } "
152
+ clause += @order_by_order == ASC ? 'asc' : 'desc'
153
+ clause
154
+ end
155
+ end
156
+
157
+ def implies?( other_query )
158
+ if other_query == self
159
+ true
160
+ elsif @domain_class == other_query.domain_class
161
+ if other_query.condition.nil? and !self.condition.nil?
162
+ true
163
+ else
164
+ self.condition and self.condition.implies?( other_query.condition )
165
+ end
166
+ end
167
+ end
168
+
169
+ def sql_primary_key_field(domain_class)
170
+ "#{ domain_class.table_name }.#{ domain_class.sql_primary_key_name }"
171
+ end
172
+
173
+ def tables
174
+ concrete_classes = domain_class.self_and_concrete_superclasses.reverse
175
+ table_names = concrete_classes.collect { |domain_class|
176
+ domain_class.table_name
177
+ }
178
+ table_names.join( ', ' )
179
+ end
180
+
181
+ def to_sql
182
+ clauses = [ "select #{ fields }", "from #{ tables }" ]
183
+ clauses << where_clause if where_clause
184
+ clauses << order_clause if order_clause
185
+ clauses << limit_clause if limit_clause
186
+ clauses.join ' '
187
+ end
188
+
189
+ def where_clause
190
+ concrete_classes = domain_class.self_and_concrete_superclasses.reverse
191
+ where_clauses = []
192
+ concrete_classes.each_with_index { |domain_class, i|
193
+ if i < concrete_classes.size - 1
194
+ join_clause = sql_primary_key_field( domain_class ) + ' = ' +
195
+ sql_primary_key_field( concrete_classes[i+1] )
196
+ where_clauses << join_clause
197
+ else
198
+ where_clauses << @condition.to_sql if @condition
199
+ end
200
+ }
201
+ where_clauses.size > 0 ? 'where ' + where_clauses.join( ' and ' ) : nil
202
+ end
203
+
204
+ class Condition #:nodoc:
205
+ def Condition.search_term_type
206
+ Object
207
+ end
208
+
209
+ attr_reader :domain_class
210
+
211
+ def initialize(fieldName, searchTerm, domain_class)
212
+ @fieldName = fieldName
213
+ @searchTerm = searchTerm
214
+ unless @searchTerm.class <= self.class.search_term_type
215
+ raise "Incorrect searchTerm type #{ searchTerm.class }"
216
+ end
217
+ @domain_class = domain_class
218
+ if @domain_class
219
+ unless @domain_class <= DomainObject
220
+ raise "Incorrect object type #{ @domain_class.to_s }"
221
+ end
222
+ end
223
+ end
224
+
225
+ def implies?( other_condition )
226
+ self.eql?( other_condition ) or (
227
+ other_condition.respond_to?( :implied_by? ) and
228
+ other_condition.implied_by?( self )
229
+ )
230
+ end
231
+
232
+ def db_field_name; get_field.db_table_and_field_name; end
233
+
234
+ def eql?( other_cond )
235
+ other_cond.is_a?( Condition ) and other_cond.to_sql == to_sql
236
+ end
237
+
238
+ def get_field; @domain_class.get_field( @fieldName ); end
239
+
240
+ def query; Query.new( @domain_class, self ); end
241
+
242
+ def not
243
+ Query::Not.new( self )
244
+ end
245
+
246
+ def primary_key_field?; 'pk_id' == @fieldName; end
247
+
248
+ def to_condition; self; end
249
+ end
250
+
251
+ class Compare < Condition #:nodoc:
252
+ LESS_THAN = 1
253
+ LESS_THAN_OR_EQUAL = 2
254
+ GREATER_THAN_OR_EQUAL = 3
255
+ GREATER_THAN = 4
256
+
257
+ @@comparators = {
258
+ LESS_THAN => '<',
259
+ LESS_THAN_OR_EQUAL => '<=',
260
+ GREATER_THAN_OR_EQUAL => '>=',
261
+ GREATER_THAN => '>'
262
+ }
263
+
264
+ @@mockComparators = {
265
+ LESS_THAN => Proc.new { |d1, d2| d1 < d2 },
266
+ LESS_THAN_OR_EQUAL => Proc.new { |d1, d2| d1 <= d2 },
267
+ GREATER_THAN_OR_EQUAL => Proc.new { |d1, d2| d1 >= d2 },
268
+ GREATER_THAN => Proc.new { |d1, d2| d1 > d2 }
269
+ }
270
+
271
+ def initialize(fieldName, searchTerm, domain_class, compareType)
272
+ super fieldName, searchTerm, domain_class
273
+ @compareType = compareType
274
+ end
275
+
276
+ def to_sql
277
+ if ( get_field.kind_of?( DomainObjectField ) &&
278
+ !@searchTerm.respond_to?( :pk_id ) )
279
+ search_val = @searchTerm.to_s
280
+ else
281
+ search_val = get_field.value_for_sql( @searchTerm ).to_s
282
+ end
283
+ "#{ db_field_name } #{ @@comparators[@compareType] } " + search_val
284
+ end
285
+
286
+ def object_meets(anObj)
287
+ value = anObj.send @fieldName
288
+ value = value.pk_id if value.class <= DomainObject
289
+ if value
290
+ @@mockComparators[@compareType].call(value, @searchTerm)
291
+ else
292
+ false
293
+ end
294
+ end
295
+ end
296
+
297
+ class CompoundCondition < Condition #:nodoc:
298
+ AND = 1
299
+ OR = 2
300
+
301
+ def initialize( *args )
302
+ if( [ AND, OR ].index( args.last) )
303
+ @compound_type = args.last
304
+ args.pop
305
+ else
306
+ @compound_type = AND
307
+ end
308
+ @conditions = args.map { |arg|
309
+ arg.respond_to?( :to_condition ) ? arg.to_condition : arg
310
+ }
311
+ @domain_class = @conditions[0].domain_class
312
+ end
313
+
314
+ def implied_by?( other_condition )
315
+ @compound_type == OR && @conditions.any? { |cond|
316
+ cond.implies?( other_condition )
317
+ }
318
+ end
319
+
320
+ def implies?( other_condition )
321
+ super( other_condition ) or (
322
+ @compound_type == AND and @conditions.any? { |cond|
323
+ cond.implies?( other_condition )
324
+ }
325
+ ) or (
326
+ @compound_type == OR and @conditions.all? { |cond|
327
+ cond.implies?( other_condition )
328
+ }
329
+ )
330
+ end
331
+
332
+ def object_meets(anObj)
333
+ if @compound_type == AND
334
+ @conditions.inject( true ) { |result, cond|
335
+ result && cond.object_meets( anObj )
336
+ }
337
+ else
338
+ @conditions.inject( false ) { |result, cond|
339
+ result || cond.object_meets( anObj )
340
+ }
341
+ end
342
+ end
343
+
344
+ def to_sql
345
+ booleanString = @compound_type == AND ? 'and' : 'or'
346
+ subSqlStrings = @conditions.collect { |cond| cond.to_sql }
347
+ "(#{ subSqlStrings.join(" #{ booleanString } ") })"
348
+ end
349
+ end
350
+
351
+ module DomainObjectImpostor #:nodoc:
352
+ @@impostor_classes = {}
353
+
354
+ def self.impostor( domain_class )
355
+ unless @@impostor_classes[domain_class]
356
+ i_class = Class.new
357
+ i_class.module_eval <<-CLASS_DEF
358
+ attr_reader :domain_class
359
+
360
+ def initialize; @domain_class = #{ domain_class.name }; end
361
+
362
+ def method_missing( methId, *args )
363
+ fieldName = methId.id2name
364
+ begin
365
+ classField = self.domain_class.get_field( fieldName )
366
+ ObjectFieldImpostor.new( self, classField )
367
+ rescue MissingError
368
+ super( methId, *args )
369
+ end
370
+ end
371
+
372
+ #{ domain_class.name }.class_fields.each do |class_field|
373
+ begin
374
+ undef_method class_field.name.to_sym
375
+ rescue NameError
376
+ # not defined globally or in an included Module, skip it
377
+ end
378
+ end
379
+ CLASS_DEF
380
+ @@impostor_classes[domain_class] = i_class
381
+ end
382
+ i_class = @@impostor_classes[domain_class]
383
+ i_class.new
384
+ end
385
+ end
386
+
387
+ class Equals < Condition #:nodoc:
388
+ def r_val_string
389
+ field = get_field
390
+ if @searchTerm.class <= ObjectField
391
+ @searchTerm.db_table_and_field_name
392
+ else
393
+ begin
394
+ field.value_for_sql( @searchTerm ).to_s
395
+ rescue DomainObjectInitError
396
+ raise(
397
+ ArgumentError,
398
+ "Can't query using an uncommitted domain object as a search " +
399
+ "term.",
400
+ caller
401
+ )
402
+ end
403
+ end
404
+ end
405
+
406
+ def object_meets(anObj)
407
+ if @searchTerm.class <= ObjectField
408
+ compare_value = anObj.send( @searchTerm.name )
409
+ else
410
+ compare_value = @searchTerm
411
+ end
412
+ compare_value == anObj.send( @fieldName )
413
+ end
414
+
415
+ def to_sql
416
+ sql = "#{ db_field_name } "
417
+ unless @searchTerm.nil?
418
+ sql += "= " + r_val_string
419
+ else
420
+ sql += "is null"
421
+ end
422
+ sql
423
+ end
424
+ end
425
+
426
+ class In < Condition #:nodoc:
427
+ def self.search_term_type
428
+ Array
429
+ end
430
+
431
+ def object_meets(anObj)
432
+ value = anObj.send @fieldName
433
+ @searchTerm.index(value) != nil
434
+ end
435
+
436
+ def to_sql
437
+ if get_field.is_a?( StringField )
438
+ quoted = @searchTerm.map do |str| "'#{ str }'"; end
439
+ end_clause = quoted.join ', '
440
+ else
441
+ end_clause = @searchTerm.join ', '
442
+ end
443
+ "#{ db_field_name } in (#{ end_clause })"
444
+ end
445
+ end
446
+
447
+ class Include < CompoundCondition
448
+ def initialize( field_name, search_term, domain_class )
449
+ begin_cond = Like.new( field_name, search_term + ',', domain_class,
450
+ Like::POST_ONLY )
451
+ mid_cond = Like.new( field_name, ',' + search_term + ',',
452
+ domain_class )
453
+ end_cond = Like.new( field_name, ',' + search_term, domain_class,
454
+ Like::PRE_ONLY )
455
+ only_cond = Equals.new( field_name, search_term, domain_class )
456
+ super( begin_cond, mid_cond, end_cond, only_cond, OR )
457
+ end
458
+ end
459
+
460
+ class Inferrer #:nodoc:
461
+ def initialize( domain_class, &action )
462
+ @domain_class = domain_class; @action = action
463
+ end
464
+
465
+ def execute
466
+ impostor = DomainObjectImpostor.impostor( @domain_class )
467
+ condition = @action.call( impostor ).to_condition
468
+ query = Query.new( @domain_class, condition )
469
+ end
470
+ end
471
+
472
+ class Like < Condition #:nodoc:
473
+ PRE_AND_POST = 1
474
+ PRE_ONLY = 2
475
+ POST_ONLY = 3
476
+
477
+ def initialize(
478
+ fieldName, searchTerm, domain_class, matchType = PRE_AND_POST)
479
+ super fieldName, searchTerm, domain_class
480
+ @matchType = matchType
481
+ end
482
+
483
+ def get_regexp
484
+ if @matchType == PRE_AND_POST
485
+ Regexp.new( @searchTerm, Regexp::IGNORECASE )
486
+ elsif @matchType == PRE_ONLY
487
+ Regexp.new( @searchTerm.to_s + "$", Regexp::IGNORECASE )
488
+ elsif @matchType == POST_ONLY
489
+ Regexp.new( "^" + @searchTerm, Regexp::IGNORECASE )
490
+ end
491
+ end
492
+
493
+ def object_meets(anObj)
494
+ value = anObj.send @fieldName
495
+ if value.class <= DomainObject || value.class == DomainObjectProxy
496
+ value = value.pk_id.to_s
497
+ end
498
+ if value.class <= Array
499
+ (value.index(@searchTerm) != nil)
500
+ else
501
+ get_regexp.match(value) != nil
502
+ end
503
+ end
504
+
505
+ def to_sql
506
+ withWildcards = @searchTerm
507
+ if @matchType == PRE_AND_POST
508
+ withWildcards = "%" + withWildcards + "%"
509
+ elsif @matchType == PRE_ONLY
510
+ withWildcards = "%" + withWildcards
511
+ elsif @matchType == POST_ONLY
512
+ withWildcards += "%"
513
+ end
514
+ "#{ db_field_name } like '#{ withWildcards }'"
515
+ end
516
+ end
517
+
518
+ class Max < Query #:nodoc:
519
+ attr_reader :field_name
520
+
521
+ def initialize( domain_class, field_name = 'pk_id' )
522
+ super( domain_class )
523
+ @field_name = field_name
524
+ end
525
+
526
+ def collect( coll )
527
+ max = coll.inject( nil ) { |max, d_obj|
528
+ a_value = d_obj.send( @field_name )
529
+ ( max.nil? || a_value > max ) ? a_value : max
530
+ }
531
+ [ max ]
532
+ end
533
+
534
+ def fields
535
+ "max(#{ @domain_class.get_field( @field_name ).db_field_name })"
536
+ end
537
+ end
538
+
539
+ class Not < Condition #:nodoc:
540
+ def initialize(unCondition)
541
+ @unCondition = unCondition
542
+ end
543
+
544
+ def object_meets(obj)
545
+ !@unCondition.object_meets(obj)
546
+ end
547
+
548
+ def domain_class; @unCondition.domain_class; end
549
+
550
+ def to_sql
551
+ "!(#{ @unCondition.to_sql })"
552
+ end
553
+ end
554
+
555
+ class ObjectFieldImpostor #:nodoc:
556
+ def self.comparators
557
+ {
558
+ 'lt' => Compare::LESS_THAN, 'lte' => Compare::LESS_THAN_OR_EQUAL,
559
+ 'gte' => Compare::GREATER_THAN_OR_EQUAL,
560
+ 'gt' => Compare::GREATER_THAN
561
+ }
562
+ end
563
+
564
+ attr_reader :class_field
565
+
566
+ def initialize( domainObjectImpostor, class_field_or_name )
567
+ @domainObjectImpostor = domainObjectImpostor
568
+ if class_field_or_name == 'pk_id'
569
+ @field_name = 'pk_id'
570
+ else
571
+ @class_field = class_field_or_name
572
+ @field_name = class_field_or_name.name
573
+ end
574
+ end
575
+
576
+ def method_missing( methId, *args )
577
+ methodName = methId.id2name
578
+ if !ObjectFieldImpostor.comparators.keys.index( methodName ).nil?
579
+ register_compare_condition( methodName, *args )
580
+ else
581
+ super( methId, *args )
582
+ end
583
+ end
584
+
585
+ def register_compare_condition( compareStr, searchTerm)
586
+ compareVal = ObjectFieldImpostor.comparators[compareStr]
587
+ Compare.new( @field_name, searchTerm,
588
+ @domainObjectImpostor.domain_class, compareVal )
589
+ end
590
+
591
+ def equals( searchTerm )
592
+ Equals.new( @field_name, field_or_field_name( searchTerm ),
593
+ @domainObjectImpostor.domain_class )
594
+ end
595
+
596
+ def field_or_field_name( search_term )
597
+ if search_term.class == ObjectFieldImpostor
598
+ search_term.class_field
599
+ else
600
+ search_term
601
+ end
602
+ end
603
+
604
+ def include?( search_term )
605
+ if @class_field.instance_of?( TextListField )
606
+ Include.new( @field_name, search_term,
607
+ @domainObjectImpostor.domain_class )
608
+ else
609
+ raise ArgumentError
610
+ end
611
+ end
612
+
613
+ def like( regexp )
614
+ if regexp.is_a?( Regexp )
615
+ if regexp.source =~ /^\^(.*)/
616
+ searchTerm = $1
617
+ matchType = Query::Like::POST_ONLY
618
+ elsif regexp.source =~ /(.*)\$$/
619
+ searchTerm = $1
620
+ matchType = Query::Like::PRE_ONLY
621
+ else
622
+ searchTerm = regexp.source
623
+ matchType = Query::Like::PRE_AND_POST
624
+ end
625
+ Query::Like.new( @field_name, searchTerm,
626
+ @domainObjectImpostor.domain_class, matchType )
627
+ else
628
+ raise(
629
+ ArgumentError, "#{ @field_name }#like needs to receive a Regexp",
630
+ caller
631
+ )
632
+ end
633
+ end
634
+
635
+ def in( *searchTerms )
636
+ Query::In.new( @field_name, searchTerms,
637
+ @domainObjectImpostor.domain_class )
638
+ end
639
+
640
+ def nil?; equals( nil ); end
641
+
642
+ def to_condition
643
+ if @class_field.instance_of?( BooleanField )
644
+ Query::Equals.new( @field_name, true,
645
+ @domainObjectImpostor.domain_class )
646
+ else
647
+ raise
648
+ end
649
+ end
650
+
651
+ def not; to_condition.not; end
652
+ end
653
+ end
654
+ end