paper_trail_diff 0.13.1 → 0.14.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +287 -0
  3. data/QUICKSTART.md +9 -6
  4. data/README.md +84 -9
  5. data/docs/architecture.md +32 -9
  6. data/docs/performance.md +106 -13
  7. data/docs/reference.md +242 -29
  8. data/docs/testing.md +34 -11
  9. data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +19 -9
  10. data/lib/paper_trail_diff/activity_collection_event_applier.rb +11 -1
  11. data/lib/paper_trail_diff/activity_collection_record_updater.rb +16 -2
  12. data/lib/paper_trail_diff/activity_event_record_resolver.rb +20 -1
  13. data/lib/paper_trail_diff/activity_event_route_finder.rb +2 -1
  14. data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +1 -1
  15. data/lib/paper_trail_diff/activity_range.rb +14 -2
  16. data/lib/paper_trail_diff/activity_relationship.rb +12 -0
  17. data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +5 -2
  18. data/lib/paper_trail_diff/activity_target_candidate_loader.rb +131 -0
  19. data/lib/paper_trail_diff/activity_timeline_builder.rb +30 -24
  20. data/lib/paper_trail_diff/activity_version_collector.rb +31 -20
  21. data/lib/paper_trail_diff/association_activity_edge.rb +236 -0
  22. data/lib/paper_trail_diff/association_activity_selection.rb +197 -0
  23. data/lib/paper_trail_diff/association_discovery.rb +8 -13
  24. data/lib/paper_trail_diff/association_traversal.rb +169 -45
  25. data/lib/paper_trail_diff/batch_safety.rb +23 -15
  26. data/lib/paper_trail_diff/batched_root_versions.rb +21 -39
  27. data/lib/paper_trail_diff/comparison_batch.rb +37 -10
  28. data/lib/paper_trail_diff/deleted_analysis_batch.rb +34 -16
  29. data/lib/paper_trail_diff/diagnostics.rb +26 -8
  30. data/lib/paper_trail_diff/direct_record_state_loader.rb +10 -1
  31. data/lib/paper_trail_diff/errors.rb +2 -1
  32. data/lib/paper_trail_diff/habtm_transaction_owners.rb +68 -0
  33. data/lib/paper_trail_diff/historical_association_reifier.rb +32 -2
  34. data/lib/paper_trail_diff/historical_snapshot_store.rb +6 -9
  35. data/lib/paper_trail_diff/live_endpoint_batch_loader.rb +35 -2
  36. data/lib/paper_trail_diff/live_graph_collector.rb +1 -1
  37. data/lib/paper_trail_diff/paper_trail_adapter.rb +22 -6
  38. data/lib/paper_trail_diff/preloaded_endpoint_batch_loader.rb +1 -1
  39. data/lib/paper_trail_diff/prepared_activity_index.rb +110 -36
  40. data/lib/paper_trail_diff/prepared_edge_loader.rb +29 -45
  41. data/lib/paper_trail_diff/prepared_history.rb +63 -54
  42. data/lib/paper_trail_diff/prepared_history_loader.rb +33 -43
  43. data/lib/paper_trail_diff/prepared_record_index.rb +26 -0
  44. data/lib/paper_trail_diff/prepared_snapshot_builder.rb +1 -1
  45. data/lib/paper_trail_diff/prepared_state_association_reader.rb +119 -29
  46. data/lib/paper_trail_diff/recorded_changes.rb +83 -0
  47. data/lib/paper_trail_diff/recorded_class.rb +53 -0
  48. data/lib/paper_trail_diff/scope_condition.rb +331 -0
  49. data/lib/paper_trail_diff/scope_ranking.rb +196 -0
  50. data/lib/paper_trail_diff/scoped_analysis_builder.rb +4 -3
  51. data/lib/paper_trail_diff/scoped_root_selection.rb +93 -43
  52. data/lib/paper_trail_diff/snapshot_normalizer.rb +10 -3
  53. data/lib/paper_trail_diff/support.rb +9 -4
  54. data/lib/paper_trail_diff/time_activity_timeline_builder.rb +0 -1
  55. data/lib/paper_trail_diff/time_range.rb +9 -12
  56. data/lib/paper_trail_diff/time_version_range.rb +5 -5
  57. data/lib/paper_trail_diff/version.rb +1 -1
  58. data/lib/paper_trail_diff/version_association_candidate_scope.rb +13 -3
  59. data/lib/paper_trail_diff/version_range.rb +8 -15
  60. data/lib/paper_trail_diff/version_scope_filter.rb +11 -3
  61. data/lib/paper_trail_diff.rb +31 -2
  62. data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +10 -2
  63. data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +5 -0
  64. data/sig/generated/paper_trail_diff/activity_collection_record_updater.rbs +5 -0
  65. data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +7 -0
  66. data/sig/generated/paper_trail_diff/activity_range.rbs +6 -2
  67. data/sig/generated/paper_trail_diff/activity_relationship.rbs +3 -0
  68. data/sig/generated/paper_trail_diff/activity_root_snapshot_refresher.rbs +2 -0
  69. data/sig/generated/paper_trail_diff/activity_target_candidate_loader.rbs +79 -0
  70. data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +11 -5
  71. data/sig/generated/paper_trail_diff/activity_version_collector.rbs +10 -4
  72. data/sig/generated/paper_trail_diff/association_activity_edge.rbs +148 -0
  73. data/sig/generated/paper_trail_diff/association_activity_selection.rbs +112 -0
  74. data/sig/generated/paper_trail_diff/association_discovery.rbs +3 -5
  75. data/sig/generated/paper_trail_diff/association_traversal.rbs +84 -9
  76. data/sig/generated/paper_trail_diff/batch_safety.rbs +10 -4
  77. data/sig/generated/paper_trail_diff/batched_root_versions.rbs +2 -8
  78. data/sig/generated/paper_trail_diff/comparison_batch.rbs +22 -4
  79. data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +16 -4
  80. data/sig/generated/paper_trail_diff/diagnostics.rbs +7 -2
  81. data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +6 -0
  82. data/sig/generated/paper_trail_diff/errors.rbs +2 -1
  83. data/sig/generated/paper_trail_diff/habtm_transaction_owners.rbs +40 -0
  84. data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +13 -0
  85. data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +3 -2
  86. data/sig/generated/paper_trail_diff/live_endpoint_batch_loader.rbs +17 -0
  87. data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +5 -0
  88. data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +52 -10
  89. data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +4 -12
  90. data/sig/generated/paper_trail_diff/prepared_history.rbs +33 -22
  91. data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +15 -13
  92. data/sig/generated/paper_trail_diff/prepared_record_index.rbs +15 -0
  93. data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +56 -11
  94. data/sig/generated/paper_trail_diff/recorded_changes.rbs +47 -0
  95. data/sig/generated/paper_trail_diff/recorded_class.rbs +22 -0
  96. data/sig/generated/paper_trail_diff/scope_condition.rbs +185 -0
  97. data/sig/generated/paper_trail_diff/scope_ranking.rbs +117 -0
  98. data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +2 -2
  99. data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +41 -7
  100. data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +6 -0
  101. data/sig/generated/paper_trail_diff/support.rbs +5 -2
  102. data/sig/generated/paper_trail_diff/time_range.rbs +4 -0
  103. data/sig/generated/paper_trail_diff/time_version_range.rbs +4 -4
  104. data/sig/generated/paper_trail_diff/version_association_candidate_scope.rbs +7 -0
  105. data/sig/generated/paper_trail_diff/version_range.rbs +4 -2
  106. data/sig/generated/paper_trail_diff/version_scope_filter.rbs +4 -0
  107. data/sig/generated/paper_trail_diff.rbs +21 -2
  108. metadata +20 -4
@@ -0,0 +1,112 @@
1
+ # Generated from lib/paper_trail_diff/association_activity_selection.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Finds the roots whose requested associations changed in a window, so that
5
+ # selecting by activity includes a record whose own row did not change.
6
+ #
7
+ # A root qualifies when a record on one of its requested paths has a version
8
+ # in the window while it belonged to that root: when the window opened, or
9
+ # after a change inside it. Those are the memberships PT-AT recorded, and the
10
+ # ones an activity analysis reads. For `has_many :through`, the join rows are
11
+ # on the path too, since creating or destroying one changes membership.
12
+ #
13
+ # Each edge is one query, walked from the leaves toward the root, never one
14
+ # per root. Ids stay in a subquery while the columns compared share a type;
15
+ # otherwise they are read and the edge runs over them in slices.
16
+ class AssociationActivitySelection
17
+ SLICE: ::Integer
18
+
19
+ # The ids a one-column subquery selects, with that column's type: a
20
+ # database compares columns of one type only, and PostgreSQL rejects an
21
+ # integer key against a string item_id outright.
22
+ class Query
23
+ attr_reader relation: untyped
24
+
25
+ attr_reader column: untyped
26
+
27
+ attr_reader type: Symbol?
28
+
29
+ # : (untyped, untyped, Symbol?) -> void
30
+ def initialize: (untyped, untyped, Symbol?) -> void
31
+
32
+ # A key PT-AT recorded as empty names no record, so it is left out.
33
+ # : () -> Array[untyped]
34
+ def values: () -> Array[untyped]
35
+
36
+ # The ids with no match in `keys`, a subquery of the keys that exist.
37
+ # : (untyped) -> Array[untyped]
38
+ def missing: (untyped) -> Array[untyped]
39
+ end
40
+
41
+ type source = Query | Array[untyped]
42
+
43
+ # Every id the sources name, read from the database where needed.
44
+ # : (Array[source]) -> Array[untyped]
45
+ def self.values: (Array[source]) -> Array[untyped]
46
+
47
+ # : (untyped, tree: AssociationTree, time_range: TimeRange?) -> void
48
+ def initialize: (untyped, tree: AssociationTree, time_range: TimeRange?) -> void
49
+
50
+ # The roots' ids, each source a subquery or an array of values.
51
+ # : () -> Array[source]
52
+ def call: () -> Array[source]
53
+
54
+ private
55
+
56
+ @model: untyped
57
+
58
+ @time_range: TimeRange?
59
+
60
+ @traversal: AssociationTraversal
61
+
62
+ # Ids of `model` records with a change somewhere below them in `tree`: the
63
+ # targets that changed, or that reach a change further down.
64
+ # : (untyped, AssociationTree, String) -> Array[source]
65
+ def reach: (untyped, AssociationTree, String) -> Array[source]
66
+
67
+ # : (untyped, Array[source]) -> Array[source]
68
+ def owners: (untyped, Array[source]) -> Array[source]
69
+
70
+ # A join row that changes moves membership as surely as a target that does.
71
+ # : (untyped, Array[source]) -> Array[source]
72
+ def through_owners: (untyped, Array[source]) -> Array[source]
73
+
74
+ # Under `source_type:`, only join rows of that type are members, so only
75
+ # targets of that type and changes to those rows count.
76
+ # : (untyped, Array[source], untyped) -> Array[source]
77
+ def typed_joins: (untyped, Array[source], untyped) -> Array[source]
78
+
79
+ # A polymorphic key's targets are known only from the types its rows name,
80
+ # and a path stops at one, so only the targets' own changes count.
81
+ # : (untyped) -> Array[source]
82
+ def polymorphic_owners: (untyped) -> Array[source]
83
+
84
+ # : (untyped) -> Array[AssociationActivityEdge]
85
+ def edges: (untyped) -> Array[AssociationActivityEdge]
86
+
87
+ # Records with a version in the window, or only those of them `item_ids`
88
+ # selects: under `source_type:`, the join rows that had that type.
89
+ # : (untyped, ?untyped) -> Array[source]
90
+ def changed: (untyped, ?untyped) -> Array[source]
91
+
92
+ # One edge for each source. A subquery whose column type matches the
93
+ # filtered column stays a subquery; anything else is read, and the edge
94
+ # runs over its values in slices.
95
+ # : (AssociationActivityEdge, Array[source]) -> Array[source]
96
+ def follow: (AssociationActivityEdge, Array[source]) -> Array[source]
97
+
98
+ # : (AssociationActivityEdge, Array[untyped]) -> Array[untyped]
99
+ def read: (AssociationActivityEdge, Array[untyped]) -> Array[untyped]
100
+
101
+ # Sources are only ever a Query or a plain Array, so the one mutation
102
+ # left, `instance_of?` for `is_a?`, cannot change an answer.
103
+ # mutant:disable
104
+ # : (source, Symbol?) -> Query?
105
+ def subquery: (source, Symbol?) -> Query?
106
+
107
+ # A type naming no class has no versions to read; `changed` skips one that
108
+ # names an unversioned class.
109
+ # : (String) -> untyped
110
+ def target_class: (String) -> untyped
111
+ end
112
+ end
@@ -45,11 +45,9 @@ module PaperTrailDiff
45
45
  # : (untyped, String, untyped, bool) -> AssociationDescriptor
46
46
  def descriptor_for: (untyped, String, untyped, bool) -> AssociationDescriptor
47
47
 
48
- # : (untyped) -> Array[untyped]
49
- def supported_reflections: (untyped) -> Array[untyped]
50
-
51
- # : (untyped, untyped) -> bool
52
- def paper_trail_versions?: (untyped, untyped) -> bool
48
+ # Only what a request accepts, so every listed path can be requested.
49
+ # : (untyped, root: bool) -> Array[untyped]
50
+ def supported_reflections: (untyped, root: bool) -> Array[untyped]
53
51
 
54
52
  # : (untyped) -> untyped
55
53
  def reflection_target: (untyped) -> untyped
@@ -4,10 +4,80 @@ module PaperTrailDiff
4
4
  # Resolves and validates ActiveRecord reflections for a bounded association tree.
5
5
  class AssociationTraversal
6
6
  # The class a `belongs_to` points at for this record, reading a polymorphic
7
- # type column when there is one. Nil when that type names no class.
7
+ # type column when there is one. Nil when the record points at nothing.
8
8
  # : (untyped, untyped) -> untyped
9
9
  def self.belongs_to_target: (untyped, untyped) -> untyped
10
10
 
11
+ # A through with `source_type:` reaches only the join records whose type
12
+ # column names that class, as Active Record's `where(type => source_type)`.
13
+ # A through's `foreign_type` is its polymorphic source's.
14
+ # : (untyped, untyped) -> bool
15
+ def self.source_type_join?: (untyped, untyped) -> bool
16
+
17
+ # The same check for a join row read as stored attributes, given the
18
+ # value of its type column.
19
+ # : (untyped, untyped) -> bool
20
+ def self.source_type_member?: (untyped, untyped) -> bool
21
+
22
+ # The class a stored type names, or nil when it names none.
23
+ # This class defines no constants of its own, so the one mutation left,
24
+ # `const_get` on it rather than on Object, finds the same class.
25
+ # mutant:disable
26
+ # : (String) -> untyped
27
+ def self.named_class: (String) -> untyped
28
+
29
+ # A type left by a renamed or removed model names no class. Active Record
30
+ # and PT-AT raise a bare NameError for it, which does not say where it is.
31
+ # : (untyped, untyped, String) -> untyped
32
+ def self.polymorphic_target: (untyped, untyped, String) -> untyped
33
+
34
+ # Why a request cannot name this association of the model, or nil when it
35
+ # can; `root` says whether the model is the record the request compares.
36
+ # Discovery lists only the associations this accepts, so every path it
37
+ # lists can be requested. A has_many :through Active Record cannot follow
38
+ # raises Active Record's own error.
39
+ # : (untyped, untyped, root: bool) -> String?
40
+ def self.unsupported_reason: (untyped, untyped, root: bool) -> String?
41
+
42
+ # PT-AT records a HABTM's membership only when its owner saves a version,
43
+ # in rows keyed by transaction and association name, not by owner. A root
44
+ # checkpoint so holds the root's membership and no one else's: another
45
+ # record's HABTM would read the root's rows, or none.
46
+ # : (untyped) -> String
47
+ def self.habtm_below_root: (untyped) -> String
48
+
49
+ # PT-AT records a HABTM's membership by loading the association, scope and
50
+ # target default scope applied, so a target outside the scope when the
51
+ # owner saved is never recorded. One that enters it before the next save
52
+ # is a member then that history cannot hold.
53
+ # : (untyped, bool) -> String?
54
+ def self.habtm_reason: (untyped, bool) -> String?
55
+
56
+ # : (untyped, untyped) -> bool
57
+ def self.paper_trail_versions?: (untyped, untyped) -> bool
58
+
59
+ # Active Record joins a HABTM through a hidden model of its own, which has
60
+ # no versions and no reader that PT-AT or prepared history can follow, so a
61
+ # through that crosses one would fail partway through reconstruction.
62
+ #
63
+ # The through and source reflections of a through are the hidden ones, a
64
+ # has_many in place of each HABTM, so only the public reflection of the
65
+ # same name shows the HABTM. Active Record's own check, which it runs on
66
+ # first use anyway, explains a through it cannot follow before the chain
67
+ # below it is read.
68
+ # : (untyped) -> untyped
69
+ def self.habtm_in_chain: (untyped) -> untyped
70
+
71
+ attr_reader tree: AssociationTree
72
+
73
+ # Whether a row belongs to an association whose class is `model_class`.
74
+ # Children are found by key across the whole table, so an association to an
75
+ # STI subclass has to leave out its siblings, as Active Record's type
76
+ # condition does for the live association: the row's type must name the
77
+ # class or one of its descendants. A base class takes every row.
78
+ # : (untyped, Hash[String, untyped]) -> bool
79
+ def self.sti_member?: (untyped, Hash[String, untyped]) -> bool
80
+
11
81
  # : (AssociationTree) -> void
