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
@@ -4,8 +4,8 @@
4
4
  module PaperTrailDiff
5
5
  # Selects an inclusive, chronological range from a record's root versions.
6
6
  class VersionRange
7
- #: (untyped, from: untyped, to: untyped, ?version_scope: untyped) -> void
8
- def initialize(record, from:, to:, version_scope: nil)
7
+ #: (untyped, from: untyped, to: untyped, version_scope: untyped) -> void
8
+ def initialize(record, from:, to:, version_scope:)
9
9
  @record = record
10
10
  @from = from
11
11
  @to = to
@@ -51,11 +51,9 @@ module PaperTrailDiff
51
51
 
52
52
  #: (untyped, ?through: untyped, ?live_endpoint: untyped) -> RootVersionPlan
53
53
  def select_plan(relation, through: nil, live_endpoint: nil)
54
- in_range = ordered(relation.to_a.select do |version|
55
- next false if Support.compare_versions(@from, version).positive?
56
- next true unless through
57
-
58
- Support.compare_versions(version, through) <= 0
54
+ in_range = ordered(relation.select do |version|
55
+ Support.compare_versions(@from, version) <= 0 &&
56
+ (!through || Support.compare_versions(version, through) <= 0)
59
57
  end)
60
58
  RootVersionSelection.new(
61
59
  in_range: in_range,
@@ -73,21 +71,16 @@ module PaperTrailDiff
73
71
 
74
72
  #: (untyped, untyped, boundary: Symbol) -> void
75
73
  def validate_boundary!(requested, relation, boundary:)
76
- Endpoint.validate!(requested)
77
74
  return if valid_boundary?(requested, relation)
78
75
 
79
- raise InvalidTimelineRangeError, "`#{boundary}` version is not in the record history"
80
- rescue InvalidEndpointError, NoMethodError
81
76
  raise InvalidTimelineRangeError, "`#{boundary}` version is not in the record history"
82
77
  end
83
78
 
79
+ # The relation holds only this record's versions, so finding the id there
80
+ # also settles which record the version belongs to.
84
81
  #: (untyped, untyped) -> bool
85
82
  def valid_boundary?(requested, relation)
86
- expected_identity = [@record.class.base_class.name.to_s, @record.id.to_s]
87
- Endpoint.version?(requested) &&
88
- requested.instance_of?(relation.klass) &&
89
- Endpoint.identity(requested) == expected_identity &&
90
- relation.where(id: requested.id).exists?
83
+ requested.instance_of?(relation.klass) && relation.exists?(requested.id)
91
84
  end
92
85
  end
93
86
  end
@@ -14,13 +14,21 @@ module PaperTrailDiff
14
14
 
15
15
  #: (untyped, Array[untyped]) -> Array[untyped]
16
16
  def call(relation, in_range)
17
+ chosen = chosen_ids(relation)
18
+ return in_range unless chosen
19
+
20
+ in_range.select { |version| chosen.include?(version.id) }
21
+ end
22
+
23
+ # The ids of the versions the scope keeps, or nil when there is no scope.
24
+ #: (untyped) -> Set[untyped]?
25
+ def chosen_ids(relation)
17
26
  scope = @scope
18
- return in_range unless scope
27
+ return unless scope
19
28
 
20
29
  # A narrowed relation is the expected return. Active Support also gives
21
30
  # `pluck` to plain enumerables, so an array of versions works too.
22
- chosen = Set.new(scope.call(relation).pluck(:id))
23
- in_range.select { |version| chosen.include?(version.id) }
31
+ Set.new(scope.call(relation).pluck(:id))
24
32
  end
25
33
 
26
34
  private
@@ -12,7 +12,10 @@ require_relative 'paper_trail_diff/instrumentation'
12
12
  require_relative 'paper_trail_diff/errors'
13
13
  require_relative 'paper_trail_diff/configuration'
14
14
  require_relative 'paper_trail_diff/endpoint'
15
+ require_relative 'paper_trail_diff/recorded_class'
15
16
  require_relative 'paper_trail_diff/association_traversal'
17
+ require_relative 'paper_trail_diff/scope_ranking'
18
+ require_relative 'paper_trail_diff/scope_condition'
16
19
  require_relative 'paper_trail_diff/batch_safety'
17
20
  require_relative 'paper_trail_diff/traversal_preparer'
18
21
  require_relative 'paper_trail_diff/association_discovery'
@@ -30,6 +33,7 @@ require_relative 'paper_trail_diff/traversal'
30
33
  require_relative 'paper_trail_diff/collection_comparator'
31
34
  require_relative 'paper_trail_diff/nested_comparator'
32
35
  require_relative 'paper_trail_diff/engine'
36
+ require_relative 'paper_trail_diff/habtm_transaction_owners'
33
37
  require_relative 'paper_trail_diff/historical_association_reifier'
34
38
  require_relative 'paper_trail_diff/prepared_record_index'
35
39
  require_relative 'paper_trail_diff/direct_record_state_loader'
@@ -56,6 +60,7 @@ require_relative 'paper_trail_diff/snapshot_normalizer'
56
60
  require_relative 'paper_trail_diff/historical_snapshot_store'
57
61
  require_relative 'paper_trail_diff/timeline_snapshot_provider'
58
62
  require_relative 'paper_trail_diff/activity_event_record_resolver'
63
+ require_relative 'paper_trail_diff/recorded_changes'
59
64
  require_relative 'paper_trail_diff/activity_event_route_finder'
60
65
  require_relative 'paper_trail_diff/activity_relationship'
61
66
  require_relative 'paper_trail_diff/activity_collection_route_change'
@@ -77,6 +82,8 @@ require_relative 'paper_trail_diff/analysis_batch'
77
82
  require_relative 'paper_trail_diff/deleted_analysis_batch'
78
83
  require_relative 'paper_trail_diff/scoped_analysis'
79
84
  require_relative 'paper_trail_diff/historical_filter'
85
+ require_relative 'paper_trail_diff/association_activity_edge'
86
+ require_relative 'paper_trail_diff/association_activity_selection'
80
87
  require_relative 'paper_trail_diff/scoped_root_selection'
81
88
  require_relative 'paper_trail_diff/scoped_analysis_builder'
82
89
  require_relative 'paper_trail_diff/batched_root_analyzer'
@@ -89,6 +96,7 @@ require_relative 'paper_trail_diff/timeline_builder'
89
96
  require_relative 'paper_trail_diff/activity_range'
90
97
  require_relative 'paper_trail_diff/activity_event'
91
98
  require_relative 'paper_trail_diff/activity_child_candidate_loader'
99
+ require_relative 'paper_trail_diff/activity_target_candidate_loader'
92
100
  require_relative 'paper_trail_diff/activity_version_collector'
93
101
  require_relative 'paper_trail_diff/activity_snapshot_sequence'
94
102
  require_relative 'paper_trail_diff/activity_history'
@@ -278,7 +286,9 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
278
286
  end
279
287
 
280
288
  # Analyzes every root the relation reaches whose history moved inside the
281
- # window. Joined duplicates are read in bounded pages and analyzed once.
289
+ # window. With `activity: true`, a change to a record on one of the
290
+ # requested association paths also counts, because the activity timeline
291
+ # reports it. Joined duplicates are read in bounded pages and analyzed once.
282
292
  #
283
293
  # `limit:` is required and exceeding it raises. Selection moves into the gem
284
294
  # here. The relation's own limit/offset select the live page; this separate
@@ -328,7 +338,8 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
328
338
  )
329
339
  end
330
340
 
331
- # Recent root and selected-child activity through the current saved state.
341
+ # Recent root and selected-child activity through the current saved state,
342
+ # for every root that changed or whose requested associations did.
332
343
  # Historical windows remain explicit on analyze_scope; this entry point is
333
344
  # deliberately named for a report through now rather than an as-of query.
334
345
  #: (untyped, since: untyped, limit: Integer, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?snapshots: bool, ?group: Symbol?, ?include_deleted: bool, ?historical_filter: untyped, ?max_filter_versions: Integer, ?allow_expensive: bool) -> ScopedAnalysis
@@ -390,5 +401,23 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
390
401
 
391
402
  NestedComparator.call(change, to_value)
392
403
  end
404
+
405
+ # Reads what PaperTrail recorded with one version: its `object_changes`,
406
+ # typed by the version's model, as `{ "title" => <from "Draft" to "Published"> }`.
407
+ #
408
+ # versions = PaperTrail::Version.where(id: steps.map { _1.source_boundary.version_id })
409
+ # versions.to_h { [_1.id, PaperTrailDiff.recorded_changes(_1)] }
410
+ #
411
+ # A step's diff compares the states before and after an event, so it also
412
+ # includes anything saved without a version in between; this does not.
413
+ # Returns nil when nothing was recorded (no `object_changes` column, or no
414
+ # value for this version), which differs from a recorded empty hash. Unlike
415
+ # PaperTrail's `version.changeset`, it never loads the record, so it costs no
416
+ # query per version and still types a deleted record's changes. `ignore:`
417
+ # takes attribute names, as an array, and defaults to `updated_at`.
418
+ #: (untyped, ?ignore: Array[String | Symbol]) -> Hash[String, ValueChange]?
419
+ def recorded_changes(version, ignore: DEFAULT_IGNORED_ATTRIBUTES)
420
+ RecordedChanges.call(version, ignore: ignore)
421
+ end
393
422
  end
394
423
  end
@@ -17,6 +17,11 @@ module PaperTrailDiff
17
17
 
18
18
  @route_finder: ActivityEventRouteFinder
19
19
 
20
+ # An edit can move a target into or out of a scope, which a replacement
21
+ # in place cannot show, so a scoped route's branch is read again instead.
22
+ # : (Array[Array[untyped]]) -> bool
23
+ def scoped_route?: (Array[Array[untyped]]) -> bool
24
+
20
25
  # : (Array[untyped], untyped, SnapshotNormalizer, untyped, untyped) -> RecordSnapshot?
21
26
  def normalized_route_record: (Array[untyped], untyped, SnapshotNormalizer, untyped, untyped) -> RecordSnapshot?
22
27
 
@@ -29,8 +34,11 @@ module PaperTrailDiff
29
34
  # : (Array[RecordSnapshot], untyped, RecordSnapshot?) -> [Array[RecordSnapshot], bool]
30
35
  def replace_existing_target: (Array[RecordSnapshot], untyped, RecordSnapshot?) -> [ Array[RecordSnapshot], bool ]
31
36
 
32
- # : (untyped, RecordSnapshot?) -> String
33
- def replacement_type: (untyped, RecordSnapshot?) -> String
37
+ # The target is matched by id within its table rather than by type name.
38
+ # Without `item_subtype`, a version names only the STI base class, and a
39
+ # row whose type changes is replaced by a record of another class.
40
+ # : (RecordSnapshot, untyped) -> bool
41
+ def same_table?: (RecordSnapshot, untyped) -> bool
34
42
 
35
43
  # : (untyped) -> untyped
36
44
  def endpoint_model_class: (untyped) -> untyped
@@ -25,6 +25,11 @@ module PaperTrailDiff
25
25
  # : (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped], ?direct: bool) -> RecordSnapshot
26
26
  def apply_route: (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped], ?direct: bool) -> RecordSnapshot
