criteria 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,101 @@
1
+ = Criteria for ActiveRecord
2
+
3
+ == What is it?
4
+
5
+ Users of Hibernate, Torque (in Java) and Propel (in PHP) will be familiar with the concept of criteria as a method of building, in an object orientated manner, complex queries for the underlying Object Relational Mapping (ORM) framework.
6
+
7
+ == Example
8
+
9
+ Imagine you have an ActiveRecord class User, and it has the following columns/fields: email, password, name, createdAt, role, active. When a user logs in, we want to find the correct user object for the email provided and confirm that the password is correct:
10
+
11
+ u = User.find_by_email_and_password(email, password)
12
+
13
+ Now, what if we wanted to then delegate to another method to add some additional criteria? We may end up doing something like this:
14
+
15
+ where = "email = '#{email}' AND password = '#{password}'"
16
+ where = apply_filter(where)
17
+ u = User.find(:first, :where => where)
18
+
19
+ All that icky SQL, not so pretty. Depending on what you need to do, you might find a nicer way around things, but with criteria, we always stay in OO land:
20
+
21
+ c = Criteria.new(User.email.eq(email)) do |c|
22
+ c.and User.password.eq(password)
23
+ end
24
+ c = apply_filter(c)
25
+ u = User.find(:first, c)
26
+
27
+ The important thing to notice here is that you can get a Column object by calling ActiveRecord::Base.column_name, e.g. User.email in the above example. This Column object allows you to then express some criteria on it, in the above example we use eq() to mean this column must be equal to the value passed to it. This in turn returns a Criterion object. A Criteria object is a collection of AND'd and OR'd Criterion objects.
28
+
29
+ As a further example, imagine we are implementing apply_filer to only allow a user to log in if their 'role' field is one of :admin, :user or :editor
30
+
31
+ def apply_filter(c)
32
+ if apply_fiter?
33
+ c.and User.role.in([:admin, :user, :editor])
34
+ end
35
+ end
36
+
37
+ As you can see, appending criteria is pretty easy.
38
+
39
+ == Complex Boolean
40
+
41
+ The above was quite a simple example, and we only used ANDs and no nested boolean operations.
42
+
43
+ criteria = Criteria.new
44
+ criteria.and do |c|
45
+ c.or User.role.eq(:admin)
46
+ c.or User.active.eq(false)
47
+ c.or User.created_at.gt(10.hours.ago)
48
+ end
49
+ criteria.and do |c|
50
+ c.or User.role.eq(:editor)
51
+ c.or User.active.eq(true)
52
+ c.or do |c2|
53
+ c2.and User.created_at.gt(20.days.go)
54
+ c2.and User.created_at.lt(10.hours.ago)
55
+ end
56
+ end
57
+
58
+ The above query demonstrates how to nest Criteria objects, using them as if they were Criterion, and thus nest ANDs and ORs. If we call to_where_sql on the criteria, we would get:
59
+
60
+ ((users.role=":admin" OR users.active=0 OR users.created_at>"2008-04-04 13:55:42") AND (users.role=":editor" OR users.active=1 OR (users.created_at>"2008-03-15 23:57:04" AND users.created_at<"2008-04-04 13:55:42")))
61
+
62
+ == Chaining
63
+
64
+ Most methods on Criteria or Criterion return self, so you can chain calls together:
65
+
66
+ u = User.email.eq(email).and(User.password.eq(password))
67
+
68
+
69
+ == Alternative Syntax
70
+
71
+ Alternatively, rather than express the column constraint like this:
72
+
73
+ User.createdAt.gt(20.days.ago)
74
+
75
+ We can use a more natural ruby language approach
76
+
77
+ User.createdAt > 20.days.ago
78
+
79
+ Rather than return a boolean true or false, this will return the same Criterion as would be returned in the above statement. When writing lots of criteria, you can see how much nicer this looks
80
+
81
+ c.and( User.createAt > 20.days.ago )
82
+
83
+ We can go further and then express AND and OR in a similar way:
84
+
85
+ (User.createdAt < 10.hours.ago) & (User.createdAt > 20.days.ago)
86
+
87
+ The above will return a Criteria object populated with the two statements ANDed together.
88
+
89
+ So, in fact, you could query for the User's email and password like this:
90
+
91
+ User.find((User.email == email) & (User.password == password))
92
+
93
+ Although this looks nicer, it might be removed as it breaks the semantics of the language. Normally you would expect an == or <= to return a boolean value and such comparisons would no longer be possible with these objects.
94
+
95
+ == Joins
96
+
97
+ ActiveRecord does much of the heavy lifting for us, so joins, etc, are made pretty simple. By virtue of including a criterion for a particular column in a particular table, that table will be added to the list of included tables. It is then delegated to your model hierarchy (via belongs_to, has_many, etc) to determine how these relationships are actually manifest in terms of JOINs in the SQL
98
+
99
+ == More info
100
+
101
+ Please see the rdoc for more information on the API
data/lib/criteria.rb ADDED
@@ -0,0 +1,603 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+
4
+ # Criteria is a collection of Criterion as well as additional constraints regarding the order, limit and offset
5
+ #
6
+ # see the readme for usage examples.
7
+ class Criteria
8
+ module VERSION #:nodoc:
9
+ MAJOR = 0
10
+ MINOR = 0
11
+ TINY = 1
12
+
13
+ STRING = [MAJOR, MINOR, TINY].join('.')
14
+ end
15
+
16
+ attr_accessor :limit, :offset, :default_operator
17
+
18
+ # Create a new criteria, optionally pass in a Criterion object
19
+ # You can also pass a block, and self will be yielded
20
+ def initialize(c=nil) # :yields: self
21
+ @and = []
22
+ @or = []
23
+ @order_by = []
24
+ @group_by = []
25
+ @select = []
26
+ @limit = nil
27
+ @offset = nil
28
+ @joins = []
29
+ @default_operator = :or
30
+ self.add(c) if c.is_a? Criterion
31
+
32
+ yield(self) if block_given?
33
+ end
34
+
35
+ # Return a modifiable array of AND'd criteria
36
+ def ands
37
+ @and
38
+ end
39
+
40
+ # Returns a modifiable array of OR'd criteria
41
+ def ors
42
+ @ors
43
+ end
44
+
45
+ # Returns an array of column's to be selected
46
+ def select
47
+ @select
48
+ end
49
+
50
+ # Get the collection of columns to group by
51
+ def group_by
52
+ @group_by
53
+ end
54
+
55
+ # Get the collection of Order objects to order by
56
+ def order_by
57
+ @order_by
58
+ end
59
+
60
+ # AND a criterion with the existing Criteria
61
+ def and(c=nil, &block)
62
+ add(c, :and, &block)
63
+ end
64
+
65
+ # OR a criterion with the existing Criteria
66
+ def or(c=nil, &block)
67
+ add(c, :or, &block)
68
+ end
69
+
70
+ def <<(c)
71
+ raise "<< does not accept a block, perhaps you were trying to pass it to Criteria.new?" if block_given?
72
+ add(c)
73
+ end
74
+
75
+ def add(c=nil, operator=self.default_operator)
76
+ yield(c = Criteria.new) if c.nil? and block_given?
77
+
78
+ # puts "#{self} << OR #{c}"
79
+ if c.is_a? Column
80
+ raise "You cannot directly #{operator.to_s.upcase} an instanceof Column, you must call some sort of expression (eq, ne, gt, ge, etc) on it."
81
+ end
82
+
83
+ if operator==:or
84
+ @or << c
85
+ else
86
+ @and << c
87
+ end
88
+ self
89
+ end
90
+
91
+ # AND this with another criterion
92
+ def &(criterion)
93
+ self.and(criterion)
94
+ end
95
+
96
+ # OR this with another criterion
97
+ def |(criterion)
98
+ self.or(criterion)
99
+ end
100
+
101
+ # Convert the AND and OR statements into the WHERE SQL
102
+ def to_where_sql
103
+ and_clauses = []
104
+
105
+ if @or.size>0
106
+ c = @or.collect {|c| c.to_where_sql}.join(" OR ")
107
+ if @and.size>0
108
+ and_clauses << "(#{c})"
109
+ else
110
+ and_clauses << c
111
+ end
112
+ end
113
+
114
+ if @and.size>0
115
+ and_clauses << @and.collect {|c| c.to_where_sql}
116
+ end
117
+
118
+ and_clauses.size>0 ? "(#{and_clauses.join(" AND ")})" : ""
119
+ end
120
+
121
+ def to_order_by_sql
122
+ @order_by.size>0 ? @order_by.collect {|o| o.to_s}.join(",") : nil
123
+ end
124
+
125
+ def to_group_by_sql
126
+ @group_by.size>0 ? @group_by.collect {|g| g.to_s}.join(",") : nil
127
+ end
128
+
129
+ def to_select_sql
130
+ @select.size>0 ? @select.collect{|s| s.to_s}.join(",") : nil
131
+ end
132
+
133
+ # Return a unique list of column objects that are referenced in this query
134
+ def columns
135
+ columns = []
136
+ (@and + @or).each do |c|
137
+ c.columns.each do |c2|
138
+ columns << c2
139
+ end
140
+ end
141
+ columns.uniq
142
+ end
143
+
144
+ # Get a read-only array of all the table names that will be included in
145
+ # this query
146
+ def tables
147
+ columns.collect {|c| c.table_name }.uniq
148
+ end
149
+
150
+ def associations
151
+ out = []
152
+ (@and + @or).each do |c|
153
+ out+=c.associations
154
+ end
155
+ out
156
+ end
157
+
158
+ # def list
159
+ # @clazz.find(:all, self.to_hash)
160
+ # end
161
+ #
162
+ # def count
163
+ # h = self.to_hash
164
+ # h.delete :order
165
+ # h.delete :group
166
+ # @clazz.count(h)
167
+ # end
168
+
169
+ # def [](key)
170
+ # if key==:include
171
+ # self.tables
172
+ # elsif key==:conditions
173
+ # self.to_where_sql
174
+ # elsif key==:limit
175
+ # self.limit
176
+ # elsif key==:offset
177
+ # self.offset
178
+ # elsif key==:order
179
+ # self.to_order_by_sql
180
+ # elsif key==:group
181
+ # self.to_group_by_sql
182
+ # elsif key==:select
183
+ # self.to_select_sql
184
+ # end
185
+ # end
186
+
187
+ # FIXME: this returns a list of table names in :include, whereas it should contain the
188
+ # relationship names
189
+ def to_hash
190
+ {
191
+ :include => self.associations,
192
+ :conditions => self.to_where_sql,
193
+ :limit => self.limit,
194
+ :offset => self.offset,
195
+ :order => self.to_order_by_sql,
196
+ :group => self.to_group_by_sql,
197
+ :select => self.to_select_sql
198
+ }
199
+ end
200
+
201
+ def to_s
202
+ "Criteria(#{to_hash.inspect})"
203
+ end
204
+
205
+ # This class reprsents an ordering by a particular column. The normal way to retreive and instance of
206
+ # this object is by calling the direction on the Column instance, e.g. User.email.asc
207
+ class Order
208
+ attr_reader :dir, :column
209
+ ASC = "ASC"
210
+ DESC = "DESC"
211
+
212
+ def initialize(column, dir = ASC)
213
+ @column = column
214
+ @dir = dir
215
+ end
216
+
217
+ def to_s
218
+ "#{@column.to_sql_name} #{@dir}"
219
+ end
220
+
221
+ def self.asc(col)
222
+ Order.new(col, ASC)
223
+ end
224
+
225
+ def self.desc(col)
226
+ Order.new(col, DESC)
227
+ end
228
+ end
229
+
230
+ class Column
231
+ def initialize(clazz, column, adapter=ActiveRecord::Base.connection)
232
+ @adapter = adapter
233
+ if clazz.is_a? Class
234
+ @clazz = clazz
235
+ else
236
+ @clazz = (clazz.is_a? Symbol) ? clazz : clazz.intern
237
+ end
238
+ @column = (column.is_a? String) ? column.intern : column
239
+ end
240
+
241
+ def column_name
242
+ (@column.is_a? Symbol) ? @column : @column.name.intern
243
+ end
244
+
245
+ def quote_value(val)
246
+ @adapter.quote(val, (@column.is_a? Symbol) ? nil : @column)
247
+ end
248
+
249
+ def adapter
250
+ @adapter
251
+ end
252
+
253
+ def table_name
254
+ if @clazz.is_a? Class
255
+ @clazz.table_name.intern
256
+ else
257
+ @clazz
258
+ end
259
+ end
260
+
261
+ def asc
262
+ Order.asc(self)
263
+ end
264
+
265
+ def desc
266
+ Order.desc(self)
267
+ end
268
+
269
+ def not_in(values)
270
+ create_criterion(Criterion::NOT_IN, values)
271
+ end
272
+
273
+ def in(values)
274
+ create_criterion(Criterion::IN, values)
275
+ end
276
+
277
+ def ne(value)
278
+ create_criterion(Criterion::NOT_EQUAL, value)
279
+ end
280
+
281
+ def eq(value)
282
+ create_criterion(Criterion::EQUAL, value)
283
+ end
284
+
285
+ def gt(value)
286
+ create_criterion(Criterion::GREATER_THAN, value)
287
+ end
288
+
289
+ def ge(value)
290
+ create_criterion(Criterion::GREATER_THAN_OR_EQUAL, value)
291
+ end
292
+
293
+ def lt(value)
294
+ create_criterion(Criterion::LESS_THAN, value)
295
+ end
296
+
297
+ def le(value)
298
+ create_criterion(Criterion::LESS_THAN_OR_EQUAL, value)
299
+ end
300
+
301
+ def ==(value)
302
+ eq(value)
303
+ end
304
+
305
+ def >(value)
306
+ gt(value)
307
+ end
308
+
309
+ def >=(value)
310
+ ge(value)
311
+ end
312
+
313
+ def <(value)
314
+ lt(value)
315
+ end
316
+
317
+ def <=(value)
318
+ le(value)
319
+ end
320
+
321
+ def to_sql_name
322
+ "#{@adapter.quote_table_name(table_name)}.#{@adapter.quote_column_name(column_name)}"
323
+ end
324
+
325
+ def to_s
326
+ self.to_sql_name
327
+ end
328
+
329
+ protected
330
+
331
+ def create_criterion(operator, value)
332
+ Criterion.new(self, operator, value)
333
+ end
334
+ end
335
+
336
+ # Association can be treated like a Column, except that values should be an
337
+ # instance of the associated class
338
+ class Association < Column
339
+ def association_name
340
+ @column
341
+ end
342
+ end
343
+
344
+ class OneToManyAssociation < Association
345
+ # This is a special case and can only be applied to OneToManyAssociation
346
+ def contains(value)
347
+ create_criterion(Criterion::EQUAL, value)
348
+ end
349
+
350
+ protected
351
+
352
+ # We flip the association round once we know who we are associating with
353
+ # def create_criterion(operator, value)
354
+ # a = OneToManyAssociation.new(value.class, @column)
355
+ # a.create_criterion(operator, self)
356
+ # end
357
+
358
+ def column_name
359
+ "id"
360
+ end
361
+
362
+ def table_name
363
+ @column
364
+ end
365
+ end
366
+
367
+ class ManyToOneAssociation < Association
368
+ def column_name
369
+ "#{@column}_id"
370
+ end
371
+ end
372
+
373
+ # module SqlUtil
374
+ # # RESERVED_WORDS = ["table", "column", "type"]
375
+ # def self.escape_name(name)
376
+ # # if !name =~/[^a-zA-Z0-9_]/
377
+ # # "`#{name}`"
378
+ # # else
379
+ # name
380
+ # # end
381
+ # end
382
+ #
383
+ # def self.value_to_sql(column, v)
384
+ # if column.is_a? Association and v.is_a? ActiveRecord::Base
385
+ # # Special case, we need to extract the object's PK and add make sure the association is in the
386
+ # # criteria
387
+ # v = v.id
388
+ # end
389
+ #
390
+ # if v.is_a? Array
391
+ # "(" + v.collect {|value| value_to_sql(value)}.join(",") + ")"
392
+ # elsif v.is_a? String
393
+ # "'#{v}'"
394
+ # elsif v.is_a? TrueClass
395
+ # "1"
396
+ # elsif v.is_a? FalseClass
397
+ # "0"
398
+ # elsif v.is_a? Time
399
+ # "'#{v.to_s(:db)}'"
400
+ # elsif v.is_a? Symbol
401
+ # "':#{v}'"
402
+ # else
403
+ # v
404
+ # end
405
+ # end
406
+ # end
407
+
408
+
409
+ class Criterion
410
+ attr_accessor :value
411
+ attr_reader :column, :operator
412
+
413
+ NOT_EQUAL = "<>"
414
+ EQUAL = "="
415
+ GREATER_THAN = ">"
416
+ GREATER_THAN_OR_EQUAL = ">="
417
+ LESS_THAN = "<"
418
+ LESS_THAN_OR_EQUAL = "<="
419
+ IN = "IN"
420
+ NOT_IN = "NOT IN"
421
+
422
+ # Create a new criteria. Generally, you will not call this directly, rather create it via
423
+ # the Column instance.
424
+ #
425
+ # The constructor takes a Column instance, an operator string and an optional value
426
+ def initialize(column, operator=nil, value=nil, opts={})
427
+ @column = column
428
+ @operator = operator
429
+ @value = value
430
+ @opts = opts
431
+ end
432
+
433
+ def associations
434
+ if @column.is_a? Association
435
+ [@column.association_name]
436
+ else
437
+ []
438
+ end
439
+ end
440
+
441
+ def not
442
+ NotCriterion.new(self)
443
+ end
444
+
445
+ # AND this with another criterion
446
+ def &(criterion)
447
+ if !criterion.nil?
448
+ c = Criteria.new
449
+ c.and self
450
+ c.and criterion
451
+ c
452
+ end
453
+ end
454
+
455
+ # OR this with another criterion
456
+ def |(criterion)
457
+ if !criterion.nil?
458
+ c = Criteria.new
459
+ c.or self
460
+ c.or criterion
461
+ c
462
+ end
463
+ end
464
+
465
+ # Return a list of columns associated with this criterion, always an array with one element
466
+ def columns
467
+ [@column]
468
+ end
469
+
470
+ # Convert this criterion into WHERE SQL
471
+ def to_where_sql
472
+ "#{@column.to_sql_name} #{@operator} #{@column.quote_value(@value)}"
473
+ end
474
+
475
+ def to_hash
476
+ {
477
+ :include => self.associations,
478
+ :conditions => to_where_sql
479
+ }
480
+ end
481
+
482
+ def to_s
483
+ "#{self.class}(#{to_where_sql})"
484
+ end
485
+
486
+ end
487
+
488
+ class NotCriterion < Criterion
489
+ attr_accessor :criterion
490
+ def initialize(c)
491
+ @criterion = c
492
+ end
493
+
494
+ def not
495
+ @criterion
496
+ end
497
+
498
+ def columns
499
+ @criterion.colums
500
+ end
501
+
502
+ def to_where_sql
503
+ "NOT (#{@criterion.to_where_sql})"
504
+ end
505
+ end
506
+ end
507
+
508
+ class ActiveRecord::Base
509
+ @@one_to_many_associations = []
510
+ @@many_to_one_associations = []
511
+ @@critera_columns = {}
512
+
513
+ # Access the column object by using the class as a hash. This is useful if there are other
514
+ # static methods that have the same name as your field, or the field name is not, for some reason,
515
+ # allowed as a ruby method name
516
+ #
517
+ # Usually, you can just call Class.column_name to access this column object
518
+ def self.[](column)
519
+
520
+ # First check to see if there is a column on this class
521
+ if self.columns.collect {|c| c.name }.include? column.to_s
522
+ col = self.columns.reject {|c| c.name!=column.to_s }.first
523
+ @@critera_columns[column.to_s] = Criteria::Column.new(self, col, self.connection)
524
+
525
+ # No? well lets check if there is an association on this class
526
+ # FIXME: this is very prone to error, we need a way of confirming this
527
+ elsif self.one_to_many_associations.include? column.to_s.intern
528
+ @@critera_columns[column.to_s] = Criteria::OneToManyAssociation.new(self, column, self.connection)
529
+ elsif self.many_to_one_associations.include? column.to_s.intern
530
+ @@critera_columns[column.to_s] = Criteria::ManyToOneAssociation.new(self, column, self.connection)
531
+ end
532
+
533
+ if @@critera_columns.has_key? column.to_s
534
+ @@critera_columns[column.to_s]
535
+ else
536
+ nil
537
+ end
538
+ end
539
+
540
+ def self.many_to_one_associations
541
+ @@many_to_one_associations
542
+ end
543
+
544
+ def self.one_to_many_associations
545
+ @@one_to_many_associations
546
+ end
547
+
548
+ def self.__criteria_method_missing(*a)
549
+ # puts a.inspect
550
+ self[a[0]] || __ar_method_missing(*a)
551
+ end
552
+
553
+ # Provide the ability to search by criteria using the normal find() syntax. Basically we
554
+ # just turn the criteria object into a normal query hash (:conditions, :order, :limit, etc)
555
+ #
556
+ # Since this can come as the first or second argument in the find() method, we just scan for
557
+ # any instances of Criteria and then call to_hash
558
+ def self.__criteria_find(*a)
559
+ a = rewrite_criteria_in_args(a)
560
+ # puts a.inspect
561
+ __ar_find(*a)
562
+ end
563
+
564
+ class << self
565
+ alias_method :__ar_method_missing, :method_missing
566
+ alias_method :method_missing, :__criteria_method_missing
567
+ alias_method :__ar_find, :find
568
+ alias_method :find, :__criteria_find
569
+ end
570
+
571
+ protected
572
+
573
+ def self.rewrite_criteria_in_args(args)
574
+ args.collect do |arg|
575
+ if arg.is_a? Criteria or arg.is_a? Criteria::Criterion
576
+ arg.to_hash
577
+ else
578
+ arg
579
+ end
580
+ end
581
+ end
582
+ end
583
+
584
+
585
+ module ActiveRecord::Associations::ClassMethods
586
+ def __criteria_belongs_to(id, opts={})
587
+ # puts "#{self} belongs to #{id} with #{opts.inspect}"
588
+ self.many_to_one_associations << id
589
+ __ar_belongs_to(id, opts)
590
+ end
591
+
592
+ alias_method :__ar_belongs_to, :belongs_to
593
+ alias_method :belongs_to, :__criteria_belongs_to
594
+
595
+ def __criteria_has_many(id, opts={}, &block)
596
+ # puts "#{self} has many #{id} with #{opts.inspect}"
597
+ self.one_to_many_associations << id
598
+ __ar_has_many(id, opts, &block)
599
+ end
600
+
601
+ alias_method :__ar_has_many, :has_many
602
+ alias_method :has_many, :__criteria_has_many
603
+ end
@@ -0,0 +1,25 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/criteria.rb'
3
+ require 'test/mock_classes.rb'
4
+
5
+ class ActiveRecordTest < Test::Unit::TestCase
6
+
7
+ def test_find
8
+ u = User.find(1)
9
+ c = (User.email == "test@example.com") & (User.password=="password")
10
+ c.order_by << User.email.asc
11
+ # c.select << User.email
12
+ # c.select << User.password
13
+ # c.select << User.role
14
+ c.limit = 1
15
+ c.offset = 0
16
+ # puts c.to_hash.inspect
17
+ # puts c.to_hash.inspect
18
+ # puts User.find_by_email_and_password("test@example.com", "password")
19
+ u2 = User.find(:first, c)
20
+ assert_equal u.email, u2.email
21
+ assert_equal u.password, u2.password
22
+ assert_equal u.id, u2.id
23
+ end
24
+
25
+ end
@@ -0,0 +1,40 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/criteria.rb'
3
+ require 'test/mock_classes.rb'
4
+
5
+ class AssociationTest < Test::Unit::TestCase
6
+
7
+ def test_many_to_one
8
+ u = User.find(1)
9
+ c = Receipt.user.eq(u)
10
+ assert c.column.is_a? Criteria::ManyToOneAssociation
11
+ assert_equal "receipts.\"user_id\" = 1", c.to_where_sql
12
+ assert c.associations.include? :user
13
+
14
+ r = Receipt.find(:all, c)
15
+ assert_equal 2, r.size
16
+ assert_equal Receipt.find(1), r[0]
17
+ assert_equal Receipt.find(2), r[1]
18
+ end
19
+
20
+ def test_one_to_many
21
+ r = Receipt.find(1)
22
+ c = User.receipts.contains(r)
23
+
24
+ assert c.column.is_a? Criteria::OneToManyAssociation
25
+ assert_equal "receipts.\"id\" = 1", c.to_where_sql
26
+ puts c.associations.inspect
27
+ assert c.associations.include? :receipts
28
+
29
+ u = User.find(:first, c)
30
+ assert_equal User.find(1), u
31
+ assert u.receipts.include? r
32
+ end
33
+
34
+ # If we perform a query from the User context, then the associations should be named from
35
+ # the user's perspective
36
+ def test_context
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,36 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/criteria.rb'
3
+ require 'test/mock_classes.rb'
4
+
5
+ class ColumnTest < Test::Unit::TestCase
6
+
7
+ def test_create_from_symbols
8
+ c = Criteria::Column.new(:some_table, :a_column)
9
+ assert_equal :some_table, c.table_name
10
+ assert_equal :a_column, c.column_name
11
+ end
12
+
13
+ def test_create_from_strings
14
+ c = Criteria::Column.new("some_table", "a_column")
15
+ assert_equal :some_table, c.table_name
16
+ assert_equal :a_column, c.column_name
17
+ end
18
+
19
+ def test_create_from_class
20
+ c = Criteria::Column.new(SomeTable, "a_column")
21
+ assert_equal :some_table, c.table_name
22
+ assert_equal :a_column, c.column_name
23
+ end
24
+
25
+ def test_escaped
26
+ c = Criteria::Column.new(SomeTable, "a_column")
27
+ assert_equal quote("some_table.a_column"), c.to_sql_name
28
+ end
29
+
30
+ class SomeTable
31
+ def self.table_name
32
+ "some_table"
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,83 @@
1
+ require "test/unit"
2
+ require File.dirname(__FILE__) + "/../lib/criteria.rb"
3
+ require "test/mock_classes.rb"
4
+
5
+ class CriteriaTest < Test::Unit::TestCase
6
+
7
+ def test_order_by
8
+ c = Criteria.new
9
+ assert_equal nil, c.to_order_by_sql
10
+ c.order_by << User.email
11
+ assert_equal "#{quote("users.email")}", c.to_order_by_sql
12
+ c.order_by << User.password
13
+ assert_equal "#{quote("users.email")},#{quote("users.password")}", c.to_order_by_sql
14
+ c.order_by << :id
15
+ assert_equal "#{quote("users.email")},#{quote("users.password")},id", c.to_order_by_sql
16
+ end
17
+
18
+ def test_group_by
19
+ c = Criteria.new
20
+ assert_equal nil, c.to_group_by_sql
21
+ c.group_by << User.email
22
+ assert_equal "#{quote("users.email")}", c.to_group_by_sql
23
+ c.group_by << User.password
24
+ assert_equal "#{quote("users.email")},#{quote("users.password")}", c.to_group_by_sql
25
+ c.group_by << :id
26
+ assert_equal "#{quote("users.email")},#{quote("users.password")},id", c.to_group_by_sql
27
+ end
28
+
29
+ def test_select
30
+ c = Criteria.new
31
+ c.select << User.email
32
+ c.select << :id
33
+ c.select << "password"
34
+
35
+ assert_equal "#{quote("users.email")},id,password", c.to_select_sql
36
+ end
37
+
38
+ def test_include
39
+ c = Criteria.new
40
+ c.and User.email.eq("test@example.com")
41
+ c.and User.password.eq("blah")
42
+ assert_equal "#{quote("users.email")}", c.columns[0].to_s
43
+ assert_equal "#{quote("users.password")}", c.columns[1].to_s
44
+ assert_equal [:users], c.tables
45
+ c.and Criteria::Column.new("another_table", "foo").eq("bar")
46
+
47
+ assert_equal "#{quote("users.email")}", c.columns[0].to_s
48
+ assert_equal "#{quote("users.password")}", c.columns[1].to_s
49
+ assert_equal "#{quote("another_table.foo")}", c.columns[2].to_s
50
+ assert_equal [:users, :another_table], c.tables
51
+ end
52
+
53
+ def test_limit
54
+ c = Criteria.new
55
+ assert_equal nil, c.limit
56
+ assert_equal nil, c.to_hash[:limit]
57
+ c.limit = 1
58
+ assert_equal 1, c.limit
59
+ assert_equal 1, c.to_hash[:limit]
60
+ c.limit = 1000
61
+ assert_equal 1000, c.limit
62
+ assert_equal 1000, c.to_hash[:limit]
63
+ c.limit = nil
64
+ assert_equal nil, c.limit
65
+ assert_equal nil, c.to_hash[:limit]
66
+ end
67
+
68
+ def test_offset
69
+ c = Criteria.new
70
+ assert_equal nil, c.offset
71
+ assert_equal nil, c.to_hash[:offset]
72
+ c.offset = 1
73
+ assert_equal 1, c.offset
74
+ assert_equal 1, c.to_hash[:offset]
75
+ c.offset = 1000
76
+ assert_equal 1000, c.offset
77
+ assert_equal 1000, c.to_hash[:offset]
78
+ c.offset = nil
79
+ assert_equal nil, c.offset
80
+ assert_equal nil, c.to_hash[:offset]
81
+ end
82
+
83
+ end
@@ -0,0 +1,83 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/criteria.rb'
3
+ require 'test/mock_classes.rb'
4
+
5
+ class CriterionTest < Test::Unit::TestCase
6
+
7
+ def test_eq
8
+ a = User.email == "test@example.com"
9
+ b = User.email.eq("test@example.com")
10
+ assert_equal quote("users.email") + " = 'test@example.com'", a.to_where_sql
11
+ assert_equal quote("users.email") + " = 'test@example.com'", b.to_where_sql
12
+ end
13
+
14
+ def test_ne
15
+ a = User.email.ne("test@example.com")
16
+ assert_equal quote("users.email") + " <> 'test@example.com'", a.to_where_sql
17
+ end
18
+
19
+ def test_not
20
+ a = User.email.eq("test@example.com")
21
+ b = a.not
22
+ c = b.not
23
+ assert_equal quote("users.email") + " = 'test@example.com'", a.to_where_sql
24
+ assert_equal "NOT (#{quote("users.email")} = 'test@example.com')", b.to_where_sql
25
+ assert_equal quote("users.email") + " = 'test@example.com'", c.to_where_sql
26
+ end
27
+
28
+ def test_time
29
+ time = Time.now
30
+ a = User.created_at > time
31
+ assert_equal quote("users.created_at") + " > '#{time.to_s(:db)}'", a.to_where_sql
32
+ end
33
+
34
+ def test_symbol
35
+ a = User.role == :admin
36
+ assert_equal "#{quote("users.role")} = '--- :admin\n'", a.to_where_sql
37
+ end
38
+
39
+ def test_string
40
+ a = User.role == "admin"
41
+ assert_equal "#{quote("users.role")} = 'admin'", a.to_where_sql
42
+ end
43
+
44
+ def test_float
45
+ a = User.number > 1.3122
46
+ assert_equal "#{quote("users.number")} > 1.3122", a.to_where_sql
47
+ end
48
+
49
+ def test_int
50
+ a = User.number > 1234
51
+ assert_equal "#{quote("users.number")} > 1234", a.to_where_sql
52
+ end
53
+
54
+ def test_nested
55
+ a = User.email == "test@example.com"
56
+ b = User.password == "secure password"
57
+ c = User.active == true
58
+ d = ((User.email == "test@example.com") | (User.password == "secure password")) & (User.active == true)
59
+ assert d.is_a?(Criteria)
60
+ assert_equal "((#{quote("users.email")} = 'test@example.com' OR #{quote("users.password")} = 'secure password') AND #{quote("users.active")} = 't')", d.to_where_sql
61
+ assert_equal ((a|b)&c).to_where_sql, d.to_where_sql
62
+ end
63
+
64
+ def test_or
65
+ a = User.email == "test@example.com"
66
+ b = User.password == "secure password"
67
+ c = (User.email == "test@example.com") | (User.password == "secure password")
68
+ assert c.is_a?(Criteria)
69
+ assert_equal "(#{quote("users.email")} = 'test@example.com' OR #{quote("users.password")} = 'secure password')", c.to_where_sql
70
+ # assert_equal "(users.email = 'test@example.com' OR users.password = 'secure password')", c.to_where_sql
71
+ assert_equal (a|b).to_where_sql, c.to_where_sql
72
+ end
73
+
74
+ def test_and
75
+ a = User.email == "test@example.com"
76
+ b = User.password == "secure password"
77
+ c = (User.email == "test@example.com") & (User.password == "secure password")
78
+ assert c.is_a?(Criteria)
79
+ assert_equal "(#{quote("users.email")} = 'test@example.com' AND #{quote("users.password")} = 'secure password')", c.to_where_sql
80
+ # assert_equal "(users.email = 'test@example.com' AND users.password = 'secure password')", c.to_where_sql
81
+ assert_equal (a&b).to_where_sql, c.to_where_sql
82
+ end
83
+ end
@@ -0,0 +1,46 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/criteria.rb'
3
+ require 'test/mock_classes.rb'
4
+
5
+ # Test examples given in docs
6
+ class ExamplesTest < Test::Unit::TestCase
7
+ TIME1=10.hours.ago
8
+ TIME2=20.days.ago
9
+ EXPECTED_HASH = {
10
+ :select => nil,
11
+ :order => nil,
12
+ :group => nil,
13
+ :include => [],
14
+ :offset => nil,
15
+ :limit => nil,
16
+ :conditions => "((users.\"role\" = '--- :admin\n' OR users.\"active\" = 'f' OR users.\"created_at\" > '#{TIME2.to_s(:db)}') AND (users.\"role\" = '--- :editor\n' OR users.\"active\" = 't' OR (users.\"created_at\" > '#{TIME1.to_s(:db)}' AND users.\"created_at\" < '#{TIME2.to_s(:db)}')))"
17
+ }
18
+ def test_big_terse
19
+ a = (User.role == :admin) | (User.active == false) | (User.created_at > TIME2)
20
+ b = (User.role==:editor) | (User.active==true)
21
+ b|= ((User.created_at>TIME1) & (User.created_at<TIME2))
22
+ assert_equal EXPECTED_HASH, (a&b).to_hash
23
+ end
24
+
25
+ def test_big_full
26
+ criteria = Criteria.new
27
+ criteria.and do |c|
28
+ c.or User.role.eq(:admin)
29
+ c.or User.active.eq(false)
30
+ c.or User.created_at.gt(TIME2)
31
+ end
32
+ criteria.and do |c|
33
+ c.or User.role.eq(:editor)
34
+ c.or User.active.eq(true)
35
+ c.or do |c2|
36
+ c2.and User.created_at.gt(TIME1)
37
+ c2.and User.created_at.lt(TIME2)
38
+ end
39
+ end
40
+
41
+ assert_equal EXPECTED_HASH, criteria.to_hash
42
+ EXPECTED_HASH.each do |k,v|
43
+ assert_equal EXPECTED_HASH[k], criteria.to_hash[k]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/criteria.rb'
3
+ require 'test/mock_classes.rb'
4
+
5
+ class OrderTest < Test::Unit::TestCase
6
+
7
+ def test_order_asc
8
+ a = User.email.asc
9
+ assert_equal "#{quote("users.email")} ASC", a.to_s
10
+ assert_equal "ASC", a.dir
11
+ assert_equal User.email, a.column
12
+ end
13
+
14
+ def test_order_desc
15
+ a = User.email.desc
16
+ assert_equal "#{quote("users.email")} DESC", a.to_s
17
+ assert_equal "DESC", a.dir
18
+ assert_equal User.email, a.column
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ require 'test/test_criterion.rb'
3
+ require 'test/test_criteria.rb'
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: criteria
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2008-04-08 00:00:00 +10:00
8
+ summary: Object-orientated Criteria for ActiveRecord
9
+ require_paths:
10
+ - lib
11
+ email: ray@wirestorm.net
12
+ homepage: http://criteria.rubyforge.org
13
+ rubyforge_project:
14
+ description: An object-orientated approach to assembling complex criteria for querying ActiveRecord.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Ray Hilton
31
+ files:
32
+ - lib/criteria.rb
33
+ - README
34
+ test_files:
35
+ - test/test_active_record.rb
36
+ - test/test_associations.rb
37
+ - test/test_column.rb
38
+ - test/test_criteria.rb
39
+ - test/test_criterion.rb
40
+ - test/test_examples.rb
41
+ - test/test_order.rb
42
+ - test/test_suite.rb
43
+ rdoc_options: []
44
+
45
+ extra_rdoc_files:
46
+ - README
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ requirements: []
52
+
53
+ dependencies:
54
+ - !ruby/object:Gem::Dependency
55
+ name: activerecord
56
+ version_requirement:
57
+ version_requirements: !ruby/object:Gem::Version::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.0
62
+ version: