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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +287 -0
- data/QUICKSTART.md +9 -6
- data/README.md +84 -9
- data/docs/architecture.md +32 -9
- data/docs/performance.md +106 -13
- data/docs/reference.md +242 -29
- data/docs/testing.md +34 -11
- data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +19 -9
- data/lib/paper_trail_diff/activity_collection_event_applier.rb +11 -1
- data/lib/paper_trail_diff/activity_collection_record_updater.rb +16 -2
- data/lib/paper_trail_diff/activity_event_record_resolver.rb +20 -1
- data/lib/paper_trail_diff/activity_event_route_finder.rb +2 -1
- data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +1 -1
- data/lib/paper_trail_diff/activity_range.rb +14 -2
- data/lib/paper_trail_diff/activity_relationship.rb +12 -0
- data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +5 -2
- data/lib/paper_trail_diff/activity_target_candidate_loader.rb +131 -0
- data/lib/paper_trail_diff/activity_timeline_builder.rb +30 -24
- data/lib/paper_trail_diff/activity_version_collector.rb +31 -20
- data/lib/paper_trail_diff/association_activity_edge.rb +236 -0
- data/lib/paper_trail_diff/association_activity_selection.rb +197 -0
- data/lib/paper_trail_diff/association_discovery.rb +8 -13
- data/lib/paper_trail_diff/association_traversal.rb +169 -45
- data/lib/paper_trail_diff/batch_safety.rb +23 -15
- data/lib/paper_trail_diff/batched_root_versions.rb +21 -39
- data/lib/paper_trail_diff/comparison_batch.rb +37 -10
- data/lib/paper_trail_diff/deleted_analysis_batch.rb +34 -16
- data/lib/paper_trail_diff/diagnostics.rb +26 -8
- data/lib/paper_trail_diff/direct_record_state_loader.rb +10 -1
- data/lib/paper_trail_diff/errors.rb +2 -1
- data/lib/paper_trail_diff/habtm_transaction_owners.rb +68 -0
- data/lib/paper_trail_diff/historical_association_reifier.rb +32 -2
- data/lib/paper_trail_diff/historical_snapshot_store.rb +6 -9
- data/lib/paper_trail_diff/live_endpoint_batch_loader.rb +35 -2
- data/lib/paper_trail_diff/live_graph_collector.rb +1 -1
- data/lib/paper_trail_diff/paper_trail_adapter.rb +22 -6
- data/lib/paper_trail_diff/preloaded_endpoint_batch_loader.rb +1 -1
- data/lib/paper_trail_diff/prepared_activity_index.rb +110 -36
- data/lib/paper_trail_diff/prepared_edge_loader.rb +29 -45
- data/lib/paper_trail_diff/prepared_history.rb +63 -54
- data/lib/paper_trail_diff/prepared_history_loader.rb +33 -43
- data/lib/paper_trail_diff/prepared_record_index.rb +26 -0
- data/lib/paper_trail_diff/prepared_snapshot_builder.rb +1 -1
- data/lib/paper_trail_diff/prepared_state_association_reader.rb +119 -29
- data/lib/paper_trail_diff/recorded_changes.rb +83 -0
- data/lib/paper_trail_diff/recorded_class.rb +53 -0
- data/lib/paper_trail_diff/scope_condition.rb +331 -0
- data/lib/paper_trail_diff/scope_ranking.rb +196 -0
- data/lib/paper_trail_diff/scoped_analysis_builder.rb +4 -3
- data/lib/paper_trail_diff/scoped_root_selection.rb +93 -43
- data/lib/paper_trail_diff/snapshot_normalizer.rb +10 -3
- data/lib/paper_trail_diff/support.rb +9 -4
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +0 -1
- data/lib/paper_trail_diff/time_range.rb +9 -12
- data/lib/paper_trail_diff/time_version_range.rb +5 -5
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff/version_association_candidate_scope.rb +13 -3
- data/lib/paper_trail_diff/version_range.rb +8 -15
- data/lib/paper_trail_diff/version_scope_filter.rb +11 -3
- data/lib/paper_trail_diff.rb +31 -2
- data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +10 -2
- data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +5 -0
- data/sig/generated/paper_trail_diff/activity_collection_record_updater.rbs +5 -0
- data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +7 -0
- data/sig/generated/paper_trail_diff/activity_range.rbs +6 -2
- data/sig/generated/paper_trail_diff/activity_relationship.rbs +3 -0
- data/sig/generated/paper_trail_diff/activity_root_snapshot_refresher.rbs +2 -0
- data/sig/generated/paper_trail_diff/activity_target_candidate_loader.rbs +79 -0
- data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +11 -5
- data/sig/generated/paper_trail_diff/activity_version_collector.rbs +10 -4
- data/sig/generated/paper_trail_diff/association_activity_edge.rbs +148 -0
- data/sig/generated/paper_trail_diff/association_activity_selection.rbs +112 -0
- data/sig/generated/paper_trail_diff/association_discovery.rbs +3 -5
- data/sig/generated/paper_trail_diff/association_traversal.rbs +84 -9
- data/sig/generated/paper_trail_diff/batch_safety.rbs +10 -4
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +2 -8
- data/sig/generated/paper_trail_diff/comparison_batch.rbs +22 -4
- data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +16 -4
- data/sig/generated/paper_trail_diff/diagnostics.rbs +7 -2
- data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +6 -0
- data/sig/generated/paper_trail_diff/errors.rbs +2 -1
- data/sig/generated/paper_trail_diff/habtm_transaction_owners.rbs +40 -0
- data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +13 -0
- data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +3 -2
- data/sig/generated/paper_trail_diff/live_endpoint_batch_loader.rbs +17 -0
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +5 -0
- data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +52 -10
- data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +4 -12
- data/sig/generated/paper_trail_diff/prepared_history.rbs +33 -22
- data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +15 -13
- data/sig/generated/paper_trail_diff/prepared_record_index.rbs +15 -0
- data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +56 -11
- data/sig/generated/paper_trail_diff/recorded_changes.rbs +47 -0
- data/sig/generated/paper_trail_diff/recorded_class.rbs +22 -0
- data/sig/generated/paper_trail_diff/scope_condition.rbs +185 -0
- data/sig/generated/paper_trail_diff/scope_ranking.rbs +117 -0
- data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +2 -2
- data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +41 -7
- data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +6 -0
- data/sig/generated/paper_trail_diff/support.rbs +5 -2
- data/sig/generated/paper_trail_diff/time_range.rbs +4 -0
- data/sig/generated/paper_trail_diff/time_version_range.rbs +4 -4
- data/sig/generated/paper_trail_diff/version_association_candidate_scope.rbs +7 -0
- data/sig/generated/paper_trail_diff/version_range.rbs +4 -2
- data/sig/generated/paper_trail_diff/version_scope_filter.rbs +4 -0
- data/sig/generated/paper_trail_diff.rbs +21 -2
- 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
|
-
#
|
|
49
|
-
|
|
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
|
|
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
|
|
10
|
-
def call: (untyped
|
|
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
|
-
#
|
|
19
|
-
|
|
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?,
|
|
9
|
-
def initialize: (Array[untyped], time_range: TimeRange?,
|
|
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
|
-
#
|
|
7
|
-
|
|
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
|
-
#
|
|
57
|
-
|
|
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
|
-
#
|
|
7
|
-
|
|
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
|
|
16
|
-
#
|
|
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
|
|