27
27
 
28
+ # Any edit can move a record into or out of a scope, so a scoped route's
29
+ # branch is read again from history instead.
30
+ # : (Array[Array[untyped]]) -> bool
31
+ def scoped_route?: (Array[Array[untyped]]) -> bool
32
+
28
33
  # : (Array[Array[untyped]], untyped) -> bool
29
34
  def unsafe_through_membership_event?: (Array[Array[untyped]], untyped) -> bool
30
35
 
@@ -19,6 +19,11 @@ module PaperTrailDiff
19
19
 
20
20
  @relationship: ActivityRelationship
21
21
 
22
+ # A row whose STI type changed is the same row under a new identity, so it
23
+ # is found by id among the records of its table.
24
+ # : (AssociationSnapshot, untyped) -> Integer?
25
+ def retyped_position: (AssociationSnapshot, untyped) -> Integer?
26
+
22
27
  # : (Array[RecordSnapshot], Integer?, RecordSnapshot?) -> Array[RecordSnapshot]
23
28
  def replacement_records: (Array[RecordSnapshot], Integer?, RecordSnapshot?) -> Array[RecordSnapshot]
24
29
  end
@@ -45,6 +45,13 @@ module PaperTrailDiff
45
45
  # : (untyped) -> PreparedRecordState?
