trellis-pg 0.1.0-aarch64-linux-musl
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/README.md +620 -0
- data/lib/trellis/3.3/trellis_ruby.so +0 -0
- data/lib/trellis/3.4/trellis_ruby.so +0 -0
- data/lib/trellis/4.0/trellis_ruby.so +0 -0
- data/lib/trellis/error.rb +103 -0
- data/lib/trellis/instance.rb +561 -0
- data/lib/trellis/metrics.rb +38 -0
- data/lib/trellis/migration.rb +186 -0
- data/lib/trellis/pg.rb +142 -0
- data/lib/trellis/railtie.rb +293 -0
- data/lib/trellis/schema_dump.rb +278 -0
- data/lib/trellis/test_support.rb +161 -0
- data/lib/trellis/values.rb +386 -0
- data/lib/trellis/version.rb +7 -0
- metadata +67 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trellis
|
|
4
|
+
# Times cross the native boundary as signed microseconds since the Unix
|
|
5
|
+
# epoch (ADR-0010 decision 4) and become Times here, in Ruby, not in Rust.
|
|
6
|
+
# Not part of the public API.
|
|
7
|
+
module EpochMicros
|
|
8
|
+
# The largest and smallest epoch microseconds the engine takes (an i64).
|
|
9
|
+
RANGE = (-(2**63))..((2**63) - 1)
|
|
10
|
+
|
|
11
|
+
def self.to_time(micros)
|
|
12
|
+
::Time.at(0, micros, :microsecond, in: "UTC")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# `time` in whole microseconds, truncated toward the past, or nil if the
|
|
16
|
+
# engine can't represent it.
|
|
17
|
+
def self.from_time(time)
|
|
18
|
+
micros = (time.to_r * 1_000_000).floor
|
|
19
|
+
micros if RANGE.cover?(micros)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A registered transform definition, as Trellis.definitions lists it.
|
|
24
|
+
# target_table (fully qualified, "schema.table") identifies the transform;
|
|
25
|
+
# status is a symbol (:waiting_to_backfill, :backfilling, :live, ...) from
|
|
26
|
+
# a closed set, never one made from a string the database returned;
|
|
27
|
+
# created_at is when it was registered (a Time). backfill_failure is nil unless its build keeps
|
|
28
|
+
# failing. halt is nil unless the drain, or its build's merge, halted on
|
|
29
|
+
# it: a CaptureFailure of kind :halt, the definition :paused until the
|
|
30
|
+
# cause is fixed and it is resumed. Listing the definitions and looking for
|
|
31
|
+
# a halt is the health check for halts.
|
|
32
|
+
DefinitionSummary = Data.define(:target_table, :status, :created_at, :backfill_failure, :halt) do
|
|
33
|
+
def self.from_native(hash)
|
|
34
|
+
failure = hash[:backfill_failure]
|
|
35
|
+
halt = hash[:halt]
|
|
36
|
+
new(**hash.except(:created_at_micros, :backfill_failure, :halt),
|
|
37
|
+
created_at: EpochMicros.to_time(hash.fetch(:created_at_micros)),
|
|
38
|
+
backfill_failure: failure && BackfillFailure.from_native(failure),
|
|
39
|
+
halt: halt && CaptureFailure.from_native(halt))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A definition's status, as Trellis.status returns it.
|
|
44
|
+
#
|
|
45
|
+
# status is one of :waiting_to_backfill, :backfilling, :catching_up,
|
|
46
|
+
# :live, :quarantined or :paused. A newly defined transform starts
|
|
47
|
+
# :waiting_to_backfill and reaches :live once some process in the fleet
|
|
48
|
+
# runs the staging worker and drain threads (see Trellis.connect).
|
|
49
|
+
#
|
|
50
|
+
# The other fields say what the definition is stuck on, and are nil when
|
|
51
|
+
# nothing holds it up:
|
|
52
|
+
# - backfill_failure: its build keeps failing (one of its build chunks).
|
|
53
|
+
# - capture_wait: installing or widening capture on a table it reads waits
|
|
54
|
+
# for a lock another session holds. It clears once that session lets go.
|
|
55
|
+
# - capture_failure: capture of a table it reads is broken, or the drain
|
|
56
|
+
# halted on it. A schema change paused it (resume it once fixed),
|
|
57
|
+
# installing capture keeps failing (it clears once the cause is fixed),
|
|
58
|
+
# or, with kind :halt, a failure no retry gets past reached it, so the
|
|
59
|
+
# drain (or its build's merge) paused it and what depends on it (resume
|
|
60
|
+
# it once fixed). A definition an operator paused has none.
|
|
61
|
+
# - held_keys: it holds keys in quarantine (a HeldKeys): source keys whose
|
|
62
|
+
# changes kept failing in its apply, so it leaves them out and their
|
|
63
|
+
# target rows stay as they were, whatever its status, :live included.
|
|
64
|
+
# Trellis.sample_quarantined lists them, and Trellis.release_key releases
|
|
65
|
+
# one once its cause is fixed.
|
|
66
|
+
# - drain_failure: the drain keeps failing on a page holding changes to a
|
|
67
|
+
# table it reads, with nothing charged or paused (a DrainFailure), so its
|
|
68
|
+
# target stops short of that page, whatever its status, :live included.
|
|
69
|
+
# Every drain pass retries the page; fix the cause its error names. A
|
|
70
|
+
# paused or quarantined definition has none.
|
|
71
|
+
# Every process sees them, whichever one runs the staging worker.
|
|
72
|
+
#
|
|
73
|
+
# source_index_gaps is a warning, never a hold-up: the SourceIndexGaps for
|
|
74
|
+
# the source indexes a GROUP BY or JOIN target needs and lacks ([] when it
|
|
75
|
+
# needs none or they exist). Define and a resume refuse a definition
|
|
76
|
+
# without them, so a gap is an index dropped since, or one the definition
|
|
77
|
+
# opted out of with WITHOUT SOURCE INDEX. Nothing is paused, but
|
|
78
|
+
# Trellis.self_check refuses to walk the target unless it opted out, when
|
|
79
|
+
# the walk scans the source once per page.
|
|
80
|
+
#
|
|
81
|
+
# correcting lists the CorrectingNodes of its partitioned source it is being
|
|
82
|
+
# corrected for, oldest first ([] when none): a partition was attached,
|
|
83
|
+
# detached, dropped or truncated, and the definition re-derives the keys
|
|
84
|
+
# it touched. It reads :backfilling while one is listed, and :live again
|
|
85
|
+
# once they are re-derived and folded into its target. A paused or
|
|
86
|
+
# quarantined definition lists none.
|
|
87
|
+
#
|
|
88
|
+
# paused_upstream_columns names the paused columns of upstream 1-1 targets
|
|
89
|
+
# that make a :live definition report :catching_up, each "target.column"
|
|
90
|
+
# ([] when none). A column is paused when its fuse tripped or PAUSE
|
|
91
|
+
# TRANSFORM t.col froze it; an aggregate or JOIN reading it keeps applying
|
|
92
|
+
# over its frozen values, so it is stale against the source. Nothing is
|
|
93
|
+
# paused or rebuilt: resuming the column writes through to the reader and
|
|
94
|
+
# it reads :live again.
|
|
95
|
+
Status = Data.define(:status, :backfill_failure, :capture_wait, :capture_failure,
|
|
96
|
+
:held_keys, :drain_failure, :source_index_gaps,
|
|
97
|
+
:correcting, :paused_upstream_columns) do
|
|
98
|
+
def self.from_native(hash)
|
|
99
|
+
failure = hash[:backfill_failure]
|
|
100
|
+
wait = hash[:capture_wait]
|
|
101
|
+
capture_failure = hash[:capture_failure]
|
|
102
|
+
held_keys = hash[:held_keys]
|
|
103
|
+
drain_failure = hash[:drain_failure]
|
|
104
|
+
new(status: hash[:status],
|
|
105
|
+
backfill_failure: failure && BackfillFailure.from_native(failure),
|
|
106
|
+
capture_wait: wait && CaptureWait.from_native(wait),
|
|
107
|
+
capture_failure: capture_failure && CaptureFailure.from_native(capture_failure),
|
|
108
|
+
held_keys: held_keys && HeldKeys.from_native(held_keys),
|
|
109
|
+
drain_failure: drain_failure && DrainFailure.from_native(drain_failure),
|
|
110
|
+
source_index_gaps: hash.fetch(:source_index_gaps).map { |g| SourceIndexGap.new(**g) },
|
|
111
|
+
correcting: hash.fetch(:correcting).map { |c| CorrectingNode.from_native(c) },
|
|
112
|
+
paused_upstream_columns: hash.fetch(:paused_upstream_columns))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A node of a partitioned source a definition is being corrected for, as
|
|
117
|
+
# Trellis.status reports it: the schema-qualified node (node), when the
|
|
118
|
+
# staging worker queued its correction (since, a Time), and the keys still
|
|
119
|
+
# to re-derive (keys_remaining, nil until the correction has counted them;
|
|
120
|
+
# an estimate while it reads the node's own rows).
|
|
121
|
+
CorrectingNode = Data.define(:node, :since, :keys_remaining) do
|
|
122
|
+
def self.from_native(hash)
|
|
123
|
+
new(node: hash.fetch(:node),
|
|
124
|
+
since: EpochMicros.to_time(hash.fetch(:since_micros)),
|
|
125
|
+
keys_remaining: hash.fetch(:keys_remaining))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A source index a GROUP BY or JOIN target needs and lacks, as
|
|
130
|
+
# Trellis.status reports it: the qualified table (table), the columns the
|
|
131
|
+
# index should lead with (columns), the part of the definition that needs it
|
|
132
|
+
# (role: :group_by for the GROUP BY columns, :join_from or :join_joined for a
|
|
133
|
+
# JOIN's ON columns on its from or joined side), whether the definition opted out with
|
|
134
|
+
# WITHOUT SOURCE INDEX (opted_out), and a create index statement to run
|
|
135
|
+
# (fix). Trellis doesn't create indexes on your tables.
|
|
136
|
+
SourceIndexGap = Data.define(:table, :columns, :role, :opted_out, :fix)
|
|
137
|
+
|
|
138
|
+
# A drain page that keeps failing with nothing charged or paused, as
|
|
139
|
+
# Trellis.status and Trellis.self_check report it: the segment whose page
|
|
140
|
+
# fails (seg_seq), the qualified source tables it holds changes to
|
|
141
|
+
# (tables), the latest failure's error and its SQLSTATE (sqlstate, nil when
|
|
142
|
+
# it didn't come from Postgres), when a drain first and last failed on it
|
|
143
|
+
# (since and last_seen, Times), and how many drain passes have (attempts).
|
|
144
|
+
DrainFailure = Data.define(:seg_seq, :tables, :error, :sqlstate, :since, :last_seen,
|
|
145
|
+
:attempts) do
|
|
146
|
+
def self.from_native(hash)
|
|
147
|
+
new(**hash.except(:since_micros, :last_seen_micros),
|
|
148
|
+
since: EpochMicros.to_time(hash.fetch(:since_micros)),
|
|
149
|
+
last_seen: EpochMicros.to_time(hash.fetch(:last_seen_micros)))
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The keys a definition holds in quarantine, as Trellis.status and
|
|
154
|
+
# Trellis.self_check report them: how many (count), and when the one held
|
|
155
|
+
# longest was last poisoned (oldest_poisoned_at, a Time).
|
|
156
|
+
HeldKeys = Data.define(:count, :oldest_poisoned_at) do
|
|
157
|
+
def self.from_native(hash)
|
|
158
|
+
new(count: hash.fetch(:count),
|
|
159
|
+
oldest_poisoned_at: EpochMicros.to_time(hash.fetch(:oldest_poisoned_at_micros)))
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# What a definition's capture waits on: the staging worker's install,
|
|
164
|
+
# widen or uninstall (operation, a String) of the capture triggers on
|
|
165
|
+
# table needs lock_mode, and another session holds or is queued for a
|
|
166
|
+
# conflicting lock. blockers has one line per such session. waiting_since
|
|
167
|
+
# and observed_at are Times.
|
|
168
|
+
CaptureWait = Data.define(:table, :operation, :lock_mode, :waiting_since, :observed_at,
|
|
169
|
+
:blockers) do
|
|
170
|
+
def self.from_native(hash)
|
|
171
|
+
new(**hash.except(:waiting_since_micros, :observed_at_micros),
|
|
172
|
+
waiting_since: EpochMicros.to_time(hash.fetch(:waiting_since_micros)),
|
|
173
|
+
observed_at: EpochMicros.to_time(hash.fetch(:observed_at_micros)))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Why capture of source_table is broken, or why the drain (or a build's
|
|
178
|
+
# merge) halted on the definition: kind is :capture or :halt, error is a
|
|
179
|
+
# sentence naming the cause, columns the columns it is about (empty when it
|
|
180
|
+
# isn't about a column, and for a halt), and detected_at (a Time) when it
|
|
181
|
+
# was first found.
|
|
182
|
+
CaptureFailure = Data.define(:kind, :source_table, :columns, :error, :detected_at) do
|
|
183
|
+
def self.from_native(hash)
|
|
184
|
+
new(**hash.except(:detected_at_micros),
|
|
185
|
+
detected_at: EpochMicros.to_time(hash.fetch(:detected_at_micros)))
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Why a definition's build isn't finishing: the build of source_table has
|
|
190
|
+
# failed `attempts` times, the latest being last_error, and won't be
|
|
191
|
+
# retried before next_attempt_at (a Time). It retries on its own, except
|
|
192
|
+
# that a build failing for a reason no row explains pauses the definition
|
|
193
|
+
# after a few attempts (next_attempt_at is then when it paused). Fix the
|
|
194
|
+
# cause, resume a paused definition, and the next attempt goes through.
|
|
195
|
+
BackfillFailure = Data.define(:source_table, :attempts, :last_error, :next_attempt_at) do
|
|
196
|
+
def self.from_native(hash)
|
|
197
|
+
new(**hash.except(:next_attempt_at_micros),
|
|
198
|
+
next_attempt_at: EpochMicros.to_time(hash.fetch(:next_attempt_at_micros)))
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# What Trellis.apply did. `kind` says which statement form ran, and each
|
|
203
|
+
# other field is set only for the kinds that carry it (nil otherwise):
|
|
204
|
+
#
|
|
205
|
+
# - :transform_defined: a TRANSFORM statement registered the transform
|
|
206
|
+
# writing `target_table` (fully qualified), at :waiting_to_backfill.
|
|
207
|
+
# - :paused: a PAUSE TRANSFORM statement froze its subject, or found it
|
|
208
|
+
# already frozen.
|
|
209
|
+
# - :resumed: a RESUME TRANSFORM statement unfroze its subject. `columns`
|
|
210
|
+
# lists every column a column resume resumed, as "transform.column": the
|
|
211
|
+
# one it named, then any dependent whose pause was only that one's
|
|
212
|
+
# cascade. It is [] for a whole-transform resume, which drops the
|
|
213
|
+
# transform to :waiting_to_backfill to rebuild.
|
|
214
|
+
# - :dropped: a DROP statement removed its subject, or found it already
|
|
215
|
+
# gone.
|
|
216
|
+
# - :altered: an ALTER TRANSFORM statement edited the transform writing `target_table`. `added`,
|
|
217
|
+
# `dropped` and `altered` name only the fields this call changed.
|
|
218
|
+
# - :unknown: the statement was applied, but its outcome is newer than this
|
|
219
|
+
# version of the binding, which has no shape for it. It is still a
|
|
220
|
+
# success: don't retry the statement, which would apply it twice.
|
|
221
|
+
#
|
|
222
|
+
# It pattern-matches by kind:
|
|
223
|
+
#
|
|
224
|
+
# case Trellis.apply("RESUME TRANSFORM order_totals.total")
|
|
225
|
+
# in { kind: :resumed, columns: } then puts "resumed #{columns.join(', ')}"
|
|
226
|
+
# end
|
|
227
|
+
Applied = Data.define(:kind, :target_table, :columns, :added, :dropped, :altered) do
|
|
228
|
+
def self.from_native(hash)
|
|
229
|
+
new(**hash)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# One target's quarantine state, as Trellis.quarantined lists it and
|
|
234
|
+
# Trellis.quarantine_status reports it.
|
|
235
|
+
#
|
|
236
|
+
# target is an address: a transform's bare target table name
|
|
237
|
+
# ("order_totals") for the whole transform, or "order_totals.total" for one
|
|
238
|
+
# of its columns. Pass it straight back to Trellis.quarantine_status,
|
|
239
|
+
# Trellis.sample_quarantined, or a RESUME TRANSFORM statement.
|
|
240
|
+
#
|
|
241
|
+
# state is :live or :paused for a column; a whole transform reports its
|
|
242
|
+
# lifecycle status (see Status). paused_at (a Time) and last_error are set
|
|
243
|
+
# only for a paused column: when its pause tripped, and its most recent
|
|
244
|
+
# failure (nil for a pause that cascaded from an upstream column rather
|
|
245
|
+
# than failing on its own).
|
|
246
|
+
QuarantineEntry = Data.define(:target, :state, :paused_at, :last_error) do
|
|
247
|
+
def self.from_native(hash)
|
|
248
|
+
micros = hash[:paused_at_micros]
|
|
249
|
+
new(**hash.except(:paused_at_micros), paused_at: micros && EpochMicros.to_time(micros))
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# One source row the apply path gave up on, as Trellis.poisoned_since
|
|
254
|
+
# lists it: the transform it's held for (every other transform reading the
|
|
255
|
+
# row keeps applying it), its fully-qualified src_table, its key as Trellis
|
|
256
|
+
# renders it, the error that poisoned it, and when (poisoned_at, a Time).
|
|
257
|
+
PoisonEntry = Data.define(:transform, :src_table, :key, :last_error, :poisoned_at) do
|
|
258
|
+
def self.from_native(hash)
|
|
259
|
+
new(**hash.except(:poisoned_at_micros),
|
|
260
|
+
poisoned_at: EpochMicros.to_time(hash.fetch(:poisoned_at_micros)))
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# One quarantined source row, as Trellis.sample_quarantined pages them: its
|
|
265
|
+
# fully-qualified src_table, its key as Trellis renders it, and the error
|
|
266
|
+
# it failed with.
|
|
267
|
+
PoisonSample = Data.define(:src_table, :key, :error_message)
|
|
268
|
+
|
|
269
|
+
# One page of Trellis.sample_quarantined, ordered by (src_table, key).
|
|
270
|
+
#
|
|
271
|
+
# next_cursor is opaque: pass it back as the `after:` option for the next
|
|
272
|
+
# page, and don't read or build one. The last page is the one with fewer
|
|
273
|
+
# rows than the limit. An empty page hands back the cursor it was asked
|
|
274
|
+
# for, so polling from where you got to never falls back to the first page;
|
|
275
|
+
# next_cursor is nil only for an empty first page.
|
|
276
|
+
SamplePage = Data.define(:samples, :next_cursor) do
|
|
277
|
+
def self.from_native(hash)
|
|
278
|
+
new(samples: hash.fetch(:samples).map { |sample| PoisonSample.new(**sample) },
|
|
279
|
+
next_cursor: hash.fetch(:next_cursor))
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# A background check of a target table, as Trellis.self_check starts it and
|
|
284
|
+
# Trellis.self_check_job reads it back.
|
|
285
|
+
#
|
|
286
|
+
# - id: what Trellis.self_check_job takes.
|
|
287
|
+
# - target: the audited target's bare table name.
|
|
288
|
+
# - mode: :standard or :strict, as the job was started. A second
|
|
289
|
+
# Trellis.self_check of a target whose job is still running returns that
|
|
290
|
+
# job, so its mode is the first call's.
|
|
291
|
+
# - state: :queued (no worker has taken it up yet; with no drain worker
|
|
292
|
+
# anywhere in the fleet it stays so), :running, then :done, :failed
|
|
293
|
+
# (error says why) or :cancelled (the worker running it shut down; error
|
|
294
|
+
# says so). Only :done carries a report.
|
|
295
|
+
# - rows_compared: the distinct keys the pages so far compared; it moves
|
|
296
|
+
# while the job is :running.
|
|
297
|
+
# - report: the SelfCheckReport, once the job is :done; nil before.
|
|
298
|
+
# - error: why a :failed or :cancelled job ended; nil otherwise.
|
|
299
|
+
#
|
|
300
|
+
# A job ends when its transform is dropped, and Trellis.self_check_job then
|
|
301
|
+
# returns nil for it.
|
|
302
|
+
SelfCheckJob = Data.define(:id, :target, :mode, :state, :rows_compared, :report, :error) do
|
|
303
|
+
def self.from_native(hash)
|
|
304
|
+
report = hash[:report]
|
|
305
|
+
new(**hash, report: report && SelfCheckReport.from_native(report))
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Whether the job has ended, so polling it again changes nothing.
|
|
309
|
+
def finished?
|
|
310
|
+
%i[done failed cancelled].include?(state)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# What a finished Trellis.self_check job found auditing the whole of a
|
|
315
|
+
# target table against a fresh recompute of its definition from the source.
|
|
316
|
+
#
|
|
317
|
+
# - outcome: :converged (every compared cell, row and column matched),
|
|
318
|
+
# :not_caught_up (the target didn't catch up within timeout_ms: not a
|
|
319
|
+
# verdict on correctness), :not_live (the transform isn't :live, so
|
|
320
|
+
# nothing was awaited or compared; see status), or :diverged (see
|
|
321
|
+
# divergences).
|
|
322
|
+
# - status: the transform's status when outcome is :not_live (a rebuild
|
|
323
|
+
# Trellis.request_backfill starts reads :backfilling from the call's
|
|
324
|
+
# return, so poll status until it is :live, then check again); nil for
|
|
325
|
+
# every other outcome.
|
|
326
|
+
# - divergences: the Divergences found; [] unless outcome is :diverged.
|
|
327
|
+
# - rows_compared: the distinct keys compared.
|
|
328
|
+
# - truncated: true when the job stopped before the end of the target's
|
|
329
|
+
# keys: it found 1,000 divergences, or the transform stopped being :live,
|
|
330
|
+
# stopped catching up or lost its capture partway. rows_compared says how
|
|
331
|
+
# far it got.
|
|
332
|
+
# - checked_through: the watermark the outcome holds through, a token
|
|
333
|
+
# Trellis.await_converged takes.
|
|
334
|
+
# - held_keys: whatever the outcome, a HeldKeys while the transform holds
|
|
335
|
+
# keys in quarantine, nil otherwise; read when the job is polled. Their
|
|
336
|
+
# target rows are ones the audit can't vouch for, and a key with held
|
|
337
|
+
# changes keeps the target from catching up, so the outcome is
|
|
338
|
+
# :not_caught_up until it is released.
|
|
339
|
+
# - drain_failures: whatever the outcome, every DrainFailure open on the
|
|
340
|
+
# instance when the job is polled, oldest first, whichever definitions
|
|
341
|
+
# read its tables; [] when there is none. Each holds back the targets of
|
|
342
|
+
# the tables it holds changes to, and with them the convergence the audit
|
|
343
|
+
# waits on.
|
|
344
|
+
SelfCheckReport = Data.define(:target, :outcome, :status, :divergences, :rows_compared,
|
|
345
|
+
:truncated, :checked_through, :held_keys, :drain_failures) do
|
|
346
|
+
def self.from_native(hash)
|
|
347
|
+
held_keys = hash[:held_keys]
|
|
348
|
+
new(**hash, divergences: hash.fetch(:divergences).map { |d| Divergence.new(**d) },
|
|
349
|
+
held_keys: held_keys && HeldKeys.from_native(held_keys),
|
|
350
|
+
drain_failures: hash.fetch(:drain_failures).map { |f| DrainFailure.from_native(f) })
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# One thing Trellis.self_check found out of step, by kind:
|
|
355
|
+
#
|
|
356
|
+
# - :cell: the row exists on both sides but `column` differs at `key`.
|
|
357
|
+
# `persisted` is the target table's value and `recomputed` the fresh
|
|
358
|
+
# recompute's, both as Postgres renders them as text (nil for NULL).
|
|
359
|
+
# - :missing_row: the recompute produced `key`, but the target has no row
|
|
360
|
+
# for it.
|
|
361
|
+
# - :extra_row: the target has a row for `key` that the recompute didn't
|
|
362
|
+
# produce.
|
|
363
|
+
# - :missing_column: the definition expects `column`, but the target table
|
|
364
|
+
# doesn't have it.
|
|
365
|
+
# - :extra_column: the target table has `column`, but the definition
|
|
366
|
+
# doesn't expect it.
|
|
367
|
+
# - :capture: a table the target is computed from isn't being captured as
|
|
368
|
+
# Trellis installed it (a capture trigger missing, disabled or calling
|
|
369
|
+
# the wrong function, a capture function missing or with the wrong
|
|
370
|
+
# owner, the Trellis role missing a privilege, or the table joined a
|
|
371
|
+
# partition or inheritance hierarchy). `detail` says what, and `table`
|
|
372
|
+
# names the table (nil for a missing privilege). self_check reports these
|
|
373
|
+
# before, and instead of, comparing any rows.
|
|
374
|
+
#
|
|
375
|
+
# Fields a kind doesn't carry are nil.
|
|
376
|
+
Divergence = Data.define(:kind, :key, :column, :persisted, :recomputed, :table, :detail)
|
|
377
|
+
|
|
378
|
+
# The configuration this process's handle connected with, as Trellis.config
|
|
379
|
+
# returns it: connect's schema and target_schema, and the handle's call pool
|
|
380
|
+
# as it was built: max_call_pool_size and call_pool_wait_timeout_ms.
|
|
381
|
+
#
|
|
382
|
+
# Not the url: a connection string can carry a password, and a Config is
|
|
383
|
+
# the kind of value that ends up in a log line whole. The caller already
|
|
384
|
+
# has the url it connected with.
|
|
385
|
+
Config = Data.define(:schema, :target_schema, :max_call_pool_size, :call_pool_wait_timeout_ms)
|
|
386
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: trellis-pg
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: aarch64-linux-musl
|
|
6
|
+
authors:
|
|
7
|
+
- Salesforce
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-31 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
A native extension over the trellis crate's BlockingTrellis, so a Rails
|
|
15
|
+
app can define and run Trellis's streaming transforms in its own process,
|
|
16
|
+
against its own Postgres database, without deploying a separate service.
|
|
17
|
+
email:
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/trellis/3.3/trellis_ruby.so
|
|
24
|
+
- lib/trellis/3.4/trellis_ruby.so
|
|
25
|
+
- lib/trellis/4.0/trellis_ruby.so
|
|
26
|
+
- lib/trellis/error.rb
|
|
27
|
+
- lib/trellis/instance.rb
|
|
28
|
+
- lib/trellis/metrics.rb
|
|
29
|
+
- lib/trellis/migration.rb
|
|
30
|
+
- lib/trellis/pg.rb
|
|
31
|
+
- lib/trellis/railtie.rb
|
|
32
|
+
- lib/trellis/schema_dump.rb
|
|
33
|
+
- lib/trellis/test_support.rb
|
|
34
|
+
- lib/trellis/values.rb
|
|
35
|
+
- lib/trellis/version.rb
|
|
36
|
+
homepage: https://github.com/salesforce-misc/trellis/tree/main/clients/ruby
|
|
37
|
+
licenses:
|
|
38
|
+
- Apache-2.0
|
|
39
|
+
metadata:
|
|
40
|
+
source_code_uri: https://github.com/salesforce-misc/trellis/tree/main/clients/ruby
|
|
41
|
+
bug_tracker_uri: https://github.com/salesforce-misc/trellis/issues
|
|
42
|
+
rubygems_mfa_required: 'true'
|
|
43
|
+
cargo_crate_name: trellis_ruby
|
|
44
|
+
post_install_message:
|
|
45
|
+
rdoc_options: []
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '3.3'
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 4.1.dev
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 3.3.22
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.5.23
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: 'Embedded Trellis: streaming SQL transforms inside your Postgres, run from
|
|
66
|
+
a Ruby app'
|
|
67
|
+
test_files: []
|