terrestrial 0.3.0 → 0.5.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 +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
|