46
46
  def direct_record_after: (untyped) -> PreparedRecordState?
47
47
 
48
+ # Without `item_subtype` a version names only the STI base class, and a
49
+ # change can move the row to another class, so the type the row holds
50
+ # after the change decides. A type that names no class has no direct
51
+ # state; the caller falls back to reifying the record.
52
+ # : (untyped, Hash[untyped, untyped]) -> untyped
53
+ def class_after: (untyped, Hash[untyped, untyped]) -> untyped
54
+
48
55
  # : (untyped) -> untyped
49
56
  def prepared_changeset: (untyped) -> untyped
50
57
 
@@ -21,14 +21,18 @@ module PaperTrailDiff
21
21
  # : () -> bool
22
22
  def current_end?: () -> bool
23
23
 
24
+ # Whether the state a version recorded after its change held inside the
25
+ # range. A range ending at a version ends at the state before it, so the
26
+ # change that version records is past the end, unlike #include?.
27
+ # : (untyped, untyped) -> bool
28
+ def recorded_before_end?: (untyped, untyped) -> bool
29
+
24
30
  private
25
31
 
26
32
  @end_boundary: untyped
27
33
 
28
34
  @end_key: Array[untyped]?
29
35
 
30
- @start_boundary: untyped
31
-
32
36
  @start_key: Array[untyped]?
