TABLE
TABLE = 'strata_watermark'
The table watermarks are stored in.
Proves capture is complete, and closes the gap when it is not.
Every other part of the capture path records a change it was told about. This one goes looking for changes nobody told anyone about: a module writing straight to its own table, a query that ran before the statement tap was enabled, a migration executed outside Drupal entirely. From inside the capture path all three are invisible, and a backup that cannot detect its own gaps is a backup nobody should trust.
The method is a bounded fingerprint per table, compared against the last one stored. A table
whose fingerprint moved while the journal and the commit log hold no operation for it has
drifted, which raises watermark.drift and, when asked, captures the table's current rows so the
gap closes.
What this covers and what it does not. Row count, highest key and highest changed-timestamp are exact. The sampled digest is a sample: a change confined to rows outside the sample is not seen. So a clean pass means "no drift detected over what was measured", which is why the finding is a warning that names the table rather than a silent repair, and why the sample size is a setting. A table with no single-column primary key gets count and digest only.
JOURNAL_SCAN = 10000
How much of the journal is scanned when asking whether a table was captured.
A pass runs after a flush, so the journal holds at most one window. This is a bound in case it does not: a backlog deeper than this makes the answer "not captured", which errs toward reporting drift that turns out to be accounted for rather than missing drift that is real.
$journal : \Drupal\strata\Journal\JournalInterface
$scope : \Drupal\strata\Capture\CaptureScope
$tripwires : \Drupal\strata\Health\TripwireRegistry
$ledger : \Drupal\strata\Health\HealthLedgerInterface
__construct(\Drupal\Core\Database\Connection $database, \Drupal\strata\Journal\JournalInterface $journal, \Drupal\strata\Capture\CaptureScope $scope, \Drupal\strata\Health\TripwireRegistry $tripwires, \Drupal\strata\Health\HealthLedgerInterface $ledger, \Psr\Log\LoggerInterface $logger, int $sampleSize = \self::DEFAULT_SAMPLE) : mixed
Constructs a reconciler.
| \Drupal\Core\Database\Connection | $database | The connection to inspect. |
| \Drupal\strata\Journal\JournalInterface | $journal | Where captured rows are appended. |
| \Drupal\strata\Capture\CaptureScope | $scope | Decides which tables are in scope. |
| \Drupal\strata\Health\TripwireRegistry | $tripwires | Runs the drift check over each observation. |
| \Drupal\strata\Health\HealthLedgerInterface | $ledger | Where drift is recorded. |
| \Psr\Log\LoggerInterface | $logger | Records what a pass did. |
| int | $sampleSize | How many rows the digest covers. |
reconcile(list$tables = [], int $limit = 100, bool $capture = true) : \Drupal\strata\Capture\ReconcileReport
Compares every captured table against its stored watermark.
| list |
$tables | Tables to examine, or an empty list for every captured table. |
| int | $limit | Most tables to examine in this pass. |
| bool | $capture | TRUE to capture the rows of a drifted table, closing the gap. FALSE reports the drift and leaves it, which is what a dry run and a status page want. |
What the pass found.
tables() : list<string>
Tables the reconciler watches.
Every table the connection holds that capture covers. Strata's own tables are excluded by the scope, and so is anything the site has told it to leave alone.
Table names, in a stable order so a bounded pass covers them evenly over time.
observe(string $table) : \Drupal\strata\Capture\Watermark
Takes a fresh reading of one table.
| string | $table | The table name. |
When the table cannot be read, which the caller reports rather than swallows.
The reading.
digest(string $table, string|null $key, list$columns) : string
A digest over a bounded sample of rows.
Ordered by the primary key when there is one, so two readings sample the same rows and a difference means the contents moved rather than the order did. Without a key the order is whatever the database gives, so the digest is only meaningful for a table small enough that the sample covers all of it.
| string | $table | The table name. |
| string|null | $key | The primary key column, or NULL. |
| list |
$columns | The table's columns. |
The digest, or an empty string when no sample could be taken.
orderColumn(list$columns) : string|null
The column a sample is ordered by, so two readings cover the same rows.
A heuristic, and only ever used for ordering: any column that sorts deterministically makes two
readings comparable, so being wrong about which one is the key costs nothing here. Drupal names
its keys predictably - id, or nid, uid, fid, tid - which is what the preference order
follows.
| list |
$columns | The table's columns. |
The column, or NULL when the table has none.
identityColumn(string $table, list$columns) : string|null
A single column whose values are unique, when one exists.
Used only when capturing the rows of a table that has already drifted, because a row needs an identity before it can be a restorable subject and a guess would give two different rows the same subject. Verified rather than assumed: one aggregate query per candidate, on a table the pass has already decided is worth the work.
| string | $table | The table name. |
| list |
$columns | The table's columns. |
The column, or NULL when no single column identifies a row.
wasCaptured(string $table, int $since) : bool
Whether an operation was captured for a table since a given time.
Read from the journal, which holds what has been captured and not yet sealed. A window already sealed into a commit is not consulted: the reconciler runs on cron after a flush, so anything sealed was seen, and re-reading the commit log per table would cost a request per table.
| string | $table | The table name. |
| int | $since | Unix timestamp of the previous reading. |
TRUE when something was captured for this table.
captureRows(string $table, \Drupal\strata\Capture\Watermark $stored, list$problems) : int
Captures the rows of a drifted table, so the gap closes.
Only rows the reading can identify are captured: the table needs a single-column primary key, because a row with no identity has no subject to be restored into. A table without one keeps its finding open, which is the honest outcome - the drift is real and this cannot fix it.
| string | $table | The table name. |
| \Drupal\strata\Capture\Watermark | $stored | The previous reading, used to capture only what moved past it where possible. |
| list |
$problems | Collects a line when the capture itself fails. |
How many rows were captured.
appendRow(string $table, string $key, array$row, list $problems) : bool
Appends one row as a captured operation.
A row holding bytes that are not valid UTF-8 - a BLOB column, a binary primary key - cannot be
described in JSON, and the reading's fresh watermark is already stored by the time this runs.
So a row that cannot be encoded is reported rather than appended: appending
(string) json_encode($row) would file an operation whose payload is empty, whose address is
Hash::of('') and whose length is zero, and the drift would never be looked at again.
| string | $table | The table name. |
| string | $key | The primary key column. |
| array |
$row | The row. |
| list |
$problems | Collects a line when the row cannot be captured. |
TRUE when the row was appended.