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,185 @@
1
+ # Generated from lib/paper_trail_diff/scope_condition.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # An association scope, or its target's default scope, read as conditions
5
+ # on stored attributes, so membership at a boundary can be decided from each
6
+ # record's state there rather than by a query. A limit, offset or has_one
7
+ # order then ranks the records that meet the conditions (ScopeRanking). It
8
+ # reads the relation the scope builds once and never runs application code
9
+ # per record. A scope it cannot read exactly builds no condition, and the
10
+ # caller keeps its slower path.
11
+ class ScopeCondition
12
+ # Relation parts that leave membership to the where clause and the
13
+ # ranking alone.
14
+ RELATION_VALUES: untyped
15
+
16
+ # Types whose stored values compare in Ruby as the database compares them,
17
+ # named rather than referenced so that loading needs no Active Model.
18
+ VALUE_TYPES: untyped
19
+
20
+ TEXT_TYPE: ::String
21
+
22
+ ENUM_TYPE: ::String
23
+
24
+ BIND_PARAM: ::String
25
+
26
+ ATTRIBUTE: ::String
27
+
28
+ # Adapters that compare strings exactly by default. MySQL and SQL Server
29
+ # ignore case and trailing spaces by default, which Ruby cannot follow.
30
+ EXACT_TEXT_ADAPTERS: untyped
31
+
32
+ # Raised while reading a node that has no exact plain-Ruby reading.
33
+ class Unreadable < StandardError
34
+ end
35
+
36
+ # Whether the reflection filters its target beyond its keys at all.
37
+ # : (untyped) -> bool
38
+ def self.scoped?: (untyped) -> bool
39
+
40
+ # A through also filters by the scopes of each reflection it passes.
41
+ # : (untyped) -> bool
42
+ def self.scoped_path?: (untyped) -> bool
43
+
44
+ # Returns nil when the scope cannot be read as plain conditions and an
45
+ # exact ranking. A part of a has_many through is read unranked: the
46
+ # through's query keeps only its conditions.
47
+ # : (untyped, ?untyped, ?ranked: bool) -> ScopeCondition?
48
+ def self.build: (untyped, ?untyped, ?ranked: bool) -> ScopeCondition?
49
+
50
+ # nil when the relation picks its members by conditions alone.
51
+ # : (untyped, untyped, untyped) -> ScopeRanking?
52
+ def self.ranking: (untyped, untyped, untyped) -> ScopeRanking?
53
+
54
+ # A has_one through orders its rows by its own order and then its parts',
55
+ # and takes the first, so one that passes a ranked part is not read. A
56
+ # has_many through keeps only its parts' conditions.
57
+ # : (untyped) -> bool
58
+ def self.ranked_through?: (untyped) -> bool
59
+
60
+ # Whether any part of a through, at any depth, has a limit, an offset or
61
+ # a has_one order.
62
+ # : (untyped) -> bool
63
+ def self.ranked_parts?: (untyped) -> bool
64
+
65
+ # A polymorphic part's class, and so its scope, is known only per record.
66
+ # : (untyped) -> bool
67
+ def self.ranked_part?: (untyped) -> bool
68
+
69
+ # A scope that takes the owner differs per record, so it is never read.
70
+ # : (untyped, untyped) -> untyped
71
+ def self.scoped_relation: (untyped, untyped) -> untyped
72
+
73
+ # Some parts, such as the having clause `or` sets, may be present but
74
+ # empty.
75
+ # : (untyped) -> bool
76
+ def self.plain_relation?: (untyped) -> bool
77
+
78
+ # : (untyped, Array[untyped], ?ScopeRanking?) -> void
79
+ def initialize: (untyped, Array[untyped], ?ScopeRanking?) -> void
80
+
81
+ # : (Hash[String, untyped]) -> bool
82
+ def matches?: (Hash[String, untyped]) -> bool
83
+
84
+ # The members among records that are each the owner's by key: those that
85
+ # meet the conditions, then ranked.
86
+ # : (Array[untyped]) -> Array[untyped]
87
+ def members: (Array[untyped]) -> Array[untyped]
88
+
89
+ # Records that meet the conditions, as ranked; unchanged without a
90
+ # ranking.
91
+ # : (Array[untyped]) -> Array[untyped]
92
+ def rank: (Array[untyped]) -> Array[untyped]
93
+
94
+ # SQL compares NULL with nothing, so only an IS NULL test, an equality
95
+ # with nil, matches it.
96
+ COMPARISONS: untyped
97
+
98
+ private
99
+
100
+ @model_class: untyped
101
+
102
+ @predicate: Array[untyped]
103
+
104
+ @ranking: ScopeRanking?
105
+
106
+ # : (Array[untyped], Hash[String, untyped]) -> bool
107
+ def evaluate: (Array[untyped], Hash[String, untyped]) -> bool
108
+
109
+ # The form the database holds, as the scope's own values were cast.
110
+ # : (String, Hash[String, untyped]) -> untyped
111
+ def stored: (String, Hash[String, untyped]) -> untyped
112
+
113
+ # Builds each reflection's condition once per target class.
114
+ class Cache
115
+ # : () -> void
116
+ def initialize: () -> void
117
+
118
+ # nil when the scope cannot be read from stored attributes.
119
+ # : (untyped, ?untyped, ?ranked: bool) -> ScopeCondition?
120
+ def call: (untyped, ?untyped, ?ranked: bool) -> ScopeCondition?
121
+
122
+ # The records the reflection's scope admits, as it ranks them. Its
123
+ # condition must exist.
124
+ # : (untyped, Array[untyped], ?untyped, ranked: bool) -> Array[untyped]
125
+ def select: (untyped, Array[untyped], ?untyped, ranked: bool) -> Array[untyped]
126
+
127
+ @conditions: Hash[Array[untyped], ScopeCondition?]
128
+ end
129
+
130
+ # Compiles a where clause into nested arrays of plain comparisons.
131
+ class Reader
132
+ # : (untyped) -> void
133
+ def initialize: (untyped) -> void
134
+
135
+ # : (untyped) -> Array[untyped]
136
+ def call: (untyped) -> Array[untyped]
137
+
138
+ private
139
+
140
+ @model_class: untyped
141
+
142
+ # An Or is binary before Rails 7.2 and n-ary from it.
143
+ # : (untyped) -> Array[untyped]
144
+ def either: (untyped) -> Array[untyped]
145
+
146
+ # : (untyped) -> Array[untyped]
147
+ def comparison: (untyped) -> Array[untyped]
148
+
149
+ # : (Symbol, untyped) -> Array[untyped]
150
+ def binary: (Symbol, untyped) -> Array[untyped]
151
+
152
+ # : (untyped) -> Array[untyped]
153
+ def homogeneous_in: (untyped) -> Array[untyped]
154
+
155
+ # Active Record writes an empty NOT IN as a condition that is always
156
+ # true, and a NOT IN list holding NULL matches no row.
157
+ # : (Symbol, untyped, Array[untyped]) -> Array[untyped]
158
+ def list: (Symbol, untyped, Array[untyped]) -> Array[untyped]
159
+
160
+ # Ruby comparison stands in for SQL only on the record's own column of
161
+ # a type both compare alike.
162
+ # : (untyped) -> String
163
+ def column: (untyped) -> String
164
+
165
+ # : (String) -> bool
166
+ def comparable?: (String) -> bool
167
+
168
+ # An enum compares the values it stores, which its subtype describes. A
169
+ # wrapper such as a serialized or encrypted type is none of these.
170
+ # : (untyped) -> untyped
171
+ def value_type: (untyped) -> untyped
172
+
173
+ # A list that is not plain values, such as a subquery, is unreadable.
174
+ # : (untyped) -> Array[untyped]
175
+ def values: (untyped) -> Array[untyped]
176
+
177
+ # A value out of the column's range makes Active Record write a
178
+ # constant condition instead, so it is left unread. Active Record
179
+ # writes IS NULL for a value that is nil before casting, but `= NULL`,
180
+ # which matches nothing, for one that only casts to nil.
181
+ # : (untyped) -> untyped
182
+ def value: (untyped) -> untyped
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,117 @@
1
+ # Generated from lib/paper_trail_diff/scope_ranking.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # The order, offset and limit by which a scope picks its members from the
5
+ # records that meet its conditions, as the database would rank them. It is
6
+ # read only when the order is total and every key compares in Ruby as SQL
7
+ # compares it; otherwise build returns nil and the caller keeps its slower
8
+ # path.
9
+ class ScopeRanking
10
+ DIRECTIONS: untyped
11
+
12
+ ATTRIBUTE: ::String
13
+
14
+ ENUM_TYPE: ::String
15
+
16
+ # Integers, booleans and dates sort alike everywhere. Strings follow a
17
+ # collation, so they are not read.
18
+ ORDER_TYPES: untyped
19
+
20
+ # Times sort alike only at the precision both the column and history
21
+ # keep. A time of day is not read: in memory it keeps the date it was
22
+ # given, which the database drops.
23
+ DATETIME_TYPE: ::String
24
+
25
+ # Where each adapter sorts NULL in an ascending order by default; a
26
+ # descending order reverses it. Another adapter's ranking is not read.
27
+ NULLS_LAST: untyped
28
+
29
+ # Places in a sort key, of which only the order counts: a value between a
30
+ # NULL sorted first and one sorted last, and false before true. Every
31
+ # value is a number, so that a descending key can negate it.
32
+ NULL_PLACE: untyped
33
+
34
+ VALUE_PLACE: ::Integer
35
+
36
+ BOOLEAN_VALUE: untyped
37
+
38
+ # Whether the relation picks members by rank rather than by condition
39
+ # alone. A has_one takes the first record its order names.
40
+ # : (untyped, untyped) -> bool
41
+ def self.ranks?: (untyped, untyped) -> bool
42
+
43
+ # A has_many, through or not, or a direct has_one is ranked per owner as
44
+ # its own query ranks it. A has_many through ranks the rows its join
45
+ # yields, a record reached twice counting twice, by its own order first.
46
+ # Returns nil when the ranking cannot be read exactly.
47
+ # : (untyped, untyped, untyped) -> ScopeRanking?
48
+ def self.build: (untyped, untyped, untyped) -> ScopeRanking?
49
+
50
+ # : (untyped) -> bool
51
+ def self.own?: (untyped) -> bool
52
+
53
+ # Active Record casts a limit to an integer but keeps an offset as given,
54
+ # which may be a String or SQL.
55
+ # : (untyped) -> bool
56
+ def self.plain_offset?: (untyped) -> bool
57
+
58
+ # The order's keys up to the primary key, which makes it total. Keys
59
+ # after it never decide anything.
60
+ # : (Array[untyped], untyped) -> Array[[String, bool]]?
61
+ def self.keys: (Array[untyped], untyped) -> Array[[ String, bool ]]?
62
+
63
+ # The record's own column, sorted plainly in one direction.
64
+ # : (untyped, untyped) -> String?
65
+ def self.column: (untyped, untyped) -> String?
66
+
67
+ # : (untyped, String) -> bool
68
+ def self.orderable?: (untyped, String) -> bool
69
+
70
+ # Active Record truncates a time to its column's precision before writing
71
+ # it, so history holds the stored value when it keeps that many digits.
72
+ # Times the type stores in UTC sort as the instants they name; local
73
+ # times repeat an hour when clocks go back.
74
+ # : (untyped, untyped) -> bool
75
+ def self.exact_time?: (untyped, untyped) -> bool
76
+
77
+ # : (untyped, Array[[String, bool]], bool, Integer?, Integer?) -> void
78
+ def initialize: (untyped, Array[[ String, bool ]], bool, Integer?, Integer?) -> void
79
+
80
+ # The members among records that met the scope's conditions, in order.
81
+ # : (Array[untyped]) -> Array[untyped]
82
+ def call: (Array[untyped]) -> Array[untyped]
83
+
84
+ private
85
+
86
+ @keys: Array[[ String, bool ]]
87
+
88
+ @limit: Integer?
89
+
90
+ @model_class: untyped
91
+
92
+ @nulls_last: bool
93
+
94
+ @offset: Integer?
95
+
96
+ # Keys compare in turn, and the primary key always decides. A descending
97
+ # key negates its places, which also moves NULL to the other end, as each
98
+ # adapter does by default.
99
+ # : (Hash[String, untyped]) -> Array[Array[Numeric]]
100
+ def sort_key: (Hash[String, untyped]) -> Array[Array[Numeric]]
101
+
102
+ # NULL holds the adapter's place alone, so it never compares as a value.
103
+ # A version saved before its column existed holds NULL there too.
104
+ # : (untyped) -> Array[Numeric]
105
+ def place: (untyped) -> Array[Numeric]
106
+
107
+ # A time as its seconds, exactly, and a date as its day number.
108
+ # : (untyped) -> Numeric
109
+ def number: (untyped) -> Numeric
110
+
111
+ # The value the database sorts by, as the key's type casts it. An enum
112
+ # sorts by the integer it stores; other types are compared cast, since an
113
+ # adapter may serialize a time or date as a wrapper, as SQL Server does.
114
+ # : (String, Hash[String, untyped]) -> untyped
115
+ def stored: (String, Hash[String, untyped]) -> untyped
116
+ end
117
+ end
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Selects both populations and checks their shared ceiling before analysis.
5
5
  class ScopedAnalysisBuilder
