hanami-model 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/EXAMPLE.md +212 -0
  4. data/LICENSE.md +22 -0
  5. data/README.md +600 -7
  6. data/hanami-model.gemspec +17 -12
  7. data/lib/hanami-model.rb +1 -0
  8. data/lib/hanami/entity.rb +298 -0
  9. data/lib/hanami/entity/dirty_tracking.rb +74 -0
  10. data/lib/hanami/model.rb +204 -2
  11. data/lib/hanami/model/adapters/abstract.rb +281 -0
  12. data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
  13. data/lib/hanami/model/adapters/implementation.rb +111 -0
  14. data/lib/hanami/model/adapters/memory/collection.rb +132 -0
  15. data/lib/hanami/model/adapters/memory/command.rb +113 -0
  16. data/lib/hanami/model/adapters/memory/query.rb +653 -0
  17. data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
  18. data/lib/hanami/model/adapters/null_adapter.rb +24 -0
  19. data/lib/hanami/model/adapters/sql/collection.rb +287 -0
  20. data/lib/hanami/model/adapters/sql/command.rb +73 -0
  21. data/lib/hanami/model/adapters/sql/console.rb +33 -0
  22. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
  23. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
  24. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
  25. data/lib/hanami/model/adapters/sql/query.rb +788 -0
  26. data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
  27. data/lib/hanami/model/coercer.rb +74 -0
  28. data/lib/hanami/model/config/adapter.rb +116 -0
  29. data/lib/hanami/model/config/mapper.rb +45 -0
  30. data/lib/hanami/model/configuration.rb +275 -0
  31. data/lib/hanami/model/error.rb +7 -0
  32. data/lib/hanami/model/mapper.rb +124 -0
  33. data/lib/hanami/model/mapping.rb +48 -0
  34. data/lib/hanami/model/mapping/attribute.rb +85 -0
  35. data/lib/hanami/model/mapping/coercers.rb +314 -0
  36. data/lib/hanami/model/mapping/collection.rb +490 -0
  37. data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
  38. data/lib/hanami/model/migrator.rb +324 -0
  39. data/lib/hanami/model/migrator/adapter.rb +170 -0
  40. data/lib/hanami/model/migrator/connection.rb +133 -0
  41. data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
  42. data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
  43. data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
  44. data/lib/hanami/model/version.rb +4 -1
  45. data/lib/hanami/repository.rb +872 -0
  46. metadata +100 -16
  47. data/.gitignore +0 -9
  48. data/Gemfile +0 -4
  49. data/Rakefile +0 -2
  50. data/bin/console +0 -14
  51. data/bin/setup +0 -8
