lotus-model 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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