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,73 @@
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
+ # Initialize a command
13
+ #
14
+ # @param query [Hanami::Model::Adapters::Sql::Query]
15
+ #
16
+ # @api private
17
+ # @since 0.1.0
18
+ def initialize(query)
19
+ @collection = query.scoped
20
+ end
21
+
22
+ # Creates a record for the given entity.
23
+ #
24
+ # @param entity [Object] the entity to persist
25
+ #
26
+ # @see Hanami::Model::Adapters::Sql::Collection#insert
27
+ #
28
+ # @return the primary key of the just created record.
29
+ #
30
+ # @api private
31
+ # @since 0.1.0
32
+ def create(entity)
33
+ @collection.insert(entity)
34
+ rescue Sequel::DatabaseError => e
35
+ raise Hanami::Model::Error.new(e.message)
36
+ end
37
+
38
+ # Updates the corresponding record for the given entity.
39
+ #
40
+ # @param entity [Object] the entity to persist
41
+ #
42
+ # @see Hanami::Model::Adapters::Sql::Collection#update
43
+ #
44
+ # @api private
45
+ # @since 0.1.0
46
+ def update(entity)
47
+ @collection.update(entity)
48
+ rescue Sequel::DatabaseError => e
49
+ raise Hanami::Model::Error.new(e.message)
50
+ end
51
+
52
+ # Deletes all the records for the current query.
53
+ #
54
+ # It's used to delete a single record or an entire database table.
55
+ #
56
+ # @see Hanami::Model::Adapters::SqlAdapter#delete
57
+ # @see Hanami::Model::Adapters::SqlAdapter#clear
58
+ #
59
+ # @api private
60
+ # @since 0.1.0
61
+ def delete
62
+ @collection.delete
63
+ rescue Sequel::DatabaseError => e
64
+ raise Hanami::Model::Error.new(e.message)
65
+ end
66
+
67
+ alias_method :clear, :delete
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,49 @@
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
+
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,788 @@
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