paper_trail_diff 0.14.0 → 0.15.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +343 -0
  3. data/QUICKSTART.md +21 -4
  4. data/README.md +58 -55
  5. data/docs/architecture.md +32 -11
  6. data/docs/performance.md +116 -391
  7. data/docs/reference.md +345 -106
  8. data/docs/testing.md +63 -7
  9. data/lib/paper_trail_diff/activity_boundary.rb +83 -26
  10. data/lib/paper_trail_diff/activity_child_candidate_loader.rb +2 -1
  11. data/lib/paper_trail_diff/activity_event.rb +41 -10
  12. data/lib/paper_trail_diff/activity_history.rb +34 -9
  13. data/lib/paper_trail_diff/activity_range.rb +18 -3
  14. data/lib/paper_trail_diff/activity_snapshot_sequence.rb +76 -37
  15. data/lib/paper_trail_diff/activity_target_candidate_loader.rb +5 -5
  16. data/lib/paper_trail_diff/activity_timeline_builder.rb +266 -111
  17. data/lib/paper_trail_diff/activity_transaction_grouper.rb +2 -1
  18. data/lib/paper_trail_diff/activity_tree_filter.rb +114 -0
  19. data/lib/paper_trail_diff/activity_version_collector.rb +7 -5
  20. data/lib/paper_trail_diff/activity_view.rb +69 -0
  21. data/lib/paper_trail_diff/analysis.rb +13 -1
  22. data/lib/paper_trail_diff/analysis_batch.rb +24 -30
  23. data/lib/paper_trail_diff/approximate_paths.rb +48 -0
  24. data/lib/paper_trail_diff/association_activity_edge.rb +6 -6
  25. data/lib/paper_trail_diff/association_discovery.rb +9 -1
  26. data/lib/paper_trail_diff/association_key.rb +56 -0
  27. data/lib/paper_trail_diff/association_traversal.rb +13 -4
  28. data/lib/paper_trail_diff/batch_boundary_resolver.rb +3 -4
  29. data/lib/paper_trail_diff/batch_preload.rb +53 -0
  30. data/lib/paper_trail_diff/batched_root_analyzer.rb +12 -14
  31. data/lib/paper_trail_diff/batched_root_versions.rb +38 -14
  32. data/lib/paper_trail_diff/change_row.rb +133 -0
  33. data/lib/paper_trail_diff/collection_comparator.rb +5 -32
  34. data/lib/paper_trail_diff/configuration.rb +35 -12
  35. data/lib/paper_trail_diff/deleted_analysis_batch.rb +18 -18
  36. data/lib/paper_trail_diff/deleted_root_filter.rb +79 -0
  37. data/lib/paper_trail_diff/deprecation.rb +61 -0
  38. data/lib/paper_trail_diff/diagnostics.rb +84 -99
  39. data/lib/paper_trail_diff/endpoint.rb +26 -3
  40. data/lib/paper_trail_diff/errors.rb +3 -1
  41. data/lib/paper_trail_diff/fallback_paths.rb +38 -0
  42. data/lib/paper_trail_diff/historical_association_reifier.rb +2 -4
  43. data/lib/paper_trail_diff/historical_filter.rb +19 -10
  44. data/lib/paper_trail_diff/historical_snapshot_store.rb +57 -67
  45. data/lib/paper_trail_diff/live_endpoint_batch_loader.rb +1 -35
  46. data/lib/paper_trail_diff/nested_comparator.rb +19 -18
  47. data/lib/paper_trail_diff/paper_trail_adapter.rb +122 -127
  48. data/lib/paper_trail_diff/preloaded_endpoint_batch_loader.rb +13 -1
  49. data/lib/paper_trail_diff/prepared_activity_index.rb +1 -1
  50. data/lib/paper_trail_diff/prepared_edge_loader.rb +2 -1
  51. data/lib/paper_trail_diff/prepared_history.rb +9 -1
  52. data/lib/paper_trail_diff/prepared_history_loader.rb +1 -0
  53. data/lib/paper_trail_diff/prepared_record_index.rb +38 -7
  54. data/lib/paper_trail_diff/root_version_selection.rb +4 -2
  55. data/lib/paper_trail_diff/scope_condition.rb +76 -21
  56. data/lib/paper_trail_diff/scope_ranking.rb +12 -2
  57. data/lib/paper_trail_diff/scoped_analysis.rb +11 -0
  58. data/lib/paper_trail_diff/scoped_analysis_builder.rb +16 -8
  59. data/lib/paper_trail_diff/scoped_root_selection.rb +18 -12
  60. data/lib/paper_trail_diff/snapshot.rb +16 -1
  61. data/lib/paper_trail_diff/support.rb +29 -2
  62. data/lib/paper_trail_diff/time_range.rb +32 -10
  63. data/lib/paper_trail_diff/timeline_builder.rb +7 -7
  64. data/lib/paper_trail_diff/timeline_range.rb +9 -1
  65. data/lib/paper_trail_diff/timeline_snapshot_provider.rb +24 -17
  66. data/lib/paper_trail_diff/transaction_cut.rb +181 -0
  67. data/lib/paper_trail_diff/traversal_preparer.rb +134 -29
  68. data/lib/paper_trail_diff/version.rb +1 -1
  69. data/lib/paper_trail_diff/version_boundary.rb +37 -8
  70. data/lib/paper_trail_diff/version_query.rb +34 -10
  71. data/lib/paper_trail_diff/version_scope_filter.rb +9 -10
  72. data/lib/paper_trail_diff/version_sequence_diagnostics.rb +31 -16
  73. data/lib/paper_trail_diff/version_table.rb +60 -0
  74. data/lib/paper_trail_diff.rb +98 -56
  75. data/sig/generated/paper_trail_diff/activity_boundary.rbs +54 -19
  76. data/sig/generated/paper_trail_diff/activity_event.rbs +25 -4
  77. data/sig/generated/paper_trail_diff/activity_history.rbs +21 -6
  78. data/sig/generated/paper_trail_diff/activity_range.rbs +9 -2
  79. data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +48 -9
  80. data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +134 -30
  81. data/sig/generated/paper_trail_diff/activity_tree_filter.rbs +73 -0
  82. data/sig/generated/paper_trail_diff/activity_version_collector.rbs +2 -0
  83. data/sig/generated/paper_trail_diff/activity_view.rbs +42 -0
  84. data/sig/generated/paper_trail_diff/analysis.rbs +8 -1
  85. data/sig/generated/paper_trail_diff/analysis_batch.rbs +8 -15
  86. data/sig/generated/paper_trail_diff/approximate_paths.rbs +23 -0
  87. data/sig/generated/paper_trail_diff/association_discovery.rbs +5 -0
  88. data/sig/generated/paper_trail_diff/association_key.rbs +29 -0
  89. data/sig/generated/paper_trail_diff/association_traversal.rbs +4 -0
  90. data/sig/generated/paper_trail_diff/batch_preload.rbs +29 -0
  91. data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +9 -10
  92. data/sig/generated/paper_trail_diff/batched_root_versions.rbs +14 -4
  93. data/sig/generated/paper_trail_diff/change_row.rbs +86 -0
  94. data/sig/generated/paper_trail_diff/collection_comparator.rbs +2 -16
  95. data/sig/generated/paper_trail_diff/configuration.rbs +19 -3
  96. data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +8 -8
  97. data/sig/generated/paper_trail_diff/deleted_root_filter.rbs +48 -0
  98. data/sig/generated/paper_trail_diff/deprecation.rbs +35 -0
  99. data/sig/generated/paper_trail_diff/diagnostics.rbs +37 -30
  100. data/sig/generated/paper_trail_diff/endpoint.rbs +11 -2
  101. data/sig/generated/paper_trail_diff/errors.rbs +3 -1
  102. data/sig/generated/paper_trail_diff/fallback_paths.rbs +22 -0
  103. data/sig/generated/paper_trail_diff/historical_filter.rbs +12 -2
  104. data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +33 -28
  105. data/sig/generated/paper_trail_diff/live_endpoint_batch_loader.rbs +0 -15
  106. data/sig/generated/paper_trail_diff/nested_comparator.rbs +10 -7
  107. data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +54 -52
  108. data/sig/generated/paper_trail_diff/preloaded_endpoint_batch_loader.rbs +9 -1
  109. data/sig/generated/paper_trail_diff/prepared_history.rbs +5 -0
  110. data/sig/generated/paper_trail_diff/prepared_record_index.rbs +19 -2
  111. data/sig/generated/paper_trail_diff/root_version_selection.rbs +2 -0
  112. data/sig/generated/paper_trail_diff/scope_condition.rbs +43 -9
  113. data/sig/generated/paper_trail_diff/scope_ranking.rbs +8 -0
  114. data/sig/generated/paper_trail_diff/scoped_analysis.rbs +6 -0
  115. data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +6 -1
  116. data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +8 -4
  117. data/sig/generated/paper_trail_diff/snapshot.rbs +8 -0
  118. data/sig/generated/paper_trail_diff/support.rbs +15 -0
  119. data/sig/generated/paper_trail_diff/time_range.rbs +20 -6
  120. data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
  121. data/sig/generated/paper_trail_diff/timeline_range.rbs +2 -0
  122. data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +17 -7
  123. data/sig/generated/paper_trail_diff/transaction_cut.rbs +94 -0
  124. data/sig/generated/paper_trail_diff/traversal_preparer.rbs +66 -8
  125. data/sig/generated/paper_trail_diff/version_boundary.rbs +20 -3
  126. data/sig/generated/paper_trail_diff/version_query.rbs +18 -2
  127. data/sig/generated/paper_trail_diff/version_scope_filter.rbs +4 -5
  128. data/sig/generated/paper_trail_diff/version_sequence_diagnostics.rbs +16 -9
  129. data/sig/generated/paper_trail_diff/version_table.rbs +30 -0
  130. data/sig/generated/paper_trail_diff.rbs +29 -26
  131. metadata +46 -10
  132. data/lib/paper_trail_diff/activity_grouping.rb +0 -38
  133. data/lib/paper_trail_diff/batch_safety.rb +0 -63
  134. data/lib/paper_trail_diff/time_activity_timeline_builder.rb +0 -223
  135. data/sig/generated/paper_trail_diff/activity_grouping.rbs +0 -22
  136. data/sig/generated/paper_trail_diff/batch_safety.rbs +0 -30
  137. data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +0 -92