12
82
  def initialize: (AssociationTree) -> void
13
83
 
@@ -29,9 +99,15 @@ module PaperTrailDiff
29
99
  # : (untyped) -> void
30
100
  def reject_live_habtm_activity!: (untyped) -> void
31
101
 
102
+ # A model and a tree resolve to the same reflections wherever they are
103
+ # read: a tree is only ever walked from one place, the root tree at the
104
+ # root and a subtree below it. Beyond that, the path only names a failure,
105
+ # and a failure is not cached.
32
106
  # : (untyped, AssociationTree, path: String) -> Array[untyped]
33
107
  def reflections_for: (untyped, AssociationTree, path: String) -> Array[untyped]
34
108
 
109
+ # Active Record names keys as strings. A polymorphic child also points back
110
+ # with its type column; `type` is nil for any other.
35
111
  # : (untyped) -> Array[String]
36
112
  def incoming_relationship_columns: (untyped) -> Array[String]
37
113
 
@@ -47,19 +123,18 @@ module PaperTrailDiff
47
123
  # : (untyped, AssociationTree, path: String) -> void
48
124
  def validate_tree!: (untyped, AssociationTree, path: String) -> void
49
125
 
126
+ # A polymorphic target's class differs from record to record, so the
127
+ # associations a path names below it may not exist on every target.
128
+ # : (untyped, AssociationTree, String) -> void
129
+ def reject_polymorphic_descent!: (untyped, AssociationTree, String) -> void
130
+
50
131
  # : (untyped, AssociationTree, String) -> Array[untyped]
