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
@@ -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
|