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
@@ -0,0 +1,97 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Configurations
|
3
|
+
class MappingConfigOptionsProxy
|
4
|
+
def initialize(configuration, mapping_name)
|
5
|
+
@configuration = configuration
|
6
|
+
@mapping_name = mapping_name
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :configuration, :mapping_name
|
10
|
+
private :configuration, :mapping_name
|
11
|
+
|
12
|
+
def relation_name(name)
|
13
|
+
add_override(relation_name: name)
|
14
|
+
end
|
15
|
+
alias_method :table_name, :relation_name
|
16
|
+
|
17
|
+
def subset(subset_name, &block)
|
18
|
+
configuration.add_subset(mapping_name, subset_name, block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_many(*args)
|
22
|
+
add_association(:has_many, args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_many_through(*args)
|
26
|
+
add_association(:has_many_through, args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def belongs_to(*args)
|
30
|
+
add_association(:belongs_to, args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def fields(field_names)
|
34
|
+
add_override(fields: field_names)
|
35
|
+
end
|
36
|
+
|
37
|
+
def primary_key(field_names)
|
38
|
+
add_override(primary_key: field_names)
|
39
|
+
end
|
40
|
+
|
41
|
+
def use_database_id(&block)
|
42
|
+
add_override(use_database_id: true)
|
43
|
+
block && add_override(database_id_setter: block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def database_owned_field(field_name, &object_setter)
|
47
|
+
configuration.overrides.fetch(mapping_name)[:database_owned_fields_setter_map] ||= {}
|
48
|
+
db_owned_fields = configuration.overrides.fetch(mapping_name).fetch(:database_owned_fields_setter_map)
|
49
|
+
|
50
|
+
db_owned_fields.merge!({field_name => object_setter})
|
51
|
+
end
|
52
|
+
|
53
|
+
def database_default_field(field_name, &object_setter)
|
54
|
+
configuration.overrides.fetch(mapping_name)[:database_default_fields_setter_map] ||= {}
|
55
|
+
db_default_fields = configuration.overrides.fetch(mapping_name).fetch(:database_default_fields_setter_map)
|
56
|
+
|
57
|
+
db_default_fields.merge!({field_name => object_setter})
|
58
|
+
end
|
59
|
+
|
60
|
+
def created_at_timestamp(field_name = Default, &block)
|
61
|
+
add_override(created_at_field: field_name)
|
62
|
+
block && add_override(created_at_setter: block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def updated_at_timestamp(field_name = Default, &block)
|
66
|
+
add_override(updated_at_field: field_name)
|
67
|
+
block && add_override(updated_at_setter: block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def factory(callable)
|
71
|
+
add_override(factory: callable)
|
72
|
+
end
|
73
|
+
|
74
|
+
def class(entity_class)
|
75
|
+
add_override('class': entity_class)
|
76
|
+
end
|
77
|
+
|
78
|
+
def class_name(class_name)
|
79
|
+
add_override(class_name: class_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def serializer(serializer_func)
|
83
|
+
add_override(serializer: serializer_func)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def add_override(*args)
|
89
|
+
configuration.add_override(mapping_name, *args)
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_association(*args)
|
93
|
+
configuration.add_association(mapping_name, *args)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -1,20 +1,24 @@
|
|
1
|
-
require "terrestrial/
|
1
|
+
require "terrestrial/record"
|
2
2
|
|
3
3
|
module Terrestrial
|
4
|
-
class DeletedRecord <
|
4
|
+
class DeletedRecord < Record
|
5
|
+
def initialize(mapping, attributes, depth)
|
6
|
+
@mapping = mapping
|
7
|
+
@attributes = attributes
|
8
|
+
@depth = depth
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :mapping, :attributes, :depth
|
12
|
+
|
5
13
|
def if_delete(&block)
|
6
14
|
block.call(self)
|
7
15
|
self
|
8
16
|
end
|
9
17
|
|
10
|
-
def subset?(_other)
|
11
|
-
false
|
12
|
-
end
|
13
|
-
|
14
18
|
protected
|
15
19
|
|
16
|
-
def
|
17
|
-
|
20
|
+
def new_with_attributes(new_attributes)
|
21
|
+
self.class.new(mapping, new_attributes, depth)
|
18
22
|
end
|
19
23
|
end
|
20
24
|
end
|
@@ -13,15 +13,12 @@ module Terrestrial
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def load(record)
|
16
|
-
storage.store(hash_key(record), deep_clone
|
16
|
+
storage.store(hash_key(record), record.deep_clone)
|
17
17
|
record
|
18
18
|
end
|
19
19
|
|
20
20
|
def dirty?(record)
|
21
|
-
|
22
|
-
return true if record_as_loaded == NotFound
|
23
|
-
|
24
|
-
!record.subset?(record_as_loaded)
|
21
|
+
!same_as_loaded?(record) || deleted?(record)
|
25
22
|
end
|
26
23
|
|
27
24
|
def reject_unchanged_fields(record)
|
@@ -36,12 +33,23 @@ module Terrestrial
|
|
36
33
|
|
37
34
|
NotFound = Module.new
|
38
35
|
|
39
|
-
def
|
40
|
-
|
36
|
+
def same_as_loaded?(record)
|
37
|
+
record_as_loaded = storage.fetch(hash_key(record), NotFound)
|
38
|
+
|
39
|
+
if record_as_loaded == NotFound
|
40
|
+
false
|
41
|
+
else
|
42
|
+
record.subset?(record_as_loaded)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def deleted?(record)
|
47
|
+
record.if_delete { return true }
|
48
|
+
return false
|
41
49
|
end
|
42
50
|
|
43
|
-
def
|
44
|
-
|
51
|
+
def hash_key(record)
|
52
|
+
[record.namespace, record.identity]
|
45
53
|
end
|
46
54
|
end
|
47
55
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
class FunctionalPipeline
|
3
|
+
def self.from_array(steps = [])
|
4
|
+
new(steps.map { |name, func| Step.new(name, func) })
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(steps = [])
|
8
|
+
@steps = steps
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(args, &block)
|
12
|
+
result = execution_result([[:input, args]], &block)
|
13
|
+
|
14
|
+
[result.last.last, result]
|
15
|
+
end
|
16
|
+
|
17
|
+
def describe
|
18
|
+
@steps.map(&:name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def append(name, func)
|
22
|
+
self.class.new(@steps + [Step.new(name, func)])
|
23
|
+
end
|
24
|
+
|
25
|
+
def take_until(step_name)
|
26
|
+
step = @steps.detect { |step| step.name == step_name }
|
27
|
+
last_step_index = @steps.index(step)
|
28
|
+
steps = @steps.slice(0..last_step_index)
|
29
|
+
|
30
|
+
self.class.new(steps)
|
31
|
+
end
|
32
|
+
|
33
|
+
def drop_until(step_name)
|
34
|
+
step = @steps.detect { |step| step.name == step_name }
|
35
|
+
first_step_index = @steps.index(step) + 1
|
36
|
+
steps = @steps.slice(first_step_index..-1)
|
37
|
+
|
38
|
+
self.class.new(steps)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def execution_result(initial_state, &block)
|
44
|
+
@steps.reduce(initial_state) { |state, step|
|
45
|
+
new_value = step.call(state.last.last)
|
46
|
+
block && block.call(step.name, new_value)
|
47
|
+
state + [ [step.name, new_value] ]
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
class Step
|
52
|
+
def initialize(name, func)
|
53
|
+
@name = name
|
54
|
+
@func = func
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :name, :func
|
58
|
+
|
59
|
+
def call(*args, &block)
|
60
|
+
func.call(*args, &block)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,8 +1,13 @@
|
|
1
1
|
module Terrestrial
|
2
2
|
module InspectionString
|
3
3
|
def inspect
|
4
|
+
original_inspect_string = super
|
5
|
+
# this is kind of a silly way of getting the object id hex string but
|
6
|
+
# multiple Ruby versions have changed how this calculated.
|
7
|
+
hex_object_id = /#{self.class.to_s}:0x([0-9a-f]+)/.match(original_inspect_string)[1]
|
8
|
+
|
4
9
|
(
|
5
|
-
["\#<#{self.class.
|
10
|
+
["\#<#{self.class.to_s}:0x#{hex_object_id}"] +
|
6
11
|
inspectable_properties.map { |name|
|
7
12
|
[
|
8
13
|
name,
|
@@ -3,9 +3,10 @@ require "terrestrial/dataset"
|
|
3
3
|
|
4
4
|
module Terrestrial
|
5
5
|
class ManyToManyAssociation
|
6
|
-
def initialize(mapping_name:, join_mapping_name:, foreign_key:, key:, proxy_factory:, association_foreign_key:, association_key:, order:)
|
6
|
+
def initialize(mapping_name:, join_mapping_name:, join_dataset:, foreign_key:, key:, proxy_factory:, association_foreign_key:, association_key:, order:)
|
7
7
|
@mapping_name = mapping_name
|
8
8
|
@join_mapping_name = join_mapping_name
|
9
|
+
@join_dataset = join_dataset
|
9
10
|
@foreign_key = foreign_key
|
10
11
|
@key = key
|
11
12
|
@proxy_factory = proxy_factory
|
@@ -18,10 +19,18 @@ module Terrestrial
|
|
18
19
|
[mapping_name, join_mapping_name]
|
19
20
|
end
|
20
21
|
|
22
|
+
def outgoing_foreign_keys
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
def local_foreign_keys
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
|
21
30
|
attr_reader :mapping_name, :join_mapping_name
|
22
31
|
|
23
|
-
attr_reader :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :order
|
24
|
-
private :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :order
|
32
|
+
attr_reader :join_dataset, :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :order
|
33
|
+
private :join_dataset, :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :order
|
25
34
|
|
26
35
|
def build_proxy(data_superset:, loader:, record:)
|
27
36
|
proxy_factory.call(
|
@@ -37,36 +46,31 @@ module Terrestrial
|
|
37
46
|
end
|
38
47
|
|
39
48
|
def eager_superset((superset, join_superset), (associated_dataset))
|
40
|
-
|
49
|
+
subselect_data = Dataset.new(
|
41
50
|
join_superset
|
42
51
|
.where(foreign_key => associated_dataset.select(association_key))
|
43
52
|
.to_a
|
44
53
|
)
|
45
54
|
|
46
55
|
eager_superset = Dataset.new(
|
47
|
-
superset.where(key =>
|
56
|
+
superset.where(key => subselect_data.select(association_foreign_key)).to_a
|
48
57
|
)
|
49
58
|
|
50
59
|
[
|
51
60
|
eager_superset,
|
52
|
-
|
61
|
+
subselect_data,
|
53
62
|
]
|
54
63
|
end
|
55
64
|
|
56
65
|
def build_query((superset, join_superset), parent_record)
|
57
|
-
|
58
|
-
|
59
|
-
|
66
|
+
subselect_ids = join_superset
|
67
|
+
.where(foreign_key => foreign_key_value(parent_record))
|
68
|
+
.select(association_foreign_key)
|
60
69
|
|
61
70
|
order
|
62
|
-
.apply(
|
63
|
-
|
64
|
-
|
65
|
-
)
|
66
|
-
)
|
67
|
-
.lazy.map { |record|
|
68
|
-
[record, [foreign_keys(parent_record, record)]]
|
69
|
-
}
|
71
|
+
.apply(superset.where(key => subselect_ids))
|
72
|
+
.lazy
|
73
|
+
.map { |record| [record, [foreign_keys(parent_record, record)]] }
|
70
74
|
end
|
71
75
|
|
72
76
|
def dump(parent_record, collection, depth, &block)
|
@@ -102,13 +106,23 @@ module Terrestrial
|
|
102
106
|
depth + depth_modifier,
|
103
107
|
)
|
104
108
|
|
105
|
-
|
109
|
+
join_foreign_keys = foreign_keys(parent_record, record)
|
106
110
|
join_record_depth = depth + join_record_depth_modifier
|
107
111
|
|
112
|
+
# TODO: This is a bit hard to figure out
|
113
|
+
#
|
114
|
+
# The block defined in GraphSerializer#updated_nodes_recursive (inspect the block to confirm)
|
115
|
+
# join_foreign_keys is the two foreign key values in a hash
|
116
|
+
# the hash is two of the arugments here
|
117
|
+
# first one is normally an object to be serialized but serializing this hash will just return the same hash
|
118
|
+
# second one is the foreign keys that would need to accompany the object
|
119
|
+
#
|
120
|
+
# Passing it twice like this is allows it to go though the GraphSerializer like a regular user defined object
|
121
|
+
|
108
122
|
join_records = block.call(
|
109
123
|
join_mapping_name,
|
110
|
-
|
111
|
-
|
124
|
+
join_foreign_keys, # normally this is the object which gets serialized
|
125
|
+
join_foreign_keys, # normally this is the foreign key data the object doesn't know about
|
112
126
|
join_record_depth
|
113
127
|
).flatten(1)
|
114
128
|
|
@@ -13,6 +13,14 @@ module Terrestrial
|
|
13
13
|
[mapping_name]
|
14
14
|
end
|
15
15
|
|
16
|
+
def outgoing_foreign_keys
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
def local_foreign_keys
|
21
|
+
[foreign_key]
|
22
|
+
end
|
23
|
+
|
16
24
|
attr_reader :mapping_name
|
17
25
|
|
18
26
|
attr_reader :foreign_key, :key, :proxy_factory
|
@@ -42,17 +50,17 @@ module Terrestrial
|
|
42
50
|
|
43
51
|
def dump(parent_record, collection, depth, &block)
|
44
52
|
collection
|
53
|
+
.reject(&:nil?)
|
45
54
|
.flat_map { |object|
|
46
55
|
block.call(mapping_name, object, _foreign_key_does_not_go_here = {}, depth + depth_modifier)
|
47
56
|
}
|
48
|
-
.reject(&:nil?)
|
49
57
|
end
|
50
58
|
alias_method :delete, :dump
|
51
59
|
|
52
60
|
def extract_foreign_key(record)
|
53
61
|
{
|
54
|
-
foreign_key =>
|
55
|
-
}
|
62
|
+
foreign_key => record.fetch(key),
|
63
|
+
}.reject { |_k, v| v.nil? }
|
56
64
|
end
|
57
65
|
|
58
66
|
private
|
@@ -13,6 +13,15 @@ module Terrestrial
|
|
13
13
|
def mapping_names
|
14
14
|
[mapping_name]
|
15
15
|
end
|
16
|
+
|
17
|
+
def outgoing_foreign_keys
|
18
|
+
[foreign_key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def local_foreign_keys
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
16
25
|
attr_reader :mapping_name
|
17
26
|
|
18
27
|
attr_reader :foreign_key, :key, :order, :proxy_factory
|
@@ -1,9 +1,11 @@
|
|
1
1
|
require "terrestrial/identity_map"
|
2
2
|
require "terrestrial/dirty_map"
|
3
|
-
require "terrestrial/
|
3
|
+
require "terrestrial/upsert_record"
|
4
4
|
require "terrestrial/relational_store"
|
5
5
|
require "terrestrial/configurations/conventional_configuration"
|
6
6
|
require "terrestrial/inspection_string"
|
7
|
+
require "terrestrial/functional_pipeline"
|
8
|
+
require "terrestrial/adapters/sequel_postgres_adapter"
|
7
9
|
|
8
10
|
module Terrestrial
|
9
11
|
class ObjectStore
|
@@ -31,23 +33,42 @@ module Terrestrial
|
|
31
33
|
end
|
32
34
|
|
33
35
|
module PublicConveniencies
|
34
|
-
def config(
|
35
|
-
Configurations::ConventionalConfiguration.new(database_connection)
|
36
|
-
end
|
37
|
-
|
38
|
-
def object_store(mappings:, datastore:)
|
36
|
+
def config(database, clock: Time)
|
39
37
|
dirty_map = Private.build_dirty_map
|
40
38
|
identity_map = Private.build_identity_map
|
41
39
|
|
42
|
-
|
40
|
+
Configurations::ConventionalConfiguration.new(
|
41
|
+
datastore: Private.datastore_adapter(database),
|
42
|
+
clock: clock,
|
43
|
+
dirty_map: dirty_map,
|
44
|
+
identity_map: identity_map,
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def object_store(config:)
|
49
|
+
load_pipeline = Private.build_load_pipeline(
|
50
|
+
dirty_map: config.dirty_map,
|
51
|
+
identity_map: config.identity_map,
|
52
|
+
)
|
53
|
+
dump_pipeline = Private.build_dump_pipeline(
|
54
|
+
dirty_map: config.dirty_map,
|
55
|
+
datastore: config.datastore,
|
56
|
+
clock: config.clock,
|
57
|
+
)
|
58
|
+
|
59
|
+
mappings = config.mappings
|
60
|
+
mapping_names = mappings.keys
|
61
|
+
stores = Hash[mapping_names.map { |mapping_name|
|
43
62
|
[
|
44
|
-
|
45
|
-
Private.
|
63
|
+
mapping_name,
|
64
|
+
Private.relational_store(
|
65
|
+
name: mapping_name,
|
46
66
|
mappings: mappings ,
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
67
|
+
datastore: config.datastore,
|
68
|
+
identity_map: config.identity_map,
|
69
|
+
dirty_map: config.dirty_map,
|
70
|
+
load_pipeline: load_pipeline,
|
71
|
+
dump_pipeline: dump_pipeline,
|
51
72
|
)
|
52
73
|
]
|
53
74
|
}]
|
@@ -58,24 +79,13 @@ module Terrestrial
|
|
58
79
|
module Private
|
59
80
|
module_function
|
60
81
|
|
61
|
-
def
|
62
|
-
dataset = datastore[mappings.fetch(name).namespace]
|
63
|
-
|
82
|
+
def relational_store(mappings:, name:, datastore:, identity_map:, dirty_map:, load_pipeline:, dump_pipeline:)
|
64
83
|
RelationalStore.new(
|
65
84
|
mappings: mappings,
|
66
85
|
mapping_name: name,
|
67
86
|
datastore: datastore,
|
68
|
-
|
69
|
-
|
70
|
-
dirty_map: dirty_map,
|
71
|
-
identity_map: identity_map,
|
72
|
-
),
|
73
|
-
dump_pipeline: build_dump_pipeline(
|
74
|
-
dirty_map: dirty_map,
|
75
|
-
transaction: datastore.method(:transaction),
|
76
|
-
upsert: method(:upsert_record).curry.call(datastore),
|
77
|
-
delete: method(:delete_record).curry.call(datastore),
|
78
|
-
)
|
87
|
+
load_pipeline: load_pipeline,
|
88
|
+
dump_pipeline: dump_pipeline,
|
79
89
|
)
|
80
90
|
end
|
81
91
|
|
@@ -87,10 +97,23 @@ module Terrestrial
|
|
87
97
|
DirtyMap.new(storage)
|
88
98
|
end
|
89
99
|
|
100
|
+
def datastore_adapter(datastore)
|
101
|
+
if datastore.is_a?(Terrestrial::Adapters::AbstractAdapter)
|
102
|
+
return datastore
|
103
|
+
end
|
104
|
+
|
105
|
+
case datastore.class.name
|
106
|
+
when "Sequel::Postgres::Database"
|
107
|
+
Adapters::SequelPostgresAdapter.new(datastore)
|
108
|
+
else
|
109
|
+
raise "No adapter found for #{datastore.inspect}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
90
113
|
def build_load_pipeline(dirty_map:, identity_map:)
|
91
114
|
->(mapping, record, associated_fields = {}) {
|
92
115
|
[
|
93
|
-
|
116
|
+
->(record) { Record.new(mapping, record) },
|
94
117
|
dirty_map.method(:load),
|
95
118
|
->(record) {
|
96
119
|
attributes = record.to_h.select { |k,_v|
|
@@ -106,64 +129,24 @@ module Terrestrial
|
|
106
129
|
}
|
107
130
|
end
|
108
131
|
|
109
|
-
def build_dump_pipeline(dirty_map:,
|
110
|
-
|
111
|
-
[
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
transaction.call {
|
132
|
+
def build_dump_pipeline(dirty_map:, datastore:, clock:)
|
133
|
+
Terrestrial::FunctionalPipeline.from_array([
|
134
|
+
[:dedup, :uniq.to_proc],
|
135
|
+
[:sort_by_depth, ->(rs) { rs.sort_by(&:depth) }],
|
136
|
+
[:select_changed, ->(rs) { rs.select { |r| dirty_map.dirty?(r) } }],
|
137
|
+
[:remove_unchanged_fields, ->(rs) { rs.map { |r| dirty_map.reject_unchanged_fields(r) } }],
|
138
|
+
[:save_records, ->(rs) {
|
139
|
+
datastore.transaction {
|
118
140
|
rs.each { |r|
|
119
|
-
r.if_upsert(&upsert)
|
120
|
-
.if_delete(&delete)
|
141
|
+
r.if_upsert(&datastore.method(:upsert))
|
142
|
+
r.if_delete(&datastore.method(:delete))
|
121
143
|
}
|
122
144
|
}
|
123
|
-
},
|
124
|
-
->(rs) { rs.map { |r| dirty_map.load_if_new(r) } },
|
125
|
-
].reduce(records) { |agg, operation|
|
126
|
-
operation.call(agg)
|
127
|
-
}
|
128
|
-
}
|
129
|
-
end
|
130
|
-
|
131
|
-
def record_factory(mapping)
|
132
|
-
->(record_hash) {
|
133
|
-
identity = Hash[
|
134
|
-
mapping.primary_key.map { |field|
|
135
|
-
[field, record_hash.fetch(field)]
|
136
145
|
}
|
137
|
-
]
|
138
|
-
|
139
|
-
|
140
|
-
mapping.namespace,
|
141
|
-
identity,
|
142
|
-
record_hash,
|
143
|
-
)
|
144
|
-
}
|
145
|
-
end
|
146
|
-
|
147
|
-
def upsert_record(datastore, record)
|
148
|
-
row_count = 0
|
149
|
-
unless record.non_identity_attributes.empty?
|
150
|
-
row_count = datastore[record.namespace].
|
151
|
-
where(record.identity).
|
152
|
-
update(record.non_identity_attributes)
|
153
|
-
end
|
154
|
-
|
155
|
-
if row_count < 1
|
156
|
-
row_count = datastore[record.namespace].insert(record.to_h)
|
157
|
-
end
|
158
|
-
|
159
|
-
row_count
|
160
|
-
rescue Object => e
|
161
|
-
raise UpsertError.new(record.namespace, record.to_h, e)
|
146
|
+
],
|
147
|
+
[:add_new_records_to_dirty_map, ->(rs) { rs.map { |r| dirty_map.load_if_new(r) } }],
|
148
|
+
])
|
162
149
|
end
|
163
|
-
|
164
|
-
def delete_record(datastore, record)
|
165
|
-
datastore[record.namespace].where(record.identity).delete
|
166
|
-
end
|
167
|
-
end
|
150
|
+
end
|
168
151
|
end
|
169
152
|
end
|