6
- # : (untyped, limit: Integer?, within: untyped, historical_filter: untyped, max_filter_versions: Integer, include_deleted: bool, analyzer: untyped, deleted_analyzer: untyped) -> void
7
- def initialize: (untyped, limit: Integer?, within: untyped, historical_filter: untyped, max_filter_versions: Integer, include_deleted: bool, analyzer: untyped, deleted_analyzer: untyped) -> void
6
+ # : (untyped, limit: Integer?, within: untyped, historical_filter: untyped, max_filter_versions: Integer, include_deleted: bool, analyzer: untyped, deleted_analyzer: untyped, ?activity_tree: AssociationTree?) -> void
7
+ def initialize: (untyped, limit: Integer?, within: untyped, historical_filter: untyped, max_filter_versions: Integer, include_deleted: bool, analyzer: untyped, deleted_analyzer: untyped, ?activity_tree: AssociationTree?) -> void
8
8
 
9
9
  # : () -> ScopedAnalysis
10
10
  def call: () -> ScopedAnalysis
@@ -38,8 +38,11 @@ module PaperTrailDiff
38
38
  def initialize: (records: Array[untyped], unreachable: Array[Array[String]]) -> void
39
39
  end
40
40
 
41
- # : (untyped, time_range: TimeRange?, limit: Integer, ?historical_filter: untyped, ?max_filter_versions: Integer) -> void
42
- def initialize: (untyped, time_range: TimeRange?, limit: Integer, ?historical_filter: untyped, ?max_filter_versions: Integer) -> void
41
+ # `activity_tree` names the association paths whose changes also select a
42
+ # root, as an activity analysis reports them. Without one, only a root's
43
+ # own versions select it.
44
+ # : (untyped, time_range: TimeRange?, limit: Integer, ?historical_filter: untyped, ?max_filter_versions: Integer, ?activity_tree: AssociationTree?) -> void
45
+ def initialize: (untyped, time_range: TimeRange?, limit: Integer, ?historical_filter: untyped, ?max_filter_versions: Integer, ?activity_tree: AssociationTree?) -> void
43
46
 
