hanami-model 0.0.0 → 0.6.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 (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