lafcadio 0.6.1 → 0.6.2

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