44
47
  # : () -> Result
45
48
  def call: () -> Result
@@ -50,6 +53,8 @@ module PaperTrailDiff
50
53
 
51
54
  @limit: Integer
52
55
 
56
+ @related: AssociationActivitySelection?
57
+
53
58
  @scope: untyped
54
59
 
55
60
  @time_range: TimeRange?
@@ -59,14 +64,28 @@ module PaperTrailDiff
59
64
  # in the window -- not to the relation or `limit:` -- so a report on one
60
65
  # record could read every id in a busy table. A subquery keeps the work
61
66
  # bounded by what the relation and the deletions actually contain.
62
- # : () -> Result
63
- def database_selection: () -> Result
67
+ # : (Array[AssociationActivitySelection::Query]) -> Result
68
+ def database_selection: (Array[AssociationActivitySelection::Query]) -> Result
69
+
70
+ # Comparing in SQL needs every subquery's column to share the key's type.
71
+ # : (Array[AssociationActivitySelection::source]) -> Array[AssociationActivitySelection::Query]?
72
+ def subqueries: (Array[AssociationActivitySelection::source]) -> Array[AssociationActivitySelection::Query]?
73
+
74
+ # : (Array[String]) -> Result
75
+ def plucked_selection: (Array[String]) -> Result
64
76
 
