lotus-model 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +6 -0
  4. data/.yardopts +5 -0
  5. data/EXAMPLE.md +217 -0
  6. data/Gemfile +14 -2
  7. data/README.md +303 -3
  8. data/Rakefile +17 -1
  9. data/lib/lotus-model.rb +1 -0
  10. data/lib/lotus/entity.rb +157 -0
  11. data/lib/lotus/model.rb +23 -2
  12. data/lib/lotus/model/adapters/abstract.rb +167 -0
  13. data/lib/lotus/model/adapters/implementation.rb +111 -0
  14. data/lib/lotus/model/adapters/memory/collection.rb +132 -0
  15. data/lib/lotus/model/adapters/memory/command.rb +90 -0
  16. data/lib/lotus/model/adapters/memory/query.rb +457 -0
  17. data/lib/lotus/model/adapters/memory_adapter.rb +149 -0
  18. data/lib/lotus/model/adapters/sql/collection.rb +209 -0
  19. data/lib/lotus/model/adapters/sql/command.rb +67 -0
  20. data/lib/lotus/model/adapters/sql/query.rb +615 -0
  21. data/lib/lotus/model/adapters/sql_adapter.rb +154 -0
  22. data/lib/lotus/model/mapper.rb +101 -0
  23. data/lib/lotus/model/mapping.rb +23 -0
  24. data/lib/lotus/model/mapping/coercer.rb +80 -0
  25. data/lib/lotus/model/mapping/collection.rb +336 -0
  26. data/lib/lotus/model/version.rb +4 -1
  27. data/lib/lotus/repository.rb +620 -0
  28. data/lotus-model.gemspec +15 -11
  29. data/test/entity_test.rb +126 -0
  30. data/test/fixtures.rb +81 -0
  31. data/test/model/adapters/abstract_test.rb +75 -0
  32. data/test/model/adapters/implementation_test.rb +22 -0
  33. data/test/model/adapters/memory/query_test.rb +91 -0
  34. data/test/model/adapters/memory_adapter_test.rb +1044 -0
  35. data/test/model/adapters/sql/query_test.rb +121 -0
  36. data/test/model/adapters/sql_adapter_test.rb +1078 -0
  37. data/test/model/mapper_test.rb +94 -0
  38. data/test/model/mapping/coercer_test.rb +27 -0
  39. data/test/model/mapping/collection_test.rb +82 -0
  40. data/test/repository_test.rb +283 -0
  41. data/test/test_helper.rb +30 -0
  42. data/test/version_test.rb +7 -0
  43. metadata +109 -11