33
37
 
34
38
  @start_time: untyped
@@ -11,6 +11,9 @@ module PaperTrailDiff
11
11
 
12
12
  private
13
13
 
14
+ # : (untyped, untyped, Symbol) -> bool
15
+ def sti_member?: (untyped, untyped, Symbol) -> bool
16
+
14
17
  # : (untyped, untyped, state: Symbol) -> Array[String]
15
18
  def relationship_owner_values: (untyped, untyped, state: Symbol) -> Array[String]
16
19
 
@@ -36,6 +36,8 @@ module PaperTrailDiff
36
36
  # : (untyped) -> Array[String]
37
37
  def unsupported_branches: (untyped) -> Array[String]
38
38
 
39
+ # A polymorphic target's class is known only per record, so it cannot be
40
+ # checked here; a target that is not versioned raises when it is read.
39
41
  # : (untyped) -> bool
40
42
  def direct_versioned_reflection?: (untyped) -> bool
41
43
 
@@ -0,0 +1,79 @@
1
+ # Generated from lib/paper_trail_diff/activity_target_candidate_loader.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Loads the `belongs_to` targets owners pointed at during one activity range:
5
+ # at its start and after each change inside it.
6
+ #
7
+ # PT-AT records a version's foreign keys as they stood after its change, so
8
+ # the target at the start comes from the latest version before it, and the
9
+ # version a range ends at names a target only after the range. Reading only
10
+ # the owners' versions inside the range misses a target whose owner did not
11
+ # change there -- an author renamed under an article nobody edited.
12
+ class ActivityTargetCandidateLoader
13
+ # A plain class rather than Data.define, which arrived in Ruby 3.2 while this
14
+ # gem supports 3.1.
15
+ class Row
16
+ attr_reader owner_id: untyped
17
+
18
+ attr_reader foreign_key_id: untyped
19
+
20
+ attr_reader foreign_type: String?
21
+
22
+ attr_reader created_at: untyped
23
+
24
+ attr_reader event: String?
25
+
26
+ attr_reader version_id: untyped
27
+
28
+ # : (untyped, untyped, String?, ?untyped, ?String?, ?untyped) -> void
29
+ def initialize: (untyped, untyped, String?, ?untyped, ?String?, ?untyped) -> void
30
+ end
31
+
32
+ # : (untyped, Array[untyped], untyped, ActivityRange) -> void
33
+ def initialize: (untyped, Array[untyped], untyped, ActivityRange) -> void
34
+
35
+ # : () -> Array[Row]
36
+ def call: () -> Array[Row]
37
+
38
+ # One shared query keeps the owner and timestamp needed to partition a
39
+ # batch's candidates back into each root's exact activity range.
40
+ # : () -> Array[Row]
41
+ def historical_rows: () -> Array[Row]
42
+
43
+ # The live row holds the state at a range that ends at the current record.
44
+ # : () -> Array[Row]
45
+ def current_rows: () -> Array[Row]
46
+
47
+ private
48
+
49
+ @owner_class: untyped
50
+
51
+ @owner_ids: Array[untyped]
52
+
53
+ @range: ActivityRange
54
+
55
+ @reflection: untyped
56
+
57
+ # : (untyped) -> untyped
58
+ def historical_relation: (untyped) -> untyped
59
+
60
+ # : (untyped, untyped) -> Array[Array[untyped]]
61
+ def historical_columns: (untyped, untyped) -> Array[Array[untyped]]
62
+
63
+ # A polymorphic key names its target in a type column; any other names it
64
+ # in the reflection, so no type is read. Like the history, the live read
65
+ # takes owners of every class under the base class: a batch can ask for
66
+ # the owners of an association through one subclass.
67
+ # : () -> Array[Array[untyped]]
68
+ def live_rows: () -> Array[Array[untyped]]
69
+
70
+ # : (untyped) -> VersionAssociationCandidateScope
71
+ def candidate_scope: (untyped) -> VersionAssociationCandidateScope
72
+
73
+ # : () -> untyped
74
+ def association_class: () -> untyped
75
+
76
+ # : () -> bool
77
+ def composite_foreign_key?: () -> bool
78
+ end
79
+ end
@@ -5,8 +5,8 @@ module PaperTrailDiff
5
5
  class ActivityTimelineBuilder
