lafcadio 0.5.2 → 0.6.0

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,572 +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
- module Lafcadio
88
- class Query
89
- def self.And( *conditions ); CompoundCondition.new( *conditions ); end
90
-
91
- def self.infer( domain_class, &action )
92
- inferrer = Query::Inferrer.new( domain_class ) { |obj|
93
- action.call( obj )
94
- }
95
- inferrer.execute
96
- end
97
-
98
- def self.Or( *conditions )
99
- conditions << CompoundCondition::OR
100
- CompoundCondition.new( *conditions)
101
- end
102
-
103
- ASC = 1
104
- DESC = 2
105
-
106
- attr_reader :object_type, :condition
107
- attr_accessor :order_by, :order_by_order, :limit
108
-
109
- def initialize(object_type, pk_idOrCondition = nil)
110
- @object_type = object_type
111
- ( @condition, @order_by, @limit ) = [ nil, nil, nil ]
112
- if pk_idOrCondition
113
- if pk_idOrCondition.class <= Condition
114
- @condition = pk_idOrCondition
115
- else
116
- @condition = Query::Equals.new( object_type.sql_primary_key_name,
117
- pk_idOrCondition, object_type )
118
- end
119
- end
120
- @order_by_order = ASC
121
- end
122
-
123
- def and( &action ); compound( CompoundCondition::AND, action ); end
124
-
125
- def compound( comp_type, action )
126
- rquery = Query.infer( @object_type ) { |dobj| action.call( dobj ) }
127
- comp_cond = Query::CompoundCondition.new( @condition, rquery.condition,
128
- comp_type )
129
- comp_cond.query
130
- end
131
-
132
- def eql?( other ); other.class <= Query && other.to_sql == to_sql; end
133
-
134
- def fields; '*'; end
135
-
136
- def hash; to_sql.hash; end
137
-
138
- def limit_clause
139
- "limit #{ @limit.begin }, #{ @limit.end - @limit.begin + 1 }" if @limit
140
- end
141
-
142
- def or( &action ); compound( CompoundCondition::OR, action ); end
143
-
144
- def order_clause
145
- if @order_by
146
- clause = "order by #{ @order_by } "
147
- clause += @order_by_order == ASC ? 'asc' : 'desc'
148
- clause
149
- end
150
- end
151
-
152
- def sql_primary_key_field(object_type)
153
- "#{ object_type.table_name }.#{ object_type.sql_primary_key_name }"
154
- end
155
-
156
- def tables
157
- concrete_classes = object_type.self_and_concrete_superclasses.reverse
158
- table_names = concrete_classes.collect { |domain_class|
159
- domain_class.table_name
160
- }
161
- table_names.join( ', ' )
162
- end
163
-
164
- def to_sql
165
- clauses = [ "select #{ fields }", "from #{ tables }" ]
166
- clauses << where_clause if where_clause
167
- clauses << order_clause if order_clause
168
- clauses << limit_clause if limit_clause
169
- clauses.join ' '
170
- end
171
-
172
- def where_clause
173
- concrete_classes = object_type.self_and_concrete_superclasses.reverse
174
- where_clauses = []
175
- concrete_classes.each_with_index { |domain_class, i|
176
- if i < concrete_classes.size - 1
177
- join_clause = sql_primary_key_field( domain_class ) + ' = ' +
178
- sql_primary_key_field( concrete_classes[i+1] )
179
- where_clauses << join_clause
180
- else
181
- where_clauses << @condition.to_sql if @condition
182
- end
183
- }
184
- where_clauses.size > 0 ? 'where ' + where_clauses.join( ' and ' ) : nil
185
- end
186
-
187
- class Condition #:nodoc:
188
- def Condition.search_term_type
189
- Object
190
- end
191
-
192
- attr_reader :object_type
193
-
194
- def initialize(fieldName, searchTerm, object_type)
195
- @fieldName = fieldName
196
- @searchTerm = searchTerm
197
- unless @searchTerm.class <= self.class.search_term_type
198
- raise "Incorrect searchTerm type #{ searchTerm.class }"
199
- end
200
- @object_type = object_type
201
- if @object_type
202
- unless @object_type <= DomainObject
203
- raise "Incorrect object type #{ @object_type.to_s }"
204
- end
205
- end
206
- end
207
-
208
- def db_field_name; get_field.db_table_and_field_name; end
209
-
210
- def get_field
211
- anObjectType = @object_type
212
- field = nil
213
- while (anObjectType < DomainObject || anObjectType < DomainObject) &&
214
- !field
215
- field = anObjectType.get_class_field( @fieldName ) ||
216
- anObjectType.get_class_field_by_db_name( @fieldName )
217
- anObjectType = anObjectType.superclass
218
- end
219
- if field
220
- field
221
- else
222
- errStr = "Couldn't find field \"#{ @fieldName }\" in " +
223
- "#{ @object_type } domain class"
224
- raise( MissingError, errStr, caller )
225
- end
226
- end
227
-
228
- def query; Query.new( @object_type, self ); end
229
-
230
- def not
231
- Query::Not.new( self )
232
- end
233
-
234
- def primary_key_field?
235
- [ @object_type.sql_primary_key_name, 'pk_id' ].include?( @fieldName )
236
- end
237
- end
238
-
239
- class Compare < Condition #:nodoc:
240
- LESS_THAN = 1
241
- LESS_THAN_OR_EQUAL = 2
242
- GREATER_THAN_OR_EQUAL = 3
243
- GREATER_THAN = 4
244
-
245
- @@comparators = {
246
- LESS_THAN => '<',
247
- LESS_THAN_OR_EQUAL => '<=',
248
- GREATER_THAN_OR_EQUAL => '>=',
249
- GREATER_THAN => '>'
250
- }
251
-
252
- @@mockComparators = {
253
- LESS_THAN => Proc.new { |d1, d2| d1 < d2 },
254
- LESS_THAN_OR_EQUAL => Proc.new { |d1, d2| d1 <= d2 },
255
- GREATER_THAN_OR_EQUAL => Proc.new { |d1, d2| d1 >= d2 },
256
- GREATER_THAN => Proc.new { |d1, d2| d1 > d2 }
257
- }
258
-
259
- def initialize(fieldName, searchTerm, object_type, compareType)
260
- super fieldName, searchTerm, object_type
261
- @compareType = compareType
262
- end
263
-
264
- def to_sql
265
- if ( get_field.kind_of?( LinkField ) &&
266
- !@searchTerm.respond_to?( :pk_id ) )
267
- search_val = @searchTerm.to_s
268
- else
269
- search_val = get_field.value_for_sql( @searchTerm ).to_s
270
- end
271
- "#{ db_field_name } #{ @@comparators[@compareType] } " + search_val
272
- end
273
-
274
- def object_meets(anObj)
275
- value = anObj.send @fieldName
276
- value = value.pk_id if value.class <= DomainObject
277
- if value
278
- @@mockComparators[@compareType].call(value, @searchTerm)
279
- else
280
- false
281
- end
282
- end
283
- end
284
-
285
- class CompoundCondition < Condition #:nodoc:
286
- AND = 1
287
- OR = 2
288
-
289
- def initialize(*conditions)
290
- if( [ AND, OR ].index(conditions.last) )
291
- @compoundType = conditions.last
292
- conditions.pop
293
- else
294
- @compoundType = AND
295
- end
296
- @conditions = conditions
297
- @object_type = conditions[0].object_type
298
- end
299
-
300
- def object_meets(anObj)
301
- if @compoundType == AND
302
- @conditions.inject( true ) { |result, cond|
303
- result && cond.object_meets( anObj )
304
- }
305
- else
306
- @conditions.inject( false ) { |result, cond|
307
- result || cond.object_meets( anObj )
308
- }
309
- end
310
- end
311
-
312
- def to_sql
313
- booleanString = @compoundType == AND ? 'and' : 'or'
314
- subSqlStrings = @conditions.collect { |cond| cond.to_sql }
315
- "(#{ subSqlStrings.join(" #{ booleanString } ") })"
316
- end
317
- end
318
-
319
- class DomainObjectImpostor #:nodoc:
320
- attr_reader :domain_class
321
-
322
- def initialize( domain_class )
323
- @domain_class = domain_class
324
- end
325
-
326
- def method_missing( methId, *args )
327
- fieldName = methId.id2name
328
- begin
329
- classField = @domain_class.get_field( fieldName )
330
- ObjectFieldImpostor.new( self, classField )
331
- rescue MissingError
332
- super( methId, *args )
333
- end
334
- end
335
- end
336
-
337
- class Equals < Condition #:nodoc:
338
- def r_val_string
339
- field = get_field
340
- if @searchTerm.class <= ObjectField
341
- @searchTerm.db_table_and_field_name
342
- else
343
- field.value_for_sql(@searchTerm).to_s
344
- end
345
- end
346
-
347
- def object_meets(anObj)
348
- if @searchTerm.class <= ObjectField
349
- compare_value = anObj.send( @searchTerm.name )
350
- else
351
- compare_value = @searchTerm
352
- end
353
- compare_value == anObj.send( @fieldName )
354
- end
355
-
356
- def to_sql
357
- sql = "#{ db_field_name } "
358
- unless @searchTerm.nil?
359
- sql += "= " + r_val_string
360
- else
361
- sql += "is null"
362
- end
363
- sql
364
- end
365
- end
366
-
367
- class In < Condition #:nodoc:
368
- def self.search_term_type
369
- Array
370
- end
371
-
372
- def object_meets(anObj)
373
- value = anObj.send @fieldName
374
- @searchTerm.index(value) != nil
375
- end
376
-
377
- def to_sql
378
- "#{ db_field_name } in (#{ @searchTerm.join(', ') })"
379
- end
380
- end
381
-
382
- class Inferrer #:nodoc:
383
- def initialize( domain_class, &action )
384
- @domain_class = domain_class; @action = action
385
- end
386
-
387
- def execute
388
- impostor = DomainObjectImpostor.new( @domain_class )
389
- condition = @action.call( impostor )
390
- query = Query.new( @domain_class, condition )
391
- end
392
- end
393
-
394
- class Like < Condition #:nodoc:
395
- PRE_AND_POST = 1
396
- PRE_ONLY = 2
397
- POST_ONLY = 3
398
-
399
- def initialize(
400
- fieldName, searchTerm, object_type, matchType = PRE_AND_POST)
401
- super fieldName, searchTerm, object_type
402
- @matchType = matchType
403
- end
404
-
405
- def get_regexp
406
- if @matchType == PRE_AND_POST
407
- Regexp.new(@searchTerm)
408
- elsif @matchType == PRE_ONLY
409
- Regexp.new(@searchTerm.to_s + "$")
410
- elsif @matchType == POST_ONLY
411
- Regexp.new("^" + @searchTerm)
412
- end
413
- end
414
-
415
- def object_meets(anObj)
416
- value = anObj.send @fieldName
417
- if value.class <= DomainObject || value.class == DomainObjectProxy
418
- value = value.pk_id.to_s
419
- end
420
- if value.class <= Array
421
- (value.index(@searchTerm) != nil)
422
- else
423
- get_regexp.match(value) != nil
424
- end
425
- end
426
-
427
- def to_sql
428
- withWildcards = @searchTerm
429
- if @matchType == PRE_AND_POST
430
- withWildcards = "%" + withWildcards + "%"
431
- elsif @matchType == PRE_ONLY
432
- withWildcards = "%" + withWildcards
433
- elsif @matchType == POST_ONLY
434
- withWildcards += "%"
435
- end
436
- "#{ db_field_name } like '#{ withWildcards }'"
437
- end
438
- end
439
-
440
- class Link < Condition #:nodoc:
441
- def initialize( fieldName, searchTerm, object_type )
442
- if searchTerm.pk_id.nil?
443
- raise ArgumentError,
444
- "Can't query using an uncommitted domain object as a search term",
445
- caller
446
- else
447
- super( fieldName, searchTerm, object_type )
448
- end
449
- end
450
-
451
- def self.search_term_type
452
- DomainObject
453
- end
454
-
455
- def object_meets(anObj)
456
- value = anObj.send @fieldName
457
- value ? value.pk_id == @searchTerm.pk_id : false
458
- end
459
-
460
- def to_sql
461
- "#{ db_field_name } = #{ @searchTerm.pk_id }"
462
- end
463
- end
464
-
465
- class Max < Query #:nodoc:
466
- attr_reader :field_name
467
-
468
- def initialize( object_type, field_name = 'pk_id' )
469
- super( object_type )
470
- @field_name = field_name
471
- end
472
-
473
- def collect( coll )
474
- max = coll.inject( nil ) { |max, d_obj|
475
- a_value = d_obj.send( @field_name )
476
- ( max.nil? || a_value > max ) ? a_value : max
477
- }
478
- [ max ]
479
- end
480
-
481
- def fields
482
- "max(#{ @object_type.get_field( @field_name ).db_field_name })"
483
- end
484
- end
485
-
486
- class Not < Condition #:nodoc:
487
- def initialize(unCondition)
488
- @unCondition = unCondition
489
- end
490
-
491
- def object_meets(obj)
492
- !@unCondition.object_meets(obj)
493
- end
494
-
495
- def object_type; @unCondition.object_type; end
496
-
497
- def to_sql
498
- "!(#{ @unCondition.to_sql })"
499
- end
500
- end
501
-
502
- class ObjectFieldImpostor #:nodoc:
503
- def self.comparators
504
- {
505
- 'lt' => Compare::LESS_THAN, 'lte' => Compare::LESS_THAN_OR_EQUAL,
506
- 'gte' => Compare::GREATER_THAN_OR_EQUAL,
507
- 'gt' => Compare::GREATER_THAN
508
- }
509
- end
510
-
511
- attr_reader :class_field
512
-
513
- def initialize( domainObjectImpostor, class_field_or_name )
514
- @domainObjectImpostor = domainObjectImpostor
515
- if class_field_or_name == 'pk_id'
516
- @db_field_name = 'pk_id'
517
- else
518
- @class_field = class_field_or_name
519
- @db_field_name = class_field_or_name.db_field_name
520
- end
521
- end
522
-
523
- def method_missing( methId, *args )
524
- methodName = methId.id2name
525
- if !ObjectFieldImpostor.comparators.keys.index( methodName ).nil?
526
- register_compare_condition( methodName, *args )
527
- else
528
- super( methId, *args )
529
- end
530
- end
531
-
532
- def register_compare_condition( compareStr, searchTerm)
533
- compareVal = ObjectFieldImpostor.comparators[compareStr]
534
- Compare.new( @db_field_name, searchTerm,
535
- @domainObjectImpostor.domain_class, compareVal )
536
- end
537
-
538
- def equals( searchTerm )
539
- Equals.new( @db_field_name, field_or_field_name( searchTerm ),
540
- @domainObjectImpostor.domain_class )
541
- end
542
-
543
- def field_or_field_name( search_term )
544
- if search_term.class == ObjectFieldImpostor
545
- search_term.class_field
546
- else
547
- search_term
548
- end
549
- end
550
-
551
- def like( regexp )
552
- if regexp.source =~ /^\^(.*)/
553
- searchTerm = $1
554
- matchType = Query::Like::POST_ONLY
555
- elsif regexp.source =~ /(.*)\$$/
556
- searchTerm = $1
557
- matchType = Query::Like::PRE_ONLY
558
- else
559
- searchTerm = regexp.source
560
- matchType = Query::Like::PRE_AND_POST
561
- end
562
- Query::Like.new( @db_field_name, searchTerm,
563
- @domainObjectImpostor.domain_class, matchType )
564
- end
565
-
566
- def in( *searchTerms )
567
- Query::In.new( @db_field_name, searchTerms,
568
- @domainObjectImpostor.domain_class )
569
- end
570
- end
571
- end
572
- end