\Drupal\strata\Capture SqlStatement

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.

Summary

Methods
Properties
Constants
isWrite
parse
shape
subject
isStructural
verb
realm
table
keyword
WRITE_KEYWORDS
No protected methods found
No protected properties found
No protected constants found
__construct
rows
structure
index
skipBlanks
word
consume
past
identifier
No private properties found
MAX_KEYWORD
MODIFIERS
QUOTES

Constant

WRITE_KEYWORDS

WRITE_KEYWORDS = ['INSERT' => true, 'UPDATE' => true, 'DELETE' => true, 'REPLACE' => true, 'TRUNCATE' => true, 'MERGE' => true, 'UPSERT' => true, 'CREATE' => true, 'ALTER' => true, 'DROP' => true, 'RENAME' => true]

Leading keywords that mean a statement changes something.

The guard is a lookup against this, so adding a dialect's write keyword is one line here.

MAX_KEYWORD

MAX_KEYWORD = 9

Longest keyword the guard has to read, so it never scans further than this.

MODIFIERS

MODIFIERS = ['LOW_PRIORITY', 'HIGH_PRIORITY', 'DELAYED', 'IGNORE', 'OR', 'ROLLBACK', 'ABORT', 'FAIL', 'REPLACE']

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.

Properties

$verb

$verb : \Drupal\strata\Journal\Verb

Type

Verb

$realm

$realm : \Drupal\strata\Journal\Realm

Type

Realm

$table

$table : string

Type

string

$keyword

$keyword : string

Type

string

Methods

isWrite()

isWrite(string  $sql) : bool

Whether a statement changes anything.

The hot path. Reads at most SqlStatement::MAX_KEYWORD characters past any leading whitespace or comment and compares one uppercased word.

Parameters

string $sql

The statement.

Returns

bool —

TRUE when the leading keyword is one that writes.

parse()

parse(string  $sql, string  $prefix = '') : self|null

Classifies a statement, or declines to.

Parameters

string $sql

The statement.

string $prefix

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.

Returns

bool —

TRUE for DDL.

__construct()

__construct(\Drupal\strata\Journal\Verb  $verb, \Drupal\strata\Journal\Realm  $realm, string  $table, string  $keyword) : mixed

Constructs a classified statement.

Parameters

\Drupal\strata\Journal\Verb $verb

What the statement does.

\Drupal\strata\Journal\Realm $realm

Realm::TABLE for a statement changing rows, Realm::SCHEMA for one changing structure.

string $table

The table it acts on, unquoted and unqualified.

string $keyword

The leading keyword, kept for the timeline.

Returns

mixed —

rows()

rows(string  $sql, int  $offset, \Drupal\strata\Journal\Verb  $verb, string  $keyword, list  $skip) : self|null

Classifies a statement that changes rows.

Parameters

string $sql

The statement.

int $offset

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.