65
77
  # Comparing the two columns in SQL needs them to share a type: PostgreSQL
66
- # rejects an integer key against a string item_id outright.
78
+ # rejects an integer key against a string item_id outright. A composite
79
+ # key names no single attribute, so it has no type and never matches.
67
80
  # : () -> bool
68
81
  def comparable_keys?: () -> bool
69
82
 
83
+ # : () -> Symbol?
84
+ def key_type: () -> Symbol?
85
+
86
+ # : (Array[AssociationActivitySelection::Query]) -> Array[String]
87
+ def related_missing_ids: (Array[AssociationActivitySelection::Query]) -> Array[String]
88
+
70
89
  # Changed in the window but without a row, found with an anti-join instead
71
90
  # of comparing two plucked lists.
72
91
  # : () -> Array[String]
@@ -81,6 +100,10 @@ module PaperTrailDiff
81
100
  # : (untyped) -> untyped
82
101
  def normalize_scope: (untyped) -> untyped
83
102
 
103
+ # : () -> void
104
+ def reject_unversioned!: () -> void
105
+
106
+ # No Integer subclass has instances, so this is `is_a?` by another name.
84
107
  # : (Integer) -> Integer
85
108
  def validate_limit: (Integer) -> Integer
86
109
 
@@ -98,8 +121,13 @@ module PaperTrailDiff
98
121
  # Deduplicate records rather than adding SQL DISTINCT: an otherwise valid
