Skip to content

Execute the schema, rather than only reading it - #1225

Merged
darksidemilk merged 1 commit into
working-1.6from
gh-1221-schema-executes-test
Aug 20, 2026
Merged

Execute the schema, rather than only reading it#1225
darksidemilk merged 1 commit into
working-1.6from
gh-1221-schema-executes-test

Conversation

@darksidemilk

Copy link
Copy Markdown
Member

Closes #1221
Refs #1147, #1152

Merge FOGProject/fog-workflows#24 first. That PR adds the schema job that runs this test.
Merging it first means this PR's own CI run executes the new test on MariaDB 10.5, 11.4 and
MySQL 8.0 — which is the best verification either change can get. Merged the other way round,
the test sits in the tree doing nothing until #24 lands. Nothing breaks either way.

Why

tests/schema-portable-collation.test.php checks the two schema files textually and rejects a
collation outside a portable allowlist. That catches the shape of #1147, but only the shape. It
cannot catch a statement that is well-formed, names nothing unusual, and still will not run on
MariaDB 10.5 — and it cannot see what the schema resolves to once a server has applied its own
defaults, which is the whole of #1152.

It is textual by design, and its docblock says why: commons/schema.php "calls
self::$DB->query() at file scope and cannot be included without a database"
. So this test
supplies the database.

Two scratch databases, because there are two routes

Database Models Statements
1 Fresh install — replays schema.php's indexed steps in order SchemaUpdaterPage::update() slices from self::$mySchema, which is 0 on a new database, so a fresh install really does run every step
2 Reconcilerschema-expected.php's create strings into an empty database An empty one specifically

Empty specifically: run against database 1, every table already exists and
CREATE TABLE IF NOT EXISTS may short-circuit without the server ever resolving the collation
names — leaving the #1147 guard looking like it passed when nothing was checked.

