hanami-model 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +54 -420
  4. data/hanami-model.gemspec +9 -6
  5. data/lib/hanami/entity.rb +107 -191
  6. data/lib/hanami/entity/schema.rb +236 -0
  7. data/lib/hanami/model.rb +52 -138
  8. data/lib/hanami/model/association.rb +37 -0
  9. data/lib/hanami/model/associations/belongs_to.rb +19 -0
  10. data/lib/hanami/model/associations/dsl.rb +29 -0
  11. data/lib/hanami/model/associations/has_many.rb +200 -0
  12. data/lib/hanami/model/configuration.rb +52 -224
  13. data/lib/hanami/model/configurator.rb +62 -0
  14. data/lib/hanami/model/entity_name.rb +35 -0
  15. data/lib/hanami/model/error.rb +37 -24
  16. data/lib/hanami/model/mapping.rb +29 -35
  17. data/lib/hanami/model/migration.rb +31 -0
  18. data/lib/hanami/model/migrator.rb +111 -88
  19. data/lib/hanami/model/migrator/adapter.rb +39 -16
  20. data/lib/hanami/model/migrator/connection.rb +23 -11
  21. data/lib/hanami/model/migrator/mysql_adapter.rb +38 -17
  22. data/lib/hanami/model/migrator/postgres_adapter.rb +20 -19
  23. data/lib/hanami/model/migrator/sqlite_adapter.rb +9 -8
  24. data/lib/hanami/model/plugins.rb +25 -0
  25. data/lib/hanami/model/plugins/mapping.rb +55 -0
  26. data/lib/hanami/model/plugins/schema.rb +55 -0
  27. data/lib/hanami/model/plugins/timestamps.rb +118 -0
  28. data/lib/hanami/model/relation_name.rb +24 -0
  29. data/lib/hanami/model/sql.rb +161 -0
  30. data/lib/hanami/model/sql/console.rb +41 -0
  31. data/lib/hanami/model/sql/consoles/abstract.rb +33 -0
  32. data/lib/hanami/model/sql/consoles/mysql.rb +63 -0
  33. data/lib/hanami/model/sql/consoles/postgresql.rb +68 -0
  34. data/lib/hanami/model/sql/consoles/sqlite.rb +46 -0
  35. data/lib/hanami/model/sql/entity/schema.rb +125 -0
  36. data/lib/hanami/model/sql/types.rb +95 -0
  37. data/lib/hanami/model/sql/types/schema/coercions.rb +198 -0
  38. data/lib/hanami/model/types.rb +99 -0
  39. data/lib/hanami/model/version.rb +1 -1
  40. data/lib/hanami/repository.rb +287 -723
  41. metadata +77 -40
  42. data/EXAMPLE.md +0 -213
  43. data/lib/hanami/entity/dirty_tracking.rb +0 -74
  44. data/lib/hanami/model/adapters/abstract.rb +0 -281
  45. data/lib/hanami/model/adapters/file_system_adapter.rb +0 -288
  46. data/lib/hanami/model/adapters/implementation.rb +0 -111
  47. data/lib/hanami/model/adapters/memory/collection.rb +0 -132
  48. data/lib/hanami/model/adapters/memory/command.rb +0 -113
  49. data/lib/hanami/model/adapters/memory/query.rb +0 -653
  50. data/lib/hanami/model/adapters/memory_adapter.rb +0 -179
  51. data/lib/hanami/model/adapters/null_adapter.rb +0 -24
  52. data/lib/hanami/model/adapters/sql/collection.rb +0 -287
  53. data/lib/hanami/model/adapters/sql/command.rb +0 -88
  54. data/lib/hanami/model/adapters/sql/console.rb +0 -33
  55. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +0 -49
  56. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +0 -48
  57. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +0 -26
  58. data/lib/hanami/model/adapters/sql/query.rb +0 -788
  59. data/lib/hanami/model/adapters/sql_adapter.rb +0 -296
  60. data/lib/hanami/model/coercer.rb +0 -74
  61. data/lib/hanami/model/config/adapter.rb +0 -116
  62. data/lib/hanami/model/config/mapper.rb +0 -45
  63. data/lib/hanami/model/mapper.rb +0 -124
  64. data/lib/hanami/model/mapping/attribute.rb +0 -85
  65. data/lib/hanami/model/mapping/coercers.rb +0 -314
  66. data/lib/hanami/model/mapping/collection.rb +0 -490
  67. data/lib/hanami/model/mapping/collection_coercer.rb +0 -79
@@ -1,111 +0,0 @@
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
@@ -1,132 +0,0 @@
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
@@ -1,113 +0,0 @@
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
@@ -1,653 +0,0 @@
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