lotus-model 0.0.0 → 0.1.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/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/.yardopts +5 -0
- data/EXAMPLE.md +217 -0
- data/Gemfile +14 -2
- data/README.md +303 -3
- data/Rakefile +17 -1
- data/lib/lotus-model.rb +1 -0
- data/lib/lotus/entity.rb +157 -0
- data/lib/lotus/model.rb +23 -2
- data/lib/lotus/model/adapters/abstract.rb +167 -0
- data/lib/lotus/model/adapters/implementation.rb +111 -0
- data/lib/lotus/model/adapters/memory/collection.rb +132 -0
- data/lib/lotus/model/adapters/memory/command.rb +90 -0
- data/lib/lotus/model/adapters/memory/query.rb +457 -0
- data/lib/lotus/model/adapters/memory_adapter.rb +149 -0
- data/lib/lotus/model/adapters/sql/collection.rb +209 -0
- data/lib/lotus/model/adapters/sql/command.rb +67 -0
- data/lib/lotus/model/adapters/sql/query.rb +615 -0
- data/lib/lotus/model/adapters/sql_adapter.rb +154 -0
- data/lib/lotus/model/mapper.rb +101 -0
- data/lib/lotus/model/mapping.rb +23 -0
- data/lib/lotus/model/mapping/coercer.rb +80 -0
- data/lib/lotus/model/mapping/collection.rb +336 -0
- data/lib/lotus/model/version.rb +4 -1
- data/lib/lotus/repository.rb +620 -0
- data/lotus-model.gemspec +15 -11
- data/test/entity_test.rb +126 -0
- data/test/fixtures.rb +81 -0
- data/test/model/adapters/abstract_test.rb +75 -0
- data/test/model/adapters/implementation_test.rb +22 -0
- data/test/model/adapters/memory/query_test.rb +91 -0
- data/test/model/adapters/memory_adapter_test.rb +1044 -0
- data/test/model/adapters/sql/query_test.rb +121 -0
- data/test/model/adapters/sql_adapter_test.rb +1078 -0
- data/test/model/mapper_test.rb +94 -0
- data/test/model/mapping/coercer_test.rb +27 -0
- data/test/model/mapping/collection_test.rb +82 -0
- data/test/repository_test.rb +283 -0
- data/test/test_helper.rb +30 -0
- data/test/version_test.rb +7 -0
- metadata +109 -11
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'lotus/model/adapters/abstract'
|
2
|
+
require 'lotus/model/adapters/implementation'
|
3
|
+
require 'lotus/model/adapters/memory/collection'
|
4
|
+
require 'lotus/model/adapters/memory/command'
|
5
|
+
require 'lotus/model/adapters/memory/query'
|
6
|
+
|
7
|
+
module Lotus
|
8
|
+
module Model
|
9
|
+
module Adapters
|
10
|
+
# In memory adapter that behaves like a SQL database.
|
11
|
+
# Not all the features of the SQL adapter are supported.
|
12
|
+
#
|
13
|
+
# This adapter SHOULD be used only for development or testing purposes,
|
14
|
+
# because its computations are inefficient and the data is volatile.
|
15
|
+
#
|
16
|
+
# @see Lotus::Model::Adapters::Implementation
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
# @since 0.1.0
|
20
|
+
class MemoryAdapter < Abstract
|
21
|
+
include Implementation
|
22
|
+
|
23
|
+
# Initialize the adapter.
|
24
|
+
#
|
25
|
+
# @param mapper [Object] the database mapper
|
26
|
+
# @param uri [String] the connection uri (ignored)
|
27
|
+
#
|
28
|
+
# @return [Lotus::Model::Adapters::MemoryAdapter]
|
29
|
+
#
|
30
|
+
# @see Lotus::Model::Mapper
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
# @since 0.1.0
|
34
|
+
def initialize(mapper, uri = nil)
|
35
|
+
super
|
36
|
+
|
37
|
+
@mutex = Mutex.new
|
38
|
+
@collections = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Creates a record in the database for the given entity.
|
42
|
+
# It assigns the `id` attribute, in case of success.
|
43
|
+
#
|
44
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
45
|
+
# @param entity [#id=] the entity to create
|
46
|
+
#
|
47
|
+
# @return [Object] the entity
|
48
|
+
#
|
49
|
+
# @api private
|
50
|
+
# @since 0.1.0
|
51
|
+
def create(collection, entity)
|
52
|
+
@mutex.synchronize do
|
53
|
+
entity.id = command(collection).create(entity)
|
54
|
+
entity
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Updates a record in the database corresponding to the given entity.
|
59
|
+
#
|
60
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
61
|
+
# @param entity [#id] the entity to update
|
62
|
+
#
|
63
|
+
# @return [Object] the entity
|
64
|
+
#
|
65
|
+
# @api private
|
66
|
+
# @since 0.1.0
|
67
|
+
def update(collection, entity)
|
68
|
+
@mutex.synchronize do
|
69
|
+
command(collection).update(entity)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Deletes a record in the database corresponding to the given entity.
|
74
|
+
#
|
75
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
76
|
+
# @param entity [#id] the entity to delete
|
77
|
+
#
|
78
|
+
# @api private
|
79
|
+
# @since 0.1.0
|
80
|
+
def delete(collection, entity)
|
81
|
+
@mutex.synchronize do
|
82
|
+
command(collection).delete(entity)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Deletes all the records from the given collection and resets the
|
87
|
+
# identity counter.
|
88
|
+
#
|
89
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
# @since 0.1.0
|
93
|
+
def clear(collection)
|
94
|
+
@mutex.synchronize do
|
95
|
+
command(collection).clear
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Fabricates a command for the given query.
|
100
|
+
#
|
101
|
+
# @param collection [Symbol] the collection name (it must be mapped)
|
102
|
+
#
|
103
|
+
# @return [Lotus::Model::Adapters::Memory::Command]
|
104
|
+
#
|
105
|
+
# @see Lotus::Model::Adapters::Memory::Command
|
106
|
+
#
|
107
|
+
# @api private
|
108
|
+
# @since 0.1.0
|
109
|
+
def command(collection)
|
110
|
+
Memory::Command.new(_collection(collection), _mapped_collection(collection))
|
111
|
+
end
|
112
|
+
|
113
|
+
# Fabricates a query
|
114
|
+
#
|
115
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
116
|
+
# @param blk [Proc] a block of code to be executed in the context of
|
117
|
+
# the query.
|
118
|
+
#
|
119
|
+
# @return [Lotus::Model::Adapters::Memory::Query]
|
120
|
+
#
|
121
|
+
# @see Lotus::Model::Adapters::Memory::Query
|
122
|
+
#
|
123
|
+
# @api private
|
124
|
+
# @since 0.1.0
|
125
|
+
def query(collection, context = nil, &blk)
|
126
|
+
@mutex.synchronize do
|
127
|
+
Memory::Query.new(_collection(collection), _mapped_collection(collection), &blk)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Returns a collection from the given name.
|
134
|
+
#
|
135
|
+
# @param name [Symbol] a name of the collection (it must be mapped).
|
136
|
+
#
|
137
|
+
# @return [Lotus::Model::Adapters::Memory::Collection]
|
138
|
+
#
|
139
|
+
# @see Lotus::Model::Adapters::Memory::Collection
|
140
|
+
#
|
141
|
+
# @api private
|
142
|
+
# @since 0.1.0
|
143
|
+
def _collection(name)
|
144
|
+
@collections[name] ||= Memory::Collection.new(name, _identity(name))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'lotus/utils/kernel' unless RUBY_VERSION >= '2.1'
|
3
|
+
|
4
|
+
module Lotus
|
5
|
+
module Model
|
6
|
+
module Adapters
|
7
|
+
module Sql
|
8
|
+
# Maps a SQL database table and perfoms manipulations on it.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
# @since 0.1.0
|
12
|
+
#
|
13
|
+
# @see http://sequel.jeremyevans.net/rdoc/files/doc/dataset_basics_rdoc.html
|
14
|
+
# @see http://sequel.jeremyevans.net/rdoc/files/doc/dataset_filtering_rdoc.html
|
15
|
+
class Collection < SimpleDelegator
|
16
|
+
# Initialize a collection
|
17
|
+
#
|
18
|
+
# @param dataset [Sequel::Dataset] the dataset that maps a table or a
|
19
|
+
# subset of it.
|
20
|
+
# @param collection [Lotus::Model::Mapping::Collection] a mapped
|
21
|
+
# collection
|
22
|
+
#
|
23
|
+
# @return [Lotus::Model::Adapters::Sql::Collection]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
# @since 0.1.0
|
27
|
+
def initialize(dataset, collection)
|
28
|
+
super(dataset)
|
29
|
+
@collection = collection
|
30
|
+
end
|
31
|
+
|
32
|
+
# Filters the current scope with an `exclude` directive.
|
33
|
+
#
|
34
|
+
# @param args [Array] the array of arguments
|
35
|
+
#
|
36
|
+
# @see Lotus::Model::Adapters::Sql::Query#exclude
|
37
|
+
#
|
38
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
39
|
+
# collection
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
# @since 0.1.0
|
43
|
+
def exclude(*args)
|
44
|
+
Collection.new(super, @collection)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates a record for the given entity and assigns an id.
|
48
|
+
#
|
49
|
+
# @param entity [Object] the entity to persist
|
50
|
+
#
|
51
|
+
# @see Lotus::Model::Adapters::Sql::Command#create
|
52
|
+
#
|
53
|
+
# @return the primary key of the created record
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
# @since 0.1.0
|
57
|
+
def insert(entity)
|
58
|
+
super _serialize(entity)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Filters the current scope with an `limit` directive.
|
62
|
+
#
|
63
|
+
# @param args [Array] the array of arguments
|
64
|
+
#
|
65
|
+
# @see Lotus::Model::Adapters::Sql::Query#limit
|
66
|
+
#
|
67
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
68
|
+
# collection
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
# @since 0.1.0
|
72
|
+
def limit(*args)
|
73
|
+
Collection.new(super, @collection)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Filters the current scope with an `offset` directive.
|
77
|
+
#
|
78
|
+
# @param args [Array] the array of arguments
|
79
|
+
#
|
80
|
+
# @see Lotus::Model::Adapters::Sql::Query#offset
|
81
|
+
#
|
82
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
83
|
+
# collection
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
# @since 0.1.0
|
87
|
+
def offset(*args)
|
88
|
+
Collection.new(super, @collection)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Filters the current scope with an `or` directive.
|
92
|
+
#
|
93
|
+
# @param args [Array] the array of arguments
|
94
|
+
#
|
95
|
+
# @see Lotus::Model::Adapters::Sql::Query#or
|
96
|
+
#
|
97
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
98
|
+
# collection
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
# @since 0.1.0
|
102
|
+
def or(*args)
|
103
|
+
Collection.new(super, @collection)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Filters the current scope with an `order` directive.
|
107
|
+
#
|
108
|
+
# @param args [Array] the array of arguments
|
109
|
+
#
|
110
|
+
# @see Lotus::Model::Adapters::Sql::Query#order
|
111
|
+
#
|
112
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
113
|
+
# collection
|
114
|
+
#
|
115
|
+
# @api private
|
116
|
+
# @since 0.1.0
|
117
|
+
def order(*args)
|
118
|
+
Collection.new(super, @collection)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Filters the current scope with an `order` directive.
|
122
|
+
#
|
123
|
+
# @param args [Array] the array of arguments
|
124
|
+
#
|
125
|
+
# @see Lotus::Model::Adapters::Sql::Query#order
|
126
|
+
#
|
127
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
128
|
+
# collection
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
# @since 0.1.0
|
132
|
+
def order_more(*args)
|
133
|
+
Collection.new(super, @collection)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Filters the current scope with an `select` directive.
|
137
|
+
#
|
138
|
+
# @param args [Array] the array of arguments
|
139
|
+
#
|
140
|
+
# @see Lotus::Model::Adapters::Sql::Query#select
|
141
|
+
#
|
142
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
143
|
+
# collection
|
144
|
+
#
|
145
|
+
# @api private
|
146
|
+
# @since 0.1.0
|
147
|
+
if RUBY_VERSION >= '2.1'
|
148
|
+
def select(*args)
|
149
|
+
Collection.new(super, @collection)
|
150
|
+
end
|
151
|
+
else
|
152
|
+
def select(*args)
|
153
|
+
Collection.new(__getobj__.select(*Lotus::Utils::Kernel.Array(args)), @collection)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Filters the current scope with an `where` directive.
|
158
|
+
#
|
159
|
+
# @param args [Array] the array of arguments
|
160
|
+
#
|
161
|
+
# @see Lotus::Model::Adapters::Sql::Query#where
|
162
|
+
#
|
163
|
+
# @return [Lotus::Model::Adapters::Sql::Collection] the filtered
|
164
|
+
# collection
|
165
|
+
#
|
166
|
+
# @api private
|
167
|
+
# @since 0.1.0
|
168
|
+
def where(*args)
|
169
|
+
Collection.new(super, @collection)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Updates the record corresponding to the given entity.
|
173
|
+
#
|
174
|
+
# @param entity [Object] the entity to persist
|
175
|
+
#
|
176
|
+
# @see Lotus::Model::Adapters::Sql::Command#update
|
177
|
+
#
|
178
|
+
# @api private
|
179
|
+
# @since 0.1.0
|
180
|
+
def update(entity)
|
181
|
+
super _serialize(entity)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Resolves self by fetching the records from the database and
|
185
|
+
# translating them into entities.
|
186
|
+
#
|
187
|
+
# @return [Array] the result of the query
|
188
|
+
#
|
189
|
+
# @api private
|
190
|
+
# @since 0.1.0
|
191
|
+
def to_a
|
192
|
+
@collection.deserialize(self)
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
# Serialize the given entity before to persist in the database.
|
197
|
+
#
|
198
|
+
# @return [Hash] the serialized entity
|
199
|
+
#
|
200
|
+
# @api private
|
201
|
+
# @since 0.1.0
|
202
|
+
def _serialize(entity)
|
203
|
+
@collection.serialize(entity)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Model
|
3
|
+
module Adapters
|
4
|
+
module Sql
|
5
|
+
# Execute a command for the given query.
|
6
|
+
#
|
7
|
+
# @see Lotus::Model::Adapters::Sql::Query
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
# @since 0.1.0
|
11
|
+
class Command
|
12
|
+
# Initialize a command
|
13
|
+
#
|
14
|
+
# @param query [Lotus::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 Lotus::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
|
+
end
|
35
|
+
|
36
|
+
# Updates the corresponding record for the given entity.
|
37
|
+
#
|
38
|
+
# @param entity [Object] the entity to persist
|
39
|
+
#
|
40
|
+
# @see Lotus::Model::Adapters::Sql::Collection#update
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
# @since 0.1.0
|
44
|
+
def update(entity)
|
45
|
+
@collection.update(entity)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Deletes all the records for the current query.
|
49
|
+
#
|
50
|
+
# It's used to delete a single record or an entire database table.
|
51
|
+
#
|
52
|
+
# @see Lotus::Model::Adapters::SqlAdapter#delete
|
53
|
+
# @see Lotus::Model::Adapters::SqlAdapter#clear
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
# @since 0.1.0
|
57
|
+
def delete
|
58
|
+
@collection.delete
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :clear, :delete
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,615 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'lotus/utils/kernel'
|
3
|
+
|
4
|
+
module Lotus
|
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: 'lotus')
|
20
|
+
# .desc(: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 Lotus::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 [Lotus::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 [Lotus::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
|
+
# @since 0.1.0
|
74
|
+
def all
|
75
|
+
Lotus::Utils::Kernel.Array(run)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Adds a SQL `WHERE` condition.
|
79
|
+
#
|
80
|
+
# It accepts a `Hash` with only one pair.
|
81
|
+
# The key must be the name of the column expressed as a `Symbol`.
|
82
|
+
# The value is the one used by the SQL query
|
83
|
+
#
|
84
|
+
# @param condition [Hash]
|
85
|
+
#
|
86
|
+
# @return self
|
87
|
+
#
|
88
|
+
# @since 0.1.0
|
89
|
+
#
|
90
|
+
# @example Fixed value
|
91
|
+
#
|
92
|
+
# query.where(language: 'ruby')
|
93
|
+
#
|
94
|
+
# # => SELECT * FROM `projects` WHERE (`language` = 'ruby')
|
95
|
+
#
|
96
|
+
# @example Array
|
97
|
+
#
|
98
|
+
# query.where(id: [1, 3])
|
99
|
+
#
|
100
|
+
# # => SELECT * FROM `articles` WHERE (`id` IN (1, 3))
|
101
|
+
#
|
102
|
+
# @example Range
|
103
|
+
#
|
104
|
+
# query.where(year: 1900..1982)
|
105
|
+
#
|
106
|
+
# # => SELECT * FROM `people` WHERE ((`year` >= 1900) AND (`year` <= 1982))
|
107
|
+
#
|
108
|
+
# @example Multiple conditions
|
109
|
+
#
|
110
|
+
# query.where(language: 'ruby')
|
111
|
+
# .where(framework: 'lotus')
|
112
|
+
#
|
113
|
+
# # => SELECT * FROM `projects` WHERE (`language` = 'ruby') AND (`framework` = 'lotus')
|
114
|
+
def where(condition)
|
115
|
+
conditions.push([:where, condition])
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :and, :where
|
120
|
+
|
121
|
+
# Adds a SQL `OR` condition.
|
122
|
+
#
|
123
|
+
# It accepts a `Hash` with only one pair.
|
124
|
+
# The key must be the name of the column expressed as a `Symbol`.
|
125
|
+
# The value is the one used by the SQL query
|
126
|
+
#
|
127
|
+
# This condition will be ignored if not used with WHERE.
|
128
|
+
#
|
129
|
+
# @param condition [Hash]
|
130
|
+
#
|
131
|
+
# @return self
|
132
|
+
#
|
133
|
+
# @since 0.1.0
|
134
|
+
#
|
135
|
+
# @example Fixed value
|
136
|
+
#
|
137
|
+
# query.where(language: 'ruby').or(framework: 'lotus')
|
138
|
+
#
|
139
|
+
# # => SELECT * FROM `projects` WHERE ((`language` = 'ruby') OR (`framework` = 'lotus'))
|
140
|
+
#
|
141
|
+
# @example Array
|
142
|
+
#
|
143
|
+
# query.where(id: 1).or(author_id: [15, 23])
|
144
|
+
#
|
145
|
+
# # => SELECT * FROM `articles` WHERE ((`id` = 1) OR (`author_id` IN (15, 23)))
|
146
|
+
#
|
147
|
+
# @example Range
|
148
|
+
#
|
149
|
+
# query.where(country: 'italy').or(year: 1900..1982)
|
150
|
+
#
|
151
|
+
# # => SELECT * FROM `people` WHERE ((`country` = 'italy') OR ((`year` >= 1900) AND (`year` <= 1982)))
|
152
|
+
def or(condition)
|
153
|
+
conditions.push([:or, condition])
|
154
|
+
self
|
155
|
+
end
|
156
|
+
|
157
|
+
# Logical negation of a WHERE condition.
|
158
|
+
#
|
159
|
+
# It accepts a `Hash` with only one pair.
|
160
|
+
# The key must be the name of the column expressed as a `Symbol`.
|
161
|
+
# The value is the one used by the SQL query
|
162
|
+
#
|
163
|
+
# @param condition [Hash]
|
164
|
+
#
|
165
|
+
# @since 0.1.0
|
166
|
+
#
|
167
|
+
# @return self
|
168
|
+
#
|
169
|
+
# @example Fixed value
|
170
|
+
#
|
171
|
+
# query.exclude(language: 'java')
|
172
|
+
#
|
173
|
+
# # => SELECT * FROM `projects` WHERE (`language` != 'java')
|
174
|
+
#
|
175
|
+
# @example Array
|
176
|
+
#
|
177
|
+
# query.exclude(id: [4, 9])
|
178
|
+
#
|
179
|
+
# # => SELECT * FROM `articles` WHERE (`id` NOT IN (1, 3))
|
180
|
+
#
|
181
|
+
# @example Range
|
182
|
+
#
|
183
|
+
# query.where(year: 1900..1982)
|
184
|
+
#
|
185
|
+
# # => SELECT * FROM `people` WHERE ((`year` < 1900) AND (`year` > 1982))
|
186
|
+
#
|
187
|
+
# @example Multiple conditions
|
188
|
+
#
|
189
|
+
# query.where(language: 'java')
|
190
|
+
# .where(company: 'enterprise')
|
191
|
+
#
|
192
|
+
# # => SELECT * FROM `projects` WHERE (`language` != 'java') AND (`company` != 'enterprise')
|
193
|
+
def exclude(condition)
|
194
|
+
conditions.push([:exclude, condition])
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
alias_method :not, :exclude
|
199
|
+
|
200
|
+
# Select only the specified columns.
|
201
|
+
#
|
202
|
+
# By default a query selects all the columns of a table (`SELECT *`).
|
203
|
+
#
|
204
|
+
# @param columns [Array<Symbol>]
|
205
|
+
#
|
206
|
+
# @return self
|
207
|
+
#
|
208
|
+
# @since 0.1.0
|
209
|
+
#
|
210
|
+
# @example Single column
|
211
|
+
#
|
212
|
+
# query.select(:name)
|
213
|
+
#
|
214
|
+
# # => SELECT `name` FROM `people`
|
215
|
+
#
|
216
|
+
# @example Multiple columns
|
217
|
+
#
|
218
|
+
# query.select(:name, :year)
|
219
|
+
#
|
220
|
+
# # => SELECT `name`, `year` FROM `people`
|
221
|
+
def select(*columns)
|
222
|
+
conditions.push([:select, *columns])
|
223
|
+
self
|
224
|
+
end
|
225
|
+
|
226
|
+
# Limit the number of records to return.
|
227
|
+
#
|
228
|
+
# This operation is performed at the database level with `LIMIT`.
|
229
|
+
#
|
230
|
+
# @param number [Fixnum]
|
231
|
+
#
|
232
|
+
# @return self
|
233
|
+
#
|
234
|
+
# @since 0.1.0
|
235
|
+
#
|
236
|
+
# @example
|
237
|
+
#
|
238
|
+
# query.limit(1)
|
239
|
+
#
|
240
|
+
# # => SELECT * FROM `people` LIMIT 1
|
241
|
+
def limit(number)
|
242
|
+
conditions.push([:limit, number])
|
243
|
+
self
|
244
|
+
end
|
245
|
+
|
246
|
+
# Specify an `OFFSET` clause.
|
247
|
+
#
|
248
|
+
# Due to SQL syntax restriction, offset MUST be used with `#limit`.
|
249
|
+
#
|
250
|
+
# @param number [Fixnum]
|
251
|
+
#
|
252
|
+
# @return self
|
253
|
+
#
|
254
|
+
# @since 0.1.0
|
255
|
+
#
|
256
|
+
# @see Lotus::Model::Adapters::Sql::Query#limit
|
257
|
+
#
|
258
|
+
# @example
|
259
|
+
#
|
260
|
+
# query.limit(1).offset(10)
|
261
|
+
#
|
262
|
+
# # => SELECT * FROM `people` LIMIT 1 OFFSET 10
|
263
|
+
def offset(number)
|
264
|
+
conditions.push([:offset, number])
|
265
|
+
self
|
266
|
+
end
|
267
|
+
|
268
|
+
# Specify the ascending order of the records, sorted by the given
|
269
|
+
# columns.
|
270
|
+
#
|
271
|
+
# @param columns [Array<Symbol>] the column names
|
272
|
+
#
|
273
|
+
# @return self
|
274
|
+
#
|
275
|
+
# @since 0.1.0
|
276
|
+
#
|
277
|
+
# @see Lotus::Model::Adapters::Sql::Query#desc
|
278
|
+
#
|
279
|
+
# @example Single column
|
280
|
+
#
|
281
|
+
# query.order(:name)
|
282
|
+
#
|
283
|
+
# # => SELECT * FROM `people` ORDER BY (`name`)
|
284
|
+
#
|
285
|
+
# @example Multiple columns
|
286
|
+
#
|
287
|
+
# query.order(:name, :year)
|
288
|
+
#
|
289
|
+
# # => SELECT * FROM `people` ORDER BY `name`, `year`
|
290
|
+
#
|
291
|
+
# @example Multiple invokations
|
292
|
+
#
|
293
|
+
# query.order(:name).order(:year)
|
294
|
+
#
|
295
|
+
# # => SELECT * FROM `people` ORDER BY `name`, `year`
|
296
|
+
def order(*columns)
|
297
|
+
conditions.push([_order_operator, *columns])
|
298
|
+
self
|
299
|
+
end
|
300
|
+
|
301
|
+
alias_method :asc, :order
|
302
|
+
|
303
|
+
# Specify the descending order of the records, sorted by the given
|
304
|
+
# columns.
|
305
|
+
#
|
306
|
+
# @param columns [Array<Symbol>] the column names
|
307
|
+
#
|
308
|
+
# @return self
|
309
|
+
#
|
310
|
+
# @since 0.1.0
|
311
|
+
#
|
312
|
+
# @see Lotus::Model::Adapters::Sql::Query#order
|
313
|
+
#
|
314
|
+
# @example Single column
|
315
|
+
#
|
316
|
+
# query.desc(:name)
|
317
|
+
#
|
318
|
+
# # => SELECT * FROM `people` ORDER BY (`name`) DESC
|
319
|
+
#
|
320
|
+
# @example Multiple columns
|
321
|
+
#
|
322
|
+
# query.desc(:name, :year)
|
323
|
+
#
|
324
|
+
# # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
|
325
|
+
#
|
326
|
+
# @example Multiple invokations
|
327
|
+
#
|
328
|
+
# query.desc(:name).desc(:year)
|
329
|
+
#
|
330
|
+
# # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
|
331
|
+
def desc(*columns)
|
332
|
+
Array(columns).each do |column|
|
333
|
+
conditions.push([_order_operator, Sequel.desc(column)])
|
334
|
+
end
|
335
|
+
|
336
|
+
self
|
337
|
+
end
|
338
|
+
|
339
|
+
# Returns the sum of the values for the given column.
|
340
|
+
#
|
341
|
+
# @param column [Symbol] the colum name
|
342
|
+
#
|
343
|
+
# @return [Numeric]
|
344
|
+
#
|
345
|
+
# @since 0.1.0
|
346
|
+
#
|
347
|
+
# @example
|
348
|
+
#
|
349
|
+
# query.sum(:comments_count)
|
350
|
+
#
|
351
|
+
# # => SELECT SUM(`comments_count`) FROM articles
|
352
|
+
def sum(column)
|
353
|
+
run.sum(column)
|
354
|
+
end
|
355
|
+
|
356
|
+
# Returns the average of the values for the given column.
|
357
|
+
#
|
358
|
+
# @param column [Symbol] the colum name
|
359
|
+
#
|
360
|
+
# @return [Numeric]
|
361
|
+
#
|
362
|
+
# @since 0.1.0
|
363
|
+
#
|
364
|
+
# @example
|
365
|
+
#
|
366
|
+
# query.average(:comments_count)
|
367
|
+
#
|
368
|
+
# # => SELECT AVG(`comments_count`) FROM articles
|
369
|
+
def average(column)
|
370
|
+
run.avg(column)
|
371
|
+
end
|
372
|
+
|
373
|
+
alias_method :avg, :average
|
374
|
+
|
375
|
+
# Returns the maximum value for the given column.
|
376
|
+
#
|
377
|
+
# @param column [Symbol] the colum name
|
378
|
+
#
|
379
|
+
# @return result
|
380
|
+
#
|
381
|
+
# @since 0.1.0
|
382
|
+
#
|
383
|
+
# @example With numeric type
|
384
|
+
#
|
385
|
+
# query.max(:comments_count)
|
386
|
+
#
|
387
|
+
# # => SELECT MAX(`comments_count`) FROM articles
|
388
|
+
#
|
389
|
+
# @example With string type
|
390
|
+
#
|
391
|
+
# query.max(:title)
|
392
|
+
#
|
393
|
+
# # => SELECT MAX(`title`) FROM articles
|
394
|
+
def max(column)
|
395
|
+
run.max(column)
|
396
|
+
end
|
397
|
+
|
398
|
+
# Returns the minimum value for the given column.
|
399
|
+
#
|
400
|
+
# @param column [Symbol] the colum name
|
401
|
+
#
|
402
|
+
# @return result
|
403
|
+
#
|
404
|
+
# @since 0.1.0
|
405
|
+
#
|
406
|
+
# @example With numeric type
|
407
|
+
#
|
408
|
+
# query.min(:comments_count)
|
409
|
+
#
|
410
|
+
# # => SELECT MIN(`comments_count`) FROM articles
|
411
|
+
#
|
412
|
+
# @example With string type
|
413
|
+
#
|
414
|
+
# query.min(:title)
|
415
|
+
#
|
416
|
+
# # => SELECT MIN(`title`) FROM articles
|
417
|
+
def min(column)
|
418
|
+
run.min(column)
|
419
|
+
end
|
420
|
+
|
421
|
+
# Returns the difference between the MAX and MIN for the given column.
|
422
|
+
#
|
423
|
+
# @param column [Symbol] the colum name
|
424
|
+
#
|
425
|
+
# @return [Numeric]
|
426
|
+
#
|
427
|
+
# @since 0.1.0
|
428
|
+
#
|
429
|
+
# @see Lotus::Model::Adapters::Sql::Query#max
|
430
|
+
# @see Lotus::Model::Adapters::Sql::Query#min
|
431
|
+
#
|
432
|
+
# @example
|
433
|
+
#
|
434
|
+
# query.interval(:comments_count)
|
435
|
+
#
|
436
|
+
# # => SELECT (MAX(`comments_count`) - MIN(`comments_count`)) FROM articles
|
437
|
+
def interval(column)
|
438
|
+
run.interval(column)
|
439
|
+
end
|
440
|
+
|
441
|
+
# Returns a range of values between the MAX and the MIN for the given
|
442
|
+
# column.
|
443
|
+
#
|
444
|
+
# @param column [Symbol] the colum name
|
445
|
+
#
|
446
|
+
# @return [Range]
|
447
|
+
#
|
448
|
+
# @since 0.1.0
|
449
|
+
#
|
450
|
+
# @see Lotus::Model::Adapters::Sql::Query#max
|
451
|
+
# @see Lotus::Model::Adapters::Sql::Query#min
|
452
|
+
#
|
453
|
+
# @example
|
454
|
+
#
|
455
|
+
# query.range(:comments_count)
|
456
|
+
#
|
457
|
+
# # => SELECT MAX(`comments_count`) AS v1, MIN(`comments_count`) AS v2 FROM articles
|
458
|
+
def range(column)
|
459
|
+
run.range(column)
|
460
|
+
end
|
461
|
+
|
462
|
+
# Checks if at least one record exists for the current conditions.
|
463
|
+
#
|
464
|
+
# @return [TrueClass,FalseClass]
|
465
|
+
#
|
466
|
+
# @since 0.1.0
|
467
|
+
#
|
468
|
+
# @example
|
469
|
+
#
|
470
|
+
# query.where(author_id: 23).exists? # => true
|
471
|
+
def exist?
|
472
|
+
!count.zero?
|
473
|
+
end
|
474
|
+
|
475
|
+
# Returns a count of the records for the current conditions.
|
476
|
+
#
|
477
|
+
# @return [Fixnum]
|
478
|
+
#
|
479
|
+
# @since 0.1.0
|
480
|
+
#
|
481
|
+
# @example
|
482
|
+
#
|
483
|
+
# query.where(author_id: 23).count # => 5
|
484
|
+
def count
|
485
|
+
run.count
|
486
|
+
end
|
487
|
+
|
488
|
+
# Negates the current where/exclude conditions with the logical
|
489
|
+
# opposite operator.
|
490
|
+
#
|
491
|
+
# All the other conditions will be ignored.
|
492
|
+
#
|
493
|
+
# @since 0.1.0
|
494
|
+
#
|
495
|
+
# @see Lotus::Model::Adapters::Sql::Query#where
|
496
|
+
# @see Lotus::Model::Adapters::Sql::Query#exclude
|
497
|
+
# @see Lotus::Repository#exclude
|
498
|
+
#
|
499
|
+
# @example
|
500
|
+
#
|
501
|
+
# query.where(language: 'java').negate!.all
|
502
|
+
#
|
503
|
+
# # => SELECT * FROM `projects` WHERE (`language` != 'java')
|
504
|
+
def negate!
|
505
|
+
conditions.map! do |(operator, condition)|
|
506
|
+
[OPERATORS_MAPPING.fetch(operator) { operator }, condition]
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
# Apply all the conditions and returns a filtered collection.
|
511
|
+
#
|
512
|
+
# This operation is idempotent, and the returned result didn't
|
513
|
+
# fetched the records yet.
|
514
|
+
#
|
515
|
+
# @return [Lotus::Model::Adapters::Sql::Collection]
|
516
|
+
#
|
517
|
+
# @since 0.1.0
|
518
|
+
def scoped
|
519
|
+
scope = @collection
|
520
|
+
|
521
|
+
conditions.each do |(method,*args)|
|
522
|
+
scope = scope.public_send(method, *args)
|
523
|
+
end
|
524
|
+
|
525
|
+
scope
|
526
|
+
end
|
527
|
+
|
528
|
+
alias_method :run, :scoped
|
529
|
+
|
530
|
+
protected
|
531
|
+
# Handles missing methods for query combinations
|
532
|
+
#
|
533
|
+
# @api private
|
534
|
+
# @since 0.1.0
|
535
|
+
#
|
536
|
+
# @see Lotus::Model::Adapters:Sql::Query#apply
|
537
|
+
def method_missing(m, *args, &blk)
|
538
|
+
if @context.respond_to?(m)
|
539
|
+
apply @context.public_send(m, *args, &blk)
|
540
|
+
else
|
541
|
+
super
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
private
|
546
|
+
|
547
|
+
# Returns a new query that is the result of the merge of the current
|
548
|
+
# conditions with the ones of the given query.
|
549
|
+
#
|
550
|
+
# This is used to combine queries together in a Repository.
|
551
|
+
#
|
552
|
+
# @param query [Lotus::Model::Adapters::Sql::Query] the query to apply
|
553
|
+
#
|
554
|
+
# @return [Lotus::Model::Adapters::Sql::Query] a new query with the
|
555
|
+
# merged conditions
|
556
|
+
#
|
557
|
+
# @api private
|
558
|
+
# @since 0.1.0
|
559
|
+
#
|
560
|
+
# @example
|
561
|
+
# require 'lotus/model'
|
562
|
+
#
|
563
|
+
# class ArticleRepository
|
564
|
+
# include Lotus::Repository
|
565
|
+
#
|
566
|
+
# def self.by_author(author)
|
567
|
+
# query do
|
568
|
+
# where(author_id: author.id)
|
569
|
+
# end
|
570
|
+
# end
|
571
|
+
#
|
572
|
+
# def self.rank
|
573
|
+
# query.desc(:comments_count)
|
574
|
+
# end
|
575
|
+
#
|
576
|
+
# def self.rank_by_author(author)
|
577
|
+
# rank.by_author(author)
|
578
|
+
# end
|
579
|
+
# end
|
580
|
+
#
|
581
|
+
# # The code above combines two queries: `rank` and `by_author`.
|
582
|
+
# #
|
583
|
+
# # The first class method `rank` returns a `Sql::Query` instance
|
584
|
+
# # which doesn't respond to `by_author`. How to solve this problem?
|
585
|
+
# #
|
586
|
+
# # 1. When we use `query` to fabricate a `Sql::Query` we pass the
|
587
|
+
# # current context (the repository itself) to the query initializer.
|
588
|
+
# #
|
589
|
+
# # 2. When that query receives the `by_author` message, it's captured
|
590
|
+
# # by `method_missing` and dispatched to the repository.
|
591
|
+
# #
|
592
|
+
# # 3. The class method `by_author` returns a query too.
|
593
|
+
# #
|
594
|
+
# # 4. We just return a new query that is the result of the current
|
595
|
+
# # query's conditions (`rank`) and of the conditions from `by_author`.
|
596
|
+
# #
|
597
|
+
# # You're welcome ;)
|
598
|
+
def apply(query)
|
599
|
+
dup.tap do |result|
|
600
|
+
result.conditions.push(*query.conditions)
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
def _order_operator
|
605
|
+
if conditions.any? {|c, _| c == :order }
|
606
|
+
:order_more
|
607
|
+
else
|
608
|
+
:order
|
609
|
+
end
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|