99
122
  # relation may order by joined columns or select a PostgreSQL json column.
100
123
  # Read bounded pages until the relation ends or too many unique roots appear.
101
- # : (untyped) -> Array[untyped]
102
- def selected_records: (untyped) -> Array[untyped]
124
+ # Each candidate source is an id list or a subquery; a record matching any
125
+ # of them is selected.
126
+ # : (Array[untyped]) -> Array[untyped]
127
+ def selected_records: (Array[untyped]) -> Array[untyped]
128
+
129
+ # : (Array[untyped]) -> untyped
130
+ def candidate_relation: (Array[untyped]) -> untyped
103
131
 
104
132
  # : (untyped, Integer) -> Array[untyped]
105
133
  def record_page: (untyped, Integer) -> Array[untyped]
@@ -121,6 +149,12 @@ module PaperTrailDiff
121
149
  # : () -> String
122
150
  def item_type: () -> String
123
151
 
152
+ # A relation delegates class methods to its model, so every mutation here,
153
+ # which drops `.model`, answers the same.
154
+ # mutant:disable
155
+ # : () -> untyped
156
+ def model: () -> untyped
157
+
124
158
  # : () -> untyped
125
159
  def base_class: () -> untyped
126
160
 
@@ -98,6 +98,12 @@ module PaperTrailDiff
98
98
  # : (untyped, untyped, AssociationTree, String, untyped) -> AssociationSnapshot
99
99
  def association_snapshot: (untyped, untyped, AssociationTree, String, untyped) -> AssociationSnapshot
100
100
 
101
+ # A reified history can hold a member PaperTrail could not rebuild. A
102
+ # through loads a record once per join that reaches it; it is one member,
103
+ # as Active Record compares records by class and id.
104
+ # : (untyped, untyped) -> Array[untyped]
105
+ def members: (untyped, untyped) -> Array[untyped]
106
+
101
107
  # : (Array[untyped], AssociationTree, String, untyped) -> Array[RecordSnapshot]
102
108
  def normalize_children: (Array[untyped], AssociationTree, String, untyped) -> Array[RecordSnapshot]
103
109
 
@@ -15,6 +15,9 @@ module PaperTrailDiff
15
15
  # : (untyped) -> Array[untyped]
16
16
  def self?.chronological_version_key: (untyped) -> Array[untyped]
17
17
 
18
+ # : (untyped, untyped) -> Array[untyped]
19
+ def self?.version_key: (untyped, untyped) -> Array[untyped]
20
+
18
21
  # Ordering falls back to the id whenever timestamps tie, which recovers the
19
22
  # real order only while ids increase with insertion. An autoincrement id
20
23
  # does; a UUID does not, so a tie between UUID-keyed versions is genuinely
@@ -59,8 +62,8 @@ module PaperTrailDiff
59
62
  # : (untyped, untyped) -> Integer
60
63
  def self?.compare_versions: (untyped, untyped) -> Integer
61
64
 
62
- # : (String, String) -> String
63
- def self?.association_path: (String, String) -> String
65
+ # : (String, String | Symbol) -> String
66
+ def self?.association_path: (String, String | Symbol) -> String
64
67
 
65
68
  # : (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]
66
69
  def self?.merge_record_groups: (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]
@@ -41,6 +41,10 @@ module PaperTrailDiff
41
41
  # : (untyped, boundary: Symbol) -> Time
