upkeep-rails 0.2.5-aarch64-linux-gnu
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +244 -0
- data/docs/drafts/turbo-streams-is-cache-invalidation-you-write-by-hand.md +240 -0
- data/docs/how-it-works.md +329 -0
- data/docs/plans/sqlglot-active-record-migration.md +211 -0
- data/docs/plans/turbo-frame-subscription-composition.md +90 -0
- data/docs/spikes/SQLGLOT_VS_AREL_FINDINGS.md +136 -0
- data/docs/spikes/query-source-analysis-comparison/FINDINGS.md +169 -0
- data/docs/spikes/sqlglot-first-active-record/README.md +26 -0
- data/docs/spikes/sqlglot-query-analysis/FINDINGS.md +110 -0
- data/docs/spikes/sqlglot-query-analysis/PULSE_1802_FINDINGS.md +95 -0
- data/docs/spikes/sqlglot-semantic-bindings/README.md +29 -0
- data/lib/generators/upkeep/install/install_generator.rb +192 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
- data/lib/generators/upkeep/install/templates/subscription.js +288 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +65 -0
- data/lib/upkeep/active_record_query.rb +248 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +454 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +48 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +339 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +600 -0
- data/lib/upkeep/herb/developer_report.rb +135 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +149 -0
- data/lib/upkeep/herb/template_manifest.rb +548 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +411 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +1007 -0
- data/lib/upkeep/rails/activation_token.rb +55 -0
- data/lib/upkeep/rails/cable/channel.rb +165 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +361 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +65 -0
- data/lib/upkeep/rails/cluster_guard.rb +57 -0
- data/lib/upkeep/rails/configuration.rb +252 -0
- data/lib/upkeep/rails/controller_runtime.rb +187 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/job_runtime.rb +43 -0
- data/lib/upkeep/rails/railtie.rb +44 -0
- data/lib/upkeep/rails/replay.rb +244 -0
- data/lib/upkeep/rails/testing.rb +259 -0
- data/lib/upkeep/rails.rb +466 -0
- data/lib/upkeep/replay.rb +462 -0
- data/lib/upkeep/runtime.rb +1276 -0
- data/lib/upkeep/shared_streams.rb +86 -0
- data/lib/upkeep/sql_dependency_analysis.rb +553 -0
- data/lib/upkeep/sqlglot/libsqlglot_rust.so +0 -0
- data/lib/upkeep/sqlglot/native.rb +121 -0
- data/lib/upkeep/sqlglot/native_library.rb +23 -0
- data/lib/upkeep/sqlglot.rb +367 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +398 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +411 -0
- data/lib/upkeep/subscriptions/active_registry.rb +80 -0
- data/lib/upkeep/subscriptions/base_store.rb +110 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +125 -0
- data/lib/upkeep/subscriptions/lookup_instrumentation.rb +32 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +228 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +313 -0
- data/lib/upkeep/subscriptions/shape.rb +117 -0
- data/lib/upkeep/subscriptions/store.rb +349 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +146 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +15 -0
- data/upkeep-rails.gemspec +66 -0
- metadata +327 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Upkeep
|
|
7
|
+
module Dependencies
|
|
8
|
+
class Base
|
|
9
|
+
attr_reader :source, :key, :metadata
|
|
10
|
+
|
|
11
|
+
def initialize(source:, key:, metadata: {})
|
|
12
|
+
@source = source
|
|
13
|
+
@key = key
|
|
14
|
+
@metadata = metadata
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cache_key
|
|
18
|
+
[source, key]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def matches_change?(_change)
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def identity?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def identity_key
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visibility
|
|
34
|
+
:public
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def precision
|
|
38
|
+
:unknown
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def narrow_frame_safe?
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
source: source,
|
|
48
|
+
key: key,
|
|
49
|
+
visibility: visibility,
|
|
50
|
+
precision: precision,
|
|
51
|
+
metadata: metadata
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class ActiveRecordAttribute < Base
|
|
57
|
+
def initialize(table:, id:, attribute:, model: nil, scope: nil)
|
|
58
|
+
scope = normalize_scope(scope)
|
|
59
|
+
key = { table: table, id: id, attribute: attribute }
|
|
60
|
+
key[:scope] = scope if scope
|
|
61
|
+
super(
|
|
62
|
+
source: :active_record_attribute,
|
|
63
|
+
key: key,
|
|
64
|
+
metadata: { model: model }.compact
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def matches_change?(change)
|
|
69
|
+
key.fetch(:table) == change.fetch(:table) &&
|
|
70
|
+
(key.fetch(:id).nil? || !change[:id] || key.fetch(:id) == change[:id]) &&
|
|
71
|
+
change.fetch(:changed_attributes, []).include?(key.fetch(:attribute)) &&
|
|
72
|
+
scope_matches_change?(change)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def precision
|
|
76
|
+
:record_attribute
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def narrow_frame_safe?
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def normalize_scope(scope)
|
|
86
|
+
return nil unless scope.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
normalized = scope.each_with_object({}) do |(column, value), result|
|
|
89
|
+
result[column.to_s] = value unless value.nil?
|
|
90
|
+
end
|
|
91
|
+
normalized.empty? ? nil : normalized
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def scope_matches_change?(change)
|
|
95
|
+
scope = key[:scope]
|
|
96
|
+
return true unless scope
|
|
97
|
+
|
|
98
|
+
scope.all? do |column, value|
|
|
99
|
+
observed = observed_scope_values(change, column)
|
|
100
|
+
observed.empty? || observed.any? { |observed_value| scope_values_equal?(observed_value, value) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def observed_scope_values(change, column)
|
|
105
|
+
[change[:new_values], change[:old_values]].compact.filter_map do |values|
|
|
106
|
+
values[column] || values[column.to_sym]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def scope_values_equal?(observed, expected)
|
|
111
|
+
observed == expected || observed.to_s == expected.to_s
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class ActiveRecordCollection < Base
|
|
116
|
+
UNKNOWN = Object.new
|
|
117
|
+
|
|
118
|
+
def initialize(
|
|
119
|
+
primary_table:,
|
|
120
|
+
table_columns:,
|
|
121
|
+
coverage:,
|
|
122
|
+
sql:,
|
|
123
|
+
predicates: [],
|
|
124
|
+
source: :active_record_collection,
|
|
125
|
+
precision: :collection_predicate
|
|
126
|
+
)
|
|
127
|
+
table_columns = normalize_table_columns(table_columns)
|
|
128
|
+
coverage = coverage.to_sym
|
|
129
|
+
unless coverage == :columns
|
|
130
|
+
raise ArgumentError,
|
|
131
|
+
"unsupported Active Record predicate coverage: #{coverage}; collection dependencies require proven column coverage"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@precision = precision.to_sym
|
|
135
|
+
super(
|
|
136
|
+
source: source.to_sym,
|
|
137
|
+
key: {
|
|
138
|
+
table: primary_table,
|
|
139
|
+
predicate_digest: Digest::SHA256.hexdigest(sql)[0, 16]
|
|
140
|
+
},
|
|
141
|
+
metadata: {
|
|
142
|
+
primary_table: primary_table,
|
|
143
|
+
table_columns: table_columns,
|
|
144
|
+
coverage: coverage.to_s,
|
|
145
|
+
sql: sql,
|
|
146
|
+
predicates: normalize_predicates(predicates)
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def matches_change?(change)
|
|
152
|
+
return false unless table_columns.key?(change.fetch(:table))
|
|
153
|
+
|
|
154
|
+
predicate_match = predicate_match(change)
|
|
155
|
+
return predicate_match unless predicate_match == UNKNOWN
|
|
156
|
+
|
|
157
|
+
return true if create_change?(change)
|
|
158
|
+
return true if delete_change?(change)
|
|
159
|
+
|
|
160
|
+
table_columns.fetch(change.fetch(:table)).intersect?(change.fetch(:changed_attributes, []))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def precision
|
|
164
|
+
@precision
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def collection_lookup_tables
|
|
168
|
+
table_columns.keys.sort
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def collection_lookup_columns
|
|
172
|
+
table_columns.flat_map do |table, columns|
|
|
173
|
+
columns.map { |column| [table, column] }
|
|
174
|
+
end.sort
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def predicate_match(change)
|
|
180
|
+
predicates = predicates_for_table(change.fetch(:table))
|
|
181
|
+
return UNKNOWN if predicates.empty?
|
|
182
|
+
|
|
183
|
+
old_match = values_match_predicates(change.fetch(:old_values, {}), predicates)
|
|
184
|
+
new_match = values_match_predicates(change.fetch(:new_values, {}), predicates)
|
|
185
|
+
return true if old_match == true || new_match == true
|
|
186
|
+
|
|
187
|
+
if old_match == false || new_match == false
|
|
188
|
+
return false if predicate_columns(predicates).intersect?(change.fetch(:changed_attributes, [])) ||
|
|
189
|
+
create_change?(change) ||
|
|
190
|
+
delete_change?(change)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
UNKNOWN
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def create_change?(change)
|
|
197
|
+
change.fetch(:type).to_s.include?("create")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def delete_change?(change)
|
|
201
|
+
type = change.fetch(:type).to_s
|
|
202
|
+
type.include?("delete") || type.include?("destroy")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def values_match_predicates(values, predicates)
|
|
206
|
+
values = stringify_keys(values)
|
|
207
|
+
group_matches = predicate_groups(predicates).map do |_group, group_predicates|
|
|
208
|
+
values_match_predicate_group(values, group_predicates)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
return true if group_matches.include?(true)
|
|
212
|
+
return false if group_matches.all?(false)
|
|
213
|
+
|
|
214
|
+
UNKNOWN
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def values_match_predicate_group(values, predicates)
|
|
218
|
+
return UNKNOWN unless predicates.all? { |predicate| values.key?(predicate.fetch(:column)) }
|
|
219
|
+
|
|
220
|
+
matches = predicates.map do |predicate|
|
|
221
|
+
predicate_matches_value?(predicate, values.fetch(predicate.fetch(:column)))
|
|
222
|
+
end
|
|
223
|
+
return UNKNOWN if matches.include?(UNKNOWN)
|
|
224
|
+
|
|
225
|
+
matches.all?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def predicate_matches_value?(predicate, value)
|
|
229
|
+
values = predicate.fetch(:values)
|
|
230
|
+
|
|
231
|
+
case predicate.fetch(:operator).to_s
|
|
232
|
+
when "eq", "in"
|
|
233
|
+
values.include?(value)
|
|
234
|
+
when "not_eq", "not_in"
|
|
235
|
+
!value.nil? && !values.include?(value)
|
|
236
|
+
else
|
|
237
|
+
UNKNOWN
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def predicate_groups(predicates)
|
|
242
|
+
predicates.group_by { |predicate| predicate.fetch(:group, "__all__").to_s }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def predicates_for_table(table)
|
|
246
|
+
predicates.select { |predicate| predicate.fetch(:table) == table.to_s }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def predicate_columns(predicates)
|
|
250
|
+
predicates.map { |predicate| predicate.fetch(:column) }.uniq
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def coverage
|
|
254
|
+
metadata.fetch(:coverage).to_sym.tap do |value|
|
|
255
|
+
raise ArgumentError, "unsupported Active Record collection coverage: #{value}" unless value == :columns
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def table_columns
|
|
260
|
+
metadata.fetch(:table_columns)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def predicates
|
|
264
|
+
metadata.fetch(:predicates)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def normalize_table_columns(value)
|
|
268
|
+
Dependencies.symbolize_keys(value).to_h do |table, columns|
|
|
269
|
+
[table.to_s, Array(columns).map(&:to_s).uniq.sort]
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def normalize_predicates(value)
|
|
274
|
+
Array(value).filter_map do |predicate|
|
|
275
|
+
predicate = Dependencies.symbolize_keys(predicate)
|
|
276
|
+
values = Array(predicate[:values])
|
|
277
|
+
next if predicate[:table].nil? || predicate[:column].nil? || values.empty?
|
|
278
|
+
|
|
279
|
+
normalized = {
|
|
280
|
+
table: predicate.fetch(:table).to_s,
|
|
281
|
+
column: predicate.fetch(:column).to_s,
|
|
282
|
+
operator: predicate.fetch(:operator, "eq").to_s,
|
|
283
|
+
values: values.uniq
|
|
284
|
+
}
|
|
285
|
+
normalized[:group] = predicate.fetch(:group).to_s if predicate.key?(:group)
|
|
286
|
+
normalized
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def stringify_keys(values)
|
|
291
|
+
values.to_h.transform_keys(&:to_s)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
class ActiveRecordQuery < ActiveRecordCollection
|
|
296
|
+
def initialize(primary_table:, table_columns:, coverage:, sql:, predicates: [])
|
|
297
|
+
super(
|
|
298
|
+
primary_table: primary_table,
|
|
299
|
+
table_columns: table_columns,
|
|
300
|
+
coverage: coverage,
|
|
301
|
+
sql: sql,
|
|
302
|
+
predicates: predicates,
|
|
303
|
+
source: :active_record_query,
|
|
304
|
+
precision: :query_predicate
|
|
305
|
+
)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
class Identity < Base
|
|
310
|
+
def initialize(source:, key:, value:, metadata: {}, partitioning: nil, absent_by_name: nil)
|
|
311
|
+
metadata = metadata.dup
|
|
312
|
+
metadata[:partitioning_identity] = partitioning unless partitioning.nil?
|
|
313
|
+
if absent_by_name&.any?
|
|
314
|
+
metadata[:identity_absent_by_name] = absent_by_name.to_h.transform_keys(&:to_s)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
super(
|
|
318
|
+
source: source,
|
|
319
|
+
key: { key: key, value: value },
|
|
320
|
+
metadata: metadata
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def identity?
|
|
325
|
+
true
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def identity_key
|
|
329
|
+
{ source: source, key: key.fetch(:key), value: key.fetch(:value) }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def nil_identity?
|
|
333
|
+
return true if key.fetch(:value).nil?
|
|
334
|
+
|
|
335
|
+
value_class = metadata[:value_class] || metadata["value_class"]
|
|
336
|
+
value_class.to_s == "NilClass"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def visibility
|
|
340
|
+
:identity_bound
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def precision
|
|
344
|
+
:identity
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
class WardenUser < Identity
|
|
349
|
+
def initialize(scope:, user:, partitioning: nil, absent_by_name: nil)
|
|
350
|
+
super(
|
|
351
|
+
source: :warden_user,
|
|
352
|
+
key: scope.to_s,
|
|
353
|
+
value: Dependencies.model_identity(user),
|
|
354
|
+
metadata: Dependencies.model_metadata(user).merge(scope: scope.to_s),
|
|
355
|
+
partitioning: partitioning,
|
|
356
|
+
absent_by_name: absent_by_name
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
class CurrentAttribute < Identity
|
|
362
|
+
def initialize(owner:, name:, value:, partitioning: nil, absent_by_name: nil)
|
|
363
|
+
super(
|
|
364
|
+
source: :current_attribute,
|
|
365
|
+
key: "#{owner}.#{name}",
|
|
366
|
+
value: Dependencies.canonical_identity(value),
|
|
367
|
+
metadata: { owner: owner.to_s, name: name.to_s },
|
|
368
|
+
partitioning: partitioning,
|
|
369
|
+
absent_by_name: absent_by_name
|
|
370
|
+
)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
class SessionValue < Identity
|
|
375
|
+
def initialize(key:, value:, partitioning: nil, absent_by_name: nil)
|
|
376
|
+
super(
|
|
377
|
+
source: :session,
|
|
378
|
+
key: key.to_s,
|
|
379
|
+
value: Dependencies.private_fingerprint(value),
|
|
380
|
+
metadata: { key: key.to_s, value_class: value.class.name },
|
|
381
|
+
partitioning: partitioning,
|
|
382
|
+
absent_by_name: absent_by_name
|
|
383
|
+
)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
class CookieValue < Identity
|
|
388
|
+
def initialize(key:, value:, partitioning: nil, absent_by_name: nil)
|
|
389
|
+
super(
|
|
390
|
+
source: :cookie,
|
|
391
|
+
key: key.to_s,
|
|
392
|
+
value: Dependencies.private_fingerprint(value),
|
|
393
|
+
metadata: { key: key.to_s, value_class: value.class.name },
|
|
394
|
+
partitioning: partitioning,
|
|
395
|
+
absent_by_name: absent_by_name
|
|
396
|
+
)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
class RequestValue < Identity
|
|
401
|
+
# Request values that are stable for a deployment/connection rather than a viewer.
|
|
402
|
+
# They never partition subscribers, but their fingerprints are folded into shared
|
|
403
|
+
# stream names (see SharedStreams.deployment_signature_for) so viewers with
|
|
404
|
+
# different values can never share a stream.
|
|
405
|
+
DEPLOYMENT_STABLE_KEYS = %w[host port protocol ssl request_method].freeze
|
|
406
|
+
|
|
407
|
+
def initialize(key:, value:)
|
|
408
|
+
super(
|
|
409
|
+
source: :request,
|
|
410
|
+
key: key.to_s,
|
|
411
|
+
value: Dependencies.private_fingerprint(value),
|
|
412
|
+
metadata: { key: key.to_s, value_class: value.class.name },
|
|
413
|
+
partitioning: DEPLOYMENT_STABLE_KEYS.include?(key.to_s) ? false : nil
|
|
414
|
+
)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
class Unknown < Base
|
|
419
|
+
def initialize(source:, metadata: {})
|
|
420
|
+
super(
|
|
421
|
+
source: source,
|
|
422
|
+
key: Digest::SHA256.hexdigest(metadata.inspect)[0, 16],
|
|
423
|
+
metadata: metadata
|
|
424
|
+
)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def visibility
|
|
428
|
+
:private
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
class Restored < Base
|
|
433
|
+
def initialize(source:, key:, metadata:, visibility:, precision:)
|
|
434
|
+
super(source: source, key: key, metadata: metadata)
|
|
435
|
+
@visibility = visibility.to_sym
|
|
436
|
+
@precision = precision.to_sym
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def identity?
|
|
440
|
+
visibility == :identity_bound
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def identity_key
|
|
444
|
+
return unless identity?
|
|
445
|
+
|
|
446
|
+
{ source: source, key: key.fetch(:key), value: key.fetch(:value) }
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
attr_reader :visibility, :precision
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
module_function
|
|
453
|
+
|
|
454
|
+
def from_h(snapshot)
|
|
455
|
+
snapshot = symbolize_keys(snapshot)
|
|
456
|
+
source = snapshot.fetch(:source)
|
|
457
|
+
key = symbolize_keys(snapshot.fetch(:key))
|
|
458
|
+
metadata = symbolize_keys(snapshot.fetch(:metadata))
|
|
459
|
+
|
|
460
|
+
case source.to_sym
|
|
461
|
+
when :active_record_attribute
|
|
462
|
+
ActiveRecordAttribute.new(
|
|
463
|
+
table: key.fetch(:table),
|
|
464
|
+
id: key.fetch(:id),
|
|
465
|
+
attribute: key.fetch(:attribute),
|
|
466
|
+
model: metadata[:model],
|
|
467
|
+
scope: key[:scope]
|
|
468
|
+
)
|
|
469
|
+
when :active_record_collection, :active_record_query
|
|
470
|
+
dependency_class = source.to_sym == :active_record_query ? ActiveRecordQuery : ActiveRecordCollection
|
|
471
|
+
dependency_class.new(
|
|
472
|
+
primary_table: metadata.fetch(:primary_table),
|
|
473
|
+
table_columns: metadata.fetch(:table_columns),
|
|
474
|
+
coverage: metadata.fetch(:coverage),
|
|
475
|
+
sql: metadata.fetch(:sql),
|
|
476
|
+
predicates: metadata.fetch(:predicates)
|
|
477
|
+
)
|
|
478
|
+
else
|
|
479
|
+
if snapshot.fetch(:visibility).to_sym == :identity_bound
|
|
480
|
+
Identity.new(
|
|
481
|
+
source: source,
|
|
482
|
+
key: key.fetch(:key),
|
|
483
|
+
value: key.fetch(:value),
|
|
484
|
+
metadata: metadata
|
|
485
|
+
)
|
|
486
|
+
else
|
|
487
|
+
Restored.new(
|
|
488
|
+
source: source,
|
|
489
|
+
key: key,
|
|
490
|
+
metadata: metadata,
|
|
491
|
+
visibility: snapshot.fetch(:visibility),
|
|
492
|
+
precision: snapshot.fetch(:precision)
|
|
493
|
+
)
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def model_identity(value)
|
|
499
|
+
return nil unless value
|
|
500
|
+
|
|
501
|
+
if value.respond_to?(:id) && value.class.respond_to?(:name)
|
|
502
|
+
{ model: value.class.name, id: value.id }
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def model_metadata(value)
|
|
507
|
+
return {} unless value
|
|
508
|
+
|
|
509
|
+
{
|
|
510
|
+
model: value.class.name,
|
|
511
|
+
table: value.class.respond_to?(:table_name) ? value.class.table_name : nil,
|
|
512
|
+
id: value.respond_to?(:id) ? value.id : nil
|
|
513
|
+
}.compact
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def canonical_identity(value)
|
|
517
|
+
case value
|
|
518
|
+
when nil, true, false, Numeric, String, Symbol
|
|
519
|
+
value
|
|
520
|
+
else
|
|
521
|
+
model_identity(value) || private_fingerprint(value)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def partitioning_identity?(dependency)
|
|
526
|
+
return false unless dependency.identity?
|
|
527
|
+
|
|
528
|
+
flag = metadata_value(dependency, :partitioning_identity)
|
|
529
|
+
return flag if [true, false].include?(flag)
|
|
530
|
+
|
|
531
|
+
!nil_identity?(dependency)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def deployment_stable_request?(dependency)
|
|
535
|
+
return false unless dependency.identity?
|
|
536
|
+
return false unless dependency.source.to_s == "request"
|
|
537
|
+
|
|
538
|
+
RequestValue::DEPLOYMENT_STABLE_KEYS.include?(dependency.key.fetch(:key).to_s)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def identity_absent_for?(dependency, name)
|
|
542
|
+
absent_by_name = metadata_value(dependency, :identity_absent_by_name) || {}
|
|
543
|
+
absent_by_name = absent_by_name.transform_keys(&:to_s) if absent_by_name.respond_to?(:transform_keys)
|
|
544
|
+
return absent_by_name.fetch(name.to_s) if absent_by_name.key?(name.to_s)
|
|
545
|
+
|
|
546
|
+
!partitioning_identity?(dependency)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def nil_identity?(dependency)
|
|
550
|
+
dependency.respond_to?(:nil_identity?) && dependency.nil_identity?
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def metadata_value(dependency, key)
|
|
554
|
+
return unless dependency.respond_to?(:metadata)
|
|
555
|
+
|
|
556
|
+
if dependency.metadata.key?(key)
|
|
557
|
+
dependency.metadata.fetch(key)
|
|
558
|
+
elsif dependency.metadata.key?(key.to_s)
|
|
559
|
+
dependency.metadata.fetch(key.to_s)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def private_fingerprint(value)
|
|
564
|
+
Digest::SHA256.hexdigest(JSON.generate(private_fingerprint_payload(value)))[0, 16]
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def private_fingerprint_payload(value)
|
|
568
|
+
case value
|
|
569
|
+
when nil, true, false, Numeric, String
|
|
570
|
+
[value.class.name, value]
|
|
571
|
+
when Symbol
|
|
572
|
+
["Symbol", value.to_s]
|
|
573
|
+
when Array
|
|
574
|
+
["Array", value.map { |item| private_fingerprint_payload(item) }]
|
|
575
|
+
when Hash
|
|
576
|
+
entries = value.keys.sort_by { |key| JSON.generate(private_fingerprint_payload(key)) }.map do |key|
|
|
577
|
+
[private_fingerprint_payload(key), private_fingerprint_payload(value.fetch(key))]
|
|
578
|
+
end
|
|
579
|
+
["Hash", entries]
|
|
580
|
+
else
|
|
581
|
+
identity = model_identity(value)
|
|
582
|
+
identity ? ["Model", identity] : ["Object", value.class.name, value.inspect]
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def symbolize_keys(value)
|
|
587
|
+
case value
|
|
588
|
+
when Hash
|
|
589
|
+
value.each_with_object({}) do |(key, nested_value), result|
|
|
590
|
+
normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
|
591
|
+
result[normalized_key] = symbolize_keys(nested_value)
|
|
592
|
+
end
|
|
593
|
+
when Array
|
|
594
|
+
value.map { |nested_value| symbolize_keys(nested_value) }
|
|
595
|
+
else
|
|
596
|
+
value
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
end
|