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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -0
  3. data/QUICKSTART.md +149 -79
  4. data/README.md +179 -1367
  5. data/docs/architecture.md +88 -0
  6. data/docs/performance.md +309 -0
  7. data/docs/reference.md +977 -0
  8. data/docs/testing.md +146 -0
  9. data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +2 -12
  10. data/lib/paper_trail_diff/activity_boundary.rb +20 -8
  11. data/lib/paper_trail_diff/activity_child_candidate_loader.rb +29 -0
  12. data/lib/paper_trail_diff/activity_collection_event_applier.rb +10 -7
  13. data/lib/paper_trail_diff/activity_collection_route_updater.rb +1 -11
  14. data/lib/paper_trail_diff/activity_event_record_normalizer.rb +4 -0
  15. data/lib/paper_trail_diff/activity_event_record_resolver.rb +66 -6
  16. data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +6 -4
  17. data/lib/paper_trail_diff/activity_grouping.rb +12 -0
  18. data/lib/paper_trail_diff/activity_history.rb +9 -3
  19. data/lib/paper_trail_diff/activity_relationship.rb +2 -0
  20. data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +1 -0
  21. data/lib/paper_trail_diff/activity_snapshot_sequence.rb +9 -2
  22. data/lib/paper_trail_diff/activity_timeline_builder.rb +39 -35
  23. data/lib/paper_trail_diff/activity_version_collector.rb +27 -18
  24. data/lib/paper_trail_diff/analysis.rb +9 -4
  25. data/lib/paper_trail_diff/analysis_batch.rb +28 -7
  26. data/lib/paper_trail_diff/association_traversal.rb +23 -0
  27. data/lib/paper_trail_diff/batch_boundary_resolver.rb +18 -26
  28. data/lib/paper_trail_diff/batch_safety.rb +55 -0
  29. data/lib/paper_trail_diff/batched_root_analyzer.rb +14 -5
  30. data/lib/paper_trail_diff/batched_root_versions.rb +9 -37
  31. data/lib/paper_trail_diff/branch_snapshot_refresher.rb +7 -8
  32. data/lib/paper_trail_diff/deleted_analysis_batch.rb +93 -0
  33. data/lib/paper_trail_diff/direct_record_state_loader.rb +81 -0
  34. data/lib/paper_trail_diff/endpoint.rb +10 -0
  35. data/lib/paper_trail_diff/engine.rb +4 -2
  36. data/lib/paper_trail_diff/errors.rb +9 -0
  37. data/lib/paper_trail_diff/historical_association_reifier.rb +26 -1
  38. data/lib/paper_trail_diff/historical_filter.rb +181 -0
  39. data/lib/paper_trail_diff/historical_snapshot_store.rb +51 -35
  40. data/lib/paper_trail_diff/instrumentation.rb +2 -2
  41. data/lib/paper_trail_diff/nested_comparator.rb +59 -24
  42. data/lib/paper_trail_diff/paper_trail_adapter.rb +109 -55
  43. data/lib/paper_trail_diff/prepared_activity_index.rb +121 -0
  44. data/lib/paper_trail_diff/prepared_edge_loader.rb +11 -11
  45. data/lib/paper_trail_diff/prepared_history.rb +21 -30
  46. data/lib/paper_trail_diff/prepared_history_loader.rb +14 -2
  47. data/lib/paper_trail_diff/prepared_record_index.rb +102 -29
  48. data/lib/paper_trail_diff/prepared_snapshot_builder.rb +64 -0
  49. data/lib/paper_trail_diff/prepared_state_association_reader.rb +77 -0
  50. data/lib/paper_trail_diff/root_version_plan.rb +25 -2
  51. data/lib/paper_trail_diff/root_version_selection.rb +16 -5
  52. data/lib/paper_trail_diff/scoped_analysis.rb +17 -2
  53. data/lib/paper_trail_diff/scoped_analysis_builder.rb +47 -0
  54. data/lib/paper_trail_diff/scoped_root_selection.rb +106 -13
  55. data/lib/paper_trail_diff/snapshot.rb +9 -0
  56. data/lib/paper_trail_diff/snapshot_normalizer.rb +41 -2
  57. data/lib/paper_trail_diff/step.rb +10 -5
  58. data/lib/paper_trail_diff/stored_time_precision.rb +68 -0
  59. data/lib/paper_trail_diff/support.rb +13 -3
  60. data/lib/paper_trail_diff/time_activity_timeline_builder.rb +19 -7
  61. data/lib/paper_trail_diff/time_range.rb +22 -2
  62. data/lib/paper_trail_diff/time_version_range.rb +1 -15
  63. data/lib/paper_trail_diff/timeline_builder.rb +5 -4
  64. data/lib/paper_trail_diff/timeline_range.rb +25 -43
  65. data/lib/paper_trail_diff/timeline_snapshot_provider.rb +13 -2
  66. data/lib/paper_trail_diff/traversal.rb +1 -1
  67. data/lib/paper_trail_diff/value_objects.rb +66 -0
  68. data/lib/paper_trail_diff/version.rb +1 -1
  69. data/lib/paper_trail_diff/version_association_candidate_scope.rb +1 -1
  70. data/lib/paper_trail_diff/version_boundary.rb +48 -0
  71. data/lib/paper_trail_diff/version_query.rb +33 -0
  72. data/lib/paper_trail_diff/version_range.rb +10 -38
  73. data/lib/paper_trail_diff.rb +81 -43
  74. data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +0 -3
  75. data/sig/generated/paper_trail_diff/activity_boundary.rbs +9 -4
  76. data/sig/generated/paper_trail_diff/activity_child_candidate_loader.rbs +11 -0
  77. data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +4 -4
  78. data/sig/generated/paper_trail_diff/activity_collection_route_updater.rbs +0 -3
  79. data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +19 -3
  80. data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +2 -2
  81. data/sig/generated/paper_trail_diff/activity_grouping.rbs +3 -0
  82. data/sig/generated/paper_trail_diff/activity_history.rbs +6 -2
  83. data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +7 -2
  84. data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +8 -3
  85. data/sig/generated/paper_trail_diff/activity_version_collector.rbs +4 -8
  86. data/sig/generated/paper_trail_diff/analysis.rbs +2 -2
  87. data/sig/generated/paper_trail_diff/analysis_batch.rbs +14 -3
  88. data/sig/generated/paper_trail_diff/association_traversal.rbs +11 -0
  89. data/sig/generated/paper_trail_diff/batch_boundary_resolver.rbs +7 -10
  90. data/sig/generated/paper_trail_diff/batch_safety.rbs +24 -0
  91. data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +8 -2
  92. data/sig/generated/paper_trail_diff/batched_root_versions.rbs +4 -16
  93. data/sig/generated/paper_trail_diff/branch_snapshot_refresher.rbs +2 -2
  94. data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +44 -0
  95. data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +42 -0
  96. data/sig/generated/paper_trail_diff/endpoint.rbs +5 -0
  97. data/sig/generated/paper_trail_diff/engine.rbs +4 -2
  98. data/sig/generated/paper_trail_diff/errors.rbs +12 -0
  99. data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +8 -0
  100. data/sig/generated/paper_trail_diff/historical_filter.rbs +75 -0
  101. data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +17 -4
  102. data/sig/generated/paper_trail_diff/nested_comparator.rbs +31 -18
  103. data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +36 -15
  104. data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +56 -0
  105. data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +3 -3
  106. data/sig/generated/paper_trail_diff/prepared_history.rbs +12 -14
  107. data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +2 -0
  108. data/sig/generated/paper_trail_diff/prepared_record_index.rbs +43 -6
  109. data/sig/generated/paper_trail_diff/prepared_snapshot_builder.rbs +24 -0
  110. data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +33 -0
  111. data/sig/generated/paper_trail_diff/root_version_plan.rbs +15 -2
  112. data/sig/generated/paper_trail_diff/root_version_selection.rbs +8 -1
  113. data/sig/generated/paper_trail_diff/scoped_analysis.rbs +14 -2
  114. data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +22 -0
  115. data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +46 -6
  116. data/sig/generated/paper_trail_diff/snapshot.rbs +4 -0
  117. data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +13 -0
  118. data/sig/generated/paper_trail_diff/step.rbs +8 -2
  119. data/sig/generated/paper_trail_diff/stored_time_precision.rbs +36 -0
  120. data/sig/generated/paper_trail_diff/support.rbs +7 -0
  121. data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +7 -2
  122. data/sig/generated/paper_trail_diff/time_range.rbs +9 -0
  123. data/sig/generated/paper_trail_diff/time_version_range.rbs +0 -6
  124. data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
  125. data/sig/generated/paper_trail_diff/timeline_range.rbs +10 -14
  126. data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +2 -0
  127. data/sig/generated/paper_trail_diff/value_objects.rbs +45 -0
  128. data/sig/generated/paper_trail_diff/version_boundary.rbs +23 -0
  129. data/sig/generated/paper_trail_diff/version_query.rbs +15 -0
  130. data/sig/generated/paper_trail_diff/version_range.rbs +5 -14
  131. data/sig/generated/paper_trail_diff.rbs +37 -27
  132. metadata +31 -5
@@ -3,10 +3,12 @@
3
3
  module PaperTrailDiff
4
4
  # Batched record states and relationship candidates for one activity range.
5
5
  class PreparedHistory
6
+ attr_reader activity_index: PreparedActivityIndex?
7
+
6
8
  DIRECT_RESOLVERS: untyped
7
9
 
8
- # : (PreparedRecordIndex) -> void
9
- def initialize: (PreparedRecordIndex) -> void
10
+ # : (PreparedRecordIndex, ?activity_index: PreparedActivityIndex?) -> void
11
+ def initialize: (PreparedRecordIndex, ?activity_index: PreparedActivityIndex?) -> void
10
12
 
11
13
  # : (untyped, untyped, Hash[String, Hash[Symbol, untyped]]) -> void
12
14
  def merge_edge: (untyped, untyped, Hash[String, Hash[Symbol, untyped]]) -> void
@@ -23,6 +25,12 @@ module PaperTrailDiff
23
25
  # : (untyped, untyped, untyped) -> [Hash[untyped, untyped], Hash[untyped, untyped]]?
24
26
  def record_transition: (untyped, untyped, untyped) -> [ Hash[untyped, untyped], Hash[untyped, untyped] ]?
25
27
 
28
+ # : (untyped, untyped, untyped, ?direct: bool) -> [bool, untyped]
29
+ def record_after: (untyped, untyped, untyped, ?direct: bool) -> [ bool, untyped ]
30
+
31
+ # : (PreparedRecordState, untyped, untyped) -> [bool, Array[PreparedRecordState]]
32
+ def snapshot_states: (PreparedRecordState, untyped, untyped) -> [ bool, Array[PreparedRecordState] ]
33
+
26
34
  # : (untyped, untyped, untyped, habtm_boundary: untyped) -> [bool, Array[untyped]]
27
35
  def resolve: (untyped, untyped, untyped, habtm_boundary: untyped) -> [ bool, Array[untyped] ]
28
36
 
@@ -34,14 +42,13 @@ module PaperTrailDiff
34
42
 
35
43
  @records: PreparedRecordIndex
36
44
 
45
+ @state_reader: PreparedStateAssociationReader
46
+
37
47
  @unsupported: Hash[Array[String], bool]
38
48
 
39
49
  # : (untyped, untyped) -> bool
40
50
  def supported?: (untyped, untyped) -> bool
41
51
 
42
- # : (untyped) -> bool
43
- def default_scoped?: (untyped) -> bool
44
-
45
52
  # : (untyped, untyped, untyped, untyped) -> Array[untyped]?
46
53
  def direct_records: (untyped, untyped, untyped, untyped) -> Array[untyped]?
47
54
 
@@ -51,9 +58,6 @@ module PaperTrailDiff
51
58
  # : (untyped, untyped, untyped, untyped) -> Array[untyped]
52
59
  def resolve_children: (untyped, untyped, untyped, untyped) -> Array[untyped]
53
60
 
54
- # : (untyped, untyped, untyped, untyped) -> Array[untyped]?
55
- def resolve_habtm: (untyped, untyped, untyped, untyped) -> Array[untyped]?
56
-
57
61
  # : (untyped, untyped, untyped) -> Array[untyped]?
58
62
  def belongs_to_records: (untyped, untyped, untyped) -> Array[untyped]?
59
63
 
@@ -78,9 +82,6 @@ module PaperTrailDiff
78
82
  # : (untyped) -> String?
79
83
  def scalar_foreign_key: (untyped) -> String?
80
84
 
81
- # : (untyped, untyped) -> untyped
82
- def belongs_to_class: (untyped, untyped) -> untyped
83
-
84
85
  # : (untyped, untyped) -> Array[String]
85
86
  def owner_types: (untyped, untyped) -> Array[String]
86
87
 
@@ -92,8 +93,5 @@ module PaperTrailDiff
92
93
 
93
94
  # : (untyped, untyped) -> Array[String]
94
95
  def edge_key: (untyped, untyped) -> Array[String]
95
-
96
- # : (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]
97
- def merge_groups: (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]
98
96
  end
99
97
  end
@@ -23,6 +23,8 @@ module PaperTrailDiff
23
23
 
24
24
  @root_ids: Array[untyped]
25
25
 
26
+ @root_versions: Array[untyped]
27
+
26
28
  @traversal: AssociationTraversal
27
29
 
28
30
  @tree: AssociationTree
@@ -23,6 +23,12 @@ module PaperTrailDiff
23
23
 
24
24
  # : (untyped, Hash[untyped, untyped]) -> void
25
25
  def initialize_attributes: (untyped, Hash[untyped, untyped]) -> void
26
+
27
+ # A live fallback and a version decoded from history must agree on what
28
+ # the stored history could represent, or the gap between them reads as a
29
+ # change.
30
+ # : (untyped, Hash[untyped, untyped]) -> Hash[untyped, untyped]
31
+ def stored_attributes: (untyped, Hash[untyped, untyped]) -> Hash[untyped, untyped]
26
32
  end
27
33
 
28
34
  # Extracts a version's scalar state without constructing a disposable AR object.
@@ -62,9 +68,19 @@ module PaperTrailDiff
62
68
  # : (untyped) -> untyped
63
69
  def record_before: (untyped) -> untyped
64
70
 
71
+ # : (untyped) -> PreparedRecordState?
72
+ def state_before: (untyped) -> PreparedRecordState?
73
+
74
+ # A handled nil is known absence, not permission to query the current row.
75
+ # : (untyped, ?direct: bool) -> [bool, untyped]
76
+ def record_after: (untyped, ?direct: bool) -> [ bool, untyped ]
77
+
65
78
  # : () -> Array[untyped]
66
79
  def records: () -> Array[untyped]
67
80
 
81
+ # : () -> Array[PreparedRecordState]
82
+ def states: () -> Array[PreparedRecordState]
83
+
68
84
  # : (untyped) -> [Hash[untyped, untyped], Hash[untyped, untyped]]?
69
85
  def transition: (untyped) -> [ Hash[untyped, untyped], Hash[untyped, untyped] ]?
70
86
 
@@ -87,6 +103,11 @@ module PaperTrailDiff
87
103
  # boundary's transaction. The first is monotonic over the chronological
88
104
  # list and the second is indexed, so neither rescans a long history once
89
105
  # per boundary.
106
+ #
107
+ # "At or after" is decided by the version order rather than the timestamp
108
+ # alone. A version written in the same instant as the boundary but before
109
+ # it already took effect there; reading its pre-change state would put the
110
+ # boundary on the wrong side of a change it follows.
90
111
  # : (untyped) -> Integer?
91
112
  def boundary_index: (untyped) -> Integer?
92
113
 
@@ -108,20 +129,42 @@ module PaperTrailDiff
108
129
  # : (untyped, Array[untyped]) -> void
109
130
  def load: (untyped, Array[untyped]) -> void
110
131
 
132
+ # : (untyped, Array[untyped], Array[untyped]) -> void
133
+ def seed: (untyped, Array[untyped], Array[untyped]) -> void
134
+
111
135
  # : (untyped, untyped, untyped) -> untyped
112
136
  def record_before: (untyped, untyped, untyped) -> untyped
113
137
 
138
+ # : (untyped, untyped, untyped) -> [bool, PreparedRecordState?]
139
+ def snapshot_state_before: (untyped, untyped, untyped) -> [ bool, PreparedRecordState? ]
140
+
114
141
  # : (untyped, untyped, untyped) -> [Hash[untyped, untyped], Hash[untyped, untyped]]?
115
142
  def transition: (untyped, untyped, untyped) -> [ Hash[untyped, untyped], Hash[untyped, untyped] ]?
116
143
 
144
+ # : (untyped, untyped, untyped, ?direct: bool) -> [bool, untyped]
145
+ def record_after: (untyped, untyped, untyped, ?direct: bool) -> [ bool, untyped ]
146
+
117
147
  # : (untyped, Array[untyped]) -> Array[untyped]
118
148
  def records_for: (untyped, Array[untyped]) -> Array[untyped]
119
149
 
150
+ # : (untyped, Array[untyped], String) -> Array[untyped]
151
+ def attribute_values_for: (untyped, Array[untyped], String) -> Array[untyped]
152
+
153
+ # : (untyped, untyped) -> Array[untyped]
154
+ def versions_for_id: (untyped, untyped) -> Array[untyped]
155
+
120
156
  # : (untyped, untyped) -> bool
121
157
  def loaded?: (untyped, untyped) -> bool
122
158
 
123
159
  private
124
160
 
161
+ # : (untyped, String) -> bool
162
+ def generated_reader?: (untyped, String) -> bool
163
+
164
+ @direct_loader: DirectRecordStateLoader
165
+
166
+ @direct_states: Hash[PreparedRecordState, PreparedRecordState?]
167
+
125
168
  @end_time: untyped
126
169
 
127
170
  @seeded_live_records: Hash[Array[String], untyped]
@@ -157,12 +200,6 @@ module PaperTrailDiff
157
200
  # : (untyped) -> untyped
158
201
  def window_condition: (untyped) -> untyped
159
202
 
160
- # : (untyped) -> untyped
161
- def first_after_range: (untyped) -> untyped
162
-
163
- # : (untyped, untyped) -> untyped
164
- def preceding_trailing_version: (untyped, untyped) -> untyped
165
-
166
203
  # : (untyped, Array[untyped]) -> Array[untyped]
167
204
  def available_live_records: (untyped, Array[untyped]) -> Array[untyped]
168
205
 
@@ -0,0 +1,24 @@
1
+ # Generated from lib/paper_trail_diff/prepared_snapshot_builder.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Normalizes supported recorded graphs without historical ActiveRecord instances.
5
+ class PreparedSnapshotBuilder
6
+ # : (AssociationTraversal) -> void
7
+ def initialize: (AssociationTraversal) -> void
8
+
9
+ # : (untyped, untyped, tree: AssociationTree, normalizer: SnapshotNormalizer, history: PreparedHistory?) -> [bool, RecordSnapshot?]
10
+ def call: (untyped, untyped, tree: AssociationTree, normalizer: SnapshotNormalizer, history: PreparedHistory?) -> [ bool, RecordSnapshot? ]
11
+
12
+ private
13
+
14
+ @loader: DirectRecordStateLoader
15
+
16
+ @traversal: AssociationTraversal
17
+
18
+ # : (PreparedRecordState, untyped, AssociationTree, SnapshotNormalizer, PreparedHistory?, path: String, ?incoming: untyped) -> [bool, RecordSnapshot?]
19
+ def build: (PreparedRecordState, untyped, AssociationTree, SnapshotNormalizer, PreparedHistory?, path: String, ?incoming: untyped) -> [ bool, RecordSnapshot? ]
20
+
21
+ # : (PreparedRecordState, untyped, untyped, AssociationTree, SnapshotNormalizer, PreparedHistory, String) -> [bool, AssociationSnapshot?]
22
+ def association: (PreparedRecordState, untyped, untyped, AssociationTree, SnapshotNormalizer, PreparedHistory, String) -> [ bool, AssociationSnapshot? ]
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # Generated from lib/paper_trail_diff/prepared_state_association_reader.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Reads plain direct edges without creating AR records for their members.
5
+ class PreparedStateAssociationReader
6
+ # : (PreparedRecordIndex, untyped) -> void
7
+ def initialize: (PreparedRecordIndex, untyped) -> void
8
+
9
+ # : (PreparedRecordState, untyped, untyped) -> [bool, Array[PreparedRecordState]]
10
+ def call: (PreparedRecordState, untyped, untyped) -> [ bool, Array[PreparedRecordState] ]
11
+
12
+ private
13
+
14
+ @edges: untyped
15
+
16
+ @records: PreparedRecordIndex
17
+
18
+ # : (untyped) -> bool
19
+ def supported?: (untyped) -> bool
20
+
21
+ # : (PreparedRecordState, untyped, untyped) -> [bool, Array[PreparedRecordState]]
22
+ def children: (PreparedRecordState, untyped, untyped) -> [ bool, Array[PreparedRecordState] ]
23
+
24
+ # : (untyped, PreparedRecordState) -> Array[untyped]
25
+ def ids_for: (untyped, PreparedRecordState) -> Array[untyped]
26
+
27
+ # : (PreparedRecordState, PreparedRecordState, untyped) -> bool
28
+ def member?: (PreparedRecordState, PreparedRecordState, untyped) -> bool
29
+
30
+ # : (PreparedRecordState, untyped, untyped) -> bool
31
+ def matching_type?: (PreparedRecordState, untyped, untyped) -> bool
32
+ end
33
+ end
@@ -32,6 +32,11 @@ module PaperTrailDiff
32
32
  # version present only to reveal what the last of them produced.
33
33
  attr_reader mutations: Array[untyped]
34
34
 
35
+ # Whether a wall-clock window closes on the root's own destruction. Its
36
+ # final state is then the absence the destroy left, just as a window
37
+ # closing on current state ends at the live record.
38
+ attr_reader closes_on_destroy: bool
39
+
35
40
  # Adjacent boundaries, which is what an unfiltered range reports.
36
41
  # : (Array[untyped], ?context_version: untyped) -> RootVersionPlan
37
42
  def self.contiguous: (Array[untyped], ?context_version: untyped) -> RootVersionPlan
@@ -39,12 +44,20 @@ module PaperTrailDiff
39
44
  # : () -> RootVersionPlan
40
45
  def self.empty: () -> RootVersionPlan
41
46
 
42
- # : (versions: Array[untyped], steps: Array[[untyped, untyped]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?, ?closing_record: untyped) -> void
43
- def initialize: (versions: Array[untyped], steps: Array[[ untyped, untyped ]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?, ?closing_record: untyped) -> void
47
+ # No root version to report or replay: only descendants moved, and the
48
+ # live record is the root's state throughout.
49
+ # : (untyped) -> RootVersionPlan
50
+ def self.closing_on: (untyped) -> RootVersionPlan
51
+
52
+ # : (versions: Array[untyped], steps: Array[[untyped, untyped]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?, ?closing_record: untyped, ?closes_on_destroy: bool) -> void
53
+ def initialize: (versions: Array[untyped], steps: Array[[ untyped, untyped ]], ?context_version: untyped, ?reconstruction_versions: Array[untyped]?, ?mutations: Array[untyped]?, ?closing_record: untyped, ?closes_on_destroy: bool) -> void
44
54
 
45
55
  # : () -> bool
46
56
  def empty?: () -> bool
47
57
 
58
+ # : () -> RootVersionPlan
59
+ def closing_on_destroy: () -> RootVersionPlan
60
+
48
61
  # What the range's final state is read from, which is the live record when
49
62
  # the window closes on current state and the last version otherwise.
50
63
  # : () -> untyped
@@ -44,7 +44,9 @@ module PaperTrailDiff
44
44
  def contiguous_plan: (untyped) -> RootVersionPlan
45
45
 
46
46
  # An activity view still needs a root to reconstruct from even when no root
47
- # version falls inside the window, because descendants may have moved.
47
+ # version falls inside the window, because descendants may have moved. With
48
+ # no later version, a window closing on current state reads the root from
49
+ # the live record, which is then its state throughout the window.
48
50
  # : () -> RootVersionPlan
49
51
  def without_selection: () -> RootVersionPlan
50
52
 
@@ -83,6 +85,11 @@ module PaperTrailDiff
83
85
  # : () -> untyped
84
86
  def revealing_version: () -> untyped
85
87
 
88
+ # A window with nothing after its last selected mutation can only close on
89
+ # that mutation being the record's destruction.
90
+ # : (untyped) -> bool
91
+ def closes_on_destroy?: (untyped) -> bool
92
+
86
93
  # A range closing on the record's own destruction needs no later version:
87
94
  # nothing can follow it, so demanding one would reject the range forever.
88
95
  # : () -> bool
@@ -16,9 +16,21 @@ module PaperTrailDiff
16
16
 
17
17
  attr_reader unreachable: Array[Array[String]]
18
18
 
19
- # : (analyses: Hash[Array[String], Analysis], unreachable: Array[Array[String]]) -> void
20
- def initialize: (analyses: Hash[Array[String], Analysis], unreachable: Array[Array[String]]) -> void
19
+ # The live records the selection loaded, in the order it loaded them. The
20
+ # gem already has them; withholding them only makes a caller query the same
21
+ # rows again to attach its own presentation data. A destroyed root has no
22
+ # record to hand back, so `roots` and `unreachable` stay complementary.
23
+ attr_reader roots: Array[untyped]
21
24
 
25
+ # Requested missing-root analyses, kept separate because live SQL conditions
26
+ # cannot prove which deleted rows matched. nil means no destroy history.
27
+ attr_reader deleted: Hash[Array[String], Analysis?]
28
+
29
+ # : (analyses: Hash[Array[String], Analysis], unreachable: Array[Array[String]], ?roots: Array[untyped], ?deleted: Hash[Array[String], Analysis?]) -> void
30
+ def initialize: (analyses: Hash[Array[String], Analysis], unreachable: Array[Array[String]], ?roots: Array[untyped], ?deleted: Hash[Array[String], Analysis?]) -> void
31
+
32
+ # Two elements, not three: `roots` is reached by name so that existing
33
+ # destructuring keeps working.
22
34
  # : () -> [Hash[Array[String], Analysis], Array[Array[String]]]
23
35
  def to_ary: () -> [ Hash[Array[String], Analysis], Array[Array[String]] ]
24
36
  end
@@ -0,0 +1,22 @@
1
+ # Generated from lib/paper_trail_diff/scoped_analysis_builder.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Selects both populations and checks their shared ceiling before analysis.
5
+ class ScopedAnalysisBuilder
6
+ # : (untyped, limit: Integer?, within: untyped, historical_filter: untyped, max_filter_versions: Integer, include_deleted: bool, analyzer: untyped, deleted_analyzer: untyped) -> void
7
+ def initialize: (untyped, limit: Integer?, within: untyped, historical_filter: untyped, max_filter_versions: Integer, include_deleted: bool, analyzer: untyped, deleted_analyzer: untyped) -> void
8
+
9
+ # : () -> ScopedAnalysis
10
+ def call: () -> ScopedAnalysis
11
+
12
+ @analyzer: untyped
13
+
14
+ @deleted_analyzer: untyped
15
+
16
+ @include_deleted: bool
17
+
18
+ @limit: Integer
19
+
20
+ @selection: ScopedRootSelection
21
+ end
22
+ end
@@ -25,6 +25,8 @@ module PaperTrailDiff
25
25
  # window. `where(status: 'published')` selects what is published now, which is
26
26
  # not the same set as what was published while the window was open.
27
27
  class ScopedRootSelection
28
+ PAGE_SIZE: ::Integer
29
+
28
30
  # A plain class rather than Data.define, which arrived in Ruby 3.2 while this
29
31
  # gem supports 3.1.
30
32
  class Result
@@ -36,20 +38,43 @@ module PaperTrailDiff
36
38
  def initialize: (records: Array[untyped], unreachable: Array[Array[String]]) -> void
37
39
  end
38
40
 
39
- # : (untyped, time_range: TimeRange?, limit: Integer) -> void
40
- def initialize: (untyped, time_range: TimeRange?, limit: Integer) -> void
41
+ # : (untyped, time_range: TimeRange?, limit: Integer, ?historical_filter: untyped, ?max_filter_versions: Integer) -> void
42
+ def initialize: (untyped, time_range: TimeRange?, limit: Integer, ?historical_filter: untyped, ?max_filter_versions: Integer) -> void
41
43
 
42
44
  # : () -> Result
43
45
  def call: () -> Result
44
46
 
45
47
  private
46
48
 
49
+ @historical_filter: untyped
50
+
47
51
  @limit: Integer
48
52
 
49
53
  @scope: untyped
50
54
 
51
55
  @time_range: TimeRange?
52
56
 
57
+ # Leaves the candidate set in the database. Plucking every root changed in
58
+ # the window would cost memory and a statement proportional to all activity
59
+ # in the window -- not to the relation or `limit:` -- so a report on one
60
+ # record could read every id in a busy table. A subquery keeps the work
61
+ # bounded by what the relation and the deletions actually contain.
62
+ # : () -> Result
63
+ def database_selection: () -> Result
64
+
65
+ # Comparing the two columns in SQL needs them to share a type: PostgreSQL
66
+ # rejects an integer key against a string item_id outright.
67
+ # : () -> bool
68
+ def comparable_keys?: () -> bool
69
+
70
+ # Changed in the window but without a row, found with an anti-join instead
71
+ # of comparing two plucked lists.
72
+ # : () -> Array[String]
73
+ def missing_ids: () -> Array[String]
74
+
75
+ # : () -> untyped
76
+ def window_versions: () -> untyped
77
+
53
78
  # Accepts a model class as readily as a relation: `Article` and
54
79
  # `Article.where(...)` both name a population, and `all` is what makes them
55
80
  # the same kind of thing.
@@ -70,10 +95,25 @@ module PaperTrailDiff
70
95
  # : (Array[String]) -> Array[String]
71
96
  def live_ids: (Array[String]) -> Array[String]
72
97
 
73
- # Loading one past the limit is what turns an oversized page into an error
74
- # rather than a silently truncated report.
75
- # : (Array[String]) -> Array[untyped]
76
- def selected_records: (Array[String]) -> Array[untyped]
98
+ # Deduplicate records rather than adding SQL DISTINCT: an otherwise valid
99
+ # relation may order by joined columns or select a PostgreSQL json column.
100
+ # Read bounded pages until the relation ends or too many unique roots appear.
101
+ # : (untyped) -> Array[untyped]
102
+ def selected_records: (untyped) -> Array[untyped]
103
+
104
+ # : (untyped, Integer) -> Array[untyped]
105
+ def record_page: (untyped, Integer) -> Array[untyped]
106
+
107
+ # Preserve explicit ordering: SQL Server rejects duplicate ORDER BY columns,
108
+ # even when the extra ordering would only have served as a tie-breaker.
109
+ # : () -> untyped
110
+ def ordered_scope: () -> untyped
111
+
112
+ # : (Integer) -> Integer
113
+ def page_limit: (Integer) -> Integer
114
+
115
+ # : (Integer) -> void
116
+ def reject_oversized!: (Integer) -> void
77
117
 
78
118
  # : (String) -> Array[String]
79
119
  def identity: (String) -> Array[String]
@@ -113,6 +113,10 @@ module PaperTrailDiff
113
113
  # : () -> RecordReference
114
114
  def reference: () -> RecordReference
115
115
 
116
+ # A copy holding a different state for one association.
117
+ # : (String, AssociationSnapshot) -> RecordSnapshot
118
+ def with_association: (String, AssociationSnapshot) -> RecordSnapshot
119
+
116
120
  # : () -> Hash[Symbol, untyped]
117
121
  def to_h: () -> Hash[Symbol, untyped]
118
122
 
@@ -37,8 +37,15 @@ module PaperTrailDiff
37
37
  # : (untyped, tree: AssociationTree, path: String) -> snapshot_attributes
38
38
  def attributes_for: (untyped, tree: AssociationTree, path: String) -> snapshot_attributes
39
39
 
40
+ # Same exclusions as AR normalization, applied to an already typed state.
41
+ # : (PreparedRecordState, tree: AssociationTree, path: String, ?incoming: untyped, ?associations: snapshot_associations) -> RecordSnapshot
42
+ def from_state: (PreparedRecordState, tree: AssociationTree, path: String, ?incoming: untyped, ?associations: snapshot_associations) -> RecordSnapshot
43
+
40
44
  private
41
45
 
46
+ # : (String, RecordSnapshot) -> RecordSnapshot
47
+ def pool_state: (String, RecordSnapshot) -> RecordSnapshot
48
+
42
49
  @excluded_attributes: Hash[Array[untyped], Array[String]]
43
50
 
44
51
  @ignore_policy: IgnorePolicy
@@ -66,6 +73,12 @@ module PaperTrailDiff
66
73
  # : (untyped, Array[untyped], String) -> Array[String]
67
74
  def build_excluded_attributes: (untyped, Array[untyped], String) -> Array[String]
68
75
 
76
+ # `has_paper_trail skip:` columns are never written to a version, so history
77
+ # holds only their defaults. Compared with a live row they would report a
78
+ # change nobody recorded.
79
+ # : (untyped) -> Array[String]
80
+ def skipped_columns: (untyped) -> Array[String]
81
+
69
82
  # : (Array[untyped]) -> Array[String]
70
83
  def relationship_columns: (Array[untyped]) -> Array[String]
71
84
 
@@ -22,7 +22,13 @@ module PaperTrailDiff
22
22
  # : () -> bool
23
23
  def empty?: () -> bool
24
24
 
25
- # : () -> Hash[Symbol, untyped]
26
- def to_h: () -> Hash[Symbol, untyped]
25
+ # The root event whose change this step reports.
26
+ # : () -> ActivityBoundary
27
+ def source_boundary: () -> ActivityBoundary
28
+
29
+ # The same shape as `ActivityStep#to_h`, so a consumer reads both step
30
+ # types alike; each boundary carries its own `version_id`.
31
+ # : (?metadata: bool) -> Hash[Symbol, untyped]
32
+ def to_h: (?metadata: bool) -> Hash[Symbol, untyped]
27
33
  end
28
34
  end
@@ -0,0 +1,36 @@
1
+ # Generated from lib/paper_trail_diff/stored_time_precision.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Matches time values to the precision PaperTrail actually stored.
5
+ #
6
+ # PaperTrail's JSON serializer, and a `json` or `jsonb` object column, write
7
+ # times through ActiveSupport's JSON encoder, which keeps only
8
+ # `ActiveSupport::JSON::Encoding.time_precision` fractional digits
9
+ # (milliseconds by default). The live row keeps the database's precision, so
10
+ # comparing the two would report every timestamp as changed even though
11
+ # nothing was edited. Truncating every side to the stored precision is
12
+ # idempotent, so values that came from history are unaffected and only the
13
+ # digits history could never have held are dropped.
14
+ module StoredTimePrecision
15
+ NANOSECOND_DIGITS: ::Integer
16
+
17
+ JSON_COLUMN_TYPES: untyped
18
+
19
+ # Fractional digits a model's history keeps for times, or nil when the
20
+ # history is stored losslessly.
21
+ # : (untyped) -> Integer?
22
+ def self?.digits: (untyped) -> Integer?
23
+
24
+ # : (Hash[untyped, untyped], Integer?) -> Hash[untyped, untyped]
25
+ def self?.apply: (Hash[untyped, untyped], Integer?) -> Hash[untyped, untyped]
26
+
27
+ # : (untyped, Integer) -> untyped
28
+ def self?.truncate: (untyped, Integer) -> untyped
29
+
30
+ # : (untyped) -> bool
31
+ def self?.json_history?: (untyped) -> bool
32
+
33
+ # : () -> bool
34
+ def self?.json_serializer?: () -> bool
35
+ end
36
+ end
@@ -43,6 +43,13 @@ module PaperTrailDiff
43
43
  # : (untyped) -> bool
44
44
  def self?.versioned?: (untyped) -> bool
45
45
 
46
+ # `versions.item_id` is often a string column -- PaperTrail's setup for UUID
47
+ # keys -- while the model's own key is an integer. An id read from history
48
+ # is cast to the key's type, or the same record read once from history and
49
+ # once from its live row would count as two.
50
+ # : (untyped, untyped) -> untyped
51
+ def self?.record_id: (untyped, untyped) -> untyped
52
+
46
53
  # : (untyped) -> bool
47
54
  def self?.sequential_id?: (untyped) -> bool
48
55
 
@@ -29,6 +29,9 @@ module PaperTrailDiff
29
29
  # : () -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]
30
30
  def history_and_versions: () -> [ ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped ]
31
31
 
32
+ # : () -> void
33
+ def reject_live_habtm!: () -> void
34
+
32
35
  # : (RootVersionPlan, untyped) -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]
33
36
  def history_for: (RootVersionPlan, untyped) -> [ ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped ]
34
37
 
@@ -40,8 +43,10 @@ module PaperTrailDiff
40
43
  # : (Array[untyped], untyped) -> Array[ActivityEvent]
41
44
  def events_through: (Array[untyped], untyped) -> Array[ActivityEvent]
42
45
 
43
- # : (Array[untyped], Array[ActivityEvent]) -> ActivityHistory
44
- def build_history: (Array[untyped], Array[ActivityEvent]) -> ActivityHistory
46
+ # Events after the last root version read the root from the live record
47
+ # when the window closes on it, as an explicit `to: record` range does.
48
+ # : (Array[untyped], Array[ActivityEvent], ?current: untyped) -> ActivityHistory
49
+ def build_history: (Array[untyped], Array[ActivityEvent], ?current: untyped) -> ActivityHistory
45
50
 
46
51
  # : (ActivityHistory, ActivityStep?) -> Array[ActivityStep]
47
52
  def activity_steps: (ActivityHistory, ActivityStep?) -> Array[ActivityStep]
@@ -22,10 +22,19 @@ module PaperTrailDiff
22
22
  # : (untyped) -> untyped
23
23
  def trailing_scope: (untyped) -> untyped
24
24
 
25
+ # Each item's earliest version after the range, keyed by item id. One row
26
+ # per item comes back from a NOT EXISTS rather than a window function, so
27
+ # the query stays portable; a timestamp tie keeps the lower id.
28
+ # : (untyped) -> Hash[String, untyped]
29
+ def first_trailing_versions: (untyped) -> Hash[String, untyped]
30
+
25
31
  private
26
32
 
27
33
  @exclude_end: bool
28
34
 
35
+ # : (untyped) -> untyped
36
+ def after_end: (untyped) -> untyped
37
+
29
38
  # : () -> Range[Time]
30
39
  def query_range: () -> Range[Time]
31
40
 
@@ -6,9 +6,6 @@ module PaperTrailDiff
6
6
  # : (untyped, time_range: TimeRange, ?version_scope: untyped, ?live_endpoint: untyped) -> void
7
7
  def initialize: (untyped, time_range: TimeRange, ?version_scope: untyped, ?live_endpoint: untyped) -> void
8
8
 
9
- # : (?context_required: bool) -> Array[untyped]
10
- def select: (?context_required: bool) -> Array[untyped]
11
-
12
9
  # : (?context_required: bool) -> RootVersionPlan
13
10
  def select_plan: (?context_required: bool) -> RootVersionPlan
14
11
 
@@ -22,9 +19,6 @@ module PaperTrailDiff
22
19
 
23
20
  @version_scope: untyped
24
21
 
25
- # : () -> untyped
26
- def versions_relation: () -> untyped
27
-
28
22
  # : (untyped) -> untyped
29
23
  def trailing_version: (untyped) -> untyped
30
24
 
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Selects and compares a chronological slice of a record's version history.
5
5
  class TimelineBuilder
6
- # : (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
7
- def initialize: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
6
+ # : (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
7
+ def initialize: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?version_scope: untyped, ?plan: RootVersionPlan?, ?live_endpoint: untyped) -> void
8
8
 
9
9
  # : () -> Array[Step]
10
10
  def build: () -> Array[Step]