Each is asserted twice: every statement executed (#1147), and every resulting table shares
one collation
(#1152).

Loading schema.php without the application

It is included from inside SchemaUpdaterPage::update() and expects that method's context —
$this->schema[], self::$DB, self::getClass(). It carries no namespace, since the real
classes reach it via class_alias into the global namespace, so global stubs are what it resolves
against.

Two dependencies are discovered rather than listed, so adding either to schema.php cannot
quietly break this test:

  • Constants are found by tokenising the file and given placeholders. They only reach INSERT
    statements seeding default settings and only DDL is executed, so the values do not matter —
    DATABASE_NAME and FOG_SCHEMA are set for real because they do.
  • Unknown classes are manufactured by an autoloader whose every call answers null — which is
    what an empty database looks like to the introspection schema.php performs while building its
    array. Empty is the correct answer: a fresh install is the state being modelled, and
    DatabaseManager::getColumns() returning nothing is exactly why the conditional ALTERs are
    included, same as on a real one.

Measured on this branch:

indexed steps:      340
string statements:  658
  of which DDL:     278
  CREATE TABLE:      74
callable steps:       9   (data migrations, skipped - no DDL)

What it does not cover

The closure steps are not executed — data migrations needing the full application, emitting no
DDL. Stated in the docblock rather than left to be discovered: this covers the schema's structure,
not its data.

Skipping

Without FOG_TEST_DSN it prints SKIP and exits 0, so sh tests/run-all.sh with no database
stays green — the convention secureboot-authvars.test.sh already uses. With a DSN set but
pdo_mysql missing it fails instead: a DSN was provided, so skipping would report a pass for a
check that never ran.

Picked up by the runner's existing *.test.php glob; nothing to register.

Also: the job name

.github/workflows/tests.yml declared a bare suite:, and the job key is what GitHub puts in the
middle of a called workflow's check name — so every check read Tests / suite / tests (PHP 7.4).
Named fogproject, because fog-workflows hosts fog-plugins-tests.yml alongside
fogproject-tests.yml, and which project's suite is running is the useful thing for that segment
to say.

This does change the reported check name. Safe because working-1.6 has neither branch
protection nor a ruleset requiring status checks — checked through the API, not assumed. The
workflow itself keeps the name Tests.

Verification

  • Lints clean; skips correctly with no DSN; run-all.sh picks it up and stays green.
  • The collection half was validated against this branch — the figures above.
  • Local execution against a real server was not possible: this machine has no Docker, no MySQL
    client and no WSL distro. The three CI legs are the first real execution, which is precisely the
    gap No CI executes the schema against a real database server #1221 describes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PqufBbuckux8kitJeW3uAK

tests/schema-portable-collation.test.php checks the two schema files textually
and rejects a collation outside a portable allowlist. That catches the shape of
GH-1147, but only the shape: it cannot catch a statement that is well-formed,
names nothing unusual and still will not run on MariaDB 10.5, and it cannot see
what the schema RESOLVES to once a server has applied its own defaults, which is
the whole of GH-1152.

It is textual by design, and its docblock says why -- commons/schema.php "calls
self::$DB->query() at file scope and cannot be included without a database". So
this test supplies the database.

TWO SCRATCH DATABASES, BECAUSE THERE ARE TWO ROUTES

The steps in commons/schema.php reach a server one way and the manifest's
`create` strings reach it another, so they get one database each.

The first replays the indexed steps in order, which is the fresh-install path:
SchemaUpdaterPage::update() slices the step array from self::$mySchema, and that
is 0 on a new database, so a fresh install really does execute every step from
the beginning.

The second runs commons/schema-expected.php's `create` strings into an EMPTY
database -- the reconciler path. An empty one specifically: run against the
first, every table already exists and CREATE TABLE IF NOT EXISTS may
short-circuit without the server ever resolving the collation names, which would
leave the GH-1147 guard looking like it passed when nothing was checked.

Each is then asserted twice: every statement executed (GH-1147), and every
resulting table shares one collation (GH-1152).

HOW schema.php IS LOADED WITHOUT THE APPLICATION

It is include'd from inside SchemaUpdaterPage::update() and expects that
method's context -- $this->schema[], self::$DB, self::getClass(). It carries no
`namespace`, since the real classes reach it through class_alias into the global
namespace, so global stubs are what it resolves against.

Two of its dependencies are discovered rather than listed, so that adding either
to schema.php cannot quietly break this test. Constants are found by tokenising
the file and given placeholders; they only ever reach INSERT statements seeding
default settings, and only DDL is executed, so the values do not matter --
DATABASE_NAME and FOG_SCHEMA are set for real because they do. Unknown classes
are manufactured by an autoloader whose every call answers null, which is what
an empty database looks like to the introspection schema.php performs while
building its array. Empty is the correct answer there: a fresh install is
exactly the state being modelled, and DatabaseManager::getColumns() returning
nothing is why the conditional ALTERs are included, same as on a real one.

Measured on this branch, the collection yields 340 indexed steps, 658 string
statements, 278 of them DDL, 74 CREATE TABLE, and 9 closures.

WHAT IT DOES NOT COVER

The closure steps are not executed. They are data migrations that need the full
application and emit no DDL. Stated in the docblock rather than left to be
discovered: this covers the schema's structure, not its data.

Without FOG_TEST_DSN it prints SKIP and exits 0, so `sh tests/run-all.sh` on a
machine with no database still reports a green suite -- the convention
secureboot-authvars.test.sh already uses for absent efitools. With a DSN set but
pdo_mysql missing it FAILS instead, deliberately: a DSN was provided, so
skipping would report a pass for a check that never ran.

The runner picks it up through its existing *.test.php glob; nothing to register.

ALSO, THE JOB NAME

.github/workflows/tests.yml declared a bare `suite:`, and the job key is what
GitHub puts in the middle of a called workflow's check name, so every check read
"Tests / suite / tests (PHP 7.4)". Named `fogproject`, because fog-workflows
hosts fog-plugins-tests.yml alongside fogproject-tests.yml and which project's
suite is running is the useful thing for that segment to say.

This does change the reported check name. Safe because working-1.6 has neither
branch protection nor a ruleset requiring status checks -- verified through the
API rather than assumed. The workflow itself keeps the name `Tests`.

Part of #1221
Refs #1147, #1152

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqufBbuckux8kitJeW3uAK
@darksidemilk
darksidemilk merged commit 67c4134 into working-1.6 Aug 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant