Classifies one SQL statement as a write, and says what it wrote to.
The database service is a factory returning a driver subclass chosen in settings.php, so it
cannot be decorated and statement events are the only driver-agnostic write tap there is. That
makes this class the thing standing between every query a site runs and the journal, which fixes
two properties it has to have.
The verb guard comes first and is the cheapest thing here. A site doing 10 million statements
a day runs this on all of them, and all but a few per cent are reads. SqlStatement::isWrite()
reads one keyword and answers, so a SELECT costs a substring compare and nothing else. Only a
statement that passes gets parsed.
A statement it cannot classify returns NULL rather than a guess. A wrong table name would
attribute a change to the wrong subject, which is worse than not recording it: the reconciler's
watermark notices an unrecorded change, and nothing notices one recorded against the wrong table.
The query string arrives after table prefixing, so the prefix has to come back off. A site
with $databases['default']['default']['prefix'] set runs INSERT INTO foo_widget, and recording
that verbatim breaks two things at once: the subject a restore looks for carries a deployment
detail that can change under it, and CaptureScope::coversTable() stops recognising this module's
own tables, so journaling a write becomes a write that gets journaled. The prefix is therefore
passed in and stripped here, where the table name is decided.
Words that may sit between a write keyword and the table it acts on.
MySQL's priority hints and SQLite's INSERT OR <conflict> clauses, which appear in different
orders and combinations, so they are consumed until none matches.
QUOTES
QUOTES = '`"[]'
Characters that quote an identifier in one dialect or another.
The connection's table prefix, removed from the name so the subject is the logical table
rather than the physical one.
Returns
self|null
—
The classification, or NULL when the statement is a read or its target cannot be read out of
it with certainty.
shape()
shape(string $sql, int $limit = 200) : string
A statement reduced to its shape, safe to show in a timeline.
Drupal's query builders bind values as placeholders, so a statement normally carries none. A
module that interpolated a value into the SQL itself would put it in the string, and that string
would then sit in the journal, the commit label and the diff viewer. So single-quoted literals
and digit runs are replaced before anything keeps the text: the query stays recognisable, and no
value rides along with it.
Parameters
string
$sql
The statement.
int
$limit
Most characters to keep.
Returns
string
—
The shape, whitespace collapsed and truncated.
subject()
subject() : string
The subject key this statement is recorded under.
A statement is attributed to its TABLE rather than to a row, because a statement says which rows
it touched only in its WHERE clause and reading that would mean evaluating it. One subject per
table keeps the tree bounded by the schema rather than by traffic, and the reconciler is what
turns a dirty table into per-row detail that can actually be restored.
Returns
string
—
The subject key.
isStructural()
isStructural() : bool
Whether this statement changed structure rather than contents.
Where to read from, just past the leading keyword.
\Drupal\strata\Journal\Verb
$verb
The verb this keyword means.
string
$keyword
The leading keyword.
list
$skip
Words that may sit between the keyword and the table name, each optional.
Returns
self|null
—
The classification, or NULL when no table name follows.
structure()
structure(string $sql, int $offset, string $keyword) : self|null
Classifies a statement that changes structure.
An index is attributed to the table it sits on, because that is the thing whose shape changed
and the thing a restore would have to rebuild. DROP INDEX without an ON clause names no
table, so it is declined rather than attributed to the index's own name.
Parameters
string
$sql
The statement.
int
$offset
Where to read from, just past the leading keyword.
string
$keyword
The leading keyword.
Returns
self|null
—
The classification, or NULL when the statement changes something that is not a table.
index()
index(string $sql, int $offset, string $keyword) : self|null
Classifies an index statement by the table it names.
Parameters
string
$sql
The statement.
int
$offset
Where to read from, just past the INDEX keyword.
string
$keyword
The leading keyword.
Returns
self|null
—
The classification, or NULL when no table is named.
skipBlanks()
skipBlanks(string $sql, int $offset) : int
Skips whitespace and comments.
Parameters
string
$sql
The statement.
int
$offset
Where to start.
Returns
int
—
The first offset holding something other than blank space or a comment.
word()
word(string $sql, int $offset) : array{0: string, 1: int}|null
Reads the next bare word, uppercased.
Parameters
string
$sql
The statement.
int
$offset
Where to read from.
Returns
array{0: string, 1: int}|null
—
The word and the offset just past it, or NULL when no word follows.
consume()
consume(string $sql, int $offset, string $expected) : int
Skips the next word when it is the one expected.
Parameters
string
$sql
The statement.
int
$offset
Where to read from.
string
$expected
The uppercased word to skip.
Returns
int
—
The offset past the word, or the offset unchanged when the next word is something else.
past()
past(string $sql, int $offset) : int
The offset just past the next identifier.
Parameters
string
$sql
The statement.
int
$offset
Where to read from.
Returns
int
—
The offset past it, or the offset unchanged when no identifier follows.
identifier()
identifier(string $sql, int $offset) : string|null
Reads the next identifier, unquoted and stripped of any schema qualifier.
Parameters
string
$sql
The statement.
int
$offset
Where to read from.
Returns
string|null
—
The identifier, or NULL when what follows is not one.