51
132
  def requested_reflections: (untyped, AssociationTree, String) -> Array[untyped]
52
133
 
53
- # : (untyped, String, String) -> untyped
54
- def resolve_reflection: (untyped, String, String) -> untyped
55
-
56
- # : (untyped, untyped) -> bool
57
- def paper_trail_versions?: (untyped, untyped) -> bool
134
+ # : (untyped, String, String, root: bool) -> untyped
135
+ def resolve_reflection: (untyped, String, String, root: bool) -> untyped
58
136
 
59
137
  # : (untyped) -> bool
60
138
  def habtm_reflection?: (untyped) -> bool
61
-
62
- # : (String, String) -> String
63
- def join_path: (String, String) -> String
64
139
  end
65
140
  end
@@ -6,8 +6,8 @@ module PaperTrailDiff
6
6
  # : (AssociationTraversal, allow_expensive: bool) -> void
7
7
  def initialize: (AssociationTraversal, allow_expensive: bool) -> void
8
8
 
9
- # : (untyped, ?historical: bool) -> void
10
- def call: (untyped, ?historical: bool) -> void
9
+ # : (untyped) -> void
10
+ def call: (untyped) -> void
11
11
 
12
12
  private
13
13
 
@@ -15,9 +15,15 @@ module PaperTrailDiff
15
15
 
16
16
  @traversal: AssociationTraversal
17
17
 
18
- # : (untyped, String, bool) -> void
19
- def check: (untyped, String, bool) -> void
18
+ # A has_many through reads its parts' conditions only, at any depth. A
19
+ # has_one through over a ranked part is refused whole, so its parts, too,
20
+ # are checked for conditions alone.
21
+ # : (untyped, String, ?ranked: bool) -> void
22
+ def check: (untyped, String, ?ranked: bool) -> void
20
23
 
24
+ # A scope prepared history can read as conditions on stored attributes
25
+ # is batched like any other path. A polymorphic target's class, and so
26
+ # its scope, is known only per record.
21
27
  # : (untyped, bool) -> String?
22
28
  def reason_for: (untyped, bool) -> String?
23
29
  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) -> void
9
+ def initialize: (Array[untyped], time_range: TimeRange?, version_scope: untyped, live_endpoints: Hash[Array[String], untyped]?, context_required: bool) -> void
10
10
 
11
11
  # Returns a plan per record identity.
12
12
  # : () -> Hash[Array[String], RootVersionPlan]
@@ -54,11 +54,5 @@ module PaperTrailDiff
54
54
 
55
55
  # : (untyped, Array[untyped]) -> untyped
56
56
  def base_scope: (untyped, Array[untyped]) -> untyped
57
-
58
- # : (untyped) -> Array[untyped]
59
- def ordered: (untyped) -> Array[untyped]
60
-
61
- # : (untyped, untyped) -> Array[String]
62
- def identity: (untyped, untyped) -> Array[String]
63
57
  end
64
58
  end
@@ -3,8 +3,11 @@
3
3
  module PaperTrailDiff