6
6
  include ActivityGrouping
7
7
 
8
- # : (untyped, range: TimelineRange, tree: AssociationTree, snapshotter: untyped, ?snapshots: bool, ?group: Symbol?) -> void
9
- def initialize: (untyped, range: TimelineRange, tree: AssociationTree, snapshotter: untyped, ?snapshots: bool, ?group: Symbol?) -> void
8
+ # : (untyped, range: TimelineRange, tree: AssociationTree, snapshotter: untyped, snapshots: bool, group: Symbol?) -> void
9
+ def initialize: (untyped, range: TimelineRange, tree: AssociationTree, snapshotter: untyped, snapshots: bool, group: Symbol?) -> void
10
10
 
11
11
  # : () -> Array[ActivityStep]
12
12
  def build: () -> Array[ActivityStep]
@@ -17,8 +17,6 @@ module PaperTrailDiff
17
17
 
18
18
  private
19
19
 
20
- @from: untyped
21
-
22
20
  @range: TimelineRange
23
21
 
24
22
  @record: untyped
@@ -37,7 +35,8 @@ module PaperTrailDiff
37
35
  # Every boundary in the span, filtered out or not. A filter narrows where the
38
36
  # span starts and ends, but the sequence inside it stays complete: dropping
39
37
  # boundaries would fold the changes they carried into a neighbouring step and
40
- # credit them to whoever made that one.
38
+ # credit them to whoever made that one. A filter that keeps no version
39
+ # leaves no span, as it does for a checkpoint timeline.
41
40
  # : () -> Array[ActivityStep]
42
41
  def build_between_versions: () -> Array[ActivityStep]
43
42
 
@@ -52,6 +51,12 @@ module PaperTrailDiff
52
51
  # : () -> [RootVersionPlan, Array[ActivityEvent], ActivityHistory, RecordSnapshot?, ActivityBoundary?]
53
52
  def current_history: () -> [ RootVersionPlan, Array[ActivityEvent], ActivityHistory, RecordSnapshot?, ActivityBoundary? ]
54
53
 
54
+ # Without a closing boundary nothing reads the events or the closing pair,
55
+ # so their mutations cannot change an answer.
56
+ # mutant:disable
57
+ # : (RootVersionPlan) -> [RootVersionPlan, Array[ActivityEvent], ActivityHistory, RecordSnapshot?, ActivityBoundary?]
58
+ def empty_history: (RootVersionPlan) -> [ RootVersionPlan, Array[ActivityEvent], ActivityHistory, RecordSnapshot?, ActivityBoundary? ]
59
+
55
60
  # : () -> [RecordSnapshot?, untyped]
56
61
  def capture_current: () -> [ RecordSnapshot?, untyped ]
57
62
 
@@ -66,6 +71,7 @@ module PaperTrailDiff
66
71
 
67
72
  # Appends the transition into an explicit closing boundary, which is either
68
73
  # a requested current record or the absence a final root destroy leaves.
74
+ # Either follows the root versions, which are themselves events.
69
75
  # : (ActivityHistory, Array[ActivityEvent], ActivityBoundary?, RecordSnapshot?) -> Array[ActivityStep]