@@ -0,0 +1,23 @@
1
+ # Generated from lib/paper_trail_diff/approximate_paths.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # The requested paths prepared history cannot read exactly, each with why.
5
+ # PT-AT reads such a path instead, one record at a time, when the caller
6
+ # passes `allow_approximate: true`.
7
+ module ApproximatePaths
8
+ # : (AssociationTraversal, untyped) -> Array[[String, String]]
9
+ def self.call: (AssociationTraversal, untyped) -> Array[[ String, String ]]
10
+
11
+ # A has_many through reads its parts' conditions only, at any depth. A
12
+ # has_one through over a ranked part is refused whole, so its parts, too,
13
+ # are checked for conditions alone.
14
+ # : (untyped, ranked: bool) -> String?
15
+ def self.fallback_reason: (untyped, ranked: bool) -> String?
16
+
17
+ # A scope prepared history can read as conditions on stored attributes
18
+ # is batched like any other path. A polymorphic target's class, and so
19
+ # its scope, is known only per record.
20
+ # : (untyped, bool) -> String?
21
+ def self.reason_for: (untyped, bool) -> String?
22
+ end
23
+ end
@@ -49,6 +49,11 @@ module PaperTrailDiff
49
49
  # : (untyped, root: bool) -> Array[untyped]
50
50
  def supported_reflections: (untyped, root: bool) -> Array[untyped]