4
4
  # Validates and executes independent comparisons with shared live endpoint loading.
5
5
  class ComparisonBatch
6
- # : (Array[comparison_input], live_loader: untyped, preparer: untyped, history_preparer: untyped, historical_snapshotter: untyped, live_normalizer: untyped) -> void
7
- def initialize: (Array[comparison_input], live_loader: untyped, preparer: untyped, history_preparer: untyped, historical_snapshotter: untyped, live_normalizer: untyped) -> void
6
+ # `history_preparer` prepares roots that have a live endpoint to collect a
7
+ # graph from; `version_history_preparer` prepares roots compared only
8
+ # between versions.
9
+ # : (Array[comparison_input], live_loader: untyped, preparer: untyped, history_preparer: untyped, version_history_preparer: untyped, historical_snapshotter: untyped, live_normalizer: untyped) -> void
10
+ def initialize: (Array[comparison_input], live_loader: untyped, preparer: untyped, history_preparer: untyped, version_history_preparer: untyped, historical_snapshotter: untyped, live_normalizer: untyped) -> void
8
11
 
9
12
  # : () -> comparison_results
10
13
  def call: () -> comparison_results
@@ -23,6 +26,8 @@ module PaperTrailDiff
23
26
 
24
27
  @preparer: untyped
25
28
 
29
+ @version_history_preparer: untyped
30
+
26
31
  # : (Array[[untyped, untyped]]) -> Hash[identity, RecordSnapshot]
27
32
  def prepared_live_snapshots: (Array[[ untyped, untyped ]]) -> Hash[identity, RecordSnapshot]
28
33
 
@@ -53,8 +58,21 @@ module PaperTrailDiff
53
58
  # : (Array[[untyped, untyped]], Hash[identity, untyped]) -> void
54
59
  def prepare_historical_batches!: (Array[[ untyped, untyped ]], Hash[identity, untyped]) -> void
55
60
 
56
- # : (Array[[untyped, untyped]], Hash[identity, untyped]) -> Array[untyped]
57
- def historical_versions: (Array[[ untyped, untyped ]], Hash[identity, untyped]) -> Array[untyped]
61
+ # A root compared only between versions has no live endpoint, and its row
62
+ # may be gone. Without prepared history, PT-AT finds a has_many's children
63
+ # by the keys each version recorded after its change, so it misses a child
64
+ # that later moved to another owner. Such roots are prepared from identity
65
+ # alone, like deleted ones. Their history still reads the live rows it
66
+ # needs: a record's current row is its state after its last version.
67
+ # : (Array[untyped]) -> void
68
+ def prepare_version_roots!: (Array[untyped]) -> void
69
+
70
+ # The record supplies identity and model metadata only.
71
+ # : (untyped, untyped) -> untyped
72
+ def historical_root: (untyped, untyped) -> untyped
73
+
74
+ # : (Array[[untyped, untyped]]) -> Array[untyped]
75
+ def historical_versions: (Array[[ untyped, untyped ]]) -> Array[untyped]
58
76
 
59
77
  # : () -> Array[[untyped, untyped]]
60
78
  def comparison_pairs: () -> Array[[ untyped, untyped ]]
@@ -3,8 +3,10 @@
3
3
  module PaperTrailDiff
4
4
  # Analyzes missing roots together without pretending that they have live rows.
5
5
  class DeletedAnalysisBatch
6
- # : (untyped, identities: Array[Array[String]], time_range: TimeRange?, analyzer: untyped, history_preparer: untyped, ?version_scope: untyped) -> void
7
- def initialize: (untyped, identities: Array[Array[String]], time_range: TimeRange?, analyzer: untyped, history_preparer: untyped, ?version_scope: untyped) -> void
6
+ # `context_required` reads a root that did not change in the window from
7
+ # the version after it, so its associations' activity can be reported.
8
+ # : (untyped, identities: Array[Array[String]], time_range: TimeRange?, analyzer: untyped, history_preparer: untyped, version_scope: untyped, context_required: bool) -> void
9
+ def initialize: (untyped, identities: Array[Array[String]], time_range: TimeRange?, analyzer: untyped, history_preparer: untyped, version_scope: untyped, context_required: bool) -> void
8
10
 
9
11
  # : () -> Hash[Array[String], Analysis?]
10
12
  def call: () -> Hash[Array[String], Analysis?]
@@ -16,6 +18,8 @@ module PaperTrailDiff
16
18
 
17
19
  @analyzer: untyped
18
20
 
