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,88 +0,0 @@
1
- module Hanami
2
- module Model
3
- module Adapters
4
- module Sql
5
- # Execute a command for the given query.
6
- #
7
- # @see Hanami::Model::Adapters::Sql::Query
8
- #
9
- # @api private
10
- # @since 0.1.0
11
- class Command
12
- # @api private
13
- # @since 0.6.1
14
- SEQUEL_TO_HANAMI_ERROR_MAPPING = {
15
- 'Sequel::UniqueConstraintViolation' => Hanami::Model::UniqueConstraintViolationError,
16
- 'Sequel::ForeignKeyConstraintViolation' => Hanami::Model::ForeignKeyConstraintViolationError,
17
- 'Sequel::NotNullConstraintViolation' => Hanami::Model::NotNullConstraintViolationError,
18
- 'Sequel::CheckConstraintViolation' => Hanami::Model::CheckConstraintViolationError
19
- }.freeze
20
-
21
- # Initialize a command
22
- #
23
- # @param query [Hanami::Model::Adapters::Sql::Query]
24
- #
25
- # @api private
26
- # @since 0.1.0
27
- def initialize(query)
28
- @collection = query.scoped
29
- end
30
-
31
- # Creates a record for the given entity.
32
- #
33
- # @param entity [Object] the entity to persist
34
- #
35
- # @see Hanami::Model::Adapters::Sql::Collection#insert
36
- #
37
- # @return the primary key of the just created record.
38
- #
39
- # @api private
40
- # @since 0.1.0
41
- def create(entity)
42
- _handle_database_error { @collection.insert(entity) }
43
- end
44
-
45
- # Updates the corresponding record for the given entity.
46
- #
47
- # @param entity [Object] the entity to persist
48
- #
49
- # @see Hanami::Model::Adapters::Sql::Collection#update
50
- #
51
- # @api private
52
- # @since 0.1.0
53
- def update(entity)
54
- _handle_database_error { @collection.update(entity) }
55
- end
56
-
57
- # Deletes all the records for the current query.
58
- #
59
- # It's used to delete a single record or an entire database table.
60
- #
61
- # @see Hanami::Model::Adapters::SqlAdapter#delete
62
- # @see Hanami::Model::Adapters::SqlAdapter#clear
63
- #
64
- # @api private
65
- # @since 0.1.0
66
- def delete
67
- _handle_database_error { @collection.delete }
68
- end
69
-
70
- alias_method :clear, :delete
71
-
72
- private
73
-
74
- # Handles any possible Adapter's Database Error
75
- #
76
- # @api private
77
- # @since 0.6.1
78
- def _handle_database_error
79
- yield
80
- rescue Sequel::DatabaseError => e
81
- error_class = SEQUEL_TO_HANAMI_ERROR_MAPPING.fetch(e.class.name, Hanami::Model::InvalidCommandError)
82
- raise error_class, e.message
83
- end
84
- end
85
- end
86
- end
87
- end
88
- end
@@ -1,33 +0,0 @@
1
- module Hanami
2
- module Model
3
- module Adapters
4
- module Sql
5
- class Console
6
- extend Forwardable
7
-
8
- def_delegator :console, :connection_string
9
-
10
- def initialize(uri)
11
- @uri = URI.parse(uri)
12
- end
13
-
14
- private
15
-
16
- def console
17
- case @uri.scheme
18
- when 'sqlite'
19
- require 'hanami/model/adapters/sql/consoles/sqlite'
20
- Consoles::Sqlite.new(@uri)
21
- when 'postgres'
22
- require 'hanami/model/adapters/sql/consoles/postgresql'
23
- Consoles::Postgresql.new(@uri)
24
- when 'mysql', 'mysql2'
25
- require 'hanami/model/adapters/sql/consoles/mysql'
26
- Consoles::Mysql.new(@uri)
27
- end
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,49 +0,0 @@
1
- require 'shellwords'
2
- module Hanami
3
- module Model
4
- module Adapters
5
- module Sql
6
- module Consoles
7
- class Mysql
8
- def initialize(uri)
9
- @uri = uri
10
- end
11
-
12
- def connection_string
13
- str = 'mysql'
14
- str << host
15
- str << database
16
- str << port if port
17
- str << username if username
18
- str << password if password
19
- str
20
- end
21
-
22
- private
23
-
24
- def host
25
- " -h #{@uri.host}"
26
- end
27
-
28
- def database
29
- " -D #{@uri.path.sub(/^\//, '')}"
30
- end
31
-
32
- def port
33
- " -P #{@uri.port}" if @uri.port
34
- end
35
-
36
- def username
37
- " -u #{@uri.user}" if @uri.user
38
- end
39
-
40
- def password
41
- " -p #{@uri.password}" if @uri.password
42
- end
43
- end
44
- end
45
- end
46
- end
47
- end
48
- end
49
-
@@ -1,48 +0,0 @@
1
- require 'shellwords'
2
- module Hanami
3
- module Model
4
- module Adapters
5
- module Sql
6
- module Consoles
7
- class Postgresql
8
- def initialize(uri)
9
- @uri = uri
10
- end
11
-
12
- def connection_string
13
- configure_password
14
- str = 'psql'
15
- str << host
16
- str << database
17
- str << port if port
18
- str << username if username
19
- str
20
- end
21
-
22
- private
23
-
24
- def host
25
- " -h #{@uri.host}"
26
- end
27
-
28
- def database
29
- " -d #{@uri.path.sub(/^\//, '')}"
30
- end
31
-
32
- def port
33
- " -p #{@uri.port}" if @uri.port
34
- end
35
-
36
- def username
37
- " -U #{@uri.user}" if @uri.user
38
- end
39
-
40
- def configure_password
41
- ENV['PGPASSWORD'] = @uri.password if @uri.password
42
- end
43
- end
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,26 +0,0 @@
1
- require 'shellwords'
2
- module Hanami
3
- module Model
4
- module Adapters
5
- module Sql
6
- module Consoles
7
- class Sqlite
8
- def initialize(uri)
9
- @uri = uri
10
- end
11
-
12
- def connection_string
13
- "sqlite3 #{@uri.host}#{database}"
14
- end
15
-
16
- private
17
-
18
- def database
19
- Shellwords.escape(@uri.path)
20
- end
21
- end
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,788 +0,0 @@
1
- require 'forwardable'
2
- require 'hanami/utils/kernel'
3
-
4
- module Hanami
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: 'hanami')
20
- # .reverse_order(: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 Hanami::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 [Hanami::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 [Hanami::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
- # @raise [Hanami::Model::InvalidQueryError] if there is some issue when
74
- # hitting the database for fetching records
75
- #
76
- # @since 0.1.0
77
- def all
78
- run.to_a
79
- rescue Sequel::DatabaseError => e
80
- raise Hanami::Model::InvalidQueryError.new(e.message)
81
- end
82
-
83
- # Adds a SQL `WHERE` condition.
84
- #
85
- # It accepts a `Hash` with only one pair.
86
- # The key must be the name of the column expressed as a `Symbol`.
87
- # The value is the one used by the SQL query
88
- #
89
- # @param condition [Hash]
90
- #
91
- # @return self
92
- #
93
- # @since 0.1.0
94
- #
95
- # @example Fixed value
96
- #
97
- # query.where(language: 'ruby')
98
- #
99
- # # => SELECT * FROM `projects` WHERE (`language` = 'ruby')
100
- #
101
- # @example Array
102
- #
103
- # query.where(id: [1, 3])
104
- #
105
- # # => SELECT * FROM `articles` WHERE (`id` IN (1, 3))
106
- #
107
- # @example Range
108
- #
109
- # query.where(year: 1900..1982)
110
- #
111
- # # => SELECT * FROM `people` WHERE ((`year` >= 1900) AND (`year` <= 1982))
112
- #
113
- # @example Multiple conditions
114
- #
115
- # query.where(language: 'ruby')
116
- # .where(framework: 'hanami')
117
- #
118
- # # => SELECT * FROM `projects` WHERE (`language` = 'ruby') AND (`framework` = 'hanami')
119
- #
120
- # @example Expressions
121
- #
122
- # query.where{ age > 10 }
123
- #
124
- # # => SELECT * FROM `users` WHERE (`age` > 31)
125
- def where(condition = nil, &blk)
126
- _push_to_conditions(:where, condition || blk)
127
- self
128
- end
129
-
130
- alias_method :and, :where
131
-
132
- # Adds a SQL `OR` condition.
133
- #
134
- # It accepts a `Hash` with only one pair.
135
- # The key must be the name of the column expressed as a `Symbol`.
136
- # The value is the one used by the SQL query
137
- #
138
- # This condition will be ignored if not used with WHERE.
139
- #
140
- # @param condition [Hash]
141
- #
142
- # @return self
143
- #
144
- # @since 0.1.0
145
- #
146
- # @example Fixed value
147
- #
148
- # query.where(language: 'ruby').or(framework: 'hanami')
149
- #
150
- # # => SELECT * FROM `projects` WHERE ((`language` = 'ruby') OR (`framework` = 'hanami'))
151
- #
152
- # @example Array
153
- #
154
- # query.where(id: 1).or(author_id: [15, 23])
155
- #
156
- # # => SELECT * FROM `articles` WHERE ((`id` = 1) OR (`author_id` IN (15, 23)))
157
- #
158
- # @example Range
159
- #
160
- # query.where(country: 'italy').or(year: 1900..1982)
161
- #
162
- # # => SELECT * FROM `people` WHERE ((`country` = 'italy') OR ((`year` >= 1900) AND (`year` <= 1982)))
163
- #
164
- # @example Expressions
165
- #
166
- # query.where(name: 'John').or{ age > 31 }
167
- #
168
- # # => SELECT * FROM `users` WHERE ((`name` = 'John') OR (`age` < 32))
169
- def or(condition = nil, &blk)
170
- _push_to_conditions(:or, condition || blk)
171
- self
172
- end
173
-
174
- # Logical negation of a WHERE condition.
175
- #
176
- # It accepts a `Hash` with only one pair.
177
- # The key must be the name of the column expressed as a `Symbol`.
178
- # The value is the one used by the SQL query
179
- #
180
- # @param condition [Hash]
181
- #
182
- # @since 0.1.0
183
- #
184
- # @return self
185
- #
186
- # @example Fixed value
187
- #
188
- # query.exclude(language: 'java')
189
- #
190
- # # => SELECT * FROM `projects` WHERE (`language` != 'java')
191
- #
192
- # @example Array
193
- #
194
- # query.exclude(id: [4, 9])
195
- #
196
- # # => SELECT * FROM `articles` WHERE (`id` NOT IN (1, 3))
197
- #
198
- # @example Range
199
- #
200
- # query.exclude(year: 1900..1982)
201
- #
202
- # # => SELECT * FROM `people` WHERE ((`year` < 1900) AND (`year` > 1982))
203
- #
204
- # @example Multiple conditions
205
- #
206
- # query.exclude(language: 'java')
207
- # .exclude(company: 'enterprise')
208
- #
209
- # # => SELECT * FROM `projects` WHERE (`language` != 'java') AND (`company` != 'enterprise')
210
- #
211
- # @example Expressions
212
- #
213
- # query.exclude{ age > 31 }
214
- #
215
- # # => SELECT * FROM `users` WHERE (`age` <= 31)
216
- def exclude(condition = nil, &blk)
217
- _push_to_conditions(:exclude, condition || blk)
218
- self
219
- end
220
-
221
- alias_method :not, :exclude
222
-
223
- # Select only the specified columns.
224
- #
225
- # By default a query selects all the columns of a table (`SELECT *`).
226
- #
227
- # @param columns [Array<Symbol>]
228
- #
229
- # @return self
230
- #
231
- # @since 0.1.0
232
- #
233
- # @example Single column
234
- #
235
- # query.select(:name)
236
- #
237
- # # => SELECT `name` FROM `people`
238
- #
239
- # @example Multiple columns
240
- #
241
- # query.select(:name, :year)
242
- #
243
- # # => SELECT `name`, `year` FROM `people`
244
- def select(*columns)
245
- conditions.push([:select, *columns])
246
- self
247
- end
248
-
249
- # Limit the number of records to return.
250
- #
251
- # This operation is performed at the database level with `LIMIT`.
252
- #
253
- # @param number [Fixnum]
254
- #
255
- # @return self
256
- #
257
- # @since 0.1.0
258
- #
259
- # @example
260
- #
261
- # query.limit(1)
262
- #
263
- # # => SELECT * FROM `people` LIMIT 1
264
- def limit(number)
265
- conditions.push([:limit, number])
266
- self
267
- end
268
-
269
- # Specify an `OFFSET` clause.
270
- #
271
- # Due to SQL syntax restriction, offset MUST be used with `#limit`.
272
- #
273
- # @param number [Fixnum]
274
- #
275
- # @return self
276
- #
277
- # @since 0.1.0
278
- #
279
- # @see Hanami::Model::Adapters::Sql::Query#limit
280
- #
281
- # @example
282
- #
283
- # query.limit(1).offset(10)
284
- #
285
- # # => SELECT * FROM `people` LIMIT 1 OFFSET 10
286
- def offset(number)
287
- conditions.push([:offset, number])
288
- self
289
- end
290
-
291
- # Specify the ascending order of the records, sorted by the given
292
- # columns.
293
- #
294
- # @param columns [Array<Symbol>] the column names
295
- #
296
- # @return self
297
- #
298
- # @since 0.1.0
299
- #
300
- # @see Hanami::Model::Adapters::Sql::Query#reverse_order
301
- #
302
- # @example Single column
303
- #
304
- # query.order(:name)
305
- #
306
- # # => SELECT * FROM `people` ORDER BY (`name`)
307
- #
308
- # @example Multiple columns
309
- #
310
- # query.order(:name, :year)
311
- #
312
- # # => SELECT * FROM `people` ORDER BY `name`, `year`
313
- #
314
- # @example Multiple invokations
315
- #
316
- # query.order(:name).order(:year)
317
- #
318
- # # => SELECT * FROM `people` ORDER BY `name`, `year`
319
- def order(*columns)
320
- conditions.push([_order_operator, *columns])
321
- self
322
- end
323
-
324
- # Alias for order
325
- #
326
- # @since 0.1.0
327
- #
328
- # @see Hanami::Model::Adapters::Sql::Query#order
329
- #
330
- # @example Single column
331
- #
332
- # query.asc(:name)
333
- #
334
- # # => SELECT * FROM `people` ORDER BY (`name`)
335
- #
336
- # @example Multiple columns
337
- #
338
- # query.asc(:name, :year)
339
- #
340
- # # => SELECT * FROM `people` ORDER BY `name`, `year`
341
- #
342
- # @example Multiple invokations
343
- #
344
- # query.asc(:name).asc(:year)
345
- #
346
- # # => SELECT * FROM `people` ORDER BY `name`, `year`
347
- alias_method :asc, :order
348
-
349
- # Specify the descending order of the records, sorted by the given
350
- # columns.
351
- #
352
- # @param columns [Array<Symbol>] the column names
353
- #
354
- # @return self
355
- #
356
- # @since 0.3.1
357
- #
358
- # @see Hanami::Model::Adapters::Sql::Query#order
359
- #
360
- # @example Single column
361
- #
362
- # query.reverse_order(:name)
363
- #
364
- # # => SELECT * FROM `people` ORDER BY (`name`) DESC
365
- #
366
- # @example Multiple columns
367
- #
368
- # query.reverse_order(:name, :year)
369
- #
370
- # # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
371
- #
372
- # @example Multiple invokations
373
- #
374
- # query.reverse_order(:name).reverse_order(:year)
375
- #
376
- # # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
377
- def reverse_order(*columns)
378
- Array(columns).each do |column|
379
- conditions.push([_order_operator, Sequel.desc(column)])
380
- end
381
-
382
- self
383
- end
384
-
385
- # Alias for reverse_order
386
- #
387
- # @since 0.1.0
388
- #
389
- # @see Hanami::Model::Adapters::Sql::Query#reverse_order
390
- #
391
- # @example Single column
392
- #
393
- # query.desc(:name)
394
- #
395
- # @example Multiple columns
396
- #
397
- # query.desc(:name, :year)
398
- #
399
- # @example Multiple invokations
400
- #
401
- # query.desc(:name).desc(:year)
402
- alias_method :desc, :reverse_order
403
-
404
- # Group by the specified columns.
405
- #
406
- # @param columns [Array<Symbol>]
407
- #
408
- # @return self
409
- #
410
- # @since 0.5.0
411
- #
412
- # @example Single column
413
- #
414
- # query.group(:name)
415
- #
416
- # # => SELECT * FROM `people` GROUP BY `name`
417
- #
418
- # @example Multiple columns
419
- #
420
- # query.group(:name, :year)
421
- #
422
- # # => SELECT * FROM `people` GROUP BY `name`, `year`
423
- def group(*columns)
424
- conditions.push([:group, *columns])
425
- self
426
- end
427
-
428
- # Returns the sum of the values for the given column.
429
- #
430
- # @param column [Symbol] the column name
431
- #
432
- # @return [Numeric]
433
- #
434
- # @since 0.1.0
435
- #
436
- # @example
437
- #
438
- # query.sum(:comments_count)
439
- #
440
- # # => SELECT SUM(`comments_count`) FROM articles
441
- def sum(column)
442
- run.sum(column)
443
- end
444
-
445
- # Returns the average of the values for the given column.
446
- #
447
- # @param column [Symbol] the column name
448
- #
449
- # @return [Numeric]
450
- #
451
- # @since 0.1.0
452
- #
453
- # @example
454
- #
455
- # query.average(:comments_count)
456
- #
457
- # # => SELECT AVG(`comments_count`) FROM articles
458
- def average(column)
459
- run.avg(column)
460
- end
461
-
462
- alias_method :avg, :average
463
-
464
- # Returns the maximum value for the given column.
465
- #
466
- # @param column [Symbol] the column name
467
- #
468
- # @return result
469
- #
470
- # @since 0.1.0
471
- #
472
- # @example With numeric type
473
- #
474
- # query.max(:comments_count)
475
- #
476
- # # => SELECT MAX(`comments_count`) FROM articles
477
- #
478
- # @example With string type
479
- #
480
- # query.max(:title)
481
- #
482
- # # => SELECT MAX(`title`) FROM articles
483
- def max(column)
484
- run.max(column)
485
- end
486
-
487
- # Returns the minimum value for the given column.
488
- #
489
- # @param column [Symbol] the column name
490
- #
491
- # @return result
492
- #
493
- # @since 0.1.0
494
- #
495
- # @example With numeric type
496
- #
497
- # query.min(:comments_count)
498
- #
499
- # # => SELECT MIN(`comments_count`) FROM articles
500
- #
501
- # @example With string type
502
- #
503
- # query.min(:title)
504
- #
505
- # # => SELECT MIN(`title`) FROM articles
506
- def min(column)
507
- run.min(column)
508
- end
509
-
510
- # Returns the difference between the MAX and MIN for the given column.
511
- #
512
- # @param column [Symbol] the column name
513
- #
514
- # @return [Numeric]
515
- #
516
- # @since 0.1.0
517
- #
518
- # @see Hanami::Model::Adapters::Sql::Query#max
519
- # @see Hanami::Model::Adapters::Sql::Query#min
520
- #
521
- # @example
522
- #
523
- # query.interval(:comments_count)
524
- #
525
- # # => SELECT (MAX(`comments_count`) - MIN(`comments_count`)) FROM articles
526
- def interval(column)
527
- run.interval(column)
528
- end
529
-
530
- # Returns a range of values between the MAX and the MIN for the given
531
- # column.
532
- #
533
- # @param column [Symbol] the column name
534
- #
535
- # @return [Range]
536
- #
537
- # @since 0.1.0
538
- #
539
- # @see Hanami::Model::Adapters::Sql::Query#max
540
- # @see Hanami::Model::Adapters::Sql::Query#min
541
- #
542
- # @example
543
- #
544
- # query.range(:comments_count)
545
- #
546
- # # => SELECT MAX(`comments_count`) AS v1, MIN(`comments_count`) AS v2 FROM articles
547
- def range(column)
548
- run.range(column)
549
- end
550
-
551
- # Checks if at least one record exists for the current conditions.
552
- #
553
- # @return [TrueClass,FalseClass]
554
- #
555
- # @since 0.1.0
556
- #
557
- # @example
558
- #
559
- # query.where(author_id: 23).exists? # => true
560
- def exist?
561
- !count.zero?
562
- end
563
-
564
- # Returns a count of the records for the current conditions.
565
- #
566
- # @return [Fixnum]
567
- #
568
- # @since 0.1.0
569
- #
570
- # @example
571
- #
572
- # query.where(author_id: 23).count # => 5
573
- def count
574
- run.count
575
- end
576
-
577
- # Negates the current where/exclude conditions with the logical
578
- # opposite operator.
579
- #
580
- # All the other conditions will be ignored.
581
- #
582
- # @since 0.1.0
583
- #
584
- # @see Hanami::Model::Adapters::Sql::Query#where
585
- # @see Hanami::Model::Adapters::Sql::Query#exclude
586
- # @see Hanami::Repository#exclude
587
- #
588
- # @example
589
- #
590
- # query.where(language: 'java').negate!.all
591
- #
592
- # # => SELECT * FROM `projects` WHERE (`language` != 'java')
593
- def negate!
594
- conditions.map! do |(operator, condition)|
595
- [OPERATORS_MAPPING.fetch(operator) { operator }, condition]
596
- end
597
- end
598
-
599
- # Apply all the conditions and returns a filtered collection.
600
- #
601
- # This operation is idempotent, and the returned result didn't
602
- # fetched the records yet.
603
- #
604
- # @return [Hanami::Model::Adapters::Sql::Collection]
605
- #
606
- # @since 0.1.0
607
- def scoped
608
- scope = @collection
609
-
610
- conditions.each do |(method,*args)|
611
- scope = scope.public_send(method, *args)
612
- end
613
-
614
- scope
615
- end
616
-
617
- alias_method :run, :scoped
618
-
619
- # Specify an `INNER JOIN` clause.
620
- #
621
- # @param collection [String]
622
- # @param options [Hash]
623
- # @option key [Symbol] the key
624
- # @option foreign_key [Symbol] the foreign key
625
- #
626
- # @return self
627
- #
628
- # @since 0.5.0
629
- #
630
- # @example
631
- #
632
- # query.join(:users)
633
- #
634
- # # => SELECT * FROM `posts` INNER JOIN `users` ON `posts`.`user_id` = `users`.`id`
635
- def join(collection, options = {})
636
- _join(collection, options.merge(join: :inner))
637
- end
638
-
639
- alias_method :inner_join, :join
640
-
641
- # Specify a `LEFT JOIN` clause.
642
- #
643
- # @param collection [String]
644
- # @param options [Hash]
645
- # @option key [Symbol] the key
646
- # @option foreign_key [Symbol] the foreign key
647
- #
648
- # @return self
649
- #
650
- # @since 0.5.0
651
- #
652
- # @example
653
- #
654
- # query.left_join(:users)
655
- #
656
- # # => SELECT * FROM `posts` LEFT JOIN `users` ON `posts`.`user_id` = `users`.`id`
657
- def left_join(collection, options = {})
658
- _join(collection, options.merge(join: :left))
659
- end
660
-
661
- alias_method :left_outer_join, :left_join
662
-
663
- protected
664
- # Handles missing methods for query combinations
665
- #
666
- # @api private
667
- # @since 0.1.0
668
- #
669
- # @see Hanami::Model::Adapters:Sql::Query#apply
670
- def method_missing(m, *args, &blk)
671
- if @context.respond_to?(m)
672
- apply @context.public_send(m, *args, &blk)
673
- else
674
- super
675
- end
676
- end
677
-
678
- private
679
-
680
- # Specify a JOIN clause. (inner or left)
681
- #
682
- # @param collection [String]
683
- # @param options [Hash]
684
- # @option key [Symbol] the key
685
- # @option foreign_key [Symbol] the foreign key
686
- # @option join [Symbol] the join type
687
- #
688
- # @return self
689
- #
690
- # @api private
691
- # @since 0.5.0
692
- def _join(collection, options = {})
693
- collection_name = Utils::String.new(collection).singularize
694
-
695
- foreign_key = options.fetch(:foreign_key) { "#{ @collection.table_name }__#{ collection_name }_id".to_sym }
696
- key = options.fetch(:key) { @collection.identity.to_sym }
697
-
698
- conditions.push([:select_all])
699
- conditions.push([:join_table, options.fetch(:join, :inner), collection, key => foreign_key])
700
-
701
- self
702
- end
703
-
704
- # Returns a new query that is the result of the merge of the current
705
- # conditions with the ones of the given query.
706
- #
707
- # This is used to combine queries together in a Repository.
708
- #
709
- # @param query [Hanami::Model::Adapters::Sql::Query] the query to apply
710
- #
711
- # @return [Hanami::Model::Adapters::Sql::Query] a new query with the
712
- # merged conditions
713
- #
714
- # @api private
715
- # @since 0.1.0
716
- #
717
- # @example
718
- # require 'hanami/model'
719
- #
720
- # class ArticleRepository
721
- # include Hanami::Repository
722
- #
723
- # def self.by_author(author)
724
- # query do
725
- # where(author_id: author.id)
726
- # end
727
- # end
728
- #
729
- # def self.rank
730
- # query.reverse_order(:comments_count)
731
- # end
732
- #
733
- # def self.rank_by_author(author)
734
- # rank.by_author(author)
735
- # end
736
- # end
737
- #
738
- # # The code above combines two queries: `rank` and `by_author`.
739
- # #
740
- # # The first class method `rank` returns a `Sql::Query` instance
741
- # # which doesn't respond to `by_author`. How to solve this problem?
742
- # #
743
- # # 1. When we use `query` to fabricate a `Sql::Query` we pass the
744
- # # current context (the repository itself) to the query initializer.
745
- # #
746
- # # 2. When that query receives the `by_author` message, it's captured
747
- # # by `method_missing` and dispatched to the repository.
748
- # #
749
- # # 3. The class method `by_author` returns a query too.
750
- # #
751
- # # 4. We just return a new query that is the result of the current
752
- # # query's conditions (`rank`) and of the conditions from `by_author`.
753
- # #
754
- # # You're welcome ;)
755
- def apply(query)
756
- dup.tap do |result|
757
- result.conditions.push(*query.conditions)
758
- end
759
- end
760
-
761
- # Stores a query condition of a specified type in the conditions array.
762
- #
763
- # @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
764
- # @param condition [Hash, Proc] the query condition to be stored.
765
- #
766
- # @return [Array<Array>] the conditions array itself.
767
- #
768
- # @raise [ArgumentError] if condition is not specified.
769
- #
770
- # @api private
771
- # @since 0.3.1
772
- def _push_to_conditions(condition_type, condition)
773
- raise ArgumentError.new('You need to specify a condition.') if condition.nil?
774
- conditions.push([condition_type, condition])
775
- end
776
-
777
- def _order_operator
778
- if conditions.any? {|c, _| c == :order }
779
- :order_more
780
- else
781
- :order
782
- end
783
- end
784
- end
785
- end
786
- end
787
- end
788
- end