paper_trail_diff 0.3.1 → 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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/README.md +316 -32
  4. data/lib/paper_trail_diff/activity_boundary.rb +21 -0
  5. data/lib/paper_trail_diff/activity_event_route_finder.rb +24 -44
  6. data/lib/paper_trail_diff/activity_root_steps.rb +27 -0
  7. data/lib/paper_trail_diff/activity_snapshot_sequence.rb +8 -3
  8. data/lib/paper_trail_diff/activity_timeline_builder.rb +47 -32
  9. data/lib/paper_trail_diff/analysis.rb +9 -0
  10. data/lib/paper_trail_diff/analysis_batch.rb +74 -0
  11. data/lib/paper_trail_diff/batch_boundary_resolver.rb +137 -0
  12. data/lib/paper_trail_diff/batched_root_analyzer.rb +50 -0
  13. data/lib/paper_trail_diff/batched_root_versions.rb +131 -0
  14. data/lib/paper_trail_diff/collection_comparator.rb +14 -1
  15. data/lib/paper_trail_diff/collection_transition.rb +6 -3
  16. data/lib/paper_trail_diff/comparison_batch.rb +37 -13
  17. data/lib/paper_trail_diff/endpoint.rb +27 -2
  18. data/lib/paper_trail_diff/errors.rb +3 -0
  19. data/lib/paper_trail_diff/historical_association_reifier.rb +8 -5
  20. data/lib/paper_trail_diff/historical_snapshot_store.rb +12 -9
  21. data/lib/paper_trail_diff/paper_trail_adapter.rb +49 -33
  22. data/lib/paper_trail_diff/prepared_association_reifier.rb +8 -5
  23. data/lib/paper_trail_diff/prepared_edge_loader.rb +6 -4
  24. data/lib/paper_trail_diff/prepared_history_loader.rb +3 -2
  25. data/lib/paper_trail_diff/prepared_record_index.rb +90 -15
  26. data/lib/paper_trail_diff/snapshot.rb +26 -0
  27. data/lib/paper_trail_diff/snapshot_normalizer.rb +14 -2
  28. data/lib/paper_trail_diff/time_activity_timeline_builder.rb +52 -32
  29. data/lib/paper_trail_diff/time_version_range.rb +11 -0
  30. data/lib/paper_trail_diff/timeline_builder.rb +3 -3
  31. data/lib/paper_trail_diff/timeline_range.rb +66 -6
  32. data/lib/paper_trail_diff/traversal_preparer.rb +40 -0
  33. data/lib/paper_trail_diff/version.rb +1 -1
  34. data/lib/paper_trail_diff/version_association_candidate_scope.rb +46 -19
  35. data/lib/paper_trail_diff.rb +37 -5
  36. data/sig/generated/paper_trail_diff/activity_boundary.rbs +9 -0
  37. data/sig/generated/paper_trail_diff/activity_event_route_finder.rbs +10 -17
  38. data/sig/generated/paper_trail_diff/activity_root_steps.rbs +13 -0
  39. data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +2 -2
  40. data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +20 -10
  41. data/sig/generated/paper_trail_diff/analysis.rbs +5 -0
  42. data/sig/generated/paper_trail_diff/analysis_batch.rbs +42 -0
  43. data/sig/generated/paper_trail_diff/batch_boundary_resolver.rbs +62 -0
  44. data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +28 -0
  45. data/sig/generated/paper_trail_diff/batched_root_versions.rbs +62 -0
  46. data/sig/generated/paper_trail_diff/collection_comparator.rbs +7 -0
  47. data/sig/generated/paper_trail_diff/collection_transition.rbs +4 -1
  48. data/sig/generated/paper_trail_diff/comparison_batch.rbs +15 -0
  49. data/sig/generated/paper_trail_diff/endpoint.rbs +12 -0
  50. data/sig/generated/paper_trail_diff/errors.rbs +4 -0
  51. data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +1 -1
  52. data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +2 -3
  53. data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +16 -7
  54. data/sig/generated/paper_trail_diff/prepared_association_reifier.rbs +1 -1
  55. data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +1 -0
  56. data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +2 -2
  57. data/sig/generated/paper_trail_diff/prepared_record_index.rbs +39 -6
  58. data/sig/generated/paper_trail_diff/snapshot.rbs +20 -0
  59. data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +8 -0
  60. data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +21 -10
  61. data/sig/generated/paper_trail_diff/time_version_range.rbs +7 -0
  62. data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
  63. data/sig/generated/paper_trail_diff/timeline_range.rbs +35 -2
  64. data/sig/generated/paper_trail_diff/traversal_preparer.rbs +22 -0
  65. data/sig/generated/paper_trail_diff/version_association_candidate_scope.rbs +17 -4
  66. data/sig/generated/paper_trail_diff.rbs +13 -3
  67. metadata +16 -4