21
+ @context_required: bool
22
+
19
23
  @history_preparer: untyped
20
24
 
21
25
  @identities: Array[Array[String]]
@@ -26,11 +30,19 @@ module PaperTrailDiff
26
30
 
27
31
  @version_scope: untyped
28
32
 
33
+ # A root destroyed before the window had no state inside it to read, so
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]
37
+
38
+ # : (Array[untyped], bool) -> Hash[Array[String], RootVersionPlan]
39
+ def plans_for: (Array[untyped], bool) -> Hash[Array[String], RootVersionPlan]
40
+
29
41
  # These records supply identity and model metadata only. Every reported
30
42
  # value comes from the batched historical plan; no per-root reify or live
31
43
  # lookup is needed to get into the comparison engine.
32
- # : () -> Array[untyped]
33
- def historical_roots: () -> Array[untyped]
44
+ # : (Array[untyped]) -> Array[untyped]
45
+ def historical_roots: (Array[untyped]) -> Array[untyped]
34
46
 
35
47
  # : () -> Array[untyped]
36
48
  def destroy_versions: () -> Array[untyped]
@@ -64,8 +64,13 @@ module PaperTrailDiff
64
64
  # : (Array[Array[untyped]]) -> void
65
65
  def inspect_targets: (Array[Array[untyped]]) -> void
66
66
 
67
- # : (Array[Array[untyped]]) -> void
68
- def inspect_habtm: (Array[Array[untyped]]) -> void
67
+ # : (untyped, Array[Array[untyped]]) -> void
68
+ def inspect_habtm: (untyped, Array[Array[untyped]]) -> void
69
+
70
+ # PT-AT does not record which owner a HABTM member belongs to, so a
71
+ # transaction that more than one owner recorded members in cannot be read.
72
+ # : (untyped, Array[Array[untyped]]) -> void
73
+ def inspect_shared_transactions: (untyped, Array[Array[untyped]]) -> void
69
74
 
70
75
  # : () -> void
71
76
  def inspect_version_sequence: () -> void
@@ -27,6 +27,12 @@ module PaperTrailDiff
27
27
  # : (untyped) -> bool
28
28
  def plain_model?: (untyped) -> bool
29
29
 
30
+ # A default scope's equality conditions are assigned to every new record,
31
+ # through the model's writers, before a reified version's attributes are.
32
+ # A subclass's own type is assigned with or without one.
33
+ # : (untyped) -> bool
34
+ def scope_assigns_attributes?: (untyped) -> bool
35
+
30
36
  # : (untyped) -> bool
31
37
  def plain_attributes?: (untyped) -> bool
32
38
 
@@ -75,7 +75,8 @@ module PaperTrailDiff
75
75
  class AssociationTrackingUnavailableError < Error
76
76
  end
77
77
 
78
- # Raised when recorded PT-AT metadata cannot reliably reconstruct a selected association.
78
+ # Raised when recorded PT-AT metadata cannot reliably reconstruct a selected association,
79
+ # or a polymorphic type names no class.
79
80
  class IncompleteAssociationHistoryError < Error
80
81
  end
81
82
 
@@ -0,0 +1,40 @@
1
+ # Generated from lib/paper_trail_diff/habtm_transaction_owners.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # PT-AT records HABTM members under a transaction id and the association's
5
+ # name, with nothing naming the record they belong to. When more than one
6
+ # record that can own that name saved a version in the transaction, their
7
+ # members are mixed together and none of them can be read back.
8
+ class HabtmTransactionOwners
9
+ # : (untyped, untyped) -> void
10
+ def initialize: (untyped, untyped) -> void
11
+
12
+ # The given transactions (one id or several) in which more than one
13
+ # possible owner saved a version, as strings.
14
+ # : (untyped) -> Array[String]
15
+ def shared: (untyped) -> Array[String]
16
+
17
+ # Members recorded in a shared transaction cannot be told apart. With no
18
+ # members recorded, every owner had none, so that reads correctly.
19
+ # : (untyped) -> bool
20
+ def ambiguous?: (untyped) -> bool
21
+
22
+ # : (untyped) -> void
23
+ def ensure_single_owner!: (untyped) -> void
24
+
25
+ private
26
+
27
+ @associations: untyped
28
+
29
+ @name: String
30
+
31
+ @owner_types: Hash[String, bool]
32
+
33
+ @versions: untyped
34
+
35
+ # Versions name the base class, so a HABTM declared on a subclass counts.
36
+ # A type that names no model may have recorded members when it existed.
37
+ # : (String) -> bool
38
+ def owner_type?: (String) -> bool
39
+ end
40
+ end
@@ -22,6 +22,14 @@ module PaperTrailDiff
22
22
  # : (untyped, untyped) -> void
