terrestrial 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/Gemfile.lock +44 -53
- data/README.md +3 -6
- data/bin/test +1 -1
- data/features/env.rb +12 -2
- data/features/example.feature +23 -26
- data/lib/terrestrial.rb +31 -0
- data/lib/terrestrial/adapters/abstract_adapter.rb +6 -0
- data/lib/terrestrial/adapters/memory_adapter.rb +82 -6
- data/lib/terrestrial/adapters/sequel_postgres_adapter.rb +191 -0
- data/lib/terrestrial/configurations/conventional_association_configuration.rb +65 -35
- data/lib/terrestrial/configurations/conventional_configuration.rb +280 -124
- data/lib/terrestrial/configurations/mapping_config_options_proxy.rb +97 -0
- data/lib/terrestrial/deleted_record.rb +12 -8
- data/lib/terrestrial/dirty_map.rb +17 -9
- data/lib/terrestrial/functional_pipeline.rb +64 -0
- data/lib/terrestrial/inspection_string.rb +6 -1
- data/lib/terrestrial/lazy_object_proxy.rb +1 -0
- data/lib/terrestrial/many_to_many_association.rb +34 -20
- data/lib/terrestrial/many_to_one_association.rb +11 -3
- data/lib/terrestrial/one_to_many_association.rb +9 -0
- data/lib/terrestrial/public_conveniencies.rb +65 -82
- data/lib/terrestrial/record.rb +106 -0
- data/lib/terrestrial/relation_mapping.rb +43 -12
- data/lib/terrestrial/relational_store.rb +33 -11
- data/lib/terrestrial/upsert_record.rb +54 -0
- data/lib/terrestrial/version.rb +1 -1
- data/spec/automatic_timestamps_spec.rb +339 -0
- data/spec/changes_api_spec.rb +81 -0
- data/spec/config_override_spec.rb +28 -19
- data/spec/custom_serializers_spec.rb +3 -2
- data/spec/database_default_fields_spec.rb +213 -0
- data/spec/database_generated_id_spec.rb +291 -0
- data/spec/database_owned_fields_and_timestamps_spec.rb +200 -0
- data/spec/deletion_spec.rb +1 -1
- data/spec/error_handling/factory_error_handling_spec.rb +1 -4
- data/spec/error_handling/serialization_error_spec.rb +1 -4
- data/spec/error_handling/upsert_error_spec.rb +7 -11
- data/spec/graph_persistence_spec.rb +52 -18
- data/spec/ordered_association_spec.rb +10 -12
- data/spec/predefined_queries_spec.rb +14 -12
- data/spec/readme_examples_spec.rb +1 -1
- data/spec/sequel_query_efficiency_spec.rb +19 -16
- data/spec/spec_helper.rb +6 -1
- data/spec/support/blog_schema.rb +7 -3
- data/spec/support/object_graph_setup.rb +30 -39
- data/spec/support/object_store_setup.rb +16 -196
- data/spec/support/seed_data_setup.rb +15 -149
- data/spec/support/seed_records.rb +141 -0
- data/spec/support/sequel_test_support.rb +46 -13
- data/spec/terrestrial/abstract_record_spec.rb +138 -106
- data/spec/terrestrial/adapters/sequel_postgres_adapter_spec.rb +138 -0
- data/spec/terrestrial/deleted_record_spec.rb +0 -27
- data/spec/terrestrial/dirty_map_spec.rb +52 -77
- data/spec/terrestrial/functional_pipeline_spec.rb +153 -0
- data/spec/terrestrial/inspection_string_spec.rb +61 -0
- data/spec/terrestrial/upsert_record_spec.rb +29 -0
- data/terrestrial.gemspec +7 -8
- metadata +43 -40
- data/MissingFeatures.md +0 -64
- data/lib/terrestrial/abstract_record.rb +0 -99
- data/lib/terrestrial/association_loaders.rb +0 -52
- data/lib/terrestrial/upserted_record.rb +0 -15
- data/spec/terrestrial/public_conveniencies_spec.rb +0 -63
- data/spec/terrestrial/upserted_record_spec.rb +0 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 67854cbc017e8771ba0794d62cfc07b7cf55548c49327cd9a8c82c3167076bc1
|
4
|
+
data.tar.gz: e0fb7f57b53e059485238e36aa00699f15664b721c353e98ec4050d1283dfa6b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a41417eaba6d60cdebab5586f61d6c6534bccffb19d736157c7ae43c3651b973b878e1c05ad012d5f8cbb9247af2126d054154c3ec6c506db3158525174e48aa
|
7
|
+
data.tar.gz: aed7b695f3457e7c25ce9c4285137ee04106bbd9692f2d18b11cf680bb002c4b75d96c3f2b066009e6f697b230896442f13aa8173451f4edccdc9fb1eee665b4
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.7.1
|
data/Gemfile.lock
CHANGED
@@ -1,78 +1,69 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
terrestrial (0.
|
5
|
-
activesupport (~> 4.0)
|
4
|
+
terrestrial (0.5.0)
|
6
5
|
fetchable (~> 1.0)
|
7
|
-
sequel (~>
|
6
|
+
sequel (~> 5.0)
|
8
7
|
|
9
8
|
GEM
|
10
9
|
remote: https://rubygems.org/
|
11
10
|
specs:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
thread_safe (~> 0.3, >= 0.3.4)
|
17
|
-
tzinfo (~> 1.1)
|
18
|
-
builder (3.2.2)
|
19
|
-
coderay (1.1.1)
|
20
|
-
cucumber (2.4.0)
|
11
|
+
backports (3.20.2)
|
12
|
+
builder (3.2.4)
|
13
|
+
coderay (1.1.3)
|
14
|
+
cucumber (3.2.0)
|
21
15
|
builder (>= 2.1.2)
|
22
|
-
cucumber-core (~>
|
16
|
+
cucumber-core (~> 3.2.0)
|
17
|
+
cucumber-expressions (~> 6.0.1)
|
23
18
|
cucumber-wire (~> 0.0.1)
|
24
|
-
diff-lcs (
|
25
|
-
gherkin (~>
|
19
|
+
diff-lcs (~> 1.3)
|
20
|
+
gherkin (~> 5.1.0)
|
26
21
|
multi_json (>= 1.7.5, < 2.0)
|
27
22
|
multi_test (>= 0.1.2)
|
28
|
-
cucumber-core (
|
29
|
-
|
23
|
+
cucumber-core (3.2.1)
|
24
|
+
backports (>= 3.8.0)
|
25
|
+
cucumber-tag_expressions (~> 1.1.0)
|
26
|
+
gherkin (~> 5.0)
|
27
|
+
cucumber-expressions (6.0.1)
|
28
|
+
cucumber-tag_expressions (1.1.1)
|
30
29
|
cucumber-wire (0.0.1)
|
31
|
-
diff-lcs (1.
|
30
|
+
diff-lcs (1.4.4)
|
32
31
|
fetchable (1.0.0)
|
33
|
-
gherkin (
|
34
|
-
|
35
|
-
|
36
|
-
method_source (0.8.2)
|
37
|
-
minitest (5.9.1)
|
38
|
-
multi_json (1.12.1)
|
32
|
+
gherkin (5.1.0)
|
33
|
+
method_source (1.0.0)
|
34
|
+
multi_json (1.15.0)
|
39
35
|
multi_test (0.1.2)
|
40
|
-
pg (
|
41
|
-
pry (0.
|
42
|
-
coderay (~> 1.1
|
43
|
-
method_source (~> 0
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
rspec-
|
48
|
-
rspec-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
rspec-expectations (3.5.0)
|
36
|
+
pg (1.2.3)
|
37
|
+
pry (0.14.0)
|
38
|
+
coderay (~> 1.1)
|
39
|
+
method_source (~> 1.0)
|
40
|
+
rake (13.0.3)
|
41
|
+
rspec (3.10.0)
|
42
|
+
rspec-core (~> 3.10.0)
|
43
|
+
rspec-expectations (~> 3.10.0)
|
44
|
+
rspec-mocks (~> 3.10.0)
|
45
|
+
rspec-core (3.10.1)
|
46
|
+
rspec-support (~> 3.10.0)
|
47
|
+
rspec-expectations (3.10.1)
|
53
48
|
diff-lcs (>= 1.2.0, < 2.0)
|
54
|
-
rspec-support (~> 3.
|
55
|
-
rspec-mocks (3.
|
49
|
+
rspec-support (~> 3.10.0)
|
50
|
+
rspec-mocks (3.10.2)
|
56
51
|
diff-lcs (>= 1.2.0, < 2.0)
|
57
|
-
rspec-support (~> 3.
|
58
|
-
rspec-support (3.
|
59
|
-
sequel (
|
60
|
-
slop (3.6.0)
|
61
|
-
thread_safe (0.3.5)
|
62
|
-
tzinfo (1.2.2)
|
63
|
-
thread_safe (~> 0.1)
|
52
|
+
rspec-support (~> 3.10.0)
|
53
|
+
rspec-support (3.10.2)
|
54
|
+
sequel (5.42.0)
|
64
55
|
|
65
56
|
PLATFORMS
|
66
57
|
ruby
|
67
58
|
|
68
59
|
DEPENDENCIES
|
69
|
-
bundler (~> 1
|
70
|
-
cucumber
|
71
|
-
pg (~> 0
|
72
|
-
pry (~> 0.
|
73
|
-
rake (~>
|
74
|
-
rspec (~> 3.
|
60
|
+
bundler (~> 1)
|
61
|
+
cucumber (~> 3.1)
|
62
|
+
pg (~> 1.0)
|
63
|
+
pry (~> 0.13)
|
64
|
+
rake (~> 13.0)
|
65
|
+
rspec (~> 3.9)
|
75
66
|
terrestrial!
|
76
67
|
|
77
68
|
BUNDLED WITH
|
78
|
-
1.
|
69
|
+
1.17.1
|
data/README.md
CHANGED
@@ -74,7 +74,7 @@ code of conduct first.
|
|
74
74
|
## This is kept separate from your domain models as knowledge of the schema
|
75
75
|
## is required to wire them up.
|
76
76
|
|
77
|
-
|
77
|
+
CONFIG = Terrestrial.config(DB)
|
78
78
|
.setup_mapping(:users) { |users|
|
79
79
|
users.class(User) # Specify a class and the constructor will be used
|
80
80
|
users.has_many(:posts, foreign_key: :author_id)
|
@@ -92,12 +92,9 @@ code of conduct first.
|
|
92
92
|
|
93
93
|
# 4. Create an object store by combining a connection and a configuration
|
94
94
|
|
95
|
-
OBJECT_STORE = Terrestrial.object_store(
|
96
|
-
datastore: DB,
|
97
|
-
mappings: MAPPINGS,
|
98
|
-
)
|
95
|
+
OBJECT_STORE = Terrestrial.object_store(config: CONFIG)
|
99
96
|
|
100
|
-
## You are not
|
97
|
+
## You are not limited to one object store configuration or one database
|
101
98
|
## connection. To handle complex situations you may create several segregated
|
102
99
|
## mappings and object stores for your separate aggregate roots, potentially
|
103
100
|
## utilising multiple databases and different domain object
|
data/bin/test
CHANGED
data/features/env.rb
CHANGED
@@ -28,18 +28,28 @@ module ExampleRunnerSupport
|
|
28
28
|
.strip
|
29
29
|
.gsub(/[\n\s]+/, " ")
|
30
30
|
.gsub(/ \>/, ">")
|
31
|
-
.gsub(/\:0x[0-9a-f]{14}/, ":<<object id removed>>")
|
31
|
+
.gsub(/\:0x[0-9a-f]{14,}/, ":<<object id removed>>")
|
32
32
|
end
|
33
33
|
|
34
34
|
def parse_schema_table(string)
|
35
35
|
string.each_line.drop(2).map { |line|
|
36
|
-
name, type = line.split("|").map(&:strip)
|
36
|
+
name, type, options = line.split("|").map(&:strip)
|
37
|
+
|
37
38
|
{
|
38
39
|
name: name,
|
39
40
|
type: Object.const_get(type),
|
41
|
+
options: string_to_schema_options(options.to_s),
|
40
42
|
}
|
41
43
|
}
|
42
44
|
end
|
45
|
+
|
46
|
+
def string_to_schema_options(string)
|
47
|
+
Hash[
|
48
|
+
string.split(",").map(&:strip).reject(&:empty?).map { |s|
|
49
|
+
[s.downcase.gsub(" ", "_").to_sym, true]
|
50
|
+
}
|
51
|
+
]
|
52
|
+
end
|
43
53
|
end
|
44
54
|
|
45
55
|
module DatabaseSupport
|
data/features/example.feature
CHANGED
@@ -9,36 +9,36 @@ Feature: Basic setup
|
|
9
9
|
"""
|
10
10
|
And a conventionally similar database schema for table "users"
|
11
11
|
"""
|
12
|
-
Column | Type
|
13
|
-
|
14
|
-
id | String
|
15
|
-
first_name | String
|
16
|
-
last_name | String
|
17
|
-
email | String
|
12
|
+
Column | Type | Options
|
13
|
+
-------------+----------+-------------
|
14
|
+
id | String | Primary key
|
15
|
+
first_name | String |
|
16
|
+
last_name | String |
|
17
|
+
email | String |
|
18
18
|
"""
|
19
19
|
And a conventionally similar database schema for table "posts"
|
20
20
|
"""
|
21
|
-
Column | Type
|
22
|
-
|
23
|
-
id | String
|
24
|
-
author_id | String
|
25
|
-
subject | String
|
26
|
-
body | String
|
27
|
-
created_at | DateTime
|
21
|
+
Column | Type | Options
|
22
|
+
-------------┼----------+-------------
|
23
|
+
id | String | Primary key
|
24
|
+
author_id | String |
|
25
|
+
subject | String |
|
26
|
+
body | String |
|
27
|
+
created_at | DateTime |
|
28
28
|
"""
|
29
29
|
And a conventionally similar database schema for table "categories"
|
30
30
|
"""
|
31
|
-
Column | Type
|
32
|
-
|
33
|
-
id | String
|
34
|
-
name | String
|
31
|
+
Column | Type | Options
|
32
|
+
-------------+---------+-------------
|
33
|
+
id | String | Primary key
|
34
|
+
name | String |
|
35
35
|
"""
|
36
36
|
And a conventionally similar database schema for table "categories_to_posts"
|
37
37
|
"""
|
38
|
-
Column | Type
|
39
|
-
|
40
|
-
post_id | String
|
41
|
-
category_id | String
|
38
|
+
Column | Type | Options
|
39
|
+
-------------+---------+-------------
|
40
|
+
post_id | String | Primary key
|
41
|
+
category_id | String |
|
42
42
|
"""
|
43
43
|
And a database connection is established
|
44
44
|
"""
|
@@ -50,7 +50,7 @@ Feature: Basic setup
|
|
50
50
|
"""
|
51
51
|
And the associations are defined in the configuration
|
52
52
|
"""
|
53
|
-
|
53
|
+
CONFIG = Terrestrial.config(DB)
|
54
54
|
.setup_mapping(:users) { |users|
|
55
55
|
users.class(User)
|
56
56
|
users.has_many(:posts, foreign_key: :author_id)
|
@@ -67,10 +67,7 @@ Feature: Basic setup
|
|
67
67
|
"""
|
68
68
|
And a object store is instantiated
|
69
69
|
"""
|
70
|
-
OBJECT_STORE = Terrestrial.object_store(
|
71
|
-
datastore: DB,
|
72
|
-
mappings: MAPPINGS,
|
73
|
-
)
|
70
|
+
OBJECT_STORE = Terrestrial.object_store(config: CONFIG)
|
74
71
|
"""
|
75
72
|
When a new graph of objects are created
|
76
73
|
"""
|
data/lib/terrestrial.rb
CHANGED
@@ -2,7 +2,38 @@ require "logger"
|
|
2
2
|
require "terrestrial/public_conveniencies"
|
3
3
|
|
4
4
|
module Terrestrial
|
5
|
+
# TODO: whoa! wtf is this? why did i?
|
5
6
|
extend PublicConveniencies
|
6
7
|
|
7
8
|
LOGGER = Logger.new(STDERR)
|
9
|
+
|
10
|
+
class DatabaseID
|
11
|
+
def initialize(val = nil)
|
12
|
+
@value = val
|
13
|
+
end
|
14
|
+
|
15
|
+
def sql_literal(_dataset)
|
16
|
+
@value.nil? ? "NULL" : @value.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def nil?
|
20
|
+
@value.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def value=(v)
|
24
|
+
@value = v
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
inspect
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#<%{class_name}>:0x%{hex_object_id} @value=%{value}>" % {
|
33
|
+
class_name: self.class.name,
|
34
|
+
hex_object_id: object_id.<<(1).to_s(16),
|
35
|
+
value: @value,
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
8
39
|
end
|
@@ -1,9 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
end
|
4
|
-
end
|
1
|
+
require "terrestrial/adapters/abstract_adapter"
|
2
|
+
require "terrestrial/error"
|
5
3
|
|
6
4
|
class Terrestrial::Adapters::MemoryAdapter
|
5
|
+
include Terrestrial::Adapters::AbstractAdapter
|
6
|
+
|
7
7
|
def self.build_from_schema(schema, raw_storage)
|
8
8
|
schema.each { |name, _| raw_storage[name] = [] }
|
9
9
|
|
@@ -55,6 +55,18 @@ class Terrestrial::Adapters::MemoryAdapter
|
|
55
55
|
@relations.fetch(table_name)
|
56
56
|
end
|
57
57
|
|
58
|
+
def upsert(record)
|
59
|
+
existing = self[record.namespace].where(record.identity)
|
60
|
+
|
61
|
+
if existing.any?
|
62
|
+
existing.update(record.updatable_attributes)
|
63
|
+
else
|
64
|
+
self[record.namespace].insert(record.to_h)
|
65
|
+
end
|
66
|
+
rescue Object => e
|
67
|
+
raise Terrestrial::UpsertError.new(record.namespace, record.to_h, e)
|
68
|
+
end
|
69
|
+
|
58
70
|
private
|
59
71
|
|
60
72
|
def rollback(relations)
|
@@ -148,10 +160,19 @@ class Terrestrial::Adapters::MemoryAdapter
|
|
148
160
|
end
|
149
161
|
end
|
150
162
|
|
163
|
+
def insert_conflict(target:, update: {})
|
164
|
+
Upsert.new(self, target: target, update: update)
|
165
|
+
end
|
166
|
+
|
151
167
|
def insert(new_row)
|
152
|
-
|
168
|
+
new_row_with_all_fields = empty_row.merge(clone(new_row))
|
169
|
+
row_id = extract_row_id(new_row_with_all_fields)
|
153
170
|
|
154
|
-
|
171
|
+
if row_id.any? && where(row_id).any?
|
172
|
+
raise DuplicateKeyError.new(row_id)
|
173
|
+
else
|
174
|
+
all_rows.push(new_row_with_all_fields)
|
175
|
+
end
|
155
176
|
end
|
156
177
|
|
157
178
|
def update(attrs)
|
@@ -237,5 +258,60 @@ class Terrestrial::Adapters::MemoryAdapter
|
|
237
258
|
def clone(object)
|
238
259
|
Marshal.load(Marshal.dump(object))
|
239
260
|
end
|
261
|
+
|
262
|
+
def extract_row_id(row)
|
263
|
+
row.select { |k, _v| primary_key.include?(k) }
|
264
|
+
end
|
265
|
+
|
266
|
+
def primary_key
|
267
|
+
@primary_key ||= schema
|
268
|
+
.select { |col| col.fetch(:options, {}).fetch(:primary_key, nil) }
|
269
|
+
.map { |col| col.fetch(:name) }
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Small amount of code necessary to simulate upserts with Sequel's API
|
274
|
+
class Upsert
|
275
|
+
def initialize(dataset, target:, update:)
|
276
|
+
@dataset = dataset
|
277
|
+
@target = target
|
278
|
+
@update_attributes = update
|
279
|
+
end
|
280
|
+
|
281
|
+
attr_reader :dataset, :target, :update_attributes
|
282
|
+
private :dataset, :target, :update_attributes
|
283
|
+
|
284
|
+
def insert(row)
|
285
|
+
dataset.insert(row)
|
286
|
+
rescue DuplicateKeyError => e
|
287
|
+
if target_matches?(e.key)
|
288
|
+
attempt_update(e.row_id)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def attempt_update(row_id)
|
293
|
+
dataset.where(row_id).update(update_attributes)
|
294
|
+
end
|
295
|
+
|
296
|
+
def target_matches?(key)
|
297
|
+
key.sort == Array(target).sort
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
class DuplicateKeyError < RuntimeError
|
303
|
+
def initialize(row_id)
|
304
|
+
@row_id = row_id
|
305
|
+
end
|
306
|
+
|
307
|
+
attr_reader :row_id
|
308
|
+
|
309
|
+
def key
|
310
|
+
row_id.keys
|
311
|
+
end
|
312
|
+
|
313
|
+
def message
|
314
|
+
"Insert conflict. Row with `#{row_id}` already exists"
|
315
|
+
end
|
240
316
|
end
|
241
317
|
end
|