paper_trail_diff 0.11.0 → 0.13.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 +4 -4
- data/CHANGELOG.md +144 -0
- data/QUICKSTART.md +149 -79
- data/README.md +179 -1367
- data/docs/architecture.md +88 -0
- data/docs/performance.md +309 -0
- data/docs/reference.md +977 -0
- data/docs/testing.md +146 -0
- data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +2 -12
- data/lib/paper_trail_diff/activity_boundary.rb +20 -8
- data/lib/paper_trail_diff/activity_child_candidate_loader.rb +29 -0
- data/lib/paper_trail_diff/activity_collection_event_applier.rb +10 -7
- data/lib/paper_trail_diff/activity_collection_route_updater.rb +1 -11
- data/lib/paper_trail_diff/activity_event_record_normalizer.rb +4 -0
- data/lib/paper_trail_diff/activity_event_record_resolver.rb +66 -6
- data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +6 -4
- data/lib/paper_trail_diff/activity_grouping.rb +12 -0
- data/lib/paper_trail_diff/activity_history.rb +9 -3
- data/lib/paper_trail_diff/activity_relationship.rb +2 -0
- data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +1 -0
- data/lib/paper_trail_diff/activity_snapshot_sequence.rb +9 -2
- data/lib/paper_trail_diff/activity_timeline_builder.rb +39 -35
- data/lib/paper_trail_diff/activity_version_collector.rb +27 -18
- data/lib/paper_trail_diff/analysis.rb +9 -4
- data/lib/paper_trail_diff/analysis_batch.rb +28 -7
- data/lib/paper_trail_diff/association_traversal.rb +23 -0
- data/lib/paper_trail_diff/batch_boundary_resolver.rb +18 -26
- data/lib/paper_trail_diff/batch_safety.rb +55 -0
- data/lib/paper_trail_diff/batched_root_analyzer.rb +14 -5
- data/lib/paper_trail_diff/batched_root_versions.rb +9 -37
- data/lib/paper_trail_diff/branch_snapshot_refresher.rb +7 -8
- data/lib/paper_trail_diff/deleted_analysis_batch.rb +93 -0
- data/lib/paper_trail_diff/direct_record_state_loader.rb +81 -0
- data/lib/paper_trail_diff/endpoint.rb +10 -0
- data/lib/paper_trail_diff/engine.rb +4 -2
- data/lib/paper_trail_diff/errors.rb +9 -0
- data/lib/paper_trail_diff/historical_association_reifier.rb +26 -1
- data/lib/paper_trail_diff/historical_filter.rb +181 -0
- data/lib/paper_trail_diff/historical_snapshot_store.rb +51 -35
- data/lib/paper_trail_diff/instrumentation.rb +2 -2
- data/lib/paper_trail_diff/nested_comparator.rb +59 -24
- data/lib/paper_trail_diff/paper_trail_adapter.rb +109 -55
- data/lib/paper_trail_diff/prepared_activity_index.rb +121 -0
- data/lib/paper_trail_diff/prepared_edge_loader.rb +11 -11
- data/lib/paper_trail_diff/prepared_history.rb +21 -30
- data/lib/paper_trail_diff/prepared_history_loader.rb +14 -2
- data/lib/paper_trail_diff/prepared_record_index.rb +102 -29
- data/lib/paper_trail_diff/prepared_snapshot_builder.rb +64 -0
- data/lib/paper_trail_diff/prepared_state_association_reader.rb +77 -0
- data/lib/paper_trail_diff/root_version_plan.rb +25 -2
- data/lib/paper_trail_diff/root_version_selection.rb +16 -5
- data/lib/paper_trail_diff/scoped_analysis.rb +17 -2
- data/lib/paper_trail_diff/scoped_analysis_builder.rb +47 -0
- data/lib/paper_trail_diff/scoped_root_selection.rb +106 -13
- data/lib/paper_trail_diff/snapshot.rb +9 -0
- data/lib/paper_trail_diff/snapshot_normalizer.rb +41 -2
- data/lib/paper_trail_diff/step.rb +10 -5
- data/lib/paper_trail_diff/stored_time_precision.rb +68 -0
- data/lib/paper_trail_diff/support.rb +13 -3
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +19 -7
- data/lib/paper_trail_diff/time_range.rb +22 -2
- data/lib/paper_trail_diff/time_version_range.rb +1 -15
- data/lib/paper_trail_diff/timeline_builder.rb +5 -4
- data/lib/paper_trail_diff/timeline_range.rb +25 -43
- data/lib/paper_trail_diff/timeline_snapshot_provider.rb +13 -2
- data/lib/paper_trail_diff/traversal.rb +1 -1
- data/lib/paper_trail_diff/value_objects.rb +66 -0
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff/version_association_candidate_scope.rb +1 -1
- data/lib/paper_trail_diff/version_boundary.rb +48 -0
- data/lib/paper_trail_diff/version_query.rb +33 -0
- data/lib/paper_trail_diff/version_range.rb +10 -38
- data/lib/paper_trail_diff.rb +81 -43
- data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +0 -3
- data/sig/generated/paper_trail_diff/activity_boundary.rbs +9 -4
- data/sig/generated/paper_trail_diff/activity_child_candidate_loader.rbs +11 -0
- data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +4 -4
- data/sig/generated/paper_trail_diff/activity_collection_route_updater.rbs +0 -3
- data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +19 -3
- data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +2 -2
- data/sig/generated/paper_trail_diff/activity_grouping.rbs +3 -0
- data/sig/generated/paper_trail_diff/activity_history.rbs +6 -2
- data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +7 -2
- data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +8 -3
- data/sig/generated/paper_trail_diff/activity_version_collector.rbs +4 -8
- data/sig/generated/paper_trail_diff/analysis.rbs +2 -2
- data/sig/generated/paper_trail_diff/analysis_batch.rbs +14 -3
- data/sig/generated/paper_trail_diff/association_traversal.rbs +11 -0
- data/sig/generated/paper_trail_diff/batch_boundary_resolver.rbs +7 -10
- data/sig/generated/paper_trail_diff/batch_safety.rbs +24 -0
- data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +8 -2
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +4 -16
- data/sig/generated/paper_trail_diff/branch_snapshot_refresher.rbs +2 -2
- data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +44 -0
- data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +42 -0
- data/sig/generated/paper_trail_diff/endpoint.rbs +5 -0
- data/sig/generated/paper_trail_diff/engine.rbs +4 -2
- data/sig/generated/paper_trail_diff/errors.rbs +12 -0
- data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +8 -0
- data/sig/generated/paper_trail_diff/historical_filter.rbs +75 -0
- data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +17 -4
- data/sig/generated/paper_trail_diff/nested_comparator.rbs +31 -18
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +36 -15
- data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +56 -0
- data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +3 -3
- data/sig/generated/paper_trail_diff/prepared_history.rbs +12 -14
- data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +2 -0
- data/sig/generated/paper_trail_diff/prepared_record_index.rbs +43 -6
- data/sig/generated/paper_trail_diff/prepared_snapshot_builder.rbs +24 -0
- data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +33 -0
- data/sig/generated/paper_trail_diff/root_version_plan.rbs +15 -2
- data/sig/generated/paper_trail_diff/root_version_selection.rbs +8 -1
- data/sig/generated/paper_trail_diff/scoped_analysis.rbs +14 -2
- data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +22 -0
- data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +46 -6
- data/sig/generated/paper_trail_diff/snapshot.rbs +4 -0
- data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +13 -0
- data/sig/generated/paper_trail_diff/step.rbs +8 -2
- data/sig/generated/paper_trail_diff/stored_time_precision.rbs +36 -0
- data/sig/generated/paper_trail_diff/support.rbs +7 -0
- data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +7 -2
- data/sig/generated/paper_trail_diff/time_range.rbs +9 -0
- data/sig/generated/paper_trail_diff/time_version_range.rbs +0 -6
- data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
- data/sig/generated/paper_trail_diff/timeline_range.rbs +10 -14
- data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +2 -0
- data/sig/generated/paper_trail_diff/value_objects.rbs +45 -0
- data/sig/generated/paper_trail_diff/version_boundary.rbs +23 -0
- data/sig/generated/paper_trail_diff/version_query.rbs +15 -0
- data/sig/generated/paper_trail_diff/version_range.rbs +5 -14
- data/sig/generated/paper_trail_diff.rbs +37 -27
- metadata +31 -5
|
@@ -4,18 +4,21 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Chooses explicit-version or wall-clock selection for one timeline request.
|
|
6
6
|
class TimelineRange
|
|
7
|
-
BOUNDARY_SYMBOLS = %i[first
|
|
7
|
+
BOUNDARY_SYMBOLS = %i[first].freeze
|
|
8
|
+
# `:last` once named the last version, whose recorded state is the one
|
|
9
|
+
# *before* the latest change, so a range ending there silently dropped it.
|
|
10
|
+
LAST_REMOVED = '`:last` was removed because it left out the latest change: omit `to:` ' \
|
|
11
|
+
'to end at current state, or pass a version'
|
|
8
12
|
|
|
9
13
|
attr_reader :from #: untyped
|
|
10
14
|
attr_reader :to #: untyped
|
|
11
15
|
attr_reader :time_range #: TimeRange?
|
|
12
16
|
|
|
13
|
-
#: (untyped, from: untyped, to: untyped, within: untyped, ?
|
|
14
|
-
def initialize(record, from:, to:, within:,
|
|
17
|
+
#: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
|
|
18
|
+
def initialize(record, from:, to:, within:, version_scope: nil, plan: nil, live_endpoint: nil) # rubocop:disable Metrics/ParameterLists
|
|
15
19
|
@record = record
|
|
16
20
|
@plan = plan
|
|
17
21
|
@live_endpoint = live_endpoint
|
|
18
|
-
@versions = (plan ? plan.versions : versions)&.freeze
|
|
19
22
|
@version_scope = version_scope
|
|
20
23
|
@requested_from = from
|
|
21
24
|
@requested_to = to
|
|
@@ -26,7 +29,7 @@ module PaperTrailDiff
|
|
|
26
29
|
freeze
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
# A record with no versions has no first
|
|
32
|
+
# A record with no versions has no first boundary to resolve, which
|
|
30
33
|
# is an empty history rather than a bad request.
|
|
31
34
|
#: () -> bool
|
|
32
35
|
def unresolved?
|
|
@@ -49,27 +52,7 @@ module PaperTrailDiff
|
|
|
49
52
|
live_endpoint: @live_endpoint
|
|
50
53
|
).select_plan(context_required: context_required)
|
|
51
54
|
end
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
VersionRange.new(
|
|
55
|
-
@record, from: @from, to: @to, version_scope: @version_scope
|
|
56
|
-
).select_plan_for_range
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
#: (?context_required: bool) -> Array[untyped]
|
|
60
|
-
def select(context_required: false)
|
|
61
|
-
preselected = @versions
|
|
62
|
-
return preselected if preselected
|
|
63
|
-
|
|
64
|
-
range = time_range
|
|
65
|
-
if range
|
|
66
|
-
return TimeVersionRange.new(
|
|
67
|
-
@record, time_range: range, version_scope: @version_scope
|
|
68
|
-
).select(context_required: context_required)
|
|
69
|
-
end
|
|
70
|
-
return empty_versions if unresolved?
|
|
71
|
-
|
|
72
|
-
VersionRange.new(@record, from: @from, to: @to, version_scope: @version_scope).select
|
|
55
|
+
explicit_plan
|
|
73
56
|
end
|
|
74
57
|
|
|
75
58
|
#: () -> bool
|
|
@@ -91,7 +74,6 @@ module PaperTrailDiff
|
|
|
91
74
|
private
|
|
92
75
|
|
|
93
76
|
# @rbs @record: untyped
|
|
94
|
-
# @rbs @versions: Array[untyped]?
|
|
95
77
|
# @rbs @plan: RootVersionPlan?
|
|
96
78
|
# @rbs @version_scope: untyped
|
|
97
79
|
# @rbs @live_endpoint: untyped
|
|
@@ -100,10 +82,17 @@ module PaperTrailDiff
|
|
|
100
82
|
# @rbs @from: untyped
|
|
101
83
|
# @rbs @to: untyped
|
|
102
84
|
|
|
103
|
-
#: () ->
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
#: () -> RootVersionPlan
|
|
86
|
+
def explicit_plan
|
|
87
|
+
return RootVersionPlan.empty if unresolved?
|
|
88
|
+
if Endpoint.record?(@to)
|
|
89
|
+
return VersionRange.new(@record, from: @from, to: @from, version_scope: @version_scope)
|
|
90
|
+
.select_plan_through_current(@to)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
VersionRange.new(
|
|
94
|
+
@record, from: @from, to: @to, version_scope: @version_scope
|
|
95
|
+
).select_plan_for_range
|
|
107
96
|
end
|
|
108
97
|
|
|
109
98
|
#: (untyped) -> bool
|
|
@@ -111,32 +100,25 @@ module PaperTrailDiff
|
|
|
111
100
|
boundary.is_a?(Symbol)
|
|
112
101
|
end
|
|
113
102
|
|
|
114
|
-
# Resolving `:first`
|
|
103
|
+
# Resolving `:first` here keeps callers from depending on the
|
|
115
104
|
# order PaperTrail happens to give its versions association, which a caller
|
|
116
105
|
# is also free to reorder.
|
|
117
106
|
#: (untyped) -> untyped
|
|
118
107
|
def resolve(boundary)
|
|
119
108
|
return boundary unless symbolic?(boundary)
|
|
109
|
+
raise InvalidTimelineRangeError, LAST_REMOVED if boundary == :last
|
|
120
110
|
|
|
121
111
|
unless BOUNDARY_SYMBOLS.include?(boundary)
|
|
122
112
|
raise InvalidTimelineRangeError,
|
|
123
|
-
"unsupported boundary: #{boundary.inspect}; use :first,
|
|
113
|
+
"unsupported boundary: #{boundary.inspect}; use :first, a version, or a record"
|
|
124
114
|
end
|
|
125
115
|
|
|
126
|
-
|
|
116
|
+
ordered_versions.first
|
|
127
117
|
end
|
|
128
118
|
|
|
129
119
|
#: () -> untyped
|
|
130
120
|
def ordered_versions
|
|
131
|
-
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
#: () -> untyped
|
|
135
|
-
def versions_relation
|
|
136
|
-
@record.public_send(@record.class.versions_association_name)
|
|
137
|
-
rescue NoMethodError => e
|
|
138
|
-
message = 'record does not expose a PaperTrail version history'
|
|
139
|
-
raise InvalidTimelineRangeError, message, cause: e
|
|
121
|
+
Endpoint.versions(@record).reorder(created_at: :asc, id: :asc)
|
|
140
122
|
end
|
|
141
123
|
|
|
142
124
|
#: (untyped) -> TimeRange?
|
|
@@ -8,10 +8,15 @@ module PaperTrailDiff
|
|
|
8
8
|
def initialize(store, live_snapshotter:)
|
|
9
9
|
@store = store
|
|
10
10
|
@live_snapshotter = live_snapshotter
|
|
11
|
+
@snapshots = {} #: Hash[Array[untyped], RecordSnapshot?]
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
#: (untyped, Array[untyped]) -> void
|
|
14
15
|
def prepare(record, versions)
|
|
16
|
+
# Adjacent steps share endpoints. Keep their immutable snapshots for this
|
|
17
|
+
# root only, including nil create states, and release them before the next
|
|
18
|
+
# root so batch memory does not retain every reconstructed graph.
|
|
19
|
+
@snapshots.clear
|
|
15
20
|
@store.prepare(record, versions)
|
|
16
21
|
end
|
|
17
22
|
|
|
@@ -19,9 +24,14 @@ module PaperTrailDiff
|
|
|
19
24
|
# version, which is the one endpoint no version history can reconstruct.
|
|
20
25
|
#: (untyped) -> RecordSnapshot?
|
|
21
26
|
def call(endpoint)
|
|
22
|
-
|
|
27
|
+
key = [endpoint.class.name, endpoint.id]
|
|
28
|
+
return @snapshots[key] if @snapshots.key?(key)
|
|
23
29
|
|
|
24
|
-
@
|
|
30
|
+
@snapshots[key] = if Endpoint.record?(endpoint)
|
|
31
|
+
live_snapshot(endpoint)
|
|
32
|
+
else
|
|
33
|
+
@store.uncached(endpoint, endpoint)
|
|
34
|
+
end
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
private
|
|
@@ -33,5 +43,6 @@ module PaperTrailDiff
|
|
|
33
43
|
|
|
34
44
|
# @rbs @store: HistoricalSnapshotStore
|
|
35
45
|
# @rbs @live_snapshotter: untyped
|
|
46
|
+
# @rbs @snapshots: Hash[Array[untyped], RecordSnapshot?]
|
|
36
47
|
end
|
|
37
48
|
end
|
|
@@ -20,6 +20,72 @@ module PaperTrailDiff
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# A change to an array inside a value the database stores whole, reported by
|
|
24
|
+
# membership rather than by position.
|
|
25
|
+
#
|
|
26
|
+
# Position would be the obvious thing to report and the wrong one: elements
|
|
27
|
+
# here carry no identity, so inserting at the front makes every later index
|
|
28
|
+
# look changed, and one insertion is described as several edits. Membership is
|
|
29
|
+
# answerable without claiming any pairing -- "gained \"saturn v\"" is true
|
|
30
|
+
# whether the array is a set, a queue, or a ranked list.
|
|
31
|
+
#
|
|
32
|
+
# That leaves one case membership cannot see, so it is named rather than
|
|
33
|
+
# hidden: the same elements in a different order. `reordered?` says so.
|
|
34
|
+
#
|
|
35
|
+
# An element that is itself a Hash or Array is reported whole. Saying which
|
|
36
|
+
# field of which object changed would require pairing before-elements with
|
|
37
|
+
# after-elements, and nothing in the value licenses that pairing.
|
|
38
|
+
class ArrayChange
|
|
39
|
+
attr_reader :from #: Array[untyped]
|
|
40
|
+
attr_reader :to #: Array[untyped]
|
|
41
|
+
attr_reader :added #: Array[untyped]
|
|
42
|
+
attr_reader :removed #: Array[untyped]
|
|
43
|
+
|
|
44
|
+
#: (from: Array[untyped], to: Array[untyped]) -> void
|
|
45
|
+
def initialize(from:, to:)
|
|
46
|
+
@from = Support.immutable_copy(from)
|
|
47
|
+
@to = Support.immutable_copy(to)
|
|
48
|
+
@added = Support.immutable_copy(ArrayChange.surplus(to, from))
|
|
49
|
+
@removed = Support.immutable_copy(ArrayChange.surplus(from, to))
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Elements of `left` with no counterpart left in `right`, matching by value
|
|
54
|
+
# and respecting duplicates, so ["a", "a"] -> ["a"] reports one removal
|
|
55
|
+
# rather than none.
|
|
56
|
+
#: (Array[untyped], Array[untyped]) -> Array[untyped]
|
|
57
|
+
def self.surplus(left, right)
|
|
58
|
+
pool = right.dup
|
|
59
|
+
left.reject do |item|
|
|
60
|
+
index = pool.index(item)
|
|
61
|
+
index ? pool.delete_at(index) || true : false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Same elements, different sequence. Only meaningful for a change that was
|
|
66
|
+
# recorded at all, which this only is when the two sides differ.
|
|
67
|
+
#: () -> bool
|
|
68
|
+
def reordered?
|
|
69
|
+
added.empty? && removed.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: () -> bool
|
|
73
|
+
def empty?
|
|
74
|
+
from == to
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: () -> Hash[Symbol, untyped]
|
|
78
|
+
def to_h
|
|
79
|
+
{
|
|
80
|
+
from: Support.serialize(from),
|
|
81
|
+
to: Support.serialize(to),
|
|
82
|
+
added: Support.serialize(added),
|
|
83
|
+
removed: Support.serialize(removed),
|
|
84
|
+
reordered: reordered?
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
23
89
|
# Attribute and nested-association changes for a record whose identity did not change.
|
|
24
90
|
class RecordChange
|
|
25
91
|
attr_reader :record #: RecordReference
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module PaperTrailDiff
|
|
5
|
+
# Places one version relative to another on the recorded timeline.
|
|
6
|
+
module VersionBoundary
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Whether `candidate` was recorded at or after `boundary`. Versions sharing
|
|
10
|
+
# a timestamp are ordered by id, which reflects insertion order only for
|
|
11
|
+
# sequential ids in one version table; any other tie cannot be ordered, and
|
|
12
|
+
# guessing would place a change on an arbitrary side of the boundary. A
|
|
13
|
+
# bare instant has no position among same-time versions, so it keeps them.
|
|
14
|
+
#: (untyped, untyped) -> bool
|
|
15
|
+
def at_or_after?(candidate, boundary)
|
|
16
|
+
tied = candidate.created_at == boundary.created_at
|
|
17
|
+
return candidate.created_at > boundary.created_at unless tied
|
|
18
|
+
return true unless version_record?(boundary)
|
|
19
|
+
return true if same_version?(candidate, boundary)
|
|
20
|
+
|
|
21
|
+
unless orderable_tie?(candidate, boundary)
|
|
22
|
+
raise AmbiguousVersionOrderError, Support.ambiguous_message([boundary, candidate])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Support.compare_versions(candidate, boundary) >= 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (untyped) -> bool
|
|
29
|
+
def version_record?(value)
|
|
30
|
+
value.respond_to?(:id) && value.respond_to?(:item_type) &&
|
|
31
|
+
value.class.respond_to?(:table_name)
|
|
32
|
+
end
|
|
33
|
+
private_class_method :version_record?
|
|
34
|
+
|
|
35
|
+
#: (untyped, untyped) -> bool
|
|
36
|
+
def same_version?(left, right)
|
|
37
|
+
left.class.table_name == right.class.table_name && left.id.to_s == right.id.to_s
|
|
38
|
+
end
|
|
39
|
+
private_class_method :same_version?
|
|
40
|
+
|
|
41
|
+
#: (untyped, untyped) -> bool
|
|
42
|
+
def orderable_tie?(left, right)
|
|
43
|
+
left.class.table_name == right.class.table_name &&
|
|
44
|
+
Support.sequential_id?(left.id) && Support.sequential_id?(right.id)
|
|
45
|
+
end
|
|
46
|
+
private_class_method :orderable_tie?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module PaperTrailDiff
|
|
5
|
+
# Arel conditions shared by the version queries that need one row per item.
|
|
6
|
+
module VersionQuery
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Keeps each item's earliest version among those the block also accepts:
|
|
10
|
+
# no other version of the same item that the block accepts is earlier. A
|
|
11
|
+
# correlated NOT EXISTS stays portable where window functions are not.
|
|
12
|
+
#: (untyped) ?{ (untyped) -> untyped } -> untyped
|
|
13
|
+
def no_earlier_version(table, &block)
|
|
14
|
+
arel = Object.const_get(:Arel) #: untyped
|
|
15
|
+
earlier = table.alias('paper_trail_diff_earlier_versions')
|
|
16
|
+
arel.const_get(:SelectManager).new
|
|
17
|
+
.from(earlier)
|
|
18
|
+
.project(arel.sql('1'))
|
|
19
|
+
.where(earlier_version(table, earlier, &block))
|
|
20
|
+
.exists
|
|
21
|
+
.not
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#: (untyped, untyped) ?{ (untyped) -> untyped } -> untyped
|
|
25
|
+
def earlier_version(table, earlier)
|
|
26
|
+
same_item = earlier[:item_type].eq(table[:item_type])
|
|
27
|
+
.and(earlier[:item_id].eq(table[:item_id]))
|
|
28
|
+
.and(earlier[:created_at].lt(table[:created_at]))
|
|
29
|
+
block_given? ? same_item.and(yield(earlier)) : same_item
|
|
30
|
+
end
|
|
31
|
+
private_class_method :earlier_version
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -12,7 +12,7 @@ module PaperTrailDiff
|
|
|
12
12
|
@version_scope = version_scope
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# Keep both reconstruction boundaries and the step pairs a filter implies.
|
|
16
16
|
#: () -> RootVersionPlan
|
|
17
17
|
def select_plan_for_range
|
|
18
18
|
relation = validated_relation
|
|
@@ -24,7 +24,7 @@ module PaperTrailDiff
|
|
|
24
24
|
|
|
25
25
|
#: () -> untyped
|
|
26
26
|
def validated_relation
|
|
27
|
-
relation =
|
|
27
|
+
relation = Endpoint.versions(@record)
|
|
28
28
|
validate_boundary!(@from, relation, boundary: :from)
|
|
29
29
|
validate_boundary!(@to, relation, boundary: :to)
|
|
30
30
|
if Support.compare_versions(@from, @to).positive?
|
|
@@ -34,26 +34,12 @@ module PaperTrailDiff
|
|
|
34
34
|
relation
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
#: () ->
|
|
38
|
-
def
|
|
39
|
-
|
|
37
|
+
#: (untyped) -> RootVersionPlan
|
|
38
|
+
def select_plan_through_current(current)
|
|
39
|
+
Endpoint.validate_pair!(@record, current)
|
|
40
|
+
relation = Endpoint.versions(@record)
|
|
40
41
|
validate_boundary!(@from, relation, boundary: :from)
|
|
41
|
-
|
|
42
|
-
if Support.compare_versions(@from, @to).positive?
|
|
43
|
-
raise InvalidTimelineRangeError, '`from` version must not follow `to` version'
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
select_relation(
|
|
47
|
-
relation.where(created_at: @from.created_at..@to.created_at),
|
|
48
|
-
through: @to
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
#: () -> Array[untyped]
|
|
53
|
-
def select_through_latest
|
|
54
|
-
relation = versions_relation
|
|
55
|
-
validate_boundary!(@from, relation, boundary: :from)
|
|
56
|
-
select_relation(relation.where(created_at: @from.created_at..))
|
|
42
|
+
select_plan(relation.where(created_at: @from.created_at..), live_endpoint: current)
|
|
57
43
|
end
|
|
58
44
|
|
|
59
45
|
private
|
|
@@ -63,22 +49,8 @@ module PaperTrailDiff
|
|
|
63
49
|
# @rbs @to: untyped
|
|
64
50
|
# @rbs @version_scope: untyped
|
|
65
51
|
|
|
66
|
-
#: () ->
|
|
67
|
-
def
|
|
68
|
-
association_name = @record.class.versions_association_name
|
|
69
|
-
@record.public_send(association_name)
|
|
70
|
-
rescue NoMethodError => e
|
|
71
|
-
message = 'record does not expose a PaperTrail version history'
|
|
72
|
-
raise InvalidTimelineRangeError, message, cause: e
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
#: (untyped, ?through: untyped) -> Array[untyped]
|
|
76
|
-
def select_relation(relation, through: nil)
|
|
77
|
-
select_plan(relation, through: through).versions
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
#: (untyped, ?through: untyped) -> RootVersionPlan
|
|
81
|
-
def select_plan(relation, through: nil)
|
|
52
|
+
#: (untyped, ?through: untyped, ?live_endpoint: untyped) -> RootVersionPlan
|
|
53
|
+
def select_plan(relation, through: nil, live_endpoint: nil)
|
|
82
54
|
in_range = ordered(relation.to_a.select do |version|
|
|
83
55
|
next false if Support.compare_versions(@from, version).positive?
|
|
84
56
|
next true unless through
|
|
@@ -90,7 +62,7 @@ module PaperTrailDiff
|
|
|
90
62
|
selected: VersionScopeFilter.new(@version_scope).call(relation, in_range),
|
|
91
63
|
after_range: nil,
|
|
92
64
|
windowed: false,
|
|
93
|
-
filtered: !@version_scope.nil
|
|
65
|
+
filtered: !@version_scope.nil?, live_endpoint: live_endpoint
|
|
94
66
|
).call
|
|
95
67
|
end
|
|
96
68
|
|