70
76
  def activity_steps: (ActivityHistory, Array[ActivityEvent], ActivityBoundary?, RecordSnapshot?) -> Array[ActivityStep]
71
77
 
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Collects root and selected-descendant versions relevant to one root history range.
5
5
  class ActivityVersionCollector
6
- # : (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?range_start: untyped, ?range_end: untyped, ?prepared: PreparedActivityIndex?) -> void
7
- def initialize: (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?range_start: untyped, ?range_end: untyped, ?prepared: PreparedActivityIndex?) -> void
6
+ # : (untyped, root_versions: Array[untyped], tree: AssociationTree, ?range_start: untyped, ?range_end: untyped, ?prepared: PreparedActivityIndex?) -> void
7
+ def initialize: (untyped, root_versions: Array[untyped], tree: AssociationTree, ?range_start: untyped, ?range_end: untyped, ?prepared: PreparedActivityIndex?) -> void
8
8
 
9
9
  # : () -> Array[ActivityEvent]
10
10
  def call: () -> Array[ActivityEvent]
@@ -37,17 +37,23 @@ module PaperTrailDiff
37
37
  # : (Hash[Symbol, untyped], untyped, String) -> Hash[String, untyped]
38
38
  def through_groups: (Hash[Symbol, untyped], untyped, String) -> Hash[String, untyped]
39
39
 
40
+ # Every target the owners pointed at in the range, not only those their own
41
+ # changes there recorded: an author renamed under an article nobody edited
42
+ # is still that article's activity.
40
43
  # : (untyped, Array[untyped], untyped) -> Hash[String, untyped]
41
44
  def belongs_to_groups: (untyped, Array[untyped], untyped) -> Hash[String, untyped]
42
45
 
46
+ # : (untyped, Array[untyped], untyped) -> Hash[String, untyped]
47
+ def unprepared_belongs_to_groups: (untyped, Array[untyped], untyped) -> Hash[String, untyped]
48
+
43
49
  # : (untyped, Array[untyped], untyped) -> Hash[String, untyped]
44
50
  def direct_child_groups: (untyped, Array[untyped], untyped) -> Hash[String, untyped]
45
51
 
46
52
  # : (untyped, Array[untyped], untyped) -> Hash[String, untyped]
47
53
  def habtm_groups: (untyped, Array[untyped], untyped) -> Hash[String, untyped]
48
54
 
49
- # : (Array[untyped], untyped) -> Hash[String, untyped]
50
- def identity_groups: (Array[untyped], untyped) -> Hash[String, untyped]
55
+ # : (Array[untyped], untyped) ?{ (untyped, untyped) -> bool } -> Hash[String, untyped]
56
+ def identity_groups: (Array[untyped], untyped) ?{ (untyped, untyped) -> bool } -> Hash[String, untyped]
51
57
 
52
58
  # : (untyped, Array[untyped]) -> Hash[Symbol, untyped]
53
59
  def group: (untyped, Array[untyped]) -> Hash[Symbol, untyped]