@@ -0,0 +1,13 @@
1
+ # Generated from lib/paper_trail_diff/activity_root_steps.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Root checkpoint steps recovered from the snapshots an activity pass retained,
5
+ # so a combined result does not reconstruct the same boundaries twice.
6
+ module ActivityRootSteps
7
+ # : (Array[untyped], Hash[Array[untyped], RecordSnapshot?]) -> Array[Step]
8
+ def self?.call: (Array[untyped], Hash[Array[untyped], RecordSnapshot?]) -> Array[Step]
9
+
10
+ # : (untyped) -> Array[untyped]
11
+ def self?.version_key: (untyped) -> Array[untyped]
12
+ end
13
+ end
@@ -6,8 +6,8 @@ module PaperTrailDiff
6
6
  # : (snapshotter: untyped, refresher: untyped, preparer: untyped) -> void
7
7
  def initialize: (snapshotter: untyped, refresher: untyped, preparer: untyped) -> void
8
8
 
9
- # : (untyped, Array[untyped], ?start_at: untyped) -> void
10
- def prepare: (untyped, Array[untyped], ?start_at: untyped) -> void
9
+ # : (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
10
+ def prepare: (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
11
11
 
12
12
  # : (untyped, untyped) -> RecordSnapshot?
13
13
  def call: (untyped, untyped) -> RecordSnapshot?
@@ -27,6 +27,9 @@ module PaperTrailDiff
27
27
 
28
28
  @tree: AssociationTree
29
29
 
30
+ # : () -> Array[ActivityStep]
31
+ def no_steps: () -> Array[ActivityStep]
32
+
30
33
  # : () -> Array[ActivityStep]
31
34
  def build_between_versions: () -> Array[ActivityStep]
32
35
 
@@ -42,23 +45,30 @@ module PaperTrailDiff
42
45
  # : (Array[untyped], ?range_start: untyped, ?range_end: untyped) -> Array[ActivityEvent]
43
46
  def collect_events: (Array[untyped], ?range_start: untyped, ?range_end: untyped) -> Array[ActivityEvent]
44
47
 
45
- # : (Array[untyped], ?start_at: untyped) -> void
46
- def prepare_history: (Array[untyped], ?start_at: untyped) -> void
48
+ # : (Array[untyped], ?end_at: untyped) -> void
49
+ def prepare_history: (Array[untyped], ?end_at: untyped) -> void
47
50
 
48
51
  # : (Array[untyped], Array[ActivityEvent], ?current: untyped, ?final_boundary: ActivityBoundary?, ?final_snapshot: RecordSnapshot?) -> Array[ActivityStep]
49
52
  def build_event_steps: (Array[untyped], Array[ActivityEvent], ?current: untyped, ?final_boundary: ActivityBoundary?, ?final_snapshot: RecordSnapshot?) -> Array[ActivityStep]
50
53
 
51
- # : (Array[untyped], Array[ActivityEvent], ?current: untyped) -> ActivityHistory
52
- def event_history: (Array[untyped], Array[ActivityEvent], ?current: untyped) -> ActivityHistory
54
+ # Appends the transition into an explicit closing boundary, which is either
55
+ # a requested current record or the absence a final root destroy leaves.
56
+ # : (ActivityHistory, Array[ActivityEvent], ActivityBoundary?, RecordSnapshot?) -> Array[ActivityStep]
57
+ def activity_steps: (ActivityHistory, Array[ActivityEvent], ActivityBoundary?, RecordSnapshot?) -> Array[ActivityStep]
53
58
 
54
- # : (Array[untyped], ActivityHistory) -> Analysis
55
- def build_analysis: (Array[untyped], ActivityHistory) -> Analysis
59
+ # A destroyed root has no later version, but its own event states that
60
+ # nothing follows it, so the removal can still close the timeline.
61
+ # : (Array[untyped]) -> ActivityBoundary?
62
+ def destroyed_boundary: (Array[untyped]) -> ActivityBoundary?
56
63
 
57
- # : (Array[untyped], Array[RecordSnapshot?]) -> Array[Step]
58
- def build_root_steps: (Array[untyped], Array[RecordSnapshot?]) -> Array[Step]
64
+ # : (Array[untyped], Array[ActivityEvent], ?current: untyped) -> ActivityHistory
65
+ def event_history: (Array[untyped], Array[ActivityEvent], ?current: untyped) -> ActivityHistory
59
66
 
60
- # : (untyped) -> Array[untyped]
61
- def version_key: (untyped) -> Array[untyped]
67
+ # Only the activity view gains the closing removal. The endpoint diff and
68
+ # the root timeline keep their `compare` and `timeline` semantics, under
69
+ # which the state at a destroy version is the state before the deletion.
70
+ # : (Array[untyped], Array[ActivityEvent], ActivityHistory) -> Analysis
71
+ def build_analysis: (Array[untyped], Array[ActivityEvent], ActivityHistory) -> Analysis
62
72
 
63
73
  # : () -> TimeActivityTimelineBuilder
64
74
  def time_builder: () -> TimeActivityTimelineBuilder
@@ -9,6 +9,11 @@ module PaperTrailDiff
9
9
 
10
10
  attr_reader activity_timeline: Array[ActivityStep]?
11
11
 
12
+ # The result for a record whose requested history contains no versions,
13
+ # which is an empty history rather than a failed request.
14
+ # : () -> Analysis
15
+ def self.empty: () -> Analysis
16
+
12
17
  # : (diff: Diff, timeline: Array[Step], ?activity_timeline: Array[ActivityStep]?) -> void
13
18
  def initialize: (diff: Diff, timeline: Array[Step], ?activity_timeline: Array[ActivityStep]?) -> void
14
19
 
@@ -0,0 +1,42 @@
1
+ # Generated from lib/paper_trail_diff/analysis_batch.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Builds one Analysis per root over a shared range, selecting every root's
5
+ # versions and preparing their association history once for the whole batch
6
+ # rather than once per root.
7
+ class AnalysisBatch
8
+ # : (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped) -> void
9
+ def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped) -> void
10
+
11
+ # : () -> Hash[identity, Analysis]
12
+ def call: () -> Hash[identity, Analysis]
13
+
14
+ private
15
+
16
+ @analyzer: untyped
17
+
18
+ @history_preparer: untyped
19
+
20
+ @live_loader: untyped
21
+
22
+ @records: Array[untyped]
23
+
24
+ @time_range: TimeRange?
25
+
26
+ # : () -> Array[untyped]
27
+ def validated_records: () -> Array[untyped]
28
+
29
+ # Association history is prepared per model class across every selected root
30
+ # version, which is the work that would otherwise repeat for each record.
31
+ # The roots are preloaded first, because preparation reads their current
32
+ # association state as a fallback and would otherwise walk it one root at a
33
+ # time.
34
+ # : (Array[untyped], Hash[Array[String], Array[untyped]]) -> void
35
+ def prepare: (Array[untyped], Hash[Array[String], Array[untyped]]) -> void
36
+
37
+ # A root with no versions in range has nothing to report, which is an empty
38
+ # result rather than a failed request.
39
+ # : (untyped, Array[untyped]) -> Analysis
40
+ def analysis_for: (untyped, Array[untyped]) -> Analysis
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ # Generated from lib/paper_trail_diff/batch_boundary_resolver.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Resolves `:first` and `:last` endpoints for a whole batch in two queries per
5
+ # model class. Left to the caller this is a per-root lookup, which reintroduces
6
+ # exactly the queries a batched comparison exists to remove.
7
+ class BatchBoundaryResolver
8
+ BOUNDARIES: untyped
9
+
10
+ # : (untyped) -> bool
11
+ def self.symbolic?: (untyped) -> bool
12
+
13
+ # : (Array[[untyped, untyped]]) -> void
14
+ def initialize: (Array[[ untyped, untyped ]]) -> void
15
+
16
+ # Returns the pairs with every resolvable symbol replaced. A symbol that
17
+ # names history the record does not have is left in place, so the caller can
18
+ # decide what an absent history means rather than being handed nil.
19
+ # : () -> Array[[untyped, untyped]]
20
+ def call: () -> Array[[ untyped, untyped ]]
21
+
22
+ private
23
+
24
+ @pairs: Array[[ untyped, untyped ]]
25
+
26
+ # : (untyped) -> bool
27
+ def symbolic?: (untyped) -> bool
28
+
29
+ # : (untyped, untyped, Hash[Array[String], Hash[Symbol, untyped]]) -> untyped
30
+ def resolve: (untyped, untyped, Hash[Array[String], Hash[Symbol, untyped]]) -> untyped
31
+
32
+ # A symbol carries no identity of its own, so the pair's other endpoint has
33
+ # to say which record is meant.
34
+ # : (untyped) -> Array[String]?
35
+ def anchor_identity: (untyped) -> Array[String]?
36
+
37
+ # : () -> Hash[Array[String], Hash[Symbol, untyped]]
38
+ def boundary_index: () -> Hash[Array[String], Hash[Symbol, untyped]]
39
+
40
+ # : () -> Array[[untyped, untyped]]
41
+ def anchors: () -> Array[[ untyped, untyped ]]
42
+
43
+ # : (untyped) -> [untyped, untyped]?
44
+ def anchor: (untyped) -> [ untyped, untyped ]?
45
+
46
+ # : (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
47
+ def add_model_boundaries: (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
48
+
49
+ # Two grouped queries name each root's outermost versions, and one more
50
+ # loads them, whatever the size of the batch.
51
+ # : (untyped, Array[untyped]) -> [Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped]]
52
+ def boundary_versions: (untyped, Array[untyped]) -> [ Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped] ]
53
+
54
+ # Grouped keys come back with whatever type the column uses, so match on the
55
+ # string form rather than assuming integers.
56
+ # : (Hash[untyped, untyped], untyped) -> untyped
57
+ def boundary_key: (Hash[untyped, untyped], untyped) -> untyped
58
+
59
+ # : (untyped, untyped) -> Array[String]
60
+ def identity_key: (untyped, untyped) -> Array[String]
61
+ end
62
+ end
@@ -0,0 +1,28 @@
1
+ # Generated from lib/paper_trail_diff/batched_root_analyzer.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Builds one root's Analysis inside a batch, from versions the batch already
5
+ # selected and history it already prepared.
6
+ class BatchedRootAnalyzer
7
+ # : (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
8
+ def initialize: (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
9
+
10
+ # : (untyped, Array[untyped]) -> Analysis
11
+ def call: (untyped, Array[untyped]) -> Analysis
12
+
13
+ private
14
+
15
+ @activity: bool
16
+
17
+ @activity_snapshotter: untyped
18
+
19
+ @preparer: untyped
20
+
21
+ @timeline_snapshotter: untyped
22
+
23
+ @tree: AssociationTree
24
+
25
+ # : (untyped, Array[untyped]) -> Analysis
26
+ def activity_analysis: (untyped, Array[untyped]) -> Analysis
27
+ end
28
+ end
@@ -0,0 +1,62 @@
1
+ # Generated from lib/paper_trail_diff/batched_root_versions.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Selects each root's versions for a batched range in a fixed number of
5
+ # queries. Only the range forms that mean the same thing for every root are
6
+ # supported: a shared wall-clock window, or each root's own whole history.
7
+ class BatchedRootVersions
8
+ # : (Array[untyped], time_range: TimeRange?) -> void
9
+ def initialize: (Array[untyped], time_range: TimeRange?) -> void
10
+
11
+ # Returns root versions per record identity, in chronological order.
12
+ # : () -> Hash[Array[String], Array[untyped]]
13
+ def call: () -> Hash[Array[String], Array[untyped]]
14
+
15
+ private
16
+
17
+ @records: Array[untyped]
18
+
19
+ @time_range: TimeRange?
20
+
21
+ # : (untyped, Array[untyped], Hash[Array[String], Array[untyped]]) -> void
22
+ def select_model: (untyped, Array[untyped], Hash[Array[String], Array[untyped]]) -> void
23
+
24
+ # The window's own versions plus, when it has one, the later version needed
25
+ # to reveal the last mutation inside it. A window closing on the root's own
26
+ # destruction needs no later version, because none can exist.
27
+ # : (Array[untyped], untyped) -> Array[untyped]
28
+ def versions_for: (Array[untyped], untyped) -> Array[untyped]
29
+
30
+ # : (Array[untyped]) -> bool
31
+ def terminal_destroy?: (Array[untyped]) -> bool
32
+
33
+ # : (untyped, Array[untyped], TimeRange?) -> Hash[String, Array[untyped]]
34
+ def grouped_versions: (untyped, Array[untyped], TimeRange?) -> Hash[String, Array[untyped]]
35
+
36
+ # One row per root: the earliest version after the window, found without a
37
+ # window function so the query stays portable.
38
+ # : (untyped, Array[untyped], TimeRange) -> Hash[String, untyped]
39
+ def trailing_versions: (untyped, Array[untyped], TimeRange) -> Hash[String, untyped]
40
+
41
+ # : (untyped, TimeRange) -> untyped
42
+ def no_earlier_trailing: (untyped, TimeRange) -> untyped
43
+
44
+ # Mirrors the window's own end handling, so a version sitting exactly on an
45
+ # inclusive boundary stays inside the window instead of masking the trailing
46
+ # version that follows it.
47
+ # : (untyped, untyped, TimeRange) -> untyped
48
+ def preceding: (untyped, untyped, TimeRange) -> untyped
49
+
50
+ # : (untyped, TimeRange) -> untyped
51
+ def after_window: (untyped, TimeRange) -> untyped
52
+
53
+ # : (untyped, Array[untyped]) -> untyped
54
+ def base_scope: (untyped, Array[untyped]) -> untyped
55
+
56
+ # : (untyped) -> Array[untyped]
57
+ def ordered: (untyped) -> Array[untyped]
58
+
59
+ # : (untyped, untyped) -> Array[String]
60
+ def identity: (untyped, untyped) -> Array[String]
61
+ end
62
+ end
@@ -50,9 +50,16 @@ module PaperTrailDiff
50
50
  # : (Array[identity]) -> Array[identity]
51
51
  def sorted_identities: (Array[identity]) -> Array[identity]
52
52
 
53
+ # Identities sort by type first so that mixed id types stay comparable, then
54
+ # naturally within one type. Ordering by the printed form instead would put
55
+ # id 10 before id 2, which is deterministic but reads as unsorted wherever a
56
+ # result is rendered.
53
57
  # : (identity) -> Array[untyped]
54
58
  def sortable_identity: (identity) -> Array[untyped]
55
59
 
60
+ # : (untyped) -> Array[untyped]
61
+ def sortable_id: (untyped) -> Array[untyped]
62
+
56
63
  # : (identity) -> void
57
64
  def raise_duplicate_identity!: (identity) -> void
58
65
  end
@@ -7,12 +7,15 @@ module PaperTrailDiff
7
7
 
8
8
  attr_reader after: RecordSnapshot?
9
9
 
10
+ # The origin is recorded by serial rather than by reference, because
11
+ # holding it would retain every earlier snapshot at the same path for the
12
+ # whole timeline.
10
13
  # : (from: AssociationSnapshot, before: RecordSnapshot?, after: RecordSnapshot?) -> void
11
14
  def initialize: (from: AssociationSnapshot, before: RecordSnapshot?, after: RecordSnapshot?) -> void
12
15
 
13
16
  # : (AssociationSnapshot) -> bool
14
17
  def from?: (AssociationSnapshot) -> bool
15
18
 
16
- @from_object_id: Integer
19
+ @from_serial: Integer
17
20
  end
18
21
  end
@@ -23,6 +23,21 @@ module PaperTrailDiff
23
23
 
24
24
  @preparer: untyped
25
25
 
26
+ # : (Array[[untyped, untyped]]) -> Hash[identity, RecordSnapshot]
27
+ def prepared_live_snapshots: (Array[[ untyped, untyped ]]) -> Hash[identity, RecordSnapshot]
28
+
29
+ # : (untyped) -> bool
30
+ def symbolic?: (untyped) -> bool
31
+
32
+ # A boundary symbol that stayed unresolved means the root has no recorded
33
+ # history. An absent history is an empty result rather than a failed
34
+ # request, matching how the timeline APIs answer the same question.
35
+ # : (Array[[untyped, untyped]], Hash[identity, RecordSnapshot]) -> comparison_results
36
+ def results: (Array[[ untyped, untyped ]], Hash[identity, RecordSnapshot]) -> comparison_results
37
+
38
+ # : (untyped, untyped) -> identity
39
+ def entry_identity: (untyped, untyped) -> identity
40
+
26
41
  # : (untyped, untyped, Hash[identity, RecordSnapshot]) -> Diff
27
42
  def compare: (untyped, untyped, Hash[identity, RecordSnapshot]) -> Diff
28
43
 
@@ -6,6 +6,18 @@ module PaperTrailDiff
6
6
  # : (untyped, untyped) -> void
7
7
  def self?.validate_pair!: (untyped, untyped) -> void
8
8
 
9
+ # Two versions carry no visible cue about which is earlier, so transposing
10
+ # them is easy to do by accident and impossible to detect afterwards: the
11
+ # result is a valid inverse diff and carries no direction of its own. It is
12
+ # also redundant, because the two orders differ only in which side of each
13
+ # change is `from`. A current-record endpoint is exempt: it is self-evidently
14
+ # the live state, so putting it first is a deliberate reverse comparison.
15
+ # : (untyped, untyped) -> void
16
+ def self?.validate_order!: (untyped, untyped) -> void
17
+
18
+ # : (untyped, untyped) -> bool
19
+ def self?.reversed_versions?: (untyped, untyped) -> bool
20
+
9
21
  # : (untyped) -> void
10
22
  def self?.validate!: (untyped) -> void
11
23
 
@@ -13,6 +13,10 @@ module PaperTrailDiff
13
13
  class VersionMismatchError < Error
14
14
  end
15
15
 
16
+ # Raised when comparison endpoints are given in reverse chronological order.
17
+ class ReversedEndpointsError < Error
18
+ end
19
+
16
20
  # Raised when a live endpoint is not a clean, persisted ActiveRecord record.
17
21
  class InvalidEndpointError < Error
18
22
  end
@@ -13,7 +13,7 @@ module PaperTrailDiff
13
13
 
14
14
  @habtm_transaction_id: untyped
15
15
 
16
- @reified_associations: Hash[Array[untyped], bool]
16
+ @reified_associations: Hash[untyped, Hash[untyped, bool]]
17
17
 
18
18
  @transaction_id: untyped
19
19
 
@@ -6,10 +6,9 @@ module PaperTrailDiff
6
6
  # : (tree: AssociationTree, traversal: AssociationTraversal, normalizer: SnapshotNormalizer, preparer: untyped) -> void
7
7
  def initialize: (tree: AssociationTree, traversal: AssociationTraversal, normalizer: SnapshotNormalizer, preparer: untyped) -> void
8
8
 
9
- # : (untyped, Array[untyped], ?start_at: untyped) -> void
10
- def prepare: (untyped, Array[untyped], ?start_at: untyped) -> void
9
+ # : (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
10
+ def prepare: (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
11
11
 
12
- # Prepares selected history for several roots of the same model class.
13
12
  # : (Array[untyped], Array[untyped]) -> void
14
13
  def prepare_batch: (Array[untyped], Array[untyped]) -> void
15
14
 
@@ -1,7 +1,10 @@
1
1
  # Generated from lib/paper_trail_diff/paper_trail_adapter.rb with RBS::Inline
2
2
 
3
3
  module PaperTrailDiff
4
- # PaperTrail/ActiveRecord boundary that produces plain record snapshots.
4
+ # PaperTrail/ActiveRecord boundary that produces plain record snapshots. It is
5
+ # deliberately the widest class here: it fronts every public operation and is
6
+ # the only place allowed to know about both PaperTrail and the pure engine.
7
+ # Reconstruction logic lives in the collaborators it wires together.
5
8
  class PaperTrailAdapter
6
9
  # : (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
7
10
  def initialize: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
@@ -22,6 +25,10 @@ module PaperTrailDiff
22
25
  # : (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
23
26
  def analyze: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
24
27
 
28
+ # Analyzes many roots over one shared range, preparing their history once.
29
+ # : (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
30
+ def analyze_many: (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
31
+
25
32
  private
26
33
 
27
34
  @activity_snapshotter: ActivitySnapshotProvider
@@ -44,6 +51,14 @@ module PaperTrailDiff
44
51
 
45
52
  @traversal: AssociationTraversal
46
53
 
54
+ @traversal_preparer: TraversalPreparer
55
+
56
+ # : () -> void
57
+ def build_snapshotters: () -> void
58
+
59
+ # : (bool) -> BatchedRootAnalyzer
60
+ def batched_root_analyzer: (bool) -> BatchedRootAnalyzer
61
+
47
62
  # : () -> HistoricalSnapshotStore
48
63
  def build_historical_store: () -> HistoricalSnapshotStore
49
64
 
@@ -73,11 +88,5 @@ module PaperTrailDiff
73
88
 
74
89
  # : (untyped) -> RecordSnapshot
75
90
  def normalize_live_snapshot: (untyped) -> RecordSnapshot
76
-
77
- # : (untyped, historical: bool) -> void
78
- def prepare_traversal!: (untyped, historical: bool) -> void
79
-
80
- # : () -> void
81
- def ensure_association_tracking!: () -> void
82
91
  end
83
92
  end
@@ -19,7 +19,7 @@ module PaperTrailDiff
19
19
 
20
20
  @history: PreparedHistory
21
21
 
22
- @reified: Hash[Array[untyped], bool]
22
+ @reified: Hash[untyped, Hash[untyped, bool]]
23
23
 
24
24
  # : (untyped, untyped) -> void
25
25
  def reify_association: (untyped, untyped) -> void
@@ -64,6 +64,7 @@ module PaperTrailDiff
64
64
  # : (untyped, Array[untyped], ?owners: Hash[String, Array[untyped]]?) -> Hash[String, Hash[Symbol, untyped]]
65
65
  def group_for: (untyped, Array[untyped], ?owners: Hash[String, Array[untyped]]?) -> Hash[String, Hash[Symbol, untyped]]
66
66
 
67
+ # Keyed rather than scanned so that a wide edge stays linear in its width.
67
68
  # : (Array[Array[untyped]]) -> Hash[String, Array[untyped]]
68
69
  def group_ids_by_owner: (Array[Array[untyped]]) -> Hash[String, Array[untyped]]
69
70
 
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Expands the explicit association tree into a request-scoped PreparedHistory.
5
5
  class PreparedHistoryLoader
6
- # : (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?root_ids: Array[untyped], ?start_at: untyped, ?live_records: Array[untyped]) -> void
7
- def initialize: (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?root_ids: Array[untyped], ?start_at: untyped, ?live_records: Array[untyped]) -> void
6
+ # : (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?root_ids: Array[untyped], ?start_at: untyped, ?end_at: untyped, ?live_records: Array[untyped]) -> void
7
+ def initialize: (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?root_ids: Array[untyped], ?start_at: untyped, ?end_at: untyped, ?live_records: Array[untyped]) -> void
8
8
 
9
9
  # : () -> PreparedHistory
10
10
  def call: () -> PreparedHistory
@@ -13,6 +13,9 @@ module PaperTrailDiff
13
13
  # : (untyped, Hash[untyped, untyped]) -> PreparedRecordState
14
14
  def self.from_attributes: (untyped, Hash[untyped, untyped]) -> PreparedRecordState
15
15
 
16
+ # Mirrors PaperTrail's reifier, which writes attributes directly. Mass
17
+ # assignment would route reconstructed state through application-defined
18
+ # attribute writers and reify a different record than the version stored.
16
19
  # : () -> untyped
17
20
  def instantiate: () -> untyped
18
21
 
@@ -73,16 +76,25 @@ module PaperTrailDiff
73
76
 
74
77
  @states: Hash[untyped, PreparedRecordState?]
75
78
 
79
+ @transaction_positions: Hash[untyped, Integer]
80
+
76
81
  @version_positions: Hash[String, Integer]
77
82
 
78
83
  # : (Integer) -> PreparedRecordState?
79
84
  def state_after: (Integer) -> PreparedRecordState?
80
85
 
81
- # : (untyped, untyped) -> bool
82
- def eligible?: (untyped, untyped) -> bool
86
+ # A version is eligible when it is at or after the boundary, or shares the
87
+ # boundary's transaction. The first is monotonic over the chronological
88
+ # list and the second is indexed, so neither rescans a long history once
89
+ # per boundary.
90
+ # : (untyped) -> Integer?
91
+ def boundary_index: (untyped) -> Integer?
83
92
 
84
- # : (untyped, untyped) -> bool
85
- def same_transaction?: (untyped, untyped) -> bool
93
+ # : (untyped) -> Integer?
94
+ def transaction_index: (untyped) -> Integer?
95
+
96
+ # : () -> Hash[untyped, Integer]
97
+ def build_transaction_positions: () -> Hash[untyped, Integer]
86
98
 
87
99
  # : (untyped) -> PreparedRecordState?
88
100
  def state_for: (untyped) -> PreparedRecordState?
@@ -90,8 +102,8 @@ module PaperTrailDiff
90
102
 
91
103
  # Request-scoped scalar history loaded once per model identity.
92
104
  class PreparedRecordIndex
93
- # : (untyped, ?live_records: Array[untyped]) -> void
94
- def initialize: (untyped, ?live_records: Array[untyped]) -> void
105
+ # : (untyped, ?end_at: untyped, ?live_records: Array[untyped]) -> void
106
+ def initialize: (untyped, ?end_at: untyped, ?live_records: Array[untyped]) -> void
95
107
 
96
108
  # : (untyped, Array[untyped]) -> void
97
109
  def load: (untyped, Array[untyped]) -> void
@@ -110,6 +122,8 @@ module PaperTrailDiff
110
122
 
111
123
  private
112
124
 
125
+ @end_time: untyped
126
+
113
127
  @seeded_live_records: Hash[Array[String], untyped]
114
128
 
115
129
  @series: Hash[Array[String], PreparedRecordSeries]
@@ -118,6 +132,10 @@ module PaperTrailDiff
118
132
 
119
133
  @state_loader: PreparedVersionStateLoader
120
134
 
135
+ # Callers bound the range with either a version or a bare timestamp.
136
+ # : (untyped) -> untyped
137
+ def boundary_time: (untyped) -> untyped
138
+
121
139
  # : (untyped, untyped) -> Array[String]
122
140
  def identity: (untyped, untyped) -> Array[String]
123
141
 
@@ -127,9 +145,24 @@ module PaperTrailDiff
127
145
  # : (untyped, untyped, Hash[String, Array[untyped]], Hash[String, untyped]) -> void
128
146
  def add_series: (untyped, untyped, Hash[String, Array[untyped]], Hash[String, untyped]) -> void
129
147
 
148
+ # A PaperTrail version is a pre-change snapshot, so the state at the last
149
+ # selected boundary can only live in the next version after it. One
150
+ # trailing version per identity is retained, rather than every version
151
+ # recorded between the requested range and the present.
130
152
  # : (untyped, Array[untyped]) -> Array[untyped]
131
153
  def versions_for: (untyped, Array[untyped]) -> Array[untyped]
132
154
 
155
+ # In range, or the earliest version after it, resolved in the same query
156
+ # rather than with a window function or one query per identity.
157
+ # : (untyped) -> untyped
158
+ def window_condition: (untyped) -> untyped
159
+
160
+ # : (untyped) -> untyped
161
+ def first_after_range: (untyped) -> untyped
162
+
163
+ # : (untyped, untyped) -> untyped
164
+ def preceding_trailing_version: (untyped, untyped) -> untyped
165
+
133
166
  # : (untyped, Array[untyped]) -> Array[untyped]
134
167
  def available_live_records: (untyped, Array[untyped]) -> Array[untyped]
135
168
 
@@ -36,6 +36,24 @@ module PaperTrailDiff
36
36
  def index: (Array[RecordSnapshot]) -> CollectionIdentityIndex
37
37
  end
38
38
 
39
+ # Identifies one snapshot for internal carry-forward. A transition cannot
40
+ # hold its origin without retaining every earlier snapshot at that path,
41
+ # and `object_id` is only guaranteed unique among live objects, so Ruby may
42
+ # hand a collected snapshot's id to a later one.
43
+ class SerialSequence
44
+ # : () -> void
45
+ def initialize: () -> void
46
+
47
+ # : () -> Integer
48
+ def next_serial: () -> Integer
49
+
50
+ @counter: Integer
51
+
52
+ @mutex: Thread::Mutex
53
+ end
54
+
55
+ SERIALS: untyped
56
+
39
57
  # : (Symbol) -> bool
40
58
  def self.collection_kind?: (Symbol) -> bool
41
59
 
@@ -43,6 +61,8 @@ module PaperTrailDiff
43
61
 
44
62
  attr_reader records: Array[RecordSnapshot]
45
63
 
64
+ attr_reader serial: Integer
65
+
46
66
  # : (kind: Symbol, records: Array[RecordSnapshot], ?identity_index_cache: untyped, ?transition: CollectionTransition?) -> void
47
67
  def initialize: (kind: Symbol, records: Array[RecordSnapshot], ?identity_index_cache: untyped, ?transition: CollectionTransition?) -> void
48
68
 
@@ -39,6 +39,8 @@ module PaperTrailDiff
39
39
 
40
40
  private
41
41
 
42
+ @excluded_attributes: Hash[Array[untyped], Array[String]]
43
+
42
44
  @ignore_policy: IgnorePolicy
43
45
 
44
46
  @pool: SnapshotPool
@@ -55,9 +57,15 @@ module PaperTrailDiff
55
57
  # : (untyped, Array[untyped], String) -> snapshot_attributes
56
58
  def normalized_attributes: (untyped, Array[untyped], String) -> snapshot_attributes
57
59
 
60
+ # Every input is fixed for one model class at one selected path, and the
61
+ # path's structural columns are recorded before its records normalize, so
62
+ # this is resolved once instead of per record per boundary.
58
63
  # : (untyped, Array[untyped], String) -> Array[String]
59
64
  def excluded_attributes: (untyped, Array[untyped], String) -> Array[String]
60
65
 
66
+ # : (untyped, Array[untyped], String) -> Array[String]
67
+ def build_excluded_attributes: (untyped, Array[untyped], String) -> Array[String]
68
+
61
69
  # : (Array[untyped]) -> Array[String]
62
70
  def relationship_columns: (Array[untyped]) -> Array[String]
63
71