lafcadio 0.8.0 → 0.8.1

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.
@@ -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