criteria 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +101 -0
- data/lib/criteria.rb +603 -0
- data/test/test_active_record.rb +25 -0
- data/test/test_associations.rb +40 -0
- data/test/test_column.rb +36 -0
- data/test/test_criteria.rb +83 -0
- data/test/test_criterion.rb +83 -0
- data/test/test_examples.rb +46 -0
- data/test/test_order.rb +20 -0
- data/test/test_suite.rb +3 -0
- metadata +62 -0
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
|
data/test/test_column.rb
ADDED
@@ -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
|
data/test/test_order.rb
ADDED
@@ -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
|
data/test/test_suite.rb
ADDED
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:
|