@@ -0,0 +1,111 @@
1
+ module Hanami
2
+ module Model
3
+ module Adapters
4
+ # Shared implementation for SqlAdapter and MemoryAdapter
5
+ #
6
+ # @api private
7
+ # @since 0.1.0
8
+ module Implementation
9
+ # Creates or updates a record in the database for the given entity.
10
+ #
11
+ # @param collection [Symbol] the target collection (it must be mapped).
12
+ # @param entity [#id, #id=] the entity to persist
13
+ #
14
+ # @return [Object] the entity
15
+ #
16
+ # @api private
17
+ # @since 0.1.0
18
+ def persist(collection, entity)
19
+ if entity.id
20
+ update(collection, entity)
21
+ else
22
+ create(collection, entity)
23
+ end
24
+ end
25
+
26
+ # Returns all the records for the given collection
27
+ #
28
+ # @param collection [Symbol] the target collection (it must be mapped).
29
+ #
30
+ # @return [Array] all the records
31
+ #
32
+ # @api private
33
+ # @since 0.1.0
34
+ def all(collection)
35
+ # TODO consider to make this lazy (aka remove #all)
36
+ query(collection).all
37
+ end
38
+
39
+ # Returns a unique record from the given collection, with the given
40
+ # id.
41
+ #
42
+ # @param collection [Symbol] the target collection (it must be mapped).
43
+ # @param id [Object] the identity of the object.
44
+ #
45
+ # @return [Object] the entity
46
+ #
47
+ # @api private
48
+ # @since 0.1.0
49
+ def find(collection, id)
50
+ _first(
51
+ _find(collection, id)
52
+ )
53
+ end
54
+
55
+ # Returns the first record in the given collection.
56
+ #
57
+ # @param collection [Symbol] the target collection (it must be mapped).
58
+ #
59
+ # @return [Object] the first entity
60
+ #
61
+ # @api private
62
+ # @since 0.1.0
63
+ def first(collection)
64
+ _first(
65
+ query(collection).asc(_identity(collection))
66
+ )
67
+ end
68
+
69
+ # Returns the last record in the given collection.
70
+ #
71
+ # @param collection [Symbol] the target collection (it must be mapped).
72
+ #
73
+ # @return [Object] the last entity
74
+ #
75
+ # @api private
76
+ # @since 0.1.0
77
+ def last(collection)
78
+ _first(
79
+ query(collection).desc(_identity(collection))
80
+ )
81
+ end
82
+
83
+ private
84
+ def _collection(name)
85
+ raise NotImplementedError
86
+ end
87
+
88
+ def _mapped_collection(name)
89
+ @mapper.collection(name)
90
+ end
91
+
92
+ def _find(collection, id)
93
+ identity = _identity(collection)
94
+ query(collection).where(identity => _id(collection, identity, id))
95
+ end
96
+
97
+ def _first(query)
98
+ query.limit(1).first
99
+ end
100
+
101
+ def _identity(collection)
102
+ _mapped_collection(collection).identity
103
+ end
104
+
105
+ def _id(collection, column, value)
106
+ _mapped_collection(collection).deserialize_attribute(column, value)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,132 @@
1
+ module Hanami
2
+ module Model
3
+ module Adapters
4
+ module Memory
5
+ # Acts like a SQL database table.
6
+ #
7
+ # @api private
8
+ # @since 0.1.0
9
+ class Collection
10
+ # A counter that simulates autoincrement primary key of a SQL table.
11
+ #
12
+ # @api private
13
+ # @since 0.1.0
14
+ class PrimaryKey
15
+ # Initialize
16
+ #
17
+ # @return [Hanami::Model::Adapters::Memory::Collection::PrimaryKey]
18
+ #
19
+ # @api private
20
+ # @since 0.1.0
21
+ def initialize
22
+ @current = 0
23
+ end
24
+
25
+ # Increment the current count by 1 and yields the given block
26
+ #
27
+ # @return [Fixnum] the incremented counter
28
+ #
29
+ # @api private
30
+ # @since 0.1.0
31
+ def increment!
32
+ yield(@current += 1)
33
+ @current
34
+ end
35
+ end
36
+
37
+ # @attr_reader name [Symbol] the name of the collection (eg. `:users`)
38
+ #
39
+ # @since 0.1.0
40
+ # @api private
41
+ attr_reader :name
42
+
43
+ # @attr_reader identity [Symbol] the primary key of the collection
44
+ # (eg. `:id`)
45
+ #
46
+ # @since 0.1.0
47
+ # @api private
48
+ attr_reader :identity
49
+
50
+ # @attr_reader records [Hash] a set of records
51
+ #
52
+ # @since 0.1.0
53
+ # @api private
54
+ attr_reader :records
55
+
56
+ # Initialize a collection
57
+ #
58
+ # @param name [Symbol] the name of the collection (eg. `:users`).
59
+ # @param identity [Symbol] the primary key of the collection
60
+ # (eg. `:id`).
61
+ #
62
+ # @api private
63
+ # @since 0.1.0
64
+ def initialize(name, identity)
65
+ @name, @identity = name, identity
66
+ clear
67
+ end
68
+
69
+ # Creates a record for the given entity and assigns an id.
70
+ #
71
+ # @param entity [Object] the entity to persist
72
+ #
73
+ # @see Hanami::Model::Adapters::Memory::Command#create
74
+ #
75
+ # @return the primary key of the created record
76
+ #
77
+ # @api private
78
+ # @since 0.1.0
79
+ def create(entity)
80
+ @primary_key.increment! do |id|
81
+ entity[identity] = id
82
+ records[id] = entity
83
+ end
84
+ end
85
+
86
+ # Updates the record corresponding to the given entity.
87
+ #
88
+ # @param entity [Object] the entity to persist
89
+ #
90
+ # @see Hanami::Model::Adapters::Memory::Command#update
91
+ #
92
+ # @api private
93
+ # @since 0.1.0
94
+ def update(entity)
95
+ records[entity.fetch(identity)] = entity
96
+ end
97
+
98
+ # Deletes the record corresponding to the given entity.
99
+ #
100
+ # @param entity [Object] the entity to delete
101
+ #
102
+ # @see Hanami::Model::Adapters::Memory::Command#delete
103
+ #
104
+ # @api private
105
+ # @since 0.1.0
106
+ def delete(entity)
107
+ records.delete(entity.id)
108
+ end
109
+
110
+ # Returns all the raw records
111
+ #
112
+ # @return [Array<Hash>]
113
+ #
114
+ # @api private
115
+ # @since 0.1.0
116
+ def all
117
+ records.values
118
+ end
119
+
120
+ # Deletes all the records and resets the identity counter.
121
+ #
122
+ # @api private
123
+ # @since 0.1.0
124
+ def clear
125
+ @records = {}
126
+ @primary_key = PrimaryKey.new
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,113 @@
1
+ module Hanami
2
+ module Model
3
+ module Adapters
4
+ module Memory
5
+ # Execute a command for the given collection.
6
+ #
7
+ # @see Hanami::Model::Adapters::Memory::Collection
8
+ # @see Hanami::Model::Mapping::Collection
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ class Command
13
+ # Initialize a command
14
+ #
15
+ # @param dataset [Hanami::Model::Adapters::Memory::Collection]
16
+ # @param collection [Hanami::Model::Mapping::Collection]
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ def initialize(dataset, collection)
21
+ @dataset = dataset
22
+ @collection = collection
23
+ end
24
+
25
+ # Creates a record for the given entity.
26
+ #
27
+ # @param entity [Object] the entity to persist
28
+ #
29
+ # @see Hanami::Model::Adapters::Memory::Collection#insert
30
+ #
31
+ # @return the primary key of the just created record.
32
+ #
33
+ # @api private
34
+ # @since 0.1.0
35
+ def create(entity)
36
+ serialized_entity = _serialize(entity)
37
+ serialized_entity[_identity] = @dataset.create(serialized_entity)
38
+
39
+ _deserialize(serialized_entity)
40
+ end
41
+
42
+ # Updates the corresponding record for the given entity.
43
+ #
44
+ # @param entity [Object] the entity to persist
45
+ #
46
+ # @see Hanami::Model::Adapters::Memory::Collection#update
47
+ #
48
+ # @api private
49
+ # @since 0.1.0
50
+ def update(entity)
51
+ serialized_entity = _serialize(entity)
52
+ @dataset.update(serialized_entity)
53
+
54
+ _deserialize(serialized_entity)
55
+ end
56
+
57
+ # Deletes the corresponding record for the given entity.
58
+ #
59
+ # @param entity [Object] the entity to delete
60
+ #
61
+ # @see Hanami::Model::Adapters::Memory::Collection#delete
62
+ #
63
+ # @api private
64
+ # @since 0.1.0
65
+ def delete(entity)
66
+ @dataset.delete(entity)
67
+ end
68
+
69
+ # Deletes all the records from the table.
70
+ #
71
+ # @see Hanami::Model::Adapters::Memory::Collection#clear
72
+ #
73
+ # @api private
74
+ # @since 0.1.0
75
+ def clear
76
+ @dataset.clear
77
+ end
78
+
79
+ private
80
+ # Serialize the given entity before to persist in the database.
81
+ #
82
+ # @return [Hash] the serialized entity
83
+ #
84
+ # @api private
85
+ # @since 0.1.0
86
+ def _serialize(entity)
87
+ @collection.serialize(entity)
88
+ end
89
+
90
+ # Deserialize the given entity after it was persisted in the database.
91
+ #
92
+ # @return [Hanami::Entity] the deserialized entity
93
+ #
94
+ # @api private
95
+ # @since 0.2.2
96
+ def _deserialize(entity)
97
+ @collection.deserialize([entity]).first
98
+ end
99
+
100
+ # Name of the identity column in database
101
+ #
102
+ # @return [Symbol] the identity name
103
+ #
104
+ # @api private
105
+ # @since 0.2.2
106
+ def _identity
107
+ @collection.identity
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,653 @@
1
+ require 'forwardable'
2
+ require 'ostruct'
3
+ require 'hanami/utils/kernel'
4
+
5
+ module Hanami
6
+ module Model
7
+ module Adapters
8
+ module Memory
9
+ # Query the in-memory database with a powerful API.
10
+ #
11
+ # All the methods are chainable, it allows advanced composition of
12
+ # conditions.
13
+ #
14
+ # This works as a lazy filtering mechanism: the records are fetched from
15
+ # the database only when needed.
16
+ #
17
+ # @example
18
+ #
19
+ # query.where(language: 'ruby')
20
+ # .and(framework: 'hanami')
21
+ # .reverse_order(:users_count).all
22
+ #
23
+ # # the records are fetched only when we invoke #all
24
+ #
25
+ # It implements Ruby's `Enumerable` and borrows some methods from `Array`.
26
+ # Expect a query to act like them.
27
+ #
28
+ # @since 0.1.0
29
+ class Query
30
+ include Enumerable
31
+ extend Forwardable
32
+
33
+ def_delegators :all, :each, :to_s, :empty?
34
+
35
+ # @attr_reader conditions [Array] an accumulator for the conditions
36
+ #
37
+ # @since 0.1.0
38
+ # @api private
39
+ attr_reader :conditions
40
+
41
+ # @attr_reader modifiers [Array] an accumulator for the modifiers
42
+ #
43
+ # @since 0.1.0
44
+ # @api private
45
+ attr_reader :modifiers
46
+
47
+ # Initialize a query
48
+ #
49
+ # @param dataset [Hanami::Model::Adapters::Memory::Collection]
50
+ # @param collection [Hanami::Model::Mapping::Collection]
51
+ # @param blk [Proc] an optional block that gets yielded in the
52
+ # context of the current query
53
+ #
54
+ # @since 0.1.0
55
+ # @api private
56
+ def initialize(dataset, collection, &blk)
57
+ @dataset = dataset
58
+ @collection = collection
59
+ @conditions = []
60
+ @modifiers = []
61
+ instance_eval(&blk) if block_given?
62
+ end
63
+
64
+ # Resolves the query by fetching records from the database and
65
+ # translating them into entities.
66
+ #
67
+ # @return [Array] a collection of entities
68
+ #
69
+ # @since 0.1.0
70
+ def all
71
+ @collection.deserialize(run)
72
+ end
73
+
74
+ # Adds a condition that behaves like SQL `WHERE`.
75
+ #
76
+ # It accepts a `Hash` with only one pair.
77
+ # The key must be the name of the column expressed as a `Symbol`.
78
+ # The value is the one used by the internal filtering logic.
79
+ #
80
+ # @param condition [Hash]
81
+ #
82
+ # @return self
83
+ #
84
+ # @since 0.1.0
85
+ #
86
+ # @example Fixed value
87
+ #
88
+ # query.where(language: 'ruby')
89
+ #
90
+ # @example Array
91
+ #
92
+ # query.where(id: [1, 3])
93
+ #
94
+ # @example Range
95
+ #
96
+ # query.where(year: 1900..1982)
97
+ #
98
+ # @example Using block
99
+ #
100
+ # query.where { age > 31 }
101
+ #
102
+ # @example Multiple conditions
103
+ #
104
+ # query.where(language: 'ruby')
105
+ # .where(framework: 'hanami')
106
+ #
107
+ # @example Multiple conditions with blocks
108
+ #
109
+ # query.where { language == 'ruby' }
110
+ # .where { framework == 'hanami' }
111
+ #
112
+ # @example Mixed hash and block conditions
113
+ #
114
+ # query.where(language: 'ruby')
115
+ # .where { framework == 'hanami' }
116
+ def where(condition = nil, &blk)
117
+ if blk
118
+ _push_evaluated_block_condition(:where, blk, :find_all)
119
+ elsif condition
120
+ _push_to_expanded_condition(:where, condition) do |column, value|
121
+ Proc.new {
122
+ find_all { |r|
123
+ case value
124
+ when Array,Set,Range
125
+ value.include?(r.fetch(column, nil))
126
+ else
127
+ r.fetch(column, nil) == value
128
+ end
129
+ }
130
+ }
131
+ end
132
+ end
133
+
134
+ self
135
+ end
136
+
137
+ alias_method :and, :where
138
+
139
+ # Adds a condition that behaves like SQL `OR`.
140
+ #
141
+ # It accepts a `Hash` with only one pair.
142
+ # The key must be the name of the column expressed as a `Symbol`.
143
+ # The value is the one used by the SQL query
144
+ #
145
+ # This condition will be ignored if not used with WHERE.
146
+ #
147
+ # @param condition [Hash]
148
+ #
149
+ # @return self
150
+ #
151
+ # @since 0.1.0
152
+ #
153
+ # @example Fixed value
154
+ #
155
+ # query.where(language: 'ruby').or(framework: 'hanami')
156
+ #
157
+ # @example Array
158
+ #
159
+ # query.where(id: 1).or(author_id: [15, 23])
160
+ #
161
+ # @example Range
162
+ #
163
+ # query.where(country: 'italy').or(year: 1900..1982)
164
+ #
165
+ # @example Using block
166
+ #
167
+ # query.where { age == 31 }.or { age == 32 }
168
+ #
169
+ # @example Mixed hash and block conditions
170
+ #
171
+ # query.where(language: 'ruby')
172
+ # .or { framework == 'hanami' }
173
+ def or(condition = nil, &blk)
174
+ if blk
175
+ _push_evaluated_block_condition(:or, blk, :find_all)
176
+ elsif condition
177
+ _push_to_expanded_condition(:or, condition) do |column, value|
178
+ Proc.new { find_all { |r| r.fetch(column) == value} }
179
+ end
180
+ end
181
+
182
+ self
183
+ end
184
+
185
+ # Logical negation of a #where condition.
186
+ #
187
+ # It accepts a `Hash` with only one pair.
188
+ # The key must be the name of the column expressed as a `Symbol`.
189
+ # The value is the one used by the internal filtering logic.
190
+ #
191
+ # @param condition [Hash]
192
+ #
193
+ # @since 0.1.0
194
+ #
195
+ # @return self
196
+ #
197
+ # @example Fixed value
198
+ #
199
+ # query.exclude(language: 'java')
200
+ #
201
+ # @example Array
202
+ #
203
+ # query.exclude(id: [4, 9])
204
+ #
205
+ # @example Range
206
+ #
207
+ # query.exclude(year: 1900..1982)
208
+ #
209
+ # @example Multiple conditions
210
+ #
211
+ # query.exclude(language: 'java')
212
+ # .exclude(company: 'enterprise')
213
+ #
214
+ # @example Using block
215
+ #
216
+ # query.exclude { age > 31 }
217
+ #
218
+ # @example Multiple conditions with blocks
219
+ #
220
+ # query.exclude { language == 'java' }
221
+ # .exclude { framework == 'spring' }
222
+ #
223
+ # @example Mixed hash and block conditions
224
+ #
225
+ # query.exclude(language: 'java')
226
+ # .exclude { framework == 'spring' }
227
+ def exclude(condition = nil, &blk)
228
+ if blk
229
+ _push_evaluated_block_condition(:where, blk, :reject)
230
+ elsif condition
231
+ _push_to_expanded_condition(:where, condition) do |column, value|
232
+ Proc.new { reject { |r| r.fetch(column) == value} }
233
+ end
234
+ end
235
+
236
+ self
237
+ end
238
+
239
+ alias_method :not, :exclude
240
+
241
+ # Select only the specified columns.
242
+ #
243
+ # By default a query selects all the mapped columns.
244
+ #
245
+ # @param columns [Array<Symbol>]
246
+ #
247
+ # @return self
248
+ #
249
+ # @since 0.1.0
250
+ #
251
+ # @example Single column
252
+ #
253
+ # query.select(:name)
254
+ #
255
+ # @example Multiple columns
256
+ #
257
+ # query.select(:name, :year)
258
+ def select(*columns)
259
+ columns = Hanami::Utils::Kernel.Array(columns)
260
+ modifiers.push(Proc.new{ flatten!; each {|r| r.delete_if {|k,_| !columns.include?(k)} } })
261
+ end
262
+
263
+ # Specify the ascending order of the records, sorted by the given
264
+ # columns.
265
+ #
266
+ # @param columns [Array<Symbol>] the column names
267
+ #
268
+ # @return self
269
+ #
270
+ # @since 0.1.0
271
+ #
272
+ # @see Hanami::Model::Adapters::Memory::Query#reverse_order
273
+ #
274
+ # @example Single column
275
+ #
276
+ # query.order(:name)
277
+ #
278
+ # @example Multiple columns
279
+ #
280
+ # query.order(:name, :year)
281
+ #
282
+ # @example Multiple invokations
283
+ #
284
+ # query.order(:name).order(:year)
285
+ def order(*columns)
286
+ Hanami::Utils::Kernel.Array(columns).each do |column|
287
+ modifiers.push(Proc.new{ sort_by!{|r| r.fetch(column)} })
288
+ end
289
+
290
+ self
291
+ end
292
+
293
+ # Alias for order
294
+ #
295
+ # @since 0.1.0
296
+ #
297
+ # @see Hanami::Model::Adapters::Memory::Query#order
298
+ #
299
+ # @example Single column
300
+ #
301
+ # query.asc(:name)
302
+ #
303
+ # @example Multiple columns
304
+ #
305
+ # query.asc(:name, :year)
306
+ #
307
+ # @example Multiple invokations
308
+ #
309
+ # query.asc(:name).asc(:year)
310
+ alias_method :asc, :order
311
+
312
+ # Specify the descending order of the records, sorted by the given
313
+ # columns.
314
+ #
315
+ # @param columns [Array<Symbol>] the column names
316
+ #
317
+ # @return self
318
+ #
319
+ # @since 0.3.1
320
+ #
321
+ # @see Hanami::Model::Adapters::Memory::Query#order
322
+ #
323
+ # @example Single column
324
+ #
325
+ # query.reverse_order(:name)
326
+ #
327
+ # @example Multiple columns
328
+ #
329
+ # query.reverse_order(:name, :year)
330
+ #
331
+ # @example Multiple invokations
332
+ #
333
+ # query.reverse_order(:name).reverse_order(:year)
334
+ def reverse_order(*columns)
335
+ Hanami::Utils::Kernel.Array(columns).each do |column|
336
+ modifiers.push(Proc.new{ sort_by!{|r| r.fetch(column)}.reverse! })
337
+ end
338
+
339
+ self
340
+ end
341
+
342
+ # Alias for reverse_order
343
+ #
344
+ # @since 0.1.0
345
+ #
346
+ # @see Hanami::Model::Adapters::Memory::Query#reverse_order
347
+ #
348
+ # @example Single column
349
+ #
350
+ # query.desc(:name)
351
+ #
352
+ # @example Multiple columns
353
+ #
354
+ # query.desc(:name, :year)
355
+ #
356
+ # @example Multiple invokations
357
+ #
358
+ # query.desc(:name).desc(:year)
359
+ alias_method :desc, :reverse_order
360
+
361
+ # Limit the number of records to return.
362
+ #
363
+ # @param number [Fixnum]
364
+ #
365
+ # @return self
366
+ #
367
+ # @since 0.1.0
368
+ #
369
+ # @example
370
+ #
371
+ # query.limit(1)
372
+ def limit(number)
373
+ modifiers.push(Proc.new{ replace(flatten.first(number)) })
374
+ self
375
+ end
376
+
377
+ # Simulate an `OFFSET` clause, without the need of specify a limit.
378
+ #
379
+ # @param number [Fixnum]
380
+ #
381
+ # @return self
382
+ #
383
+ # @since 0.1.0
384
+ #
385
+ # @example
386
+ #
387
+ # query.offset(10)
388
+ def offset(number)
389
+ modifiers.unshift(Proc.new{ replace(flatten.drop(number)) })
390
+ self
391
+ end
392
+
393
+ # Returns the sum of the values for the given column.
394
+ #
395
+ # @param column [Symbol] the column name
396
+ #
397
+ # @return [Numeric]
398
+ #
399
+ # @since 0.1.0
400
+ #
401
+ # @example
402
+ #
403
+ # query.sum(:comments_count)
404
+ def sum(column)
405
+ result = all
406
+
407
+ if result.any?
408
+ result.inject(0.0) do |acc, record|
409
+ if value = record.public_send(column)
410
+ acc += value
411
+ end
412
+
413
+ acc
414
+ end
415
+ end
416
+ end
417
+
418
+ # Returns the average of the values for the given column.
419
+ #
420
+ # @param column [Symbol] the column name
421
+ #
422
+ # @return [Numeric]
423
+ #
424
+ # @since 0.1.0
425
+ #
426
+ # @example
427
+ #
428
+ # query.average(:comments_count)
429
+ def average(column)
430
+ if s = sum(column)
431
+ s / _all_with_present_column(column).count.to_f
432
+ end
433
+ end
434
+
435
+ alias_method :avg, :average
436
+
437
+ # Returns the maximum value for the given column.
438
+ #
439
+ # @param column [Symbol] the column name
440
+ #
441
+ # @return result
442
+ #
443
+ # @since 0.1.0
444
+ #
445
+ # @example
446
+ #
447
+ # query.max(:comments_count)
448
+ def max(column)
449
+ _all_with_present_column(column).max
450
+ end
451
+
452
+ # Returns the minimum value for the given column.
453
+ #
454
+ # @param column [Symbol] the column name
455
+ #
456
+ # @return result
457
+ #
458
+ # @since 0.1.0
459
+ #
460
+ # @example
461
+ #
462
+ # query.min(:comments_count)
463
+ def min(column)
464
+ _all_with_present_column(column).min
465
+ end
466
+
467
+ # Returns the difference between the MAX and MIN for the given column.
468
+ #
469
+ # @param column [Symbol] the column name
470
+ #
471
+ # @return [Numeric]
472
+ #
473
+ # @since 0.1.0
474
+ #
475
+ # @see Hanami::Model::Adapters::Memory::Query#max
476
+ # @see Hanami::Model::Adapters::Memory::Query#min
477
+ #
478
+ # @example
479
+ #
480
+ # query.interval(:comments_count)
481
+ def interval(column)
482
+ max(column) - min(column)
483
+ rescue NoMethodError
484
+ end
485
+
486
+ # Returns a range of values between the MAX and the MIN for the given
487
+ # column.
488
+ #
489
+ # @param column [Symbol] the column name
490
+ #
491
+ # @return [Range]
492
+ #
493
+ # @since 0.1.0
494
+ #
495
+ # @see Hanami::Model::Adapters::Memory::Query#max
496
+ # @see Hanami::Model::Adapters::Memory::Query#min
497
+ #
498
+ # @example
499
+ #
500
+ # query.range(:comments_count)
501
+ def range(column)
502
+ min(column)..max(column)
503
+ end
504
+
505
+ # Checks if at least one record exists for the current conditions.
506
+ #
507
+ # @return [TrueClass,FalseClass]
508
+ #
509
+ # @since 0.1.0
510
+ #
511
+ # @example
512
+ #
513
+ # query.where(author_id: 23).exists? # => true
514
+ def exist?
515
+ !count.zero?
516
+ end
517
+
518
+ # Returns a count of the records for the current conditions.
519
+ #
520
+ # @return [Fixnum]
521
+ #
522
+ # @since 0.1.0
523
+ #
524
+ # @example
525
+ #
526
+ # query.where(author_id: 23).count # => 5
527
+ def count
528
+ run.count
529
+ end
530
+
531
+ # This method is defined in order to make the interface of
532
+ # `Memory::Query` identical to `Sql::Query`, but this feature is NOT
533
+ # implemented
534
+ #
535
+ # @raise [NotImplementedError]
536
+ #
537
+ # @since 0.1.0
538
+ #
539
+ # @see Hanami::Model::Adapters::Sql::Query#negate!
540
+ def negate!
541
+ raise NotImplementedError
542
+ end
543
+
544
+ # This method is defined in order to make the interface of
545
+ # `Memory::Query` identical to `Sql::Query`, but this feature is NOT
546
+ # implemented
547
+ #
548
+ # @raise [NotImplementedError]
549
+ #
550
+ # @since 0.5.0
551
+ #
552
+ # @see Hanami::Model::Adapters::Sql::Query#group!
553
+ def group
554
+ raise NotImplementedError
555
+ end
556
+
557
+ protected
558
+ def method_missing(m, *args, &blk)
559
+ if @context.respond_to?(m)
560
+ apply @context.public_send(m, *args, &blk)
561
+ else
562
+ super
563
+ end
564
+ end
565
+
566
+ private
567
+ # Apply all the conditions and returns a filtered collection.
568
+ #
569
+ # This operation is idempotent, but the records are actually fetched
570
+ # from the memory store.
571
+ #
572
+ # @return [Array]
573
+ #
574
+ # @api private
575
+ # @since 0.1.0
576
+ def run
577
+ result = @dataset.all.dup
578
+
579
+ if conditions.any?
580
+ prev_result = nil
581
+ conditions.each do |(type, condition)|
582
+ case type
583
+ when :where
584
+ prev_result = result
585
+ result = prev_result.instance_exec(&condition)
586
+ when :or
587
+ result |= prev_result.instance_exec(&condition)
588
+ end
589
+ end
590
+ end
591
+
592
+ modifiers.map do |modifier|
593
+ result.instance_exec(&modifier)
594
+ end
595
+
596
+ Hanami::Utils::Kernel.Array(result)
597
+ end
598
+
599
+ def _all_with_present_column(column)
600
+ all.map {|record| record.public_send(column) }.compact
601
+ end
602
+
603
+ # Expands and yields keys and values of a query hash condition and
604
+ # stores the result and condition type in the conditions array.
605
+ #
606
+ # It yields condition's keys and values to allow the caller to create a proc
607
+ # object to be stored and executed later performing the actual query.
608
+ #
609
+ # @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
610
+ # @param condition [Hash] the query condition to be expanded.
611
+ #
612
+ # @return [Array<Array>] the conditions array itself.
613
+ #
614
+ # @api private
615
+ # @since 0.3.1
616
+ def _push_to_expanded_condition(condition_type, condition)
617
+ proc = yield Array(condition).flatten(1)
618
+ conditions.push([condition_type, proc])
619
+ end
620
+
621
+ # Evaluates a block condition of a specified type and stores it in the
622
+ # conditions array.
623
+ #
624
+ # @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
625
+ # @param condition [Proc] the query condition to be evaluated and stored.
626
+ # @param strategy [Symbol] the iterator method to be executed.
627
+ # (eg. `:find_all`, `:reject`)
628
+ #
629
+ # @return [Array<Array>] the conditions array itself.
630
+ #
631
+ # @raise [Hanami::Model::InvalidQueryError] if block raises error when
632
+ # evaluated.
633
+ #
634
+ # @api private
635
+ # @since 0.3.1
636
+ def _push_evaluated_block_condition(condition_type, condition, strategy)
637
+ conditions.push([condition_type, Proc.new {
638
+ send(strategy) { |r|
639
+ begin
640
+ OpenStruct.new(r).instance_eval(&condition)
641
+ rescue NoMethodError
642
+ # TODO improve the error message, informing which
643
+ # attributes are invalid
644
+ raise Hanami::Model::InvalidQueryError.new
645
+ end
646
+ }
647
+ }])
648
+ end
649
+ end
650
+ end
651
+ end
652
+ end
653
+ end