terrestrial 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -9
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +28 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +22 -0
- data/MissingFeatures.md +64 -0
- data/README.md +161 -16
- data/Rakefile +30 -0
- data/TODO.md +41 -0
- data/features/env.rb +60 -0
- data/features/example.feature +120 -0
- data/features/step_definitions/example_steps.rb +46 -0
- data/lib/terrestrial/abstract_record.rb +99 -0
- data/lib/terrestrial/association_loaders.rb +52 -0
- data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
- data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
- data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
- data/lib/terrestrial/dataset.rb +49 -0
- data/lib/terrestrial/deleted_record.rb +20 -0
- data/lib/terrestrial/dirty_map.rb +42 -0
- data/lib/terrestrial/graph_loader.rb +63 -0
- data/lib/terrestrial/graph_serializer.rb +91 -0
- data/lib/terrestrial/identity_map.rb +22 -0
- data/lib/terrestrial/lazy_collection.rb +74 -0
- data/lib/terrestrial/lazy_object_proxy.rb +55 -0
- data/lib/terrestrial/many_to_many_association.rb +138 -0
- data/lib/terrestrial/many_to_one_association.rb +66 -0
- data/lib/terrestrial/mapper_facade.rb +137 -0
- data/lib/terrestrial/one_to_many_association.rb +66 -0
- data/lib/terrestrial/public_conveniencies.rb +139 -0
- data/lib/terrestrial/query_order.rb +32 -0
- data/lib/terrestrial/relation_mapping.rb +50 -0
- data/lib/terrestrial/serializer.rb +18 -0
- data/lib/terrestrial/short_inspection_string.rb +18 -0
- data/lib/terrestrial/struct_factory.rb +17 -0
- data/lib/terrestrial/subset_queries_proxy.rb +11 -0
- data/lib/terrestrial/upserted_record.rb +15 -0
- data/lib/terrestrial/version.rb +1 -1
- data/lib/terrestrial.rb +5 -2
- data/sequel_mapper.gemspec +31 -0
- data/spec/config_override_spec.rb +193 -0
- data/spec/custom_serializers_spec.rb +49 -0
- data/spec/deletion_spec.rb +101 -0
- data/spec/graph_persistence_spec.rb +313 -0
- data/spec/graph_traversal_spec.rb +121 -0
- data/spec/new_graph_persistence_spec.rb +71 -0
- data/spec/object_identity_spec.rb +70 -0
- data/spec/ordered_association_spec.rb +51 -0
- data/spec/persistence_efficiency_spec.rb +224 -0
- data/spec/predefined_queries_spec.rb +62 -0
- data/spec/proxying_spec.rb +88 -0
- data/spec/querying_spec.rb +48 -0
- data/spec/readme_examples_spec.rb +35 -0
- data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
- data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
- data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
- data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
- data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
- data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
- data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
- data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/blog_schema.rb +38 -0
- data/spec/support/have_persisted_matcher.rb +19 -0
- data/spec/support/mapper_setup.rb +221 -0
- data/spec/support/mock_sequel.rb +193 -0
- data/spec/support/object_graph_setup.rb +139 -0
- data/spec/support/seed_data_setup.rb +165 -0
- data/spec/support/sequel_persistence_setup.rb +19 -0
- data/spec/support/sequel_test_support.rb +166 -0
- metadata +207 -13
- data/.travis.yml +0 -4
- data/bin/console +0 -14
- data/bin/setup +0 -7
- data/terrestrial.gemspec +0 -23
@@ -0,0 +1,58 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/public_conveniencies"
|
4
|
+
|
5
|
+
RSpec.describe Terrestrial::PublicConveniencies do
|
6
|
+
subject(:conveniences) {
|
7
|
+
Module.new.extend(Terrestrial::PublicConveniencies)
|
8
|
+
}
|
9
|
+
|
10
|
+
class MockDatastore < DelegateClass(Hash)
|
11
|
+
def transaction(&block)
|
12
|
+
block.call
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#mappers" do
|
17
|
+
let(:datastore) {
|
18
|
+
MockDatastore.new(
|
19
|
+
{
|
20
|
+
things: [ thing_record ],
|
21
|
+
}
|
22
|
+
)
|
23
|
+
}
|
24
|
+
|
25
|
+
let(:mapper_config) {
|
26
|
+
{
|
27
|
+
things: double(
|
28
|
+
:thing_config,
|
29
|
+
name: mapping_name,
|
30
|
+
namespace: :things,
|
31
|
+
fields: [:id],
|
32
|
+
associations: [],
|
33
|
+
primary_key: [],
|
34
|
+
factory: ->(x){x}
|
35
|
+
)
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
let(:mapping_name) { :things }
|
40
|
+
|
41
|
+
let(:thing_record) {
|
42
|
+
{
|
43
|
+
id: "THE THING",
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
it "returns a mapper for the specified mapping" do
|
48
|
+
mappers = conveniences.mappers(
|
49
|
+
mappings: mapper_config,
|
50
|
+
datastore: datastore,
|
51
|
+
)
|
52
|
+
|
53
|
+
expect(
|
54
|
+
mappers[:things].all.first.fetch(:id)
|
55
|
+
).to eq("THE THING")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/upserted_record"
|
4
|
+
|
5
|
+
RSpec.describe Terrestrial::UpsertedRecord do
|
6
|
+
subject(:record) {
|
7
|
+
Terrestrial::UpsertedRecord.new(namespace, identity, raw_data)
|
8
|
+
}
|
9
|
+
|
10
|
+
let(:namespace) { double(:namespace) }
|
11
|
+
|
12
|
+
let(:identity) {
|
13
|
+
{ id: id }
|
14
|
+
}
|
15
|
+
|
16
|
+
let(:raw_data) {
|
17
|
+
{
|
18
|
+
name: name,
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
let(:id) { double(:id) }
|
23
|
+
let(:name) { double(:name) }
|
24
|
+
|
25
|
+
describe "#if_upsert" do
|
26
|
+
it "invokes the callback" do
|
27
|
+
expect { |callback|
|
28
|
+
record.if_upsert(&callback)
|
29
|
+
}.to yield_with_args(record)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#==" do
|
34
|
+
context "with another record that upserts" do
|
35
|
+
let(:comparitor) {
|
36
|
+
record.merge({})
|
37
|
+
}
|
38
|
+
|
39
|
+
it "is equal" do
|
40
|
+
expect(record.==(comparitor)).to be(true)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "with another record that does not upsert" do
|
45
|
+
let(:comparitor) {
|
46
|
+
Class.new(Terrestrial::AbstractRecord) do
|
47
|
+
protected
|
48
|
+
def operation
|
49
|
+
:something_else
|
50
|
+
end
|
51
|
+
end
|
52
|
+
}
|
53
|
+
|
54
|
+
it "is not equal" do
|
55
|
+
expect(record.==(comparitor)).to be(false)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "pry"
|
2
|
+
require "support/sequel_test_support"
|
3
|
+
require "support/blog_schema"
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.expect_with :rspec do |expectations|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
8
|
+
end
|
9
|
+
|
10
|
+
config.mock_with :rspec do |mocks|
|
11
|
+
mocks.verify_partial_doubles = true
|
12
|
+
end
|
13
|
+
|
14
|
+
config.filter_run :focus
|
15
|
+
config.run_all_when_everything_filtered = true
|
16
|
+
|
17
|
+
config.disable_monkey_patching!
|
18
|
+
|
19
|
+
# TODO: get everything running without warnings
|
20
|
+
config.warnings = false
|
21
|
+
|
22
|
+
if config.files_to_run.one?
|
23
|
+
config.default_formatter = 'doc'
|
24
|
+
end
|
25
|
+
|
26
|
+
# config.profile_examples = 10
|
27
|
+
|
28
|
+
# config.order = :random
|
29
|
+
|
30
|
+
# Kernel.srand config.seed
|
31
|
+
|
32
|
+
config.before(:suite) do
|
33
|
+
Terrestrial::SequelTestSupport.drop_tables
|
34
|
+
Terrestrial::SequelTestSupport.create_tables(BLOG_SCHEMA)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
BLOG_SCHEMA = {
|
2
|
+
tables: {
|
3
|
+
users: [
|
4
|
+
{ name: :id, type: String, options: { primary_key: true } },
|
5
|
+
{ name: :first_name, type: String },
|
6
|
+
{ name: :last_name, type: String },
|
7
|
+
{ name: :email, type: String },
|
8
|
+
],
|
9
|
+
posts: [
|
10
|
+
{ name: :id, type: String, options: { primary_key: true } },
|
11
|
+
{ name: :subject, type: String },
|
12
|
+
{ name: :body, type: String },
|
13
|
+
{ name: :author_id, type: String},
|
14
|
+
{ name: :created_at, type: DateTime },
|
15
|
+
],
|
16
|
+
comments: [
|
17
|
+
{ name: :id, type: String, options: { primary_key: true } },
|
18
|
+
{ name: :body, type: String },
|
19
|
+
{ name: :post_id, type: String },
|
20
|
+
{ name: :commenter_id, type: String },
|
21
|
+
],
|
22
|
+
categories: [
|
23
|
+
{ name: :id, type: String, options: { primary_key: true } },
|
24
|
+
{ name: :name, type: String },
|
25
|
+
],
|
26
|
+
categories_to_posts: [
|
27
|
+
{ name: :post_id, type: String },
|
28
|
+
{ name: :category_id, type: String },
|
29
|
+
],
|
30
|
+
},
|
31
|
+
foreign_keys: [
|
32
|
+
[:posts, :author_id, :users, :id],
|
33
|
+
[:comments, :post_id, :posts, :id],
|
34
|
+
[:comments, :commenter_id, :users, :id],
|
35
|
+
[:categories_to_posts, :post_id, :posts, :id],
|
36
|
+
[:categories_to_posts, :category_id, :categories, :id],
|
37
|
+
]
|
38
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
RSpec::Matchers.define :have_persisted do |relation_name, data|
|
2
|
+
match do |datastore|
|
3
|
+
datastore[relation_name].find { |record|
|
4
|
+
if data.respond_to?(:===)
|
5
|
+
data === record
|
6
|
+
else
|
7
|
+
data == record
|
8
|
+
end
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
failure_message do |datastore|
|
13
|
+
"expected #{datastore[relation_name]} to have persisted #{data.inspect} in #{relation_name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
failure_message_when_negated do |datastore|
|
17
|
+
failure_message.gsub("to have", "not to have")
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require "terrestrial"
|
2
|
+
require "terrestrial/mapper_facade"
|
3
|
+
require "terrestrial/relation_mapping"
|
4
|
+
require "terrestrial/lazy_collection"
|
5
|
+
require "terrestrial/collection_mutability_proxy"
|
6
|
+
require "terrestrial/lazy_object_proxy"
|
7
|
+
require "terrestrial/dataset"
|
8
|
+
require "terrestrial/query_order"
|
9
|
+
require "terrestrial/one_to_many_association"
|
10
|
+
require "terrestrial/many_to_one_association"
|
11
|
+
require "terrestrial/many_to_many_association"
|
12
|
+
require "terrestrial/subset_queries_proxy"
|
13
|
+
require "support/object_graph_setup"
|
14
|
+
|
15
|
+
RSpec.shared_context "mapper setup" do
|
16
|
+
include_context "object graph setup"
|
17
|
+
|
18
|
+
let(:mappers) {
|
19
|
+
Terrestrial.mappers(mappings: mappings, datastore: datastore)
|
20
|
+
}
|
21
|
+
|
22
|
+
let(:user_mapper) { mappers[:users] }
|
23
|
+
|
24
|
+
let(:mappings) {
|
25
|
+
Hash[
|
26
|
+
configs.map { |name, config|
|
27
|
+
fields = config.fetch(:fields) + config.fetch(:associations).keys
|
28
|
+
|
29
|
+
associations = config.fetch(:associations).map { |assoc_name, assoc_config|
|
30
|
+
[
|
31
|
+
assoc_name,
|
32
|
+
case assoc_config.fetch(:type)
|
33
|
+
when :one_to_many
|
34
|
+
Terrestrial::OneToManyAssociation.new(
|
35
|
+
**assoc_defaults.merge(
|
36
|
+
assoc_config.dup.tap { |h| h.delete(:type) }
|
37
|
+
)
|
38
|
+
)
|
39
|
+
when :many_to_one
|
40
|
+
Terrestrial::ManyToOneAssociation.new(
|
41
|
+
assoc_config.dup.tap { |h| h.delete(:type) }
|
42
|
+
)
|
43
|
+
when :many_to_many
|
44
|
+
Terrestrial::ManyToManyAssociation.new(
|
45
|
+
**assoc_defaults
|
46
|
+
.merge(
|
47
|
+
join_mapping_name: assoc_config.fetch(:join_mapping_name),
|
48
|
+
)
|
49
|
+
.merge(
|
50
|
+
assoc_config.dup.tap { |h|
|
51
|
+
h.delete(:type)
|
52
|
+
h.delete(:join_namespace)
|
53
|
+
}
|
54
|
+
)
|
55
|
+
)
|
56
|
+
else
|
57
|
+
raise "Association type not supported"
|
58
|
+
end
|
59
|
+
]
|
60
|
+
}
|
61
|
+
|
62
|
+
[
|
63
|
+
name,
|
64
|
+
Terrestrial::RelationMapping.new(
|
65
|
+
name: name,
|
66
|
+
namespace: config.fetch(:namespace),
|
67
|
+
fields: config.fetch(:fields),
|
68
|
+
primary_key: config.fetch(:primary_key),
|
69
|
+
serializer: serializers.fetch(config.fetch(:serializer)).call(fields),
|
70
|
+
associations: Hash[associations],
|
71
|
+
factory: factories.fetch(name),
|
72
|
+
subsets: Terrestrial::SubsetQueriesProxy.new(config.fetch(:subsets, {}))
|
73
|
+
)
|
74
|
+
]
|
75
|
+
}
|
76
|
+
]
|
77
|
+
}
|
78
|
+
|
79
|
+
def assoc_defaults
|
80
|
+
{
|
81
|
+
order: Terrestrial::QueryOrder.new(fields: [], direction: "ASC")
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
let(:has_many_proxy_factory) {
|
86
|
+
->(query:, loader:, mapping_name:) {
|
87
|
+
Terrestrial::CollectionMutabilityProxy.new(
|
88
|
+
Terrestrial::LazyCollection.new(
|
89
|
+
query,
|
90
|
+
loader,
|
91
|
+
mappings.fetch(mapping_name).subsets,
|
92
|
+
)
|
93
|
+
)
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
let(:many_to_one_proxy_factory) {
|
98
|
+
->(query:, loader:, preloaded_data:) {
|
99
|
+
Terrestrial::LazyObjectProxy.new(
|
100
|
+
->{ loader.call(query.first) },
|
101
|
+
preloaded_data,
|
102
|
+
)
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
let(:serializers) {
|
107
|
+
{
|
108
|
+
default: default_serializer,
|
109
|
+
null: null_serializer,
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
let(:configs) {
|
114
|
+
{
|
115
|
+
users: {
|
116
|
+
namespace: :users,
|
117
|
+
primary_key: [:id],
|
118
|
+
fields: [
|
119
|
+
:id,
|
120
|
+
:first_name,
|
121
|
+
:last_name,
|
122
|
+
:email,
|
123
|
+
],
|
124
|
+
factory: :user,
|
125
|
+
serializer: :default,
|
126
|
+
associations: {
|
127
|
+
posts: {
|
128
|
+
type: :one_to_many,
|
129
|
+
mapping_name: :posts,
|
130
|
+
foreign_key: :author_id,
|
131
|
+
key: :id,
|
132
|
+
proxy_factory: has_many_proxy_factory,
|
133
|
+
}
|
134
|
+
},
|
135
|
+
},
|
136
|
+
|
137
|
+
posts: {
|
138
|
+
namespace: :posts,
|
139
|
+
primary_key: [:id],
|
140
|
+
fields: [
|
141
|
+
:id,
|
142
|
+
:subject,
|
143
|
+
:body,
|
144
|
+
:created_at,
|
145
|
+
],
|
146
|
+
factory: :post,
|
147
|
+
serializer: :default,
|
148
|
+
associations: {
|
149
|
+
comments: {
|
150
|
+
type: :one_to_many,
|
151
|
+
mapping_name: :comments,
|
152
|
+
foreign_key: :post_id,
|
153
|
+
key: :id,
|
154
|
+
proxy_factory: has_many_proxy_factory,
|
155
|
+
},
|
156
|
+
categories: {
|
157
|
+
type: :many_to_many,
|
158
|
+
mapping_name: :categories,
|
159
|
+
key: :id,
|
160
|
+
foreign_key: :post_id,
|
161
|
+
association_foreign_key: :category_id,
|
162
|
+
association_key: :id,
|
163
|
+
join_mapping_name: :categories_to_posts,
|
164
|
+
proxy_factory: has_many_proxy_factory,
|
165
|
+
},
|
166
|
+
},
|
167
|
+
},
|
168
|
+
|
169
|
+
comments: {
|
170
|
+
namespace: :comments,
|
171
|
+
primary_key: [:id],
|
172
|
+
fields: [
|
173
|
+
:id,
|
174
|
+
:body,
|
175
|
+
],
|
176
|
+
factory: :comment,
|
177
|
+
serializer: :default,
|
178
|
+
associations: {
|
179
|
+
commenter: {
|
180
|
+
type: :many_to_one,
|
181
|
+
mapping_name: :users,
|
182
|
+
key: :id,
|
183
|
+
foreign_key: :commenter_id,
|
184
|
+
proxy_factory: many_to_one_proxy_factory,
|
185
|
+
},
|
186
|
+
},
|
187
|
+
},
|
188
|
+
|
189
|
+
categories: {
|
190
|
+
namespace: :categories,
|
191
|
+
primary_key: [:id],
|
192
|
+
fields: [
|
193
|
+
:id,
|
194
|
+
:name,
|
195
|
+
],
|
196
|
+
factory: :comment,
|
197
|
+
serializer: :default,
|
198
|
+
associations: {
|
199
|
+
posts: {
|
200
|
+
type: :many_to_many,
|
201
|
+
mapping_name: :posts,
|
202
|
+
key: :id,
|
203
|
+
foreign_key: :category_id,
|
204
|
+
association_foreign_key: :post_id,
|
205
|
+
association_key: :id,
|
206
|
+
join_mapping_name: :categories_to_posts,
|
207
|
+
proxy_factory: has_many_proxy_factory,
|
208
|
+
},
|
209
|
+
},
|
210
|
+
},
|
211
|
+
|
212
|
+
categories_to_posts: {
|
213
|
+
namespace: :categories_to_posts,
|
214
|
+
primary_key: [:category_id, :post_id],
|
215
|
+
fields: [],
|
216
|
+
serializer: :null,
|
217
|
+
associations: {},
|
218
|
+
}
|
219
|
+
}
|
220
|
+
}
|
221
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
class Terrestrial::MockSequel
|
2
|
+
def initialize(relations)
|
3
|
+
@relations = {}
|
4
|
+
|
5
|
+
relations.each do |table_name|
|
6
|
+
@relations[table_name] = Relation.new(self, [])
|
7
|
+
end
|
8
|
+
|
9
|
+
@reads, @writes, @deletes = 0, 0, 0
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :relations
|
13
|
+
private :relations
|
14
|
+
|
15
|
+
def [](table_name)
|
16
|
+
@relations.fetch(table_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def log_read
|
20
|
+
@reads += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def log_write
|
24
|
+
@writes += 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def log_delete
|
28
|
+
@deletes += 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def read_count
|
32
|
+
@reads
|
33
|
+
end
|
34
|
+
|
35
|
+
def write_count
|
36
|
+
@writes
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete_count
|
40
|
+
@deletes
|
41
|
+
end
|
42
|
+
|
43
|
+
class Query
|
44
|
+
def initialize(criteria: {}, order: [], reverse: false, &block)
|
45
|
+
if block
|
46
|
+
raise NotImplementedError.new("Block filtering not implemented")
|
47
|
+
end
|
48
|
+
|
49
|
+
@criteria = criteria
|
50
|
+
@order_columns = order
|
51
|
+
@reverse_order = reverse
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :criteria, :order_columns
|
55
|
+
|
56
|
+
def where(new_criteria, &block)
|
57
|
+
self.class.new(
|
58
|
+
criteria: criteria.merge(new_criteria),
|
59
|
+
order: order,
|
60
|
+
reverse: reverse,
|
61
|
+
&block
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def order(columns)
|
66
|
+
self.class.new(
|
67
|
+
criteria: criteria,
|
68
|
+
order: columns,
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def reverse
|
73
|
+
self.class.new(
|
74
|
+
criteria: criteria,
|
75
|
+
order: order_columns,
|
76
|
+
reverse: true,
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def reverse_order?
|
81
|
+
!!@reverse_order
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Relation
|
86
|
+
include Enumerable
|
87
|
+
|
88
|
+
def initialize(database, all_rows, applied_query: Query.new)
|
89
|
+
@database = database
|
90
|
+
@all_rows = all_rows
|
91
|
+
@applied_query = applied_query
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_reader :database, :all_rows, :applied_query
|
95
|
+
private :database, :all_rows, :applied_query
|
96
|
+
|
97
|
+
def where(criteria, &block)
|
98
|
+
new_with_query(Query.new(criteria: criteria, &block))
|
99
|
+
end
|
100
|
+
|
101
|
+
def order(columns)
|
102
|
+
new_with_query(applied_query.order(columns))
|
103
|
+
end
|
104
|
+
|
105
|
+
def reverse
|
106
|
+
@applied_query = @applied_query.reverse
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_a
|
110
|
+
database.log_read
|
111
|
+
|
112
|
+
matching_rows
|
113
|
+
end
|
114
|
+
|
115
|
+
def each(&block)
|
116
|
+
database.log_read
|
117
|
+
|
118
|
+
matching_rows.each(&block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def delete
|
122
|
+
database.log_delete
|
123
|
+
|
124
|
+
matching_rows.each do |row_to_delete|
|
125
|
+
all_rows.delete(row_to_delete)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def insert(new_row)
|
130
|
+
database.log_write
|
131
|
+
|
132
|
+
all_rows.push(new_row)
|
133
|
+
end
|
134
|
+
|
135
|
+
def update(attrs)
|
136
|
+
database.log_write
|
137
|
+
|
138
|
+
# No need to get the rows from the canonical relation as the hashes can
|
139
|
+
# just be mutated in plaace.
|
140
|
+
matching_rows.each do |row|
|
141
|
+
attrs.each do |k, v|
|
142
|
+
row[k] = v
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def empty?
|
148
|
+
database.log_read
|
149
|
+
|
150
|
+
matching_rows.empty?
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def matching_rows
|
156
|
+
apply_sort(
|
157
|
+
equality_filter(all_rows, applied_query.criteria),
|
158
|
+
applied_query.order_columns,
|
159
|
+
applied_query.reverse_order?,
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
def apply_sort(rows, order_columns, reverse_order)
|
164
|
+
sorted_rows = rows.sort_by{ |row|
|
165
|
+
order_columns.map { |col| row.fetch(col) }
|
166
|
+
}
|
167
|
+
|
168
|
+
if reverse_order
|
169
|
+
sorted_rows.reverse
|
170
|
+
else
|
171
|
+
sorted_rows
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def equality_filter(rows, criteria)
|
176
|
+
rows.select { |row|
|
177
|
+
criteria.all? { |k, v|
|
178
|
+
if v.is_a?(Enumerable)
|
179
|
+
v.include?(row.fetch(k))
|
180
|
+
else
|
181
|
+
row.fetch(k) == v
|
182
|
+
end
|
183
|
+
}
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def new_with_query(query)
|
190
|
+
self.class.new(database, all_rows, applied_query: query)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|