23
23
  def reify_association: (untyped, untyped) -> void
24
24
 
25
+ # : (untyped, untyped) -> void
26
+ def reify_belongs_to: (untyped, untyped) -> void
27
+
28
+ # A polymorphic target's class is known only from the record's type
29
+ # column, so TraversalPreparer cannot check it is versioned up front.
30
+ # : (untyped, untyped) -> void
31
+ def ensure_versioned_target!: (untyped, untyped) -> void
32
+
25
33
  # : (untyped, untyped) -> void
26
34
  def reify_has_many: (untyped, untyped) -> void
27
35
 
@@ -30,6 +38,11 @@ module PaperTrailDiff
30
38
  # : (untyped, untyped) -> void
31
39
  def reify_through: (untyped, untyped) -> void
32
40
 
41
+ # PT-AT's belongs_to source ignores `source_type:` and reads targets by id
42
+ # alone, so a typed through walks its join records instead.
43
+ # : (untyped) -> bool
44
+ def pt_at_source?: (untyped) -> bool
45
+
33
46
  # : (untyped, untyped, untyped) -> Array[untyped]
34
47
  def through_members: (untyped, untyped, untyped) -> Array[untyped]
35
48
 
@@ -12,8 +12,9 @@ module PaperTrailDiff
12
12
  # : (Array[untyped], Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
13
13
  def prepare_batch: (Array[untyped], Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
14
14
 
15
- # Missing roots have no live graph to collect. Reading their associations
16
- # here would issue a query per root and seed nonexistent rows as live state.
15
+ # Missing roots, and roots compared only between versions, have no live
16
+ # graph to collect. Reading their associations here would issue a query
17
+ # per root and seed nonexistent rows as live state.
17
18
  # : (Array[untyped], Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
18
19
  def prepare_deleted_batch: (Array[untyped], Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
19
20
 
@@ -25,6 +25,11 @@ module PaperTrailDiff
25
25
  # : (untyped, AssociationTree) -> Array[[untyped, AssociationTree]]
26
26
  def selected_reflections: (untyped, AssociationTree) -> Array[[ untyped, AssociationTree ]]
27
27
 
28
+ # Active Record raises a bare NameError for a type that names no class, so
29
+ # each type is resolved first to raise one that names the record.
30
+ # : (Array[untyped], Array[[untyped, AssociationTree]]) -> void
31
+ def resolve_polymorphic_targets: (Array[untyped], Array[[ untyped, AssociationTree ]]) -> void
32
+
28
33
  # : (Array[untyped], Array[[untyped, AssociationTree]], individually: bool) -> void
29
34
  def preload_subtrees: (Array[untyped], Array[[ untyped, AssociationTree ]], individually: bool) -> void
30
35
 
@@ -34,9 +39,21 @@ module PaperTrailDiff
34
39
  # : () -> untyped
35
40
  def active_record_preloader: () -> untyped
36
41
 
42
+ # The preloader applies a limit or offset to all owners' records at once,
43
+ # not to each owner's, so such an association is loaded per record. A
44
+ # has_one is no exception. Through a part with a limit, an offset or a
45
+ # has_one cut, the preloader keeps what the part alone would load, while
46
+ # the through's own query keeps the part's conditions only.
37
47
  # : (untyped, untyped) -> bool
38
48
  def batch_safe?: (untyped, untyped) -> bool
39
49
 
50
+ # : (untyped) -> bool
51
+ def cut_part?: (untyped) -> bool
52
+
53
+ # Every part a through passes, at any depth.
54
+ # : (untyped) -> Array[untyped]
55
+ def through_parts: (untyped) -> Array[untyped]
56
+
40
57
  # : (Array[untyped], untyped) -> Array[untyped]
41
58
  def load_individually: (Array[untyped], untyped) -> Array[untyped]
42
59
 
@@ -41,6 +41,11 @@ module PaperTrailDiff
41
41
 
42
42
  private
43
43
 
44
+ # Selecting by activity reads association history, so it must be available
45
+ # before the first selection query rather than once analysis starts.
46
+ # : (untyped, bool) -> void
47
+ def check_scope: (untyped, bool) -> void
48
+
44
49
  # : (Array[comparison_input], preparer: untyped) -> ComparisonBatch
45
50
  def comparison_batch: (Array[comparison_input], preparer: untyped) -> ComparisonBatch
46
51