42
42
  def coerce_time: (untyped, boundary: Symbol) -> Time
43
43
 
44
+ # Active Support 8.0 and older warn, once per process, that `to_time` will
45
+ # change on a zoned time, so read zoned values without it. Every branch
46
+ # gives the same instant, so no spec (and no mutation) can tell them apart.
47
+ # mutant:disable
44
48
  # : (untyped) -> Time
45
49
  def time_value: (untyped) -> Time
46
50
 
@@ -3,11 +3,11 @@
3
3
  module PaperTrailDiff
4
4
  # Selects in-range root versions plus one later reconstruction boundary.
5
5
  class TimeVersionRange
6
- # : (untyped, time_range: TimeRange, ?version_scope: untyped, ?live_endpoint: untyped) -> void
7
- def initialize: (untyped, time_range: TimeRange, ?version_scope: untyped, ?live_endpoint: untyped) -> void
6
+ # : (untyped, time_range: TimeRange, version_scope: untyped, live_endpoint: untyped) -> void
7
+ def initialize: (untyped, time_range: TimeRange, version_scope: untyped, live_endpoint: untyped) -> void
8
8
 
9
- # : (?context_required: bool) -> RootVersionPlan
10
- def select_plan: (?context_required: bool) -> RootVersionPlan
9
+ # : (context_required: bool) -> RootVersionPlan
10
+ def select_plan: (context_required: bool) -> RootVersionPlan
11
11
 
12
12
  private
13
13
 
@@ -36,6 +36,13 @@ module PaperTrailDiff
36
36
  # : (Hash[Symbol, String], String, untyped) -> String
37
37
  def activity_condition: (Hash[Symbol, String], String, untyped) -> String
38
38
 
39
+ # A time quoted on its own takes the adapter's default precision, which on
40
+ # SQL Server is `datetime` and rounds to 1/300 second. Serializing through
41
+ # the column's type quotes it as a bound value would be, at the precision
42
+ # `created_at` is stored with.
43
+ # : (untyped, untyped) -> String
44
+ def quote_time: (untyped, untyped) -> String
45
+
39
46
  # : (untyped, String, String) -> String
40
47
  def column: (untyped, String, String) -> String
41
48
 
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Selects an inclusive, chronological range from a record's root versions.
5
5
  class VersionRange
6
- # : (untyped, from: untyped, to: untyped, ?version_scope: untyped) -> void
7
- def initialize: (untyped, from: untyped, to: untyped, ?version_scope: untyped) -> void
6
+ # : (untyped, from: untyped, to: untyped, version_scope: untyped) -> void
7
+ def initialize: (untyped, from: untyped, to: untyped, version_scope: untyped) -> void
8
8
 
9
9
  # Keep both reconstruction boundaries and the step pairs a filter implies.
10
10
  # : () -> RootVersionPlan
@@ -35,6 +35,8 @@ module PaperTrailDiff
35
35
  # : (untyped, untyped, boundary: Symbol) -> void
36
36
  def validate_boundary!: (untyped, untyped, boundary: Symbol) -> void
37
37
 
38
+ # The relation holds only this record's versions, so finding the id there
39
+ # also settles which record the version belongs to.
38
40
  # : (untyped, untyped) -> bool
39
41
  def valid_boundary?: (untyped, untyped) -> bool
40
42
  end
@@ -12,6 +12,10 @@ module PaperTrailDiff
12
12
  # : (untyped, Array[untyped]) -> Array[untyped]
13
13
  def call: (untyped, Array[untyped]) -> Array[untyped]
14
14
 
15
+ # The ids of the versions the scope keeps, or nil when there is no scope.
16
+ # : (untyped) -> Set[untyped]?
17
+ def chosen_ids: (untyped) -> Set[untyped]?
18
+
15
19
  private
16
20
 
17
21
  @scope: untyped
@@ -41,7 +41,9 @@ module PaperTrailDiff
41
41
  def self.analyze_many: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?allow_expensive: bool) -> Hash[identity, Analysis]
42
42
 
43
43
  # Analyzes every root the relation reaches whose history moved inside the
44
- # window. Joined duplicates are read in bounded pages and analyzed once.
44
+ # window. With `activity: true`, a change to a record on one of the
45
+ # requested association paths also counts, because the activity timeline
46
+ # reports it. Joined duplicates are read in bounded pages and analyzed once.
45
47
  #
46
48
  # `limit:` is required and exceeding it raises. Selection moves into the gem
47
49
  # here. The relation's own limit/offset select the live page; this separate
@@ -67,7 +69,8 @@ module PaperTrailDiff
67
69
  # : (untyped, limit: Integer?, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?historical_filter: untyped, ?max_filter_versions: Integer, ?include_deleted: bool, ?allow_expensive: bool) -> ScopedAnalysis
68
70
  def self.analyze_scope: (untyped, limit: Integer?, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?historical_filter: untyped, ?max_filter_versions: Integer, ?include_deleted: bool, ?allow_expensive: bool) -> ScopedAnalysis
69
71
 
70
- # Recent root and selected-child activity through the current saved state.
72
+ # Recent root and selected-child activity through the current saved state,
73
+ # for every root that changed or whose requested associations did.
71
74
  # Historical windows remain explicit on analyze_scope; this entry point is
72
75
  # deliberately named for a report through now rather than an as-of query.
73
76
  # : (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
@@ -105,6 +108,22 @@ module PaperTrailDiff
105
108
  # JSON uses for a present null.
106
109
  # : (untyped, ?untyped) -> nested_changes
107
110
  def self.nested_changes: (untyped, ?untyped) -> nested_changes
111
+
112
+ # Reads what PaperTrail recorded with one version: its `object_changes`,
113
+ # typed by the version's model, as `{ "title" => <from "Draft" to "Published"> }`.
114
+ #
115
+ # versions = PaperTrail::Version.where(id: steps.map { _1.source_boundary.version_id })
116
+ # versions.to_h { [_1.id, PaperTrailDiff.recorded_changes(_1)] }
117
+ #
118
+ # A step's diff compares the states before and after an event, so it also
119
+ # includes anything saved without a version in between; this does not.
120
+ # Returns nil when nothing was recorded (no `object_changes` column, or no
121
+ # value for this version), which differs from a recorded empty hash. Unlike
122
+ # PaperTrail's `version.changeset`, it never loads the record, so it costs no
123
+ # query per version and still types a deleted record's changes. `ignore:`
124
+ # takes attribute names, as an array, and defaults to `updated_at`.
125
+ # : (untyped, ?ignore: Array[String | Symbol]) -> Hash[String, ValueChange]?
126
+ def self.recorded_changes: (untyped, ?ignore: Array[String | Symbol]) -> Hash[String, ValueChange]?
108
127
  end
109
128
 
110
129
  module PaperTrailDiff
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_trail_diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Williams
@@ -67,11 +67,14 @@ files:
67
67
  - lib/paper_trail_diff/activity_root_steps.rb
68
68
  - lib/paper_trail_diff/activity_snapshot_delta.rb
69
69
  - lib/paper_trail_diff/activity_snapshot_sequence.rb
70
+ - lib/paper_trail_diff/activity_target_candidate_loader.rb
70
71
  - lib/paper_trail_diff/activity_timeline_builder.rb
71
72
  - lib/paper_trail_diff/activity_transaction_grouper.rb
72
73
  - lib/paper_trail_diff/activity_version_collector.rb
73
74
  - lib/paper_trail_diff/analysis.rb
74
75
  - lib/paper_trail_diff/analysis_batch.rb
76
+ - lib/paper_trail_diff/association_activity_edge.rb
77
+ - lib/paper_trail_diff/association_activity_selection.rb
75
78
  - lib/paper_trail_diff/association_diff_traversal.rb
76
79
  - lib/paper_trail_diff/association_discovery.rb
77
80
  - lib/paper_trail_diff/association_traversal.rb
@@ -91,6 +94,7 @@ files:
91
94
  - lib/paper_trail_diff/endpoint.rb
92
95
  - lib/paper_trail_diff/engine.rb
93
96
  - lib/paper_trail_diff/errors.rb
97
+ - lib/paper_trail_diff/habtm_transaction_owners.rb
94
98
  - lib/paper_trail_diff/historical_association_reifier.rb
95
99
  - lib/paper_trail_diff/historical_filter.rb
96
100
  - lib/paper_trail_diff/historical_snapshot_store.rb
@@ -110,8 +114,12 @@ files:
110
114
  - lib/paper_trail_diff/prepared_record_index.rb
111
115
  - lib/paper_trail_diff/prepared_snapshot_builder.rb
112
116
  - lib/paper_trail_diff/prepared_state_association_reader.rb
117
+ - lib/paper_trail_diff/recorded_changes.rb
118
+ - lib/paper_trail_diff/recorded_class.rb
113
119
  - lib/paper_trail_diff/root_version_plan.rb
114
120
  - lib/paper_trail_diff/root_version_selection.rb
121
+ - lib/paper_trail_diff/scope_condition.rb
122
+ - lib/paper_trail_diff/scope_ranking.rb
115
123
  - lib/paper_trail_diff/scoped_analysis.rb
116
124
  - lib/paper_trail_diff/scoped_analysis_builder.rb
117
125
  - lib/paper_trail_diff/scoped_root_selection.rb
@@ -160,11 +168,14 @@ files:
160
168
  - sig/generated/paper_trail_diff/activity_root_steps.rbs
161
169
  - sig/generated/paper_trail_diff/activity_snapshot_delta.rbs
162
170
  - sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs
171
+ - sig/generated/paper_trail_diff/activity_target_candidate_loader.rbs
163
172
  - sig/generated/paper_trail_diff/activity_timeline_builder.rbs
164
173
  - sig/generated/paper_trail_diff/activity_transaction_grouper.rbs
165
174
  - sig/generated/paper_trail_diff/activity_version_collector.rbs
166
175
  - sig/generated/paper_trail_diff/analysis.rbs
167
176
  - sig/generated/paper_trail_diff/analysis_batch.rbs
177
+ - sig/generated/paper_trail_diff/association_activity_edge.rbs
178
+ - sig/generated/paper_trail_diff/association_activity_selection.rbs
168
179
  - sig/generated/paper_trail_diff/association_diff_traversal.rbs
169
180
  - sig/generated/paper_trail_diff/association_discovery.rbs
170
181
  - sig/generated/paper_trail_diff/association_traversal.rbs
@@ -184,6 +195,7 @@ files:
184
195
  - sig/generated/paper_trail_diff/endpoint.rbs
185
196
  - sig/generated/paper_trail_diff/engine.rbs
186
197
  - sig/generated/paper_trail_diff/errors.rbs
198
+ - sig/generated/paper_trail_diff/habtm_transaction_owners.rbs
187
199
  - sig/generated/paper_trail_diff/historical_association_reifier.rbs
188
200
  - sig/generated/paper_trail_diff/historical_filter.rbs
189
201
  - sig/generated/paper_trail_diff/historical_snapshot_store.rbs
@@ -203,8 +215,12 @@ files:
203
215
  - sig/generated/paper_trail_diff/prepared_record_index.rbs
204
216
  - sig/generated/paper_trail_diff/prepared_snapshot_builder.rbs
205
217
  - sig/generated/paper_trail_diff/prepared_state_association_reader.rbs
218
+ - sig/generated/paper_trail_diff/recorded_changes.rbs
219
+ - sig/generated/paper_trail_diff/recorded_class.rbs
206
220
  - sig/generated/paper_trail_diff/root_version_plan.rbs
207
221
  - sig/generated/paper_trail_diff/root_version_selection.rbs
222
+ - sig/generated/paper_trail_diff/scope_condition.rbs
223
+ - sig/generated/paper_trail_diff/scope_ranking.rbs
208
224
  - sig/generated/paper_trail_diff/scoped_analysis.rbs
209
225
  - sig/generated/paper_trail_diff/scoped_analysis_builder.rbs
210
226
  - sig/generated/paper_trail_diff/scoped_root_selection.rbs
@@ -238,11 +254,11 @@ licenses:
238
254
  metadata:
239
255
  allowed_push_host: https://rubygems.org
240
256
  bug_tracker_uri: https://github.com/aheathwilliams/paper_trail_diff/issues
241
- changelog_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.13.1/CHANGELOG.md
242
- documentation_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.13.1/README.md
257
+ changelog_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.14.0/CHANGELOG.md
258
+ documentation_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.14.0/README.md
243
259
  homepage_uri: https://github.com/aheathwilliams/paper_trail_diff
244
260
  rubygems_mfa_required: 'true'
245
- source_code_uri: https://github.com/aheathwilliams/paper_trail_diff/tree/v0.13.1
261
+ source_code_uri: https://github.com/aheathwilliams/paper_trail_diff/tree/v0.14.0
246
262
  rdoc_options: []
247
263
  require_paths:
248
264
  - lib