@@ -0,0 +1,148 @@
1
+ # Generated from lib/paper_trail_diff/association_activity_edge.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # One association edge read backwards for a window: given the ids of records
5
+ # on its far side, the query for the owners that held them while it was open.
6
+ #
7
+ # `filter_type` is the type of the column those ids are compared with, and
8
+ # `type` that of the owner column the query selects, so a caller can keep a
9
+ # subquery only where the database can compare the two.
10
+ class AssociationActivityEdge
11
+ attr_reader column: untyped
12
+
13
+ attr_reader type: Symbol?
14
+
15
+ attr_reader filter_type: Symbol?
16
+
17
+ # : (untyped, TimeRange?) -> void
18
+ def initialize: (untyped, TimeRange?) -> void
19
+
20
+ # : () -> bool
21
+ def available?: () -> bool
22
+
23
+ # : (untyped) -> untyped
24
+ def relation: (untyped) -> untyped
25
+
26
+ private
27
+
28
+ @owner: untyped
29
+
30
+ @reflection: untyped
31
+
32
+ @time_range: TimeRange?
33
+
34
+ # The window plus the state when it opened, which is what
35
+ # VersionAssociationCandidateScope selects for an activity analysis.
36
+ # : (untyped, untyped) -> untyped
37
+ def candidates: (untyped, untyped) -> untyped
38
+
39
+ # : (untyped, String) -> Symbol?
40
+ def column_type: (untyped, String) -> Symbol?
41
+
42
+ # A relation is compared as its SQL; ids are compared as values.
43
+ # : (untyped) -> untyped
44
+ def arel_value: (untyped) -> untyped
45
+
46
+ # The owners a child pointed at when the window opened or after a change
47
+ # inside it, as PT-AT recorded them on the child's own versions.
48
+ class Child < AssociationActivityEdge
49
+ # : (untyped, TimeRange?) -> void
50
+ def initialize: (untyped, TimeRange?) -> void
51
+
52
+ # : (untyped) -> untyped
53
+ def relation: (untyped) -> untyped
54
+
55
+ private
56
+
57
+ @associations: untyped
58
+
59
+ @versions: untyped
60
+
61
+ # : () -> Array[String?]
62
+ def owner_types: () -> Array[String?]
63
+ end
64
+
65
+ # The owners that pointed at a target when the window opened or after a
66
+ # change inside it, as PT-AT recorded them on the owners' versions.
67
+ class BelongsTo < AssociationActivityEdge
68
+ # : (untyped, TimeRange?, ?foreign_type: String?) -> void
69
+ def initialize: (untyped, TimeRange?, ?foreign_type: String?) -> void
70
+
71
+ # The types a polymorphic key's rows name, the only way to know its targets.
72
+ # : () -> Array[String]
73
+ def target_types: () -> Array[String]
74
+
75
+ # The owners that pointed at a target of this edge's type when the window
76
+ # opened or after a change inside it, whichever target that was.
77
+ # : () -> untyped
78
+ def typed_owners: () -> untyped
79
+
80
+ # : (untyped) -> untyped
81
+ def relation: (untyped) -> untyped
82
+
83
+ private
84
+
85
+ @associations: untyped
86
+
87
+ @foreign_type: String?
88
+
89
+ @versions: untyped
90
+
91
+ # : (Hash[Symbol, untyped]) -> untyped
92
+ def owner_rows: (Hash[Symbol, untyped]) -> untyped
93
+ end
94
+
95
+ # The owners a HABTM join row links to a target now. A join table has no
96
+ # model to type its columns, so the owner column is read by quoted name.
97
+ class LiveHabtm < AssociationActivityEdge
98
+ # : (untyped, TimeRange?) -> void
99
+ def initialize: (untyped, TimeRange?) -> void
100
+
101
+ # : (untyped) -> untyped
102
+ def relation: (untyped) -> untyped
103
+
104
+ private
105
+
106
+ @table: untyped
107
+
108
+ # : () -> Hash[String, Symbol?]
109
+ def join_columns: () -> Hash[String, Symbol?]
110
+
111
+ # Some adapters take the quoted name as a plain string too, but a raw SQL
112
+ # check may not, so it is marked as SQL; no SQLite spec can tell.
113
+ # mutant:disable
114
+ # : () -> untyped
115
+ def quoted_owner_column: () -> untyped
116
+
117
+ # Arel ships no signatures; see VersionQuery. Any spelling of the
118
+ # constant finds the same module.
119
+ # mutant:disable
120
+ # : () -> untyped
121
+ def arel: () -> untyped
122
+
123
+ # Active Record 7.1 has no `with_connection`; see VersionAssociationCandidateScope.
124
+ # Only that version takes the fallback, so no spec run on a newer one can
125
+ # tell its mutations apart.
126
+ # mutant:disable
127
+ # : [T] () { (untyped) -> T } -> T
128
+ def connected: [T] () { (untyped) -> T } -> T
129
+ end
130
+
131
+ # The owners whose versions PT-AT recorded a HABTM target with, keyed by
132
+ # the transaction each version was saved in.
133
+ class RecordedHabtm < AssociationActivityEdge
134
+ # : (untyped, TimeRange?) -> void
135
+ def initialize: (untyped, TimeRange?) -> void
136
+
137
+ # : () -> bool
138
+ def available?: () -> bool
139
+
140
+ # : (untyped) -> untyped
141
+ def relation: (untyped) -> untyped
142
+
143
+ @associations: untyped
144
+
145
+ @versions: untyped
146
+ end
147
+ end
148
+ end