lafcadio 0.8.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,654 +0,0 @@
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)
486
- elsif @matchType == PRE_ONLY
487
- Regexp.new(@searchTerm.to_s + "$")
488
- elsif @matchType == POST_ONLY
489
- Regexp.new("^" + @searchTerm)
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