51
51
 
52
+ # Every request that reads history refuses a path whose target, or a
53
+ # model its has_many :through goes through, has no versions.
54
+ # : (untyped) -> bool
55
+ def unversioned?: (untyped) -> bool
56
+
52
57
  # : (untyped) -> untyped
53
58
  def reflection_target: (untyped) -> untyped
54
59
 
@@ -0,0 +1,29 @@
1
+ # Generated from lib/paper_trail_diff/association_key.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # PT-AT records which record a child pointed at by that record's id, and
5
+ # history matches children to owners and targets by id. An association
6
+ # keyed on another column (`primary_key:`) would read other records'
7
+ # history, so it is refused.
8
+ module AssociationKey
9
+ # Why the association, or a part of a through, cannot be read by id.
10
+ # : (untyped) -> String?
11
+ def self.unsupported_reason: (untyped) -> String?
12
+
13
+ # The reflection, or the first part of a through, keyed on a column other
14
+ # than its owner's or target's primary key.
15
+ # : (untyped) -> untyped
16
+ def self.keyed_part: (untyped) -> untyped
17
+
18
+ # A belongs_to names a key of its target; any other macro, one of its
19
+ # owner. A polymorphic target has no single primary key to compare with.
20
+ # : (untyped) -> bool
21
+ def self.other_key?: (untyped) -> bool
22
+
23
+ # Column names are strings once the key is, so the one mutation left,
24
+ # `eql?` for `==`, answers the same.
25
+ # mutant:disable
26
+ # : (untyped, untyped) -> bool
27
+ def self.same_column?: (untyped, untyped) -> bool
28
+ end
29
+ end
@@ -39,6 +39,10 @@ module PaperTrailDiff
39
39
  # : (untyped, untyped, root: bool) -> String?
40
40
  def self.unsupported_reason: (untyped, untyped, root: bool) -> String?
41
41
 
42
+ # History matches records by primary key, within one set of version tables.
43
+ # : (untyped) -> String?
44
+ def self.storage_reason: (untyped) -> String?
45
+
42
46
  # PT-AT records a HABTM's membership only when its owner saves a version,
43
47
  # in rows keyed by transaction and association name, not by owner. A root
44
48
  # checkpoint so holds the root's membership and no one else's: another
@@ -0,0 +1,29 @@
1
+ # Generated from lib/paper_trail_diff/batch_preload.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Which associations Active Record's preloader loads correctly for many
5
+ # owners at once.
6
+ module BatchPreload
7
+ # The preloader applies a limit or offset to all owners' records at once,
8
+ # not to each owner's, so such an association is loaded per record. A
9
+ # has_one is no exception. Through a part with a limit, an offset or a
10
+ # has_one cut, the preloader keeps what the part alone would load, while
11
+ # the through's own query keeps the part's conditions only.
12
+ #
13
+ # The declared scope is read, not a record's association scope: Active
14
+ # Record adds `limit(1)` to every has_one's, which would load every
15
+ # has_one per record.
16
+ # : (untyped) -> bool
17
+ def self.safe?: (untyped) -> bool
18
+
19
+ # : (untyped) -> untyped
20
+ def self.declared_relation: (untyped) -> untyped
21
+
22
+ # : (untyped) -> bool
23
+ def self.cut_part?: (untyped) -> bool
24
+
25
+ # Every part a through passes, at any depth.
26
+ # : (untyped) -> Array[untyped]
27
+ def self.through_parts: (untyped) -> Array[untyped]
28
+ end
29
+ end
@@ -4,11 +4,12 @@ module PaperTrailDiff
4
4
  # Builds one root's Analysis inside a batch, from versions the batch already
5
5
  # selected and history it already prepared.
6
6
  class BatchedRootAnalyzer
7
- # : (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool, ?within: untyped, ?snapshots: bool, ?group: Symbol?) -> void
8
- def initialize: (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool, ?within: untyped, ?snapshots: bool, ?group: Symbol?) -> void
7
+ # : (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool, view: ActivityView, ?within: untyped) -> void
8
+ def initialize: (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool, view: ActivityView, ?within: untyped) -> void
9
9
 
