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,111 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Model
|
3
|
+
module Adapters
|
4
|
+
# Shared implementation for SqlAdapter and MemoryAdapter
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
# @since 0.1.0
|
8
|
+
module Implementation
|
9
|
+
# Creates or updates a record in the database for the given entity.
|
10
|
+
#
|
11
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
12
|
+
# @param entity [#id, #id=] the entity to persist
|
13
|
+
#
|
14
|
+
# @return [Object] the entity
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
# @since 0.1.0
|
18
|
+
def persist(collection, entity)
|
19
|
+
if entity.id
|
20
|
+
update(collection, entity)
|
21
|
+
else
|
22
|
+
create(collection, entity)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns all the records for the given collection
|
27
|
+
#
|
28
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
29
|
+
#
|
30
|
+
# @return [Array] all the records
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
# @since 0.1.0
|
34
|
+
def all(collection)
|
35
|
+
# TODO consider to make this lazy (aka remove #all)
|
36
|
+
query(collection).all
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a unique record from the given collection, with the given
|
40
|
+
# id.
|
41
|
+
#
|
42
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
43
|
+
# @param id [Object] the identity of the object.
|
44
|
+
#
|
45
|
+
# @return [Object] the entity
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
# @since 0.1.0
|
49
|
+
def find(collection, id)
|
50
|
+
_first(
|
51
|
+
_find(collection, id)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the first record in the given collection.
|
56
|
+
#
|
57
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
58
|
+
#
|
59
|
+
# @return [Object] the first entity
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
# @since 0.1.0
|
63
|
+
def first(collection)
|
64
|
+
_first(
|
65
|
+
query(collection).asc(_identity(collection))
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the last record in the given collection.
|
70
|
+
#
|
71
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
72
|
+
#
|
73
|
+
# @return [Object] the last entity
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
# @since 0.1.0
|
77
|
+
def last(collection)
|
78
|
+
_first(
|
79
|
+
query(collection).desc(_identity(collection))
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def _collection(name)
|
85
|
+
raise NotImplementedError
|
86
|
+
end
|
87
|
+
|
88
|
+
def _mapped_collection(name)
|
89
|
+
@mapper.collection(name)
|
90
|
+
end
|
91
|
+
|
92
|
+
def _find(collection, id)
|
93
|
+
identity = _identity(collection)
|
94
|
+
query(collection).where(identity => _id(collection, identity, id))
|
95
|
+
end
|
96
|
+
|
97
|
+
def _first(query)
|
98
|
+
query.limit(1).first
|
99
|
+
end
|
100
|
+
|
101
|
+
def _identity(collection)
|
102
|
+
_mapped_collection(collection).identity
|
103
|
+
end
|
104
|
+
|
105
|
+
def _id(collection, column, value)
|
106
|
+
_mapped_collection(collection).deserialize_attribute(column, value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Model
|
3
|
+
module Adapters
|
4
|
+
module Memory
|
5
|
+
# Acts like a SQL database table.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
# @since 0.1.0
|
9
|
+
class Collection
|
10
|
+
# A counter that simulates autoincrement primary key of a SQL table.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
# @since 0.1.0
|
14
|
+
class PrimaryKey
|
15
|
+
# Initialize
|
16
|
+
#
|
17
|
+
# @return [Hanami::Model::Adapters::Memory::Collection::PrimaryKey]
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
# @since 0.1.0
|
21
|
+
def initialize
|
22
|
+
@current = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
# Increment the current count by 1 and yields the given block
|
26
|
+
#
|
27
|
+
# @return [Fixnum] the incremented counter
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
# @since 0.1.0
|
31
|
+
def increment!
|
32
|
+
yield(@current += 1)
|
33
|
+
@current
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @attr_reader name [Symbol] the name of the collection (eg. `:users`)
|
38
|
+
#
|
39
|
+
# @since 0.1.0
|
40
|
+
# @api private
|
41
|
+
attr_reader :name
|
42
|
+
|
43
|
+
# @attr_reader identity [Symbol] the primary key of the collection
|
44
|
+
# (eg. `:id`)
|
45
|
+
#
|
46
|
+
# @since 0.1.0
|
47
|
+
# @api private
|
48
|
+
attr_reader :identity
|
49
|
+
|
50
|
+
# @attr_reader records [Hash] a set of records
|
51
|
+
#
|
52
|
+
# @since 0.1.0
|
53
|
+
# @api private
|
54
|
+
attr_reader :records
|
55
|
+
|
56
|
+
# Initialize a collection
|
57
|
+
#
|
58
|
+
# @param name [Symbol] the name of the collection (eg. `:users`).
|
59
|
+
# @param identity [Symbol] the primary key of the collection
|
60
|
+
# (eg. `:id`).
|
61
|
+
#
|
62
|
+
# @api private
|
63
|
+
# @since 0.1.0
|
64
|
+
def initialize(name, identity)
|
65
|
+
@name, @identity = name, identity
|
66
|
+
clear
|
67
|
+
end
|
68
|
+
|
69
|
+
# Creates a record for the given entity and assigns an id.
|
70
|
+
#
|
71
|
+
# @param entity [Object] the entity to persist
|
72
|
+
#
|
73
|
+
# @see Hanami::Model::Adapters::Memory::Command#create
|
74
|
+
#
|
75
|
+
# @return the primary key of the created record
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
# @since 0.1.0
|
79
|
+
def create(entity)
|
80
|
+
@primary_key.increment! do |id|
|
81
|
+
entity[identity] = id
|
82
|
+
records[id] = entity
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Updates the record corresponding to the given entity.
|
87
|
+
#
|
88
|
+
# @param entity [Object] the entity to persist
|
89
|
+
#
|
90
|
+
# @see Hanami::Model::Adapters::Memory::Command#update
|
91
|
+
#
|
92
|
+
# @api private
|
93
|
+
# @since 0.1.0
|
94
|
+
def update(entity)
|
95
|
+
records[entity.fetch(identity)] = entity
|
96
|
+
end
|
97
|
+
|
98
|
+
# Deletes the record corresponding to the given entity.
|
99
|
+
#
|
100
|
+
# @param entity [Object] the entity to delete
|
101
|
+
#
|
102
|
+
# @see Hanami::Model::Adapters::Memory::Command#delete
|
103
|
+
#
|
104
|
+
# @api private
|
105
|
+
# @since 0.1.0
|
106
|
+
def delete(entity)
|
107
|
+
records.delete(entity.id)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns all the raw records
|
111
|
+
#
|
112
|
+
# @return [Array<Hash>]
|
113
|
+
#
|
114
|
+
# @api private
|
115
|
+
# @since 0.1.0
|
116
|
+
def all
|
117
|
+
records.values
|
118
|
+
end
|
119
|
+
|
120
|
+
# Deletes all the records and resets the identity counter.
|
121
|
+
#
|
122
|
+
# @api private
|
123
|
+
# @since 0.1.0
|
124
|
+
def clear
|
125
|
+
@records = {}
|
126
|
+
@primary_key = PrimaryKey.new
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Model
|
3
|
+
module Adapters
|
4
|
+
module Memory
|
5
|
+
# Execute a command for the given collection.
|
6
|
+
#
|
7
|
+
# @see Hanami::Model::Adapters::Memory::Collection
|
8
|
+
# @see Hanami::Model::Mapping::Collection
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
# @since 0.1.0
|
12
|
+
class Command
|
13
|
+
# Initialize a command
|
14
|
+
#
|
15
|
+
# @param dataset [Hanami::Model::Adapters::Memory::Collection]
|
16
|
+
# @param collection [Hanami::Model::Mapping::Collection]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
# @since 0.1.0
|
20
|
+
def initialize(dataset, collection)
|
21
|
+
@dataset = dataset
|
22
|
+
@collection = collection
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates a record for the given entity.
|
26
|
+
#
|
27
|
+
# @param entity [Object] the entity to persist
|
28
|
+
#
|
29
|
+
# @see Hanami::Model::Adapters::Memory::Collection#insert
|
30
|
+
#
|
31
|
+
# @return the primary key of the just created record.
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
# @since 0.1.0
|
35
|
+
def create(entity)
|
36
|
+
serialized_entity = _serialize(entity)
|
37
|
+
serialized_entity[_identity] = @dataset.create(serialized_entity)
|
38
|
+
|
39
|
+
_deserialize(serialized_entity)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Updates the corresponding record for the given entity.
|
43
|
+
#
|
44
|
+
# @param entity [Object] the entity to persist
|
45
|
+
#
|
46
|
+
# @see Hanami::Model::Adapters::Memory::Collection#update
|
47
|
+
#
|
48
|
+
# @api private
|
49
|
+
# @since 0.1.0
|
50
|
+
def update(entity)
|
51
|
+
serialized_entity = _serialize(entity)
|
52
|
+
@dataset.update(serialized_entity)
|
53
|
+
|
54
|
+
_deserialize(serialized_entity)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Deletes the corresponding record for the given entity.
|
58
|
+
#
|
59
|
+
# @param entity [Object] the entity to delete
|
60
|
+
#
|
61
|
+
# @see Hanami::Model::Adapters::Memory::Collection#delete
|
62
|
+
#
|
63
|
+
# @api private
|
64
|
+
# @since 0.1.0
|
65
|
+
def delete(entity)
|
66
|
+
@dataset.delete(entity)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Deletes all the records from the table.
|
70
|
+
#
|
71
|
+
# @see Hanami::Model::Adapters::Memory::Collection#clear
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
# @since 0.1.0
|
75
|
+
def clear
|
76
|
+
@dataset.clear
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
# Serialize the given entity before to persist in the database.
|
81
|
+
#
|
82
|
+
# @return [Hash] the serialized entity
|
83
|
+
#
|
84
|
+
# @api private
|
85
|
+
# @since 0.1.0
|
86
|
+
def _serialize(entity)
|
87
|
+
@collection.serialize(entity)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Deserialize the given entity after it was persisted in the database.
|
91
|
+
#
|
92
|
+
# @return [Hanami::Entity] the deserialized entity
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
# @since 0.2.2
|
96
|
+
def _deserialize(entity)
|
97
|
+
@collection.deserialize([entity]).first
|
98
|
+
end
|
99
|
+
|
100
|
+
# Name of the identity column in database
|
101
|
+
#
|
102
|
+
# @return [Symbol] the identity name
|
103
|
+
#
|
104
|
+
# @api private
|
105
|
+
# @since 0.2.2
|
106
|
+
def _identity
|
107
|
+
@collection.identity
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,653 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'hanami/utils/kernel'
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Model
|
7
|
+
module Adapters
|
8
|
+
module Memory
|
9
|
+
# Query the in-memory database with a powerful API.
|
10
|
+
#
|
11
|
+
# All the methods are chainable, it allows advanced composition of
|
12
|
+
# conditions.
|
13
|
+
#
|
14
|
+
# This works as a lazy filtering mechanism: the records are fetched from
|
15
|
+
# the database only when needed.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
#
|
19
|
+
# query.where(language: 'ruby')
|
20
|
+
# .and(framework: 'hanami')
|
21
|
+
# .reverse_order(:users_count).all
|
22
|
+
#
|
23
|
+
# # the records are fetched only when we invoke #all
|
24
|
+
#
|
25
|
+
# It implements Ruby's `Enumerable` and borrows some methods from `Array`.
|
26
|
+
# Expect a query to act like them.
|
27
|
+
#
|
28
|
+
# @since 0.1.0
|
29
|
+
class Query
|
30
|
+
include Enumerable
|
31
|
+
extend Forwardable
|
32
|
+
|
33
|
+
def_delegators :all, :each, :to_s, :empty?
|
34
|
+
|
35
|
+
# @attr_reader conditions [Array] an accumulator for the conditions
|
36
|
+
#
|
37
|
+
# @since 0.1.0
|
38
|
+
# @api private
|
39
|
+
attr_reader :conditions
|
40
|
+
|
41
|
+
# @attr_reader modifiers [Array] an accumulator for the modifiers
|
42
|
+
#
|
43
|
+
# @since 0.1.0
|
44
|
+
# @api private
|
45
|
+
attr_reader :modifiers
|
46
|
+
|
47
|
+
# Initialize a query
|
48
|
+
#
|
49
|
+
# @param dataset [Hanami::Model::Adapters::Memory::Collection]
|
50
|
+
# @param collection [Hanami::Model::Mapping::Collection]
|
51
|
+
# @param blk [Proc] an optional block that gets yielded in the
|
52
|
+
# context of the current query
|
53
|
+
#
|
54
|
+
# @since 0.1.0
|
55
|
+
# @api private
|
56
|
+
def initialize(dataset, collection, &blk)
|
57
|
+
@dataset = dataset
|
58
|
+
@collection = collection
|
59
|
+
@conditions = []
|
60
|
+
@modifiers = []
|
61
|
+
instance_eval(&blk) if block_given?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Resolves the query by fetching records from the database and
|
65
|
+
# translating them into entities.
|
66
|
+
#
|
67
|
+
# @return [Array] a collection of entities
|
68
|
+
#
|
69
|
+
# @since 0.1.0
|
70
|
+
def all
|
71
|
+
@collection.deserialize(run)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Adds a condition that behaves like SQL `WHERE`.
|
75
|
+
#
|
76
|
+
# It accepts a `Hash` with only one pair.
|
77
|
+
# The key must be the name of the column expressed as a `Symbol`.
|
78
|
+
# The value is the one used by the internal filtering logic.
|
79
|
+
#
|
80
|
+
# @param condition [Hash]
|
81
|
+
#
|
82
|
+
# @return self
|
83
|
+
#
|
84
|
+
# @since 0.1.0
|
85
|
+
#
|
86
|
+
# @example Fixed value
|
87
|
+
#
|
88
|
+
# query.where(language: 'ruby')
|
89
|
+
#
|
90
|
+
# @example Array
|
91
|
+
#
|
92
|
+
# query.where(id: [1, 3])
|
93
|
+
#
|
94
|
+
# @example Range
|
95
|
+
#
|
96
|
+
# query.where(year: 1900..1982)
|
97
|
+
#
|
98
|
+
# @example Using block
|
99
|
+
#
|
100
|
+
# query.where { age > 31 }
|
101
|
+
#
|
102
|
+
# @example Multiple conditions
|
103
|
+
#
|
104
|
+
# query.where(language: 'ruby')
|
105
|
+
# .where(framework: 'hanami')
|
106
|
+
#
|
107
|
+
# @example Multiple conditions with blocks
|
108
|
+
#
|
109
|
+
# query.where { language == 'ruby' }
|
110
|
+
# .where { framework == 'hanami' }
|
111
|
+
#
|
112
|
+
# @example Mixed hash and block conditions
|
113
|
+
#
|
114
|
+
# query.where(language: 'ruby')
|
115
|
+
# .where { framework == 'hanami' }
|
116
|
+
def where(condition = nil, &blk)
|
117
|
+
if blk
|
118
|
+
_push_evaluated_block_condition(:where, blk, :find_all)
|
119
|
+
elsif condition
|
120
|
+
_push_to_expanded_condition(:where, condition) do |column, value|
|
121
|
+
Proc.new {
|
122
|
+
find_all { |r|
|
123
|
+
case value
|
124
|
+
when Array,Set,Range
|
125
|
+
value.include?(r.fetch(column, nil))
|
126
|
+
else
|
127
|
+
r.fetch(column, nil) == value
|
128
|
+
end
|
129
|
+
}
|
130
|
+
}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
alias_method :and, :where
|
138
|
+
|
139
|
+
# Adds a condition that behaves like SQL `OR`.
|
140
|
+
#
|
141
|
+
# It accepts a `Hash` with only one pair.
|
142
|
+
# The key must be the name of the column expressed as a `Symbol`.
|
143
|
+
# The value is the one used by the SQL query
|
144
|
+
#
|
145
|
+
# This condition will be ignored if not used with WHERE.
|
146
|
+
#
|
147
|
+
# @param condition [Hash]
|
148
|
+
#
|
149
|
+
# @return self
|
150
|
+
#
|
151
|
+
# @since 0.1.0
|
152
|
+
#
|
153
|
+
# @example Fixed value
|
154
|
+
#
|
155
|
+
# query.where(language: 'ruby').or(framework: 'hanami')
|
156
|
+
#
|
157
|
+
# @example Array
|
158
|
+
#
|
159
|
+
# query.where(id: 1).or(author_id: [15, 23])
|
160
|
+
#
|
161
|
+
# @example Range
|
162
|
+
#
|
163
|
+
# query.where(country: 'italy').or(year: 1900..1982)
|
164
|
+
#
|
165
|
+
# @example Using block
|
166
|
+
#
|
167
|
+
# query.where { age == 31 }.or { age == 32 }
|
168
|
+
#
|
169
|
+
# @example Mixed hash and block conditions
|
170
|
+
#
|
171
|
+
# query.where(language: 'ruby')
|
172
|
+
# .or { framework == 'hanami' }
|
173
|
+
def or(condition = nil, &blk)
|
174
|
+
if blk
|
175
|
+
_push_evaluated_block_condition(:or, blk, :find_all)
|
176
|
+
elsif condition
|
177
|
+
_push_to_expanded_condition(:or, condition) do |column, value|
|
178
|
+
Proc.new { find_all { |r| r.fetch(column) == value} }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
# Logical negation of a #where condition.
|
186
|
+
#
|
187
|
+
# It accepts a `Hash` with only one pair.
|
188
|
+
# The key must be the name of the column expressed as a `Symbol`.
|
189
|
+
# The value is the one used by the internal filtering logic.
|
190
|
+
#
|
191
|
+
# @param condition [Hash]
|
192
|
+
#
|
193
|
+
# @since 0.1.0
|
194
|
+
#
|
195
|
+
# @return self
|
196
|
+
#
|
197
|
+
# @example Fixed value
|
198
|
+
#
|
199
|
+
# query.exclude(language: 'java')
|
200
|
+
#
|
201
|
+
# @example Array
|
202
|
+
#
|
203
|
+
# query.exclude(id: [4, 9])
|
204
|
+
#
|
205
|
+
# @example Range
|
206
|
+
#
|
207
|
+
# query.exclude(year: 1900..1982)
|
208
|
+
#
|
209
|
+
# @example Multiple conditions
|
210
|
+
#
|
211
|
+
# query.exclude(language: 'java')
|
212
|
+
# .exclude(company: 'enterprise')
|
213
|
+
#
|
214
|
+
# @example Using block
|
215
|
+
#
|
216
|
+
# query.exclude { age > 31 }
|
217
|
+
#
|
218
|
+
# @example Multiple conditions with blocks
|
219
|
+
#
|
220
|
+
# query.exclude { language == 'java' }
|
221
|
+
# .exclude { framework == 'spring' }
|
222
|
+
#
|
223
|
+
# @example Mixed hash and block conditions
|
224
|
+
#
|
225
|
+
# query.exclude(language: 'java')
|
226
|
+
# .exclude { framework == 'spring' }
|
227
|
+
def exclude(condition = nil, &blk)
|
228
|
+
if blk
|
229
|
+
_push_evaluated_block_condition(:where, blk, :reject)
|
230
|
+
elsif condition
|
231
|
+
_push_to_expanded_condition(:where, condition) do |column, value|
|
232
|
+
Proc.new { reject { |r| r.fetch(column) == value} }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
self
|
237
|
+
end
|
238
|
+
|
239
|
+
alias_method :not, :exclude
|
240
|
+
|
241
|
+
# Select only the specified columns.
|
242
|
+
#
|
243
|
+
# By default a query selects all the mapped columns.
|
244
|
+
#
|
245
|
+
# @param columns [Array<Symbol>]
|
246
|
+
#
|
247
|
+
# @return self
|
248
|
+
#
|
249
|
+
# @since 0.1.0
|
250
|
+
#
|
251
|
+
# @example Single column
|
252
|
+
#
|
253
|
+
# query.select(:name)
|
254
|
+
#
|
255
|
+
# @example Multiple columns
|
256
|
+
#
|
257
|
+
# query.select(:name, :year)
|
258
|
+
def select(*columns)
|
259
|
+
columns = Hanami::Utils::Kernel.Array(columns)
|
260
|
+
modifiers.push(Proc.new{ flatten!; each {|r| r.delete_if {|k,_| !columns.include?(k)} } })
|
261
|
+
end
|
262
|
+
|
263
|
+
# Specify the ascending order of the records, sorted by the given
|
264
|
+
# columns.
|
265
|
+
#
|
266
|
+
# @param columns [Array<Symbol>] the column names
|
267
|
+
#
|
268
|
+
# @return self
|
269
|
+
#
|
270
|
+
# @since 0.1.0
|
271
|
+
#
|
272
|
+
# @see Hanami::Model::Adapters::Memory::Query#reverse_order
|
273
|
+
#
|
274
|
+
# @example Single column
|
275
|
+
#
|
276
|
+
# query.order(:name)
|
277
|
+
#
|
278
|
+
# @example Multiple columns
|
279
|
+
#
|
280
|
+
# query.order(:name, :year)
|
281
|
+
#
|
282
|
+
# @example Multiple invokations
|
283
|
+
#
|
284
|
+
# query.order(:name).order(:year)
|
285
|
+
def order(*columns)
|
286
|
+
Hanami::Utils::Kernel.Array(columns).each do |column|
|
287
|
+
modifiers.push(Proc.new{ sort_by!{|r| r.fetch(column)} })
|
288
|
+
end
|
289
|
+
|
290
|
+
self
|
291
|
+
end
|
292
|
+
|
293
|
+
# Alias for order
|
294
|
+
#
|
295
|
+
# @since 0.1.0
|
296
|
+
#
|
297
|
+
# @see Hanami::Model::Adapters::Memory::Query#order
|
298
|
+
#
|
299
|
+
# @example Single column
|
300
|
+
#
|
301
|
+
# query.asc(:name)
|
302
|
+
#
|
303
|
+
# @example Multiple columns
|
304
|
+
#
|
305
|
+
# query.asc(:name, :year)
|
306
|
+
#
|
307
|
+
# @example Multiple invokations
|
308
|
+
#
|
309
|
+
# query.asc(:name).asc(:year)
|
310
|
+
alias_method :asc, :order
|
311
|
+
|
312
|
+
# Specify the descending order of the records, sorted by the given
|
313
|
+
# columns.
|
314
|
+
#
|
315
|
+
# @param columns [Array<Symbol>] the column names
|
316
|
+
#
|
317
|
+
# @return self
|
318
|
+
#
|
319
|
+
# @since 0.3.1
|
320
|
+
#
|
321
|
+
# @see Hanami::Model::Adapters::Memory::Query#order
|
322
|
+
#
|
323
|
+
# @example Single column
|
324
|
+
#
|
325
|
+
# query.reverse_order(:name)
|
326
|
+
#
|
327
|
+
# @example Multiple columns
|
328
|
+
#
|
329
|
+
# query.reverse_order(:name, :year)
|
330
|
+
#
|
331
|
+
# @example Multiple invokations
|
332
|
+
#
|
333
|
+
# query.reverse_order(:name).reverse_order(:year)
|
334
|
+
def reverse_order(*columns)
|
335
|
+
Hanami::Utils::Kernel.Array(columns).each do |column|
|
336
|
+
modifiers.push(Proc.new{ sort_by!{|r| r.fetch(column)}.reverse! })
|
337
|
+
end
|
338
|
+
|
339
|
+
self
|
340
|
+
end
|
341
|
+
|
342
|
+
# Alias for reverse_order
|
343
|
+
#
|
344
|
+
# @since 0.1.0
|
345
|
+
#
|
346
|
+
# @see Hanami::Model::Adapters::Memory::Query#reverse_order
|
347
|
+
#
|
348
|
+
# @example Single column
|
349
|
+
#
|
350
|
+
# query.desc(:name)
|
351
|
+
#
|
352
|
+
# @example Multiple columns
|
353
|
+
#
|
354
|
+
# query.desc(:name, :year)
|
355
|
+
#
|
356
|
+
# @example Multiple invokations
|
357
|
+
#
|
358
|
+
# query.desc(:name).desc(:year)
|
359
|
+
alias_method :desc, :reverse_order
|
360
|
+
|
361
|
+
# Limit the number of records to return.
|
362
|
+
#
|
363
|
+
# @param number [Fixnum]
|
364
|
+
#
|
365
|
+
# @return self
|
366
|
+
#
|
367
|
+
# @since 0.1.0
|
368
|
+
#
|
369
|
+
# @example
|
370
|
+
#
|
371
|
+
# query.limit(1)
|
372
|
+
def limit(number)
|
373
|
+
modifiers.push(Proc.new{ replace(flatten.first(number)) })
|
374
|
+
self
|
375
|
+
end
|
376
|
+
|
377
|
+
# Simulate an `OFFSET` clause, without the need of specify a limit.
|
378
|
+
#
|
379
|
+
# @param number [Fixnum]
|
380
|
+
#
|
381
|
+
# @return self
|
382
|
+
#
|
383
|
+
# @since 0.1.0
|
384
|
+
#
|
385
|
+
# @example
|
386
|
+
#
|
387
|
+
# query.offset(10)
|
388
|
+
def offset(number)
|
389
|
+
modifiers.unshift(Proc.new{ replace(flatten.drop(number)) })
|
390
|
+
self
|
391
|
+
end
|
392
|
+
|
393
|
+
# Returns the sum of the values for the given column.
|
394
|
+
#
|
395
|
+
# @param column [Symbol] the column name
|
396
|
+
#
|
397
|
+
# @return [Numeric]
|
398
|
+
#
|
399
|
+
# @since 0.1.0
|
400
|
+
#
|
401
|
+
# @example
|
402
|
+
#
|
403
|
+
# query.sum(:comments_count)
|
404
|
+
def sum(column)
|
405
|
+
result = all
|
406
|
+
|
407
|
+
if result.any?
|
408
|
+
result.inject(0.0) do |acc, record|
|
409
|
+
if value = record.public_send(column)
|
410
|
+
acc += value
|
411
|
+
end
|
412
|
+
|
413
|
+
acc
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Returns the average of the values for the given column.
|
419
|
+
#
|
420
|
+
# @param column [Symbol] the column name
|
421
|
+
#
|
422
|
+
# @return [Numeric]
|
423
|
+
#
|
424
|
+
# @since 0.1.0
|
425
|
+
#
|
426
|
+
# @example
|
427
|
+
#
|
428
|
+
# query.average(:comments_count)
|
429
|
+
def average(column)
|
430
|
+
if s = sum(column)
|
431
|
+
s / _all_with_present_column(column).count.to_f
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
alias_method :avg, :average
|
436
|
+
|
437
|
+
# Returns the maximum value for the given column.
|
438
|
+
#
|
439
|
+
# @param column [Symbol] the column name
|
440
|
+
#
|
441
|
+
# @return result
|
442
|
+
#
|
443
|
+
# @since 0.1.0
|
444
|
+
#
|
445
|
+
# @example
|
446
|
+
#
|
447
|
+
# query.max(:comments_count)
|
448
|
+
def max(column)
|
449
|
+
_all_with_present_column(column).max
|
450
|
+
end
|
451
|
+
|
452
|
+
# Returns the minimum value for the given column.
|
453
|
+
#
|
454
|
+
# @param column [Symbol] the column name
|
455
|
+
#
|
456
|
+
# @return result
|
457
|
+
#
|
458
|
+
# @since 0.1.0
|
459
|
+
#
|
460
|
+
# @example
|
461
|
+
#
|
462
|
+
# query.min(:comments_count)
|
463
|
+
def min(column)
|
464
|
+
_all_with_present_column(column).min
|
465
|
+
end
|
466
|
+
|
467
|
+
# Returns the difference between the MAX and MIN for the given column.
|
468
|
+
#
|
469
|
+
# @param column [Symbol] the column name
|
470
|
+
#
|
471
|
+
# @return [Numeric]
|
472
|
+
#
|
473
|
+
# @since 0.1.0
|
474
|
+
#
|
475
|
+
# @see Hanami::Model::Adapters::Memory::Query#max
|
476
|
+
# @see Hanami::Model::Adapters::Memory::Query#min
|
477
|
+
#
|
478
|
+
# @example
|
479
|
+
#
|
480
|
+
# query.interval(:comments_count)
|
481
|
+
def interval(column)
|
482
|
+
max(column) - min(column)
|
483
|
+
rescue NoMethodError
|
484
|
+
end
|
485
|
+
|
486
|
+
# Returns a range of values between the MAX and the MIN for the given
|
487
|
+
# column.
|
488
|
+
#
|
489
|
+
# @param column [Symbol] the column name
|
490
|
+
#
|
491
|
+
# @return [Range]
|
492
|
+
#
|
493
|
+
# @since 0.1.0
|
494
|
+
#
|
495
|
+
# @see Hanami::Model::Adapters::Memory::Query#max
|
496
|
+
# @see Hanami::Model::Adapters::Memory::Query#min
|
497
|
+
#
|
498
|
+
# @example
|
499
|
+
#
|
500
|
+
# query.range(:comments_count)
|
501
|
+
def range(column)
|
502
|
+
min(column)..max(column)
|
503
|
+
end
|
504
|
+
|
505
|
+
# Checks if at least one record exists for the current conditions.
|
506
|
+
#
|
507
|
+
# @return [TrueClass,FalseClass]
|
508
|
+
#
|
509
|
+
# @since 0.1.0
|
510
|
+
#
|
511
|
+
# @example
|
512
|
+
#
|
513
|
+
# query.where(author_id: 23).exists? # => true
|
514
|
+
def exist?
|
515
|
+
!count.zero?
|
516
|
+
end
|
517
|
+
|
518
|
+
# Returns a count of the records for the current conditions.
|
519
|
+
#
|
520
|
+
# @return [Fixnum]
|
521
|
+
#
|
522
|
+
# @since 0.1.0
|
523
|
+
#
|
524
|
+
# @example
|
525
|
+
#
|
526
|
+
# query.where(author_id: 23).count # => 5
|
527
|
+
def count
|
528
|
+
run.count
|
529
|
+
end
|
530
|
+
|
531
|
+
# This method is defined in order to make the interface of
|
532
|
+
# `Memory::Query` identical to `Sql::Query`, but this feature is NOT
|
533
|
+
# implemented
|
534
|
+
#
|
535
|
+
# @raise [NotImplementedError]
|
536
|
+
#
|
537
|
+
# @since 0.1.0
|
538
|
+
#
|
539
|
+
# @see Hanami::Model::Adapters::Sql::Query#negate!
|
540
|
+
def negate!
|
541
|
+
raise NotImplementedError
|
542
|
+
end
|
543
|
+
|
544
|
+
# This method is defined in order to make the interface of
|
545
|
+
# `Memory::Query` identical to `Sql::Query`, but this feature is NOT
|
546
|
+
# implemented
|
547
|
+
#
|
548
|
+
# @raise [NotImplementedError]
|
549
|
+
#
|
550
|
+
# @since 0.5.0
|
551
|
+
#
|
552
|
+
# @see Hanami::Model::Adapters::Sql::Query#group!
|
553
|
+
def group
|
554
|
+
raise NotImplementedError
|
555
|
+
end
|
556
|
+
|
557
|
+
protected
|
558
|
+
def method_missing(m, *args, &blk)
|
559
|
+
if @context.respond_to?(m)
|
560
|
+
apply @context.public_send(m, *args, &blk)
|
561
|
+
else
|
562
|
+
super
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
private
|
567
|
+
# Apply all the conditions and returns a filtered collection.
|
568
|
+
#
|
569
|
+
# This operation is idempotent, but the records are actually fetched
|
570
|
+
# from the memory store.
|
571
|
+
#
|
572
|
+
# @return [Array]
|
573
|
+
#
|
574
|
+
# @api private
|
575
|
+
# @since 0.1.0
|
576
|
+
def run
|
577
|
+
result = @dataset.all.dup
|
578
|
+
|
579
|
+
if conditions.any?
|
580
|
+
prev_result = nil
|
581
|
+
conditions.each do |(type, condition)|
|
582
|
+
case type
|
583
|
+
when :where
|
584
|
+
prev_result = result
|
585
|
+
result = prev_result.instance_exec(&condition)
|
586
|
+
when :or
|
587
|
+
result |= prev_result.instance_exec(&condition)
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
modifiers.map do |modifier|
|
593
|
+
result.instance_exec(&modifier)
|
594
|
+
end
|
595
|
+
|
596
|
+
Hanami::Utils::Kernel.Array(result)
|
597
|
+
end
|
598
|
+
|
599
|
+
def _all_with_present_column(column)
|
600
|
+
all.map {|record| record.public_send(column) }.compact
|
601
|
+
end
|
602
|
+
|
603
|
+
# Expands and yields keys and values of a query hash condition and
|
604
|
+
# stores the result and condition type in the conditions array.
|
605
|
+
#
|
606
|
+
# It yields condition's keys and values to allow the caller to create a proc
|
607
|
+
# object to be stored and executed later performing the actual query.
|
608
|
+
#
|
609
|
+
# @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
|
610
|
+
# @param condition [Hash] the query condition to be expanded.
|
611
|
+
#
|
612
|
+
# @return [Array<Array>] the conditions array itself.
|
613
|
+
#
|
614
|
+
# @api private
|
615
|
+
# @since 0.3.1
|
616
|
+
def _push_to_expanded_condition(condition_type, condition)
|
617
|
+
proc = yield Array(condition).flatten(1)
|
618
|
+
conditions.push([condition_type, proc])
|
619
|
+
end
|
620
|
+
|
621
|
+
# Evaluates a block condition of a specified type and stores it in the
|
622
|
+
# conditions array.
|
623
|
+
#
|
624
|
+
# @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
|
625
|
+
# @param condition [Proc] the query condition to be evaluated and stored.
|
626
|
+
# @param strategy [Symbol] the iterator method to be executed.
|
627
|
+
# (eg. `:find_all`, `:reject`)
|
628
|
+
#
|
629
|
+
# @return [Array<Array>] the conditions array itself.
|
630
|
+
#
|
631
|
+
# @raise [Hanami::Model::InvalidQueryError] if block raises error when
|
632
|
+
# evaluated.
|
633
|
+
#
|
634
|
+
# @api private
|
635
|
+
# @since 0.3.1
|
636
|
+
def _push_evaluated_block_condition(condition_type, condition, strategy)
|
637
|
+
conditions.push([condition_type, Proc.new {
|
638
|
+
send(strategy) { |r|
|
639
|
+
begin
|
640
|
+
OpenStruct.new(r).instance_eval(&condition)
|
641
|
+
rescue NoMethodError
|
642
|
+
# TODO improve the error message, informing which
|
643
|
+
# attributes are invalid
|
644
|
+
raise Hanami::Model::InvalidQueryError.new
|
645
|
+
end
|
646
|
+
}
|
647
|
+
}])
|
648
|
+
end
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|