@@ -0,0 +1,149 @@
1
+ require 'lotus/model/adapters/abstract'
2
+ require 'lotus/model/adapters/implementation'
3
+ require 'lotus/model/adapters/memory/collection'
4
+ require 'lotus/model/adapters/memory/command'
5
+ require 'lotus/model/adapters/memory/query'
6
+
7
+ module Lotus
8
+ module Model
9
+ module Adapters
10
+ # In memory adapter that behaves like a SQL database.
11
+ # Not all the features of the SQL adapter are supported.
12
+ #
13
+ # This adapter SHOULD be used only for development or testing purposes,
14
+ # because its computations are inefficient and the data is volatile.
15
+ #
16
+ # @see Lotus::Model::Adapters::Implementation
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ class MemoryAdapter < Abstract
21
+ include Implementation
22
+
23
+ # Initialize the adapter.
24
+ #
25
+ # @param mapper [Object] the database mapper
26
+ # @param uri [String] the connection uri (ignored)
27
+ #
28
+ # @return [Lotus::Model::Adapters::MemoryAdapter]
29
+ #
30
+ # @see Lotus::Model::Mapper
31
+ #
32
+ # @api private
33
+ # @since 0.1.0
34
+ def initialize(mapper, uri = nil)
35
+ super
36
+
37
+ @mutex = Mutex.new
38
+ @collections = {}
39
+ end
40
+
41
+ # Creates a record in the database for the given entity.
42
+ # It assigns the `id` attribute, in case of success.
43
+ #
44
+ # @param collection [Symbol] the target collection (it must be mapped).
45
+ # @param entity [#id=] the entity to create
46
+ #
47
+ # @return [Object] the entity
48
+ #
49
+ # @api private
50
+ # @since 0.1.0
51
+ def create(collection, entity)
52
+ @mutex.synchronize do
53
+ entity.id = command(collection).create(entity)
54
+ entity
55
+ end
56
+ end
57
+
58
+ # Updates a record in the database corresponding to the given entity.
59
+ #
60
+ # @param collection [Symbol] the target collection (it must be mapped).
61
+ # @param entity [#id] the entity to update
62
+ #
63
+ # @return [Object] the entity
64
+ #
65
+ # @api private
66
+ # @since 0.1.0
67
+ def update(collection, entity)
68
+ @mutex.synchronize do
69
+ command(collection).update(entity)
70
+ end
71
+ end
72
+
73
+ # Deletes a record in the database corresponding to the given entity.
74
+ #
75
+ # @param collection [Symbol] the target collection (it must be mapped).
76
+ # @param entity [#id] the entity to delete
77
+ #
78
+ # @api private
79
+ # @since 0.1.0
80
+ def delete(collection, entity)
81
+ @mutex.synchronize do
82
+ command(collection).delete(entity)
83
+ end
84
+ end
85
+
86
+ # Deletes all the records from the given collection and resets the
87
+ # identity counter.
88
+ #
89
+ # @param collection [Symbol] the target collection (it must be mapped).
90
+ #
91
+ # @api private
92
+ # @since 0.1.0
93
+ def clear(collection)
94
+ @mutex.synchronize do
95
+ command(collection).clear
96
+ end
97
+ end
98
+
99
+ # Fabricates a command for the given query.
100
+ #
101
+ # @param collection [Symbol] the collection name (it must be mapped)
102
+ #
103
+ # @return [Lotus::Model::Adapters::Memory::Command]
104
+ #
105
+ # @see Lotus::Model::Adapters::Memory::Command
106
+ #
107
+ # @api private
108
+ # @since 0.1.0
109
+ def command(collection)
110
+ Memory::Command.new(_collection(collection), _mapped_collection(collection))
111
+ end
112
+
113
+ # Fabricates a query
114
+ #
115
+ # @param collection [Symbol] the target collection (it must be mapped).
116
+ # @param blk [Proc] a block of code to be executed in the context of
117
+ # the query.
118
+ #
119
+ # @return [Lotus::Model::Adapters::Memory::Query]
120
+ #
121
+ # @see Lotus::Model::Adapters::Memory::Query
122
+ #
123
+ # @api private
124
+ # @since 0.1.0
125
+ def query(collection, context = nil, &blk)
126
+ @mutex.synchronize do
127
+ Memory::Query.new(_collection(collection), _mapped_collection(collection), &blk)
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ # Returns a collection from the given name.
134
+ #
135
+ # @param name [Symbol] a name of the collection (it must be mapped).
136
+ #
137
+ # @return [Lotus::Model::Adapters::Memory::Collection]
138
+ #
139
+ # @see Lotus::Model::Adapters::Memory::Collection
140
+ #
141
+ # @api private
142
+ # @since 0.1.0
143
+ def _collection(name)
144
+ @collections[name] ||= Memory::Collection.new(name, _identity(name))
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,209 @@
1
+ require 'delegate'
2
+ require 'lotus/utils/kernel' unless RUBY_VERSION >= '2.1'
3
+
4
+ module Lotus
5
+ module Model
6
+ module Adapters
7
+ module Sql
8
+ # Maps a SQL database table and perfoms manipulations on it.
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ #
13
+ # @see http://sequel.jeremyevans.net/rdoc/files/doc/dataset_basics_rdoc.html
14
+ # @see http://sequel.jeremyevans.net/rdoc/files/doc/dataset_filtering_rdoc.html
15
+ class Collection < SimpleDelegator
16
+ # Initialize a collection
17
+ #
18
+ # @param dataset [Sequel::Dataset] the dataset that maps a table or a
19
+ # subset of it.
20
+ # @param collection [Lotus::Model::Mapping::Collection] a mapped
21
+ # collection
22
+ #
23
+ # @return [Lotus::Model::Adapters::Sql::Collection]
24
+ #
25
+ # @api private
26
+ # @since 0.1.0
27
+ def initialize(dataset, collection)
28
+ super(dataset)
29
+ @collection = collection
30
+ end
31
+
32
+ # Filters the current scope with an `exclude` directive.
33
+ #
34
+ # @param args [Array] the array of arguments
35
+ #
36
+ # @see Lotus::Model::Adapters::Sql::Query#exclude
37
+ #
38
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
39
+ # collection
40
+ #
41
+ # @api private
42
+ # @since 0.1.0
43
+ def exclude(*args)
44
+ Collection.new(super, @collection)
45
+ end
46
+
47
+ # Creates a record for the given entity and assigns an id.
48
+ #
49
+ # @param entity [Object] the entity to persist
50
+ #
51
+ # @see Lotus::Model::Adapters::Sql::Command#create
52
+ #
53
+ # @return the primary key of the created record
54
+ #
55
+ # @api private
56
+ # @since 0.1.0
57
+ def insert(entity)
58
+ super _serialize(entity)
59
+ end
60
+
61
+ # Filters the current scope with an `limit` directive.
62
+ #
63
+ # @param args [Array] the array of arguments
64
+ #
65
+ # @see Lotus::Model::Adapters::Sql::Query#limit
66
+ #
67
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
68
+ # collection
69
+ #
70
+ # @api private
71
+ # @since 0.1.0
72
+ def limit(*args)
73
+ Collection.new(super, @collection)
74
+ end
75
+
76
+ # Filters the current scope with an `offset` directive.
77
+ #
78
+ # @param args [Array] the array of arguments
79
+ #
80
+ # @see Lotus::Model::Adapters::Sql::Query#offset
81
+ #
82
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
83
+ # collection
84
+ #
85
+ # @api private
86
+ # @since 0.1.0
87
+ def offset(*args)
88
+ Collection.new(super, @collection)
89
+ end
90
+
91
+ # Filters the current scope with an `or` directive.
92
+ #
93
+ # @param args [Array] the array of arguments
94
+ #
95
+ # @see Lotus::Model::Adapters::Sql::Query#or
96
+ #
97
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
98
+ # collection
99
+ #
100
+ # @api private
101
+ # @since 0.1.0
102
+ def or(*args)
103
+ Collection.new(super, @collection)
104
+ end
105
+
106
+ # Filters the current scope with an `order` directive.
107
+ #
108
+ # @param args [Array] the array of arguments
109
+ #
110
+ # @see Lotus::Model::Adapters::Sql::Query#order
111
+ #
112
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
113
+ # collection
114
+ #
115
+ # @api private
116
+ # @since 0.1.0
117
+ def order(*args)
118
+ Collection.new(super, @collection)
119
+ end
120
+
121
+ # Filters the current scope with an `order` directive.
122
+ #
123
+ # @param args [Array] the array of arguments
124
+ #
125
+ # @see Lotus::Model::Adapters::Sql::Query#order
126
+ #
127
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
128
+ # collection
129
+ #
130
+ # @api private
131
+ # @since 0.1.0
132
+ def order_more(*args)
133
+ Collection.new(super, @collection)
134
+ end
135
+
136
+ # Filters the current scope with an `select` directive.
137
+ #
138
+ # @param args [Array] the array of arguments
139
+ #
140
+ # @see Lotus::Model::Adapters::Sql::Query#select
141
+ #
142
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
143
+ # collection
144
+ #
145
+ # @api private
146
+ # @since 0.1.0
147
+ if RUBY_VERSION >= '2.1'
148
+ def select(*args)
149
+ Collection.new(super, @collection)
150
+ end
151
+ else
152
+ def select(*args)
153
+ Collection.new(__getobj__.select(*Lotus::Utils::Kernel.Array(args)), @collection)
154
+ end
155
+ end
156
+
157
+ # Filters the current scope with an `where` directive.
158
+ #
159
+ # @param args [Array] the array of arguments
160
+ #
161
+ # @see Lotus::Model::Adapters::Sql::Query#where
162
+ #
163
+ # @return [Lotus::Model::Adapters::Sql::Collection] the filtered
164
+ # collection
165
+ #
166
+ # @api private
167
+ # @since 0.1.0
168
+ def where(*args)
169
+ Collection.new(super, @collection)
170
+ end
171
+
172
+ # Updates the record corresponding to the given entity.
173
+ #
174
+ # @param entity [Object] the entity to persist
175
+ #
176
+ # @see Lotus::Model::Adapters::Sql::Command#update
177
+ #
178
+ # @api private
179
+ # @since 0.1.0
180
+ def update(entity)
181
+ super _serialize(entity)
182
+ end
183
+
184
+ # Resolves self by fetching the records from the database and
185
+ # translating them into entities.
186
+ #
187
+ # @return [Array] the result of the query
188
+ #
189
+ # @api private
190
+ # @since 0.1.0
191
+ def to_a
192
+ @collection.deserialize(self)
193
+ end
194
+
195
+ private
196
+ # Serialize the given entity before to persist in the database.
197
+ #
198
+ # @return [Hash] the serialized entity
199
+ #
200
+ # @api private
201
+ # @since 0.1.0
202
+ def _serialize(entity)
203
+ @collection.serialize(entity)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,67 @@
1
+ module Lotus
2
+ module Model
3
+ module Adapters
4
+ module Sql
5
+ # Execute a command for the given query.
6
+ #
7
+ # @see Lotus::Model::Adapters::Sql::Query
8
+ #
9
+ # @api private
10
+ # @since 0.1.0
11
+ class Command
12
+ # Initialize a command
13
+ #
14
+ # @param query [Lotus::Model::Adapters::Sql::Query]
15
+ #
16
+ # @api private
17
+ # @since 0.1.0
18
+ def initialize(query)
19
+ @collection = query.scoped
20
+ end
21
+
22
+ # Creates a record for the given entity.
23
+ #
24
+ # @param entity [Object] the entity to persist
25
+ #
26
+ # @see Lotus::Model::Adapters::Sql::Collection#insert
27
+ #
28
+ # @return the primary key of the just created record.
29
+ #
30
+ # @api private
31
+ # @since 0.1.0
32
+ def create(entity)
33
+ @collection.insert(entity)
34
+ end
35
+
36
+ # Updates the corresponding record for the given entity.
37
+ #
38
+ # @param entity [Object] the entity to persist
39
+ #
40
+ # @see Lotus::Model::Adapters::Sql::Collection#update
41
+ #
42
+ # @api private
43
+ # @since 0.1.0
44
+ def update(entity)
45
+ @collection.update(entity)
46
+ end
47
+
48
+ # Deletes all the records for the current query.
49
+ #
50
+ # It's used to delete a single record or an entire database table.
51
+ #
52
+ # @see Lotus::Model::Adapters::SqlAdapter#delete
53
+ # @see Lotus::Model::Adapters::SqlAdapter#clear
54
+ #
55
+ # @api private
56
+ # @since 0.1.0
57
+ def delete
58
+ @collection.delete
59
+ end
60
+
61
+ alias_method :clear, :delete
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,615 @@
1
+ require 'forwardable'
2
+ require 'lotus/utils/kernel'
3
+
4
+ module Lotus
5
+ module Model
6
+ module Adapters
7
+ module Sql
8
+ # Query the database with a powerful API.
9
+ #
10
+ # All the methods are chainable, it allows advanced composition of
11
+ # SQL conditions.
12
+ #
13
+ # This works as a lazy filtering mechanism: the records are fetched from
14
+ # the database only when needed.
15
+ #
16
+ # @example
17
+ #
18
+ # query.where(language: 'ruby')
19
+ # .and(framework: 'lotus')
20
+ # .desc(:users_count).all
21
+ #
22
+ # # the records are fetched only when we invoke #all
23
+ #
24
+ # It implements Ruby's `Enumerable` and borrows some methods from `Array`.
25
+ # Expect a query to act like them.
26
+ #
27
+ # @since 0.1.0
28
+ class Query
29
+ # Define negations for operators.
30
+ #
31
+ # @see Lotus::Model::Adapters::Sql::Query#negate!
32
+ #
33
+ # @api private
34
+ # @since 0.1.0
35
+ OPERATORS_MAPPING = {
36
+ where: :exclude,
37
+ exclude: :where
38
+ }.freeze
39
+
40
+ include Enumerable
41
+ extend Forwardable
42
+
43
+ def_delegators :all, :each, :to_s, :empty?
44
+
45
+ # @attr_reader conditions [Array] an accumulator for the called
46
+ # methods
47
+ #
48
+ # @since 0.1.0
49
+ # @api private
50
+ attr_reader :conditions
51
+
52
+ # Initialize a query
53
+ #
54
+ # @param collection [Lotus::Model::Adapters::Sql::Collection] the
55
+ # collection to query
56
+ #
57
+ # @param blk [Proc] an optional block that gets yielded in the
58
+ # context of the current query
59
+ #
60
+ # @return [Lotus::Model::Adapters::Sql::Query]
61
+ def initialize(collection, context = nil, &blk)
62
+ @collection, @context = collection, context
63
+ @conditions = []
64
+
65
+ instance_eval(&blk) if block_given?
66
+ end
67
+
68
+ # Resolves the query by fetching records from the database and
69
+ # translating them into entities.
70
+ #
71
+ # @return [Array] a collection of entities
72
+ #
73
+ # @since 0.1.0
74
+ def all
75
+ Lotus::Utils::Kernel.Array(run)
76
+ end
77
+
78
+ # Adds a SQL `WHERE` condition.
79
+ #
80
+ # It accepts a `Hash` with only one pair.
81
+ # The key must be the name of the column expressed as a `Symbol`.
82
+ # The value is the one used by the SQL query
83
+ #
84
+ # @param condition [Hash]
85
+ #
86
+ # @return self
87
+ #
88
+ # @since 0.1.0
89
+ #
90
+ # @example Fixed value
91
+ #
92
+ # query.where(language: 'ruby')
93
+ #
94
+ # # => SELECT * FROM `projects` WHERE (`language` = 'ruby')
95
+ #
96
+ # @example Array
97
+ #
98
+ # query.where(id: [1, 3])
99
+ #
100
+ # # => SELECT * FROM `articles` WHERE (`id` IN (1, 3))
101
+ #
102
+ # @example Range
103
+ #
104
+ # query.where(year: 1900..1982)
105
+ #
106
+ # # => SELECT * FROM `people` WHERE ((`year` >= 1900) AND (`year` <= 1982))
107
+ #
108
+ # @example Multiple conditions
109
+ #
110
+ # query.where(language: 'ruby')
111
+ # .where(framework: 'lotus')
112
+ #
113
+ # # => SELECT * FROM `projects` WHERE (`language` = 'ruby') AND (`framework` = 'lotus')
114
+ def where(condition)
115
+ conditions.push([:where, condition])
116
+ self
117
+ end
118
+
119
+ alias_method :and, :where
120
+
121
+ # Adds a SQL `OR` condition.
122
+ #
123
+ # It accepts a `Hash` with only one pair.
124
+ # The key must be the name of the column expressed as a `Symbol`.
125
+ # The value is the one used by the SQL query
126
+ #
127
+ # This condition will be ignored if not used with WHERE.
128
+ #
129
+ # @param condition [Hash]
130
+ #
131
+ # @return self
132
+ #
133
+ # @since 0.1.0
134
+ #
135
+ # @example Fixed value
136
+ #
137
+ # query.where(language: 'ruby').or(framework: 'lotus')
138
+ #
139
+ # # => SELECT * FROM `projects` WHERE ((`language` = 'ruby') OR (`framework` = 'lotus'))
140
+ #
141
+ # @example Array
142
+ #
143
+ # query.where(id: 1).or(author_id: [15, 23])
144
+ #
145
+ # # => SELECT * FROM `articles` WHERE ((`id` = 1) OR (`author_id` IN (15, 23)))
146
+ #
147
+ # @example Range
148
+ #
149
+ # query.where(country: 'italy').or(year: 1900..1982)
150
+ #
151
+ # # => SELECT * FROM `people` WHERE ((`country` = 'italy') OR ((`year` >= 1900) AND (`year` <= 1982)))
152
+ def or(condition)
153
+ conditions.push([:or, condition])
154
+ self
155
+ end
156
+
157
+ # Logical negation of a WHERE condition.
158
+ #
159
+ # It accepts a `Hash` with only one pair.
160
+ # The key must be the name of the column expressed as a `Symbol`.
161
+ # The value is the one used by the SQL query
162
+ #
163
+ # @param condition [Hash]
164
+ #
165
+ # @since 0.1.0
166
+ #
167
+ # @return self
168
+ #
169
+ # @example Fixed value
170
+ #
171
+ # query.exclude(language: 'java')
172
+ #
173
+ # # => SELECT * FROM `projects` WHERE (`language` != 'java')
174
+ #
175
+ # @example Array
176
+ #
177
+ # query.exclude(id: [4, 9])
178
+ #
179
+ # # => SELECT * FROM `articles` WHERE (`id` NOT IN (1, 3))
180
+ #
181
+ # @example Range
182
+ #
183
+ # query.where(year: 1900..1982)
184
+ #
185
+ # # => SELECT * FROM `people` WHERE ((`year` < 1900) AND (`year` > 1982))
186
+ #
187
+ # @example Multiple conditions
188
+ #
189
+ # query.where(language: 'java')
190
+ # .where(company: 'enterprise')
191
+ #
192
+ # # => SELECT * FROM `projects` WHERE (`language` != 'java') AND (`company` != 'enterprise')
193
+ def exclude(condition)
194
+ conditions.push([:exclude, condition])
195
+ self
196
+ end
197
+
198
+ alias_method :not, :exclude
199
+
200
+ # Select only the specified columns.
201
+ #
202
+ # By default a query selects all the columns of a table (`SELECT *`).
203
+ #
204
+ # @param columns [Array<Symbol>]
205
+ #
206
+ # @return self
207
+ #
208
+ # @since 0.1.0
209
+ #
210
+ # @example Single column
211
+ #
212
+ # query.select(:name)
213
+ #
214
+ # # => SELECT `name` FROM `people`
215
+ #
216
+ # @example Multiple columns
217
+ #
218
+ # query.select(:name, :year)
219
+ #
220
+ # # => SELECT `name`, `year` FROM `people`
221
+ def select(*columns)
222
+ conditions.push([:select, *columns])
223
+ self
224
+ end
225
+
226
+ # Limit the number of records to return.
227
+ #
228
+ # This operation is performed at the database level with `LIMIT`.
229
+ #
230
+ # @param number [Fixnum]
231
+ #
232
+ # @return self
233
+ #
234
+ # @since 0.1.0
235
+ #
236
+ # @example
237
+ #
238
+ # query.limit(1)
239
+ #
240
+ # # => SELECT * FROM `people` LIMIT 1
241
+ def limit(number)
242
+ conditions.push([:limit, number])
243
+ self
244
+ end
245
+
246
+ # Specify an `OFFSET` clause.
247
+ #
248
+ # Due to SQL syntax restriction, offset MUST be used with `#limit`.
249
+ #
250
+ # @param number [Fixnum]
251
+ #
252
+ # @return self
253
+ #
254
+ # @since 0.1.0
255
+ #
256
+ # @see Lotus::Model::Adapters::Sql::Query#limit
257
+ #
258
+ # @example
259
+ #
260
+ # query.limit(1).offset(10)
261
+ #
262
+ # # => SELECT * FROM `people` LIMIT 1 OFFSET 10
263
+ def offset(number)
264
+ conditions.push([:offset, number])
265
+ self
266
+ end
267
+
268
+ # Specify the ascending order of the records, sorted by the given
269
+ # columns.
270
+ #
271
+ # @param columns [Array<Symbol>] the column names
272
+ #
273
+ # @return self
274
+ #
275
+ # @since 0.1.0
276
+ #
277
+ # @see Lotus::Model::Adapters::Sql::Query#desc
278
+ #
279
+ # @example Single column
280
+ #
281
+ # query.order(:name)
282
+ #
283
+ # # => SELECT * FROM `people` ORDER BY (`name`)
284
+ #
285
+ # @example Multiple columns
286
+ #
287
+ # query.order(:name, :year)
288
+ #
289
+ # # => SELECT * FROM `people` ORDER BY `name`, `year`
290
+ #
291
+ # @example Multiple invokations
292
+ #
293
+ # query.order(:name).order(:year)
294
+ #
295
+ # # => SELECT * FROM `people` ORDER BY `name`, `year`
296
+ def order(*columns)
297
+ conditions.push([_order_operator, *columns])
298
+ self
299
+ end
300
+
301
+ alias_method :asc, :order
302
+
303
+ # Specify the descending order of the records, sorted by the given
304
+ # columns.
305
+ #
306
+ # @param columns [Array<Symbol>] the column names
307
+ #
308
+ # @return self
309
+ #
310
+ # @since 0.1.0
311
+ #
312
+ # @see Lotus::Model::Adapters::Sql::Query#order
313
+ #
314
+ # @example Single column
315
+ #
316
+ # query.desc(:name)
317
+ #
318
+ # # => SELECT * FROM `people` ORDER BY (`name`) DESC
319
+ #
320
+ # @example Multiple columns
321
+ #
322
+ # query.desc(:name, :year)
323
+ #
324
+ # # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
325
+ #
326
+ # @example Multiple invokations
327
+ #
328
+ # query.desc(:name).desc(:year)
329
+ #
330
+ # # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
331
+ def desc(*columns)
332
+ Array(columns).each do |column|
333
+ conditions.push([_order_operator, Sequel.desc(column)])
334
+ end
335
+
336
+ self
337
+ end
338
+
339
+ # Returns the sum of the values for the given column.
340
+ #
341
+ # @param column [Symbol] the colum name
342
+ #
343
+ # @return [Numeric]
344
+ #
345
+ # @since 0.1.0
346
+ #
347
+ # @example
348
+ #
349
+ # query.sum(:comments_count)
350
+ #
351
+ # # => SELECT SUM(`comments_count`) FROM articles
352
+ def sum(column)
353
+ run.sum(column)
354
+ end
355
+
356
+ # Returns the average of the values for the given column.
357
+ #
358
+ # @param column [Symbol] the colum name
359
+ #
360
+ # @return [Numeric]
361
+ #
362
+ # @since 0.1.0
363
+ #
364
+ # @example
365
+ #
366
+ # query.average(:comments_count)
367
+ #
368
+ # # => SELECT AVG(`comments_count`) FROM articles
369
+ def average(column)
370
+ run.avg(column)
371
+ end
372
+
373
+ alias_method :avg, :average
374
+
375
+ # Returns the maximum value for the given column.
376
+ #
377
+ # @param column [Symbol] the colum name
378
+ #
379
+ # @return result
380
+ #
381
+ # @since 0.1.0
382
+ #
383
+ # @example With numeric type
384
+ #
385
+ # query.max(:comments_count)
386
+ #
387
+ # # => SELECT MAX(`comments_count`) FROM articles
388
+ #
389
+ # @example With string type
390
+ #
391
+ # query.max(:title)
392
+ #
393
+ # # => SELECT MAX(`title`) FROM articles
394
+ def max(column)
395
+ run.max(column)
396
+ end
397
+
398
+ # Returns the minimum value for the given column.
399
+ #
400
+ # @param column [Symbol] the colum name
401
+ #
402
+ # @return result
403
+ #
404
+ # @since 0.1.0
405
+ #
406
+ # @example With numeric type
407
+ #
408
+ # query.min(:comments_count)
409
+ #
410
+ # # => SELECT MIN(`comments_count`) FROM articles
411
+ #
412
+ # @example With string type
413
+ #
414
+ # query.min(:title)
415
+ #
416
+ # # => SELECT MIN(`title`) FROM articles
417
+ def min(column)
418
+ run.min(column)
419
+ end
420
+
421
+ # Returns the difference between the MAX and MIN for the given column.
422
+ #
423
+ # @param column [Symbol] the colum name
424
+ #
425
+ # @return [Numeric]
426
+ #
427
+ # @since 0.1.0
428
+ #
429
+ # @see Lotus::Model::Adapters::Sql::Query#max
430
+ # @see Lotus::Model::Adapters::Sql::Query#min
431
+ #
432
+ # @example
433
+ #
434
+ # query.interval(:comments_count)
435
+ #
436
+ # # => SELECT (MAX(`comments_count`) - MIN(`comments_count`)) FROM articles
437
+ def interval(column)
438
+ run.interval(column)
439
+ end
440
+
441
+ # Returns a range of values between the MAX and the MIN for the given
442
+ # column.
443
+ #
444
+ # @param column [Symbol] the colum name
445
+ #
446
+ # @return [Range]
447
+ #
448
+ # @since 0.1.0
449
+ #
450
+ # @see Lotus::Model::Adapters::Sql::Query#max
451
+ # @see Lotus::Model::Adapters::Sql::Query#min
452
+ #
453
+ # @example
454
+ #
455
+ # query.range(:comments_count)
456
+ #
457
+ # # => SELECT MAX(`comments_count`) AS v1, MIN(`comments_count`) AS v2 FROM articles
458
+ def range(column)
459
+ run.range(column)
460
+ end
461
+
462
+ # Checks if at least one record exists for the current conditions.
463
+ #
464
+ # @return [TrueClass,FalseClass]
465
+ #
466
+ # @since 0.1.0
467
+ #
468
+ # @example
469
+ #
470
+ # query.where(author_id: 23).exists? # => true
471
+ def exist?
472
+ !count.zero?
473
+ end
474
+
475
+ # Returns a count of the records for the current conditions.
476
+ #
477
+ # @return [Fixnum]
478
+ #
479
+ # @since 0.1.0
480
+ #
481
+ # @example
482
+ #
483
+ # query.where(author_id: 23).count # => 5
484
+ def count
485
+ run.count
486
+ end
487
+
488
+ # Negates the current where/exclude conditions with the logical
489
+ # opposite operator.
490
+ #
491
+ # All the other conditions will be ignored.
492
+ #
493
+ # @since 0.1.0
494
+ #
495
+ # @see Lotus::Model::Adapters::Sql::Query#where
496
+ # @see Lotus::Model::Adapters::Sql::Query#exclude
497
+ # @see Lotus::Repository#exclude
498
+ #
499
+ # @example
500
+ #
501
+ # query.where(language: 'java').negate!.all
502
+ #
503
+ # # => SELECT * FROM `projects` WHERE (`language` != 'java')
504
+ def negate!
505
+ conditions.map! do |(operator, condition)|
506
+ [OPERATORS_MAPPING.fetch(operator) { operator }, condition]
507
+ end
508
+ end
509
+
510
+ # Apply all the conditions and returns a filtered collection.
511
+ #
512
+ # This operation is idempotent, and the returned result didn't
513
+ # fetched the records yet.
514
+ #
515
+ # @return [Lotus::Model::Adapters::Sql::Collection]
516
+ #
517
+ # @since 0.1.0
518
+ def scoped
519
+ scope = @collection
520
+
521
+ conditions.each do |(method,*args)|
522
+ scope = scope.public_send(method, *args)
523
+ end
524
+
525
+ scope
526
+ end
527
+
528
+ alias_method :run, :scoped
529
+
530
+ protected
531
+ # Handles missing methods for query combinations
532
+ #
533
+ # @api private
534
+ # @since 0.1.0
535
+ #
536
+ # @see Lotus::Model::Adapters:Sql::Query#apply
537
+ def method_missing(m, *args, &blk)
538
+ if @context.respond_to?(m)
539
+ apply @context.public_send(m, *args, &blk)
540
+ else
541
+ super
542
+ end
543
+ end
544
+
545
+ private
546
+
547
+ # Returns a new query that is the result of the merge of the current
548
+ # conditions with the ones of the given query.
549
+ #
550
+ # This is used to combine queries together in a Repository.
551
+ #
552
+ # @param query [Lotus::Model::Adapters::Sql::Query] the query to apply
553
+ #
554
+ # @return [Lotus::Model::Adapters::Sql::Query] a new query with the
555
+ # merged conditions
556
+ #
557
+ # @api private
558
+ # @since 0.1.0
559
+ #
560
+ # @example
561
+ # require 'lotus/model'
562
+ #
563
+ # class ArticleRepository
564
+ # include Lotus::Repository
565
+ #
566
+ # def self.by_author(author)
567
+ # query do
568
+ # where(author_id: author.id)
569
+ # end
570
+ # end
571
+ #
572
+ # def self.rank
573
+ # query.desc(:comments_count)
574
+ # end
575
+ #
576
+ # def self.rank_by_author(author)
577
+ # rank.by_author(author)
578
+ # end
579
+ # end
580
+ #
581
+ # # The code above combines two queries: `rank` and `by_author`.
582
+ # #
583
+ # # The first class method `rank` returns a `Sql::Query` instance
584
+ # # which doesn't respond to `by_author`. How to solve this problem?
585
+ # #
586
+ # # 1. When we use `query` to fabricate a `Sql::Query` we pass the
587
+ # # current context (the repository itself) to the query initializer.
588
+ # #
589
+ # # 2. When that query receives the `by_author` message, it's captured
590
+ # # by `method_missing` and dispatched to the repository.
591
+ # #
592
+ # # 3. The class method `by_author` returns a query too.
593
+ # #
594
+ # # 4. We just return a new query that is the result of the current
595
+ # # query's conditions (`rank`) and of the conditions from `by_author`.
596
+ # #
597
+ # # You're welcome ;)
598
+ def apply(query)
599
+ dup.tap do |result|
600
+ result.conditions.push(*query.conditions)
601
+ end
602
+ end
603
+
604
+ def _order_operator
605
+ if conditions.any? {|c, _| c == :order }
606
+ :order_more
607
+ else
608
+ :order
609
+ end
610
+ end
611
+ end
612
+ end
613
+ end
614
+ end
615
+ end