10
- # : (untyped, RootVersionPlan) -> Analysis
11
- def call: (untyped, RootVersionPlan) -> Analysis
10
+ # `window` is the root's window as the batch moved it to transaction edges.
11
+ # : (untyped, RootVersionPlan, ?TimeRange?) -> Analysis
12
+ def call: (untyped, RootVersionPlan, ?TimeRange?) -> Analysis
12
13
 
13
14
  private
14
15
 
@@ -16,19 +17,17 @@ module PaperTrailDiff
16
17
 
17
18
  @activity_snapshotter: untyped
18
19
 
19
- @group: Symbol?
20
-
21
20
  @preparer: untyped
22
21
 
23
- @snapshots: bool
24
-
25
22
  @timeline_snapshotter: untyped
26
23
 
27
24
  @tree: AssociationTree
28
25
 
26
+ @view: ActivityView
27
+
29
28
  @within: untyped
30
29
 
31
- # : (untyped, RootVersionPlan) -> Analysis
32
- def activity_analysis: (untyped, RootVersionPlan) -> Analysis
30
+ # : (untyped, RootVersionPlan, TimeRange?) -> Analysis
31
+ def activity_analysis: (untyped, RootVersionPlan, TimeRange?) -> Analysis
33
32
  end
34
33
  end
@@ -5,8 +5,8 @@ module PaperTrailDiff
5
5
  # queries. Only the range forms that mean the same thing for every root are
6
6
  # supported: a shared wall-clock window, or each root's own whole history.
7
7
  class BatchedRootVersions
8
- # : (Array[untyped], time_range: TimeRange?, version_scope: untyped, live_endpoints: Hash[Array[String], untyped]?, context_required: bool) -> void
9
- def initialize: (Array[untyped], time_range: TimeRange?, version_scope: untyped, live_endpoints: Hash[Array[String], untyped]?, context_required: bool) -> void
8
+ # : (Array[untyped], time_range: TimeRange?, version_scope: untyped, live_endpoints: Hash[Array[String], untyped]?, context_required: bool, windows: Hash[Array[String], TimeRange]) -> void
9
+ def initialize: (Array[untyped], time_range: TimeRange?, version_scope: untyped, live_endpoints: Hash[Array[String], untyped]?, context_required: bool, windows: Hash[Array[String], TimeRange]) -> void
10
10
 
11
11
  # Returns a plan per record identity.
12
12
  # : () -> Hash[Array[String], RootVersionPlan]
@@ -24,14 +24,24 @@ module PaperTrailDiff
24
24
 
25
25
  @version_scope: untyped
26
26
 
27
+ @windows: Hash[Array[String], TimeRange]
28
+
27
29
  # : (untyped, Array[untyped], Hash[Array[String], RootVersionPlan]) -> void
28
30
  def select_model: (untyped, Array[untyped], Hash[Array[String], RootVersionPlan]) -> void
29
31
 
32
+ # Moved windows only start and end earlier, so the versions read for the
33
+ # widest of them hold each root's own, and the first left past a moved end.
34
+ # : (Array[String], String, Hash[String, Array[untyped]], Hash[String, untyped]) -> [Array[untyped], untyped]
35
+ def own_window: (Array[String], String, Hash[String, Array[untyped]], Hash[String, untyped]) -> [ Array[untyped], untyped ]
36
+
30
37
  # : (untyped, Array[untyped]) -> [Hash[String, Array[untyped]], Set[untyped]?, Hash[String, untyped]]
31
38
  def model_versions: (untyped, Array[untyped]) -> [ Hash[String, Array[untyped]], Set[untyped]?, Hash[String, untyped] ]
32
39
 
33
- # : (Array[untyped], Set[untyped]?, untyped, untyped) -> RootVersionPlan
34
- def versions_for: (Array[untyped], Set[untyped]?, untyped, untyped) -> RootVersionPlan
40
+ # : (Array[untyped]) -> TimeRange?
41
+ def widest_range: (Array[untyped]) -> TimeRange?
42
+
43
+ # : (Array[untyped], untyped, Set[untyped]?, untyped) -> RootVersionPlan
44
+ def versions_for: (Array[untyped], untyped, Set[untyped]?, untyped) -> RootVersionPlan
35
45
 
36
46
  # The batch already loaded every root, so closing on current state reuses
37
47
  # that rather than reading each record again.
@@ -0,0 +1,86 @@
1
+ # Generated from lib/paper_trail_diff/change_row.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # One change from an activity step, flat enough for a history table or an
5
+ # export: a field that changed, or a record that appeared or disappeared,
6
+ # with the event the step attributes it to.
7
+ class ChangeRow
8
+ attr_reader change: Symbol
9
+
10
+ attr_reader association_path: Array[String]
11
+
12
+ attr_reader association_kind: Symbol?
13
+
14
+ # The record the row is about, or nil for the root itself.
15
+ attr_reader record: RecordReference?
16
+
17
+ # The field the row is about, or nil for a row about a whole record.
18
+ attr_reader attribute: String?
19
+
20
+ attr_reader from: untyped
21
+
22
+ attr_reader to: untyped
23
+
24
+ # The step's source boundary: for a transaction group, its first event.
25
+ attr_reader source: ActivityBoundary
26
+
27
+ # : (change: Symbol, association_path: Array[String], association_kind: Symbol?, record: RecordReference?, attribute: String?, from: untyped, to: untyped, source: ActivityBoundary) -> void
28
+ def initialize: (change: Symbol, association_path: Array[String], association_kind: Symbol?, record: RecordReference?, attribute: String?, from: untyped, to: untyped, source: ActivityBoundary) -> void
29
+
30
+ # The source's identifying fields sit beside the change rather than nested,
31
+ # so the hash is one row of a table as it stands.
32
+ # : () -> Hash[Symbol, untyped]
33
+ def to_h: () -> Hash[Symbol, untyped]
34
+
35
+ private
36
+
37
+ # : () -> Hash[Symbol, untyped]
38
+ def source_fields: () -> Hash[Symbol, untyped]
39
+ end
40
+
41
+ # Flattens one step's diff into change rows, in the diff's walking order.
42
+ #
43
+ # A row is about one record. An edited field is `:changed`. A record that
44
+ # appeared or disappeared is one `:added` or `:removed` row, followed by a
45
+ # row for each of its fields and for each record it holds, so a table can
46
+ # list a new comment's body without a second walker. That covers a record
47
+ # joining or leaving a collection, a to-one target set, cleared or replaced
48
+ # (the old target removed, then the new one added), and the root's own
49
+ # creation or deletion.
50
+ #
51
+ # Two kinds of entry add nothing a row would show. One that only groups
52
+ # others (an edited record, an association, a relationship) is left out,
53
+ # because the rows under it name the same record. A blank field of an added
54
+ # or removed record is left out too, because the record's own row already
55
+ # says it appeared or disappeared.
56
+ class ChangeRows
57
+ RECORD_ENTRIES: untyped
58
+
59
+ # : (Diff, source: ActivityBoundary) -> Array[ChangeRow]
60
+ def self.call: (Diff, source: ActivityBoundary) -> Array[ChangeRow]
61
+
62
+ # : (ActivityBoundary) -> void
63
+ def initialize: (ActivityBoundary) -> void
64
+
65
+ # : (Diff) -> Array[ChangeRow]
66
+ def call: (Diff) -> Array[ChangeRow]
67
+
68
+ private
69
+
70
+ @source: ActivityBoundary
71
+
72
+ # : (TraversalEntry) -> ChangeRow?
73
+ def row_for: (TraversalEntry) -> ChangeRow?
74
+
75
+ # : (TraversalEntry) -> ChangeRow
76
+ def changed_row: (TraversalEntry) -> ChangeRow
77
+
78
+ # An added record's state is the after side and a removed one's the before
79
+ # side, so a field's value goes on the side that has it.
80
+ # : (TraversalEntry, String?) -> ChangeRow
81
+ def presence_row: (TraversalEntry, String?) -> ChangeRow
82
+
83
+ # : (TraversalEntry, Symbol, String?, untyped, untyped) -> ChangeRow
84
+ def row: (TraversalEntry, Symbol, String?, untyped, untyped) -> ChangeRow
85
+ end
86
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module PaperTrailDiff
4
4
  # Compares collection snapshots with a low-allocation path for stable membership.
5
+ # Snapshots hold their records in type and ID order, so results follow it
6
+ # without sorting here.
5
7
  class CollectionComparator
6
8
  # : (AssociationSnapshot, AssociationSnapshot, record_comparer: untyped) -> void
7
9
  def initialize: (AssociationSnapshot, AssociationSnapshot, record_comparer: untyped) -> void
@@ -47,22 +49,6 @@ module PaperTrailDiff
47
49
  # : (Array[RecordSnapshot]) -> Hash[identity, RecordSnapshot]
48
50
  def index_records: (Array[RecordSnapshot]) -> Hash[identity, RecordSnapshot]
49
51
 
50
- # : (Array[identity]) -> Array[identity]
51
- def sorted_identities: (Array[identity]) -> Array[identity]
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.
57
- # : (identity) -> Array[untyped]
58
- def sortable_identity: (identity) -> Array[untyped]
59
-
60
- # Only the order of the group numbers matters, so moving the lowest one
61
- # down or the highest one up changes nothing; the spec checks the order.
62
- # mutant:disable
63
- # : (untyped) -> Array[untyped]
64
- def sortable_id: (untyped) -> Array[untyped]
65
-
66
52
  # : (identity) -> void
67
53
  def raise_duplicate_identity!: (identity) -> void
68
54
  end
@@ -5,18 +5,27 @@ module PaperTrailDiff
5
5
  class AssociationTree
6
6
  attr_reader children: Hash[String, AssociationTree]
7
7
 
8
+ # Takes what `includes` takes: a name, a dotted path, a hash of names to
9
+ # what to include below them, or an array of any of these.
8
10
  # : (untyped) -> AssociationTree
9
11
  def self.build: (untyped) -> AssociationTree
10
12
 
13
+ # : (untyped, String?) -> Array[String]
14
+ private def self.paths: (untyped, String?) -> Array[String]
15
+
16
+ # `{ comments: :replies }` names comments as well as their replies.
17
+ # : (untyped, untyped, String?) -> Array[String]
18
+ private def self.nested_paths: (untyped, untyped, String?) -> Array[String]
19
+
20
+ # : (String?, String | Symbol) -> String
21
+ private def self.joined: (String?, String | Symbol) -> String
22
+
11
23
  # : (Hash[String, untyped], String) -> void
