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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/EXAMPLE.md +212 -0
- data/LICENSE.md +22 -0
- data/README.md +600 -7
- data/hanami-model.gemspec +17 -12
- data/lib/hanami-model.rb +1 -0
- data/lib/hanami/entity.rb +298 -0
- data/lib/hanami/entity/dirty_tracking.rb +74 -0
- data/lib/hanami/model.rb +204 -2
- data/lib/hanami/model/adapters/abstract.rb +281 -0
- data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
- data/lib/hanami/model/adapters/implementation.rb +111 -0
- data/lib/hanami/model/adapters/memory/collection.rb +132 -0
- data/lib/hanami/model/adapters/memory/command.rb +113 -0
- data/lib/hanami/model/adapters/memory/query.rb +653 -0
- data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
- data/lib/hanami/model/adapters/null_adapter.rb +24 -0
- data/lib/hanami/model/adapters/sql/collection.rb +287 -0
- data/lib/hanami/model/adapters/sql/command.rb +73 -0
- data/lib/hanami/model/adapters/sql/console.rb +33 -0
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
- data/lib/hanami/model/adapters/sql/query.rb +788 -0
- data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
- data/lib/hanami/model/coercer.rb +74 -0
- data/lib/hanami/model/config/adapter.rb +116 -0
- data/lib/hanami/model/config/mapper.rb +45 -0
- data/lib/hanami/model/configuration.rb +275 -0
- data/lib/hanami/model/error.rb +7 -0
- data/lib/hanami/model/mapper.rb +124 -0
- data/lib/hanami/model/mapping.rb +48 -0
- data/lib/hanami/model/mapping/attribute.rb +85 -0
- data/lib/hanami/model/mapping/coercers.rb +314 -0
- data/lib/hanami/model/mapping/collection.rb +490 -0
- data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
- data/lib/hanami/model/migrator.rb +324 -0
- data/lib/hanami/model/migrator/adapter.rb +170 -0
- data/lib/hanami/model/migrator/connection.rb +133 -0
- data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
- data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
- data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
- data/lib/hanami/model/version.rb +4 -1
- data/lib/hanami/repository.rb +872 -0
- metadata +100 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- 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
|