12
24
  private def self.add_path: (Hash[String, untyped], String) -> void
13
25
 
14
26
  # : (Hash[String, untyped]) -> AssociationTree
15
27
  private def self.from_hash: (Hash[String, untyped]) -> AssociationTree
16
28
 
17
- # : (untyped) -> bool
18
- private def self.valid_values?: (untyped) -> bool
19
-
20
29
  # : (Hash[String, AssociationTree]) -> void
21
30
  def initialize: (Hash[String, AssociationTree]) -> void
22
31
 
@@ -63,5 +72,12 @@ module PaperTrailDiff
63
72
 
64
73
  # : (String) -> Array[String]
65
74
  def attributes_for: (String) -> Array[String]
75
+
76
+ # The names to leave out of what a version recorded. A root version is
77
+ # known to be the root's, so its own path's names apply; any other version
78
+ # does not say which path reached it, so only the names ignored everywhere
79
+ # do.
80
+ # : (root: bool) -> Array[String]
81
+ def recorded_attributes: (root: bool) -> Array[String]
66
82
  end
67
83
  end
@@ -32,11 +32,11 @@ module PaperTrailDiff
32
32
 
33
33
  # A root destroyed before the window had no state inside it to read, so
34
34
  # only the others take the version after the window as context.
35
- # : (Array[untyped], Array[untyped]) -> Hash[Array[String], RootVersionPlan]
36
- def plans: (Array[untyped], Array[untyped]) -> Hash[Array[String], RootVersionPlan]
35
+ # : (Array[untyped], Array[untyped], Hash[Array[String], TimeRange]) -> Hash[Array[String], RootVersionPlan]
36
+ def plans: (Array[untyped], Array[untyped], Hash[Array[String], TimeRange]) -> Hash[Array[String], RootVersionPlan]
37
37
 
38
- # : (Array[untyped], bool) -> Hash[Array[String], RootVersionPlan]
39
- def plans_for: (Array[untyped], bool) -> Hash[Array[String], RootVersionPlan]
38
+ # : (Array[untyped], bool, Hash[Array[String], TimeRange]) -> Hash[Array[String], RootVersionPlan]
39
+ def plans_for: (Array[untyped], bool, Hash[Array[String], TimeRange]) -> Hash[Array[String], RootVersionPlan]
40
40
 
41
41
  # These records supply identity and model metadata only. Every reported
42
42
  # value comes from the batched historical plan; no per-root reify or live
@@ -47,10 +47,10 @@ module PaperTrailDiff
47
47
  # : () -> Array[untyped]
48
48
  def destroy_versions: () -> Array[untyped]
49
49
 
50
- # : (Array[untyped]) -> Hash[Symbol, untyped]
51
- def preparation_range: (Array[untyped]) -> Hash[Symbol, untyped]
50
+ # : (Array[untyped], Hash[Array[String], TimeRange]) -> Hash[Symbol, untyped]
51
+ def preparation_range: (Array[untyped], Hash[Array[String], TimeRange]) -> Hash[Symbol, untyped]
52
52
 
53
- # : (Array[untyped], Hash[Array[String], RootVersionPlan]) -> void
54
- def prepare: (Array[untyped], Hash[Array[String], RootVersionPlan]) -> void
53
+ # : (Array[untyped], Hash[Array[String], RootVersionPlan], Hash[Array[String], TimeRange]) -> void
54
+ def prepare: (Array[untyped], Hash[Array[String], RootVersionPlan], Hash[Array[String], TimeRange]) -> void
55
55
  end
56
56
  end
@@ -0,0 +1,48 @@
1
+ # Generated from lib/paper_trail_diff/deleted_root_filter.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Keeps the deleted roots the relation would have selected, by the state
5
+ # each held when it was destroyed. A relation's conditions read the live
6
+ # table, where a destroyed root has no row, so without this a report on one
7
+ # account's records would include every account's deletions.
8
+ class DeletedRootFilter
9
+ # : (untyped) -> void
10
+ def initialize: (untyped) -> void
11
+
12
+ # A root whose destroy PaperTrail did not record has no state to test
13
+ # and stays, as it has no state to report either. One whose destroy
14
+ # recorded no state is left out.
15
+ # : (Array[Array[String]]) -> Array[Array[String]]
16
+ def call: (Array[Array[String]]) -> Array[Array[String]]
17
+
18
+ private
19
+
20
+ @condition: ScopeCondition
21
+
22
+ @loader: DirectRecordStateLoader
23
+
24
+ @relation: untyped
25
+
26
+ # A relation delegates class methods to its model, so every mutation here,
27
+ # which drops `.model`, answers the same.
28
+ # mutant:disable
29
+ # : () -> untyped
30
+ def model: () -> untyped
31
+
32
+ # : (Hash[String, untyped]?) -> bool
33
+ def selected?: (Hash[String, untyped]?) -> bool
34
+
35
+ # The state each root held before its latest destroy.
36
+ # : (Array[String]) -> Hash[String, Hash[String, untyped]?]
37
+ def destroyed_states: (Array[String]) -> Hash[String, Hash[String, untyped]?]
38
+
39
+ # Read directly, with a column `skip:` left out of history as its default,
40
+ # so a report checks its ceiling before reifying anything. A version the
41
+ # direct reader declines, such as one naming a column since removed, is
42
+ # reified. Only the record's own attributes are read, so the association
43
+ # options, which only spare queries, cannot change an answer.
44
+ # mutant:disable
45
+ # : (untyped) -> Hash[String, untyped]?
46
+ def reified_attributes: (untyped) -> Hash[String, untyped]?
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ # Generated from lib/paper_trail_diff/deprecation.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Warns about options that do nothing any more and will be removed. In a
5
+ # Rails app, register the deprecator to configure it with the app's own:
6
+ #
7
+ # Rails.application.deprecators[:paper_trail_diff] = PaperTrailDiff.deprecator
8
+ module Deprecation
9
+ CLOSE_ON: ::String
10
+
11
+ ALLOW_EXPENSIVE: ::String
12
+
13
+ RELOAD_LIVE_ENDPOINTS: ::String
14
+
15
+ # Required when first used, as the library loads nothing it does not need.
16
+ # : () -> untyped
17
+ def self.deprecator: () -> untyped
18
+
19
+ # The one value close_on: took. It named what ends a wall-clock window,
20
+ # so it was meaningless without one, and until it is removed it is still
21
+ # checked as it was.
22
+ CLOSE_ON_CURRENT: ::Symbol
23
+
24
+ # : (Symbol?, untyped) -> void
25
+ def self.close_on!: (Symbol?, untyped) -> void
26
+
27
+ # : (untyped) -> void
28
+ def self.allow_expensive!: (untyped) -> void
29
+
30
+ # The value to use: the option still works until it is removed, and nil
31
+ # stands for its old default.
32
+ # : (untyped) -> untyped
33
+ def self.reload_live_endpoints!: (untyped) -> untyped
34
+ end
35
+ end
@@ -41,28 +41,49 @@ module PaperTrailDiff
41
41
  end
42
42
 
43
43
  # Performs read-only checks for known PaperTrail/PT-AT reconstruction hazards.
44
+ #
45
+ # The endpoints are a comparison's: two versions, or a version and the
46
+ # current record, in either order. Two current records read no history, so
47
+ # nothing is checked.
44
48
  class HistoryDiagnostics
45
- # : (untyped, untyped, associations: Array[String | Symbol]) -> void
46
- def initialize: (untyped, untyped, associations: Array[String | Symbol]) -> void
49
+ STOPPING_CODES: untyped
50
+
51
+ # : (untyped, untyped, associations: association_option, ?allow_approximate: bool) -> void
52
+ def initialize: (untyped, untyped, associations: association_option, ?allow_approximate: bool) -> void
47
53
 
48
54
  # : () -> DiagnosticReport
49
55
  def call: () -> DiagnosticReport
50
56
 
51
57
  private
52
58
 
53
- @from_version: untyped
59
+ # Each refusal a comparison would raise is an error, so `ok?` is false
60
+ # exactly when one would. The checks after them read reflections that must
61
+ # resolve and PT-AT records that must be there.
62
+ # : (untyped) -> void
63
+ def inspect_associations: (untyped) -> void
64
+
65
+ @allow_approximate: bool
54
66
 
55
67
  @issues: Array[DiagnosticIssue]
56
68
 
57
- @to_version: untyped
69
+ @preparer: TraversalPreparer
70
+
71
+ @traversal: AssociationTraversal
58
72
 
59
73
  @tree: AssociationTree
60
74
 
61
- # : () -> untyped
62
- def validated_model_class: () -> untyped
75
+ @versions: Array[untyped]
76
+
77
+ # The endpoints that are versions, in the order given.
78
+ # : (untyped, untyped) -> Array[untyped]
79
+ def validated_versions: (untyped, untyped) -> Array[untyped]
63
80
 
64
81
  # : (Array[Array[untyped]]) -> void
65
- def inspect_targets: (Array[Array[untyped]]) -> void
82
+ def inspect_polymorphic_targets: (Array[Array[untyped]]) -> void
83
+
84
+ # Accepted, but read the way `allow_approximate:` describes.
85
+ # : (untyped) -> void
86
+ def inspect_approximate_paths: (untyped) -> void
66
87
 
67
88
  # : (untyped, Array[Array[untyped]]) -> void
68
89
  def inspect_habtm: (untyped, Array[Array[untyped]]) -> void
@@ -72,8 +93,15 @@ module PaperTrailDiff
72
93
  # : (untyped, Array[Array[untyped]]) -> void
73
94
  def inspect_shared_transactions: (untyped, Array[Array[untyped]]) -> void
74
95
 
75
- # : () -> void
76
- def inspect_version_sequence: () -> void
96
+ # With the current record as one endpoint there is one version, and the
97
+ # sequence runs on from it.
98
+ # : (untyped) -> void
99
+ def inspect_version_sequence: (untyped) -> void
100
+
101
+ # Only a path PT-AT reads can lose a change between tied versions, and a
102
+ # traversal that does not resolve reads nothing at all.
103
+ # : (untyped) -> bool
104
+ def fallback_selected?: (untyped) -> bool
77
105
 
78
106
  # : (untyped) -> void
79
107
  def inspect_checkpoint_timestamp: (untyped) -> void
@@ -81,30 +109,9 @@ module PaperTrailDiff
81
109
  # : (Array[String]) -> void
82
110
  def inspect_transaction_metadata: (Array[String]) -> void
83
111
 
84
- # : (untyped, String) -> void
85
- def inspect_versioned_model: (untyped, String) -> void
86
-
87
- # : (untyped, String) -> void
88
- def inspect_through_model: (untyped, String) -> void
89
-
90
- # : (untyped) -> bool
91
- def model_versioned?: (untyped) -> bool
92
-
93
112
  # : (untyped) -> bool
94
113
  def synchronized_timestamp_disabled?: (untyped) -> bool
95
114
 
96
- # : () -> bool
97
- def association_tracking_available?: () -> bool
98
-
99
- # : (untyped) -> Array[String]
100
- def version_identity: (untyped) -> Array[String]
101
-
102
- # : (untyped) -> String
103
- def version_type: (untyped) -> String
104
-
105
- # : () -> String
106
- def tracking_unavailable_message: () -> String
107
-
108
115
  # : (Symbol, String, ?String?, ?untyped) -> void
109
116
  def add_error: (Symbol, String, ?String?, ?untyped) -> void
110
117
 
@@ -21,6 +21,15 @@ module PaperTrailDiff
21
21
  # : (untyped) -> void
22
22
  def self?.validate!: (untyped) -> void
23
23
 
24
+ # The record a method reads the history of: a record, saved or destroyed,
25
+ # of a model PaperTrail versions.
26
+ # : (untyped) -> void
27
+ def self?.validate_root!: (untyped) -> void
28
+
29
+ # A relation is named rather than inspected, which would load it.
30
+ # : (untyped) -> String
31
+ def self?.described: (untyped) -> String
32
+
24
33
  # : (untyped) -> bool
25
34
  def self?.version?: (untyped) -> bool
26
35
 
@@ -33,8 +42,8 @@ module PaperTrailDiff
33
42
  # : (untyped) -> untyped
34
43
  def self?.model_class: (untyped) -> untyped
35
44
 
36
- # The record's own PaperTrail versions, under whatever name the model gave
37
- # the association.
45
+ # Every PaperTrail version of the record, whatever scope its versions
46
+ # association has.
38
47
  # : (untyped) -> untyped
39
48
  def self?.versions: (untyped) -> untyped
40
49
 
@@ -54,7 +54,9 @@ module PaperTrailDiff
54
54
  class FilterLimitExceededError < Error
55
55
  end
56
56
 
57
- # A batch selected a known expensive association without explicitly opting in.
57
+ # No longer raised: `allow_approximate: true` alone accepts the paths it
58
+ # refused. Kept until `allow_expensive:` is removed, so code that rescues it
59
+ # still loads.
58
60
  class UnsupportedBatchPathError < ConfigurationError
59
61
  end
60
62
 
@@ -0,0 +1,22 @@
1
+ # Generated from lib/paper_trail_diff/fallback_paths.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Whether a request may read a path with PT-AT's reifier rather than from
5
+ # loaded history. PT-AT places association changes by timestamp alone, so
6
+ # only such a path can miss a change between two versions that share one;
7
+ # loaded history orders those versions by id.
8
+ module FallbackPaths
9
+ # A path history reads only approximately counts when it is allowed: a
10
+ # request that does not allow it is refused before anything is read.
11
+ # : (AssociationTraversal, untyped, approximate: bool) -> bool
12
+ def self.any?: (AssociationTraversal, untyped, approximate: bool) -> bool
13
+
14
+ # Loaded history cannot index a composite foreign key, and a polymorphic
15
+ # belongs_to's target, with its default scope, is known only per record.
16
+ # : (untyped) -> bool
17
+ def self.per_record?: (untyped) -> bool
18
+
19
+ # : (untyped) -> bool
20
+ def self.composite_key?: (untyped) -> bool
21
+ end
22
+ end
@@ -16,8 +16,10 @@ module PaperTrailDiff
16
16
  # : (untyped, predicate: untyped, time_range: TimeRange?, limit: Integer, max_versions: Integer) -> void
17
17
  def initialize: (untyped, predicate: untyped, time_range: TimeRange?, limit: Integer, max_versions: Integer) -> void
18
18
 
19
- # : (Array[String]) -> Array[String]
20
- def call: (Array[String]) -> Array[String]
19
+ # More matches than the limit raise, unless the caller does not count
20
+ # them: candidates it only lists, such as deleted roots.
21
+ # : (Array[String], ?counted: bool) -> Array[String]
22
+ def call: (Array[String], ?counted: bool) -> Array[String]
21
23
 
22
24
  private
23
25
 
@@ -38,6 +40,14 @@ module PaperTrailDiff
38
40
  # : (untyped) -> untyped
39
41
  def validate_predicate: (untyped) -> untyped
40
42
 
43
+ # Expected and recorded values compare as the attribute casts them, as
44
+ # `where` reads its values: an enum's symbol names its stored value, and a
45
+ # `json` or `jsonb` version column, which PaperTrail does not cast, holds
46
+ # dates and times as strings. Casting also copies a mutable value, so the
47
+ # filter keeps its own.
48
+ # : (String, untyped) -> untyped
49
+ def cast: (String, untyped) -> untyped
50
+
41
51
  # : (untyped) -> Integer
42
52
  def validate_budget: (untyped) -> Integer
43
53