Skip to content

CI matrix for MySQL, MariaDB, PostgreSQL and PHP, with fixes it uncovered - #1

Merged
twoixter merged 12 commits into
masterfrom
ci/github-actions-matrix
Sep 10, 2026
Merged

twoixter merged 12 commits into
masterfrom
ci/github-actions-matrix

Conversation

@twoixter

@twoixter twoixter commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary

Adds a GitHub Actions matrix and fixes everything it found. Every job runs the full suite in about a minute.

Matrix: PHP 8.2–8.5, MySQL 5.7/8.0/8.4, MariaDB 10.11/11.8/12.3, PostgreSQL 14/16/18 (with PostgreSQL as the default adapter for the model-level tests), SQLite through each PHP build, memcached, plus a phpcs gate. Database data lives on tmpfs so the suite's tens of thousands of small commits do not wait on fsync. compose.yaml provides the same servers locally; see CONTRIBUTING.md.

Library fixes uncovered by the matrix

  • PostgreSQL: pg_attrdef.adsrc (removed in PG 12) in column introspection; missing PgsqlAdapter import in Table left models on PostgreSQL with no column metadata; IS ? with a bound null; inflected attribute names used as quoted column names; non-numeric primary-key lookups raising a type error.
  • MySQL 5.7 / MariaDB: datetime values carried a time zone offset that they reject under strict mode, and that MySQL 8.0.19+ converted to the session time zone. Datetimes are now plain wall-clock time on every adapter, with a round-trip test that runs with PHP in a non-UTC zone.
  • Memcached: cache tests recursed in setUp (segfault on PHP 8.2) and referenced pre-namespace class names; Memcached backend threw a non-existent exception class.
  • Test harness: a failed connection in setUp left its adapter as the default and silently skipped 278 unrelated tests.

Other changes

  • Oracle support removed entirely.
  • Library is fully PSR-4 autoloaded: no require left, backend moved to lib/Cache/Memcached.php, missing exception imports added.
  • PHPAR_ADAPTER selects the default adapter for the model-level tests (replaces the dead --adapter switch).
  • Syntax modernised: [], no function aliases, explicit property visibility; phpcs.xml enforces it and now covers examples.
  • actions/checkout v7; workflow triggers on pushes to master and on pull requests, with superseded runs cancelled.

Test plan

  • Full suite locally against MySQL 5.7 (native), MySQL 8.0/8.4, MariaDB 10.11/11.8/12.3, PostgreSQL 14/16/18 and SQLite
  • CI green on the 12-job matrix for every commit since the fixes landed
  • This PR's run, which adds the phpcs job and the new triggers

Test on a baseline of PHP 8.4, MySQL 8.4 and PostgreSQL 18, plus one job
per other version of each axis (MySQL 5.7 and 8.0, PostgreSQL 14 and 16,
PHP 8.2, 8.3 and 8.5). Database data lives on tmpfs so the suite's tens
of thousands of small commits do not wait on fsync. A compose.yaml offers
the same servers locally, and CONTRIBUTING.md explains how to use them.

Fixes uncovered while making the matrix pass:

- PgsqlAdapter read pg_attrdef.adsrc, removed in PostgreSQL 12, so schema
  introspection failed on every supported PostgreSQL. Use pg_get_expr().
- MysqlAdapter emitted datetime values with a time zone offset, which
  MySQL before 8.0.19 (and MariaDB) rejects under strict sql_mode. Format
  plain wall-clock time for those servers.
- DatabaseTest left an unreachable adapter as the default connection when
  setUp() skipped, because PHPUnit 11 does not run tearDown() then. That
  silently skipped 278 unrelated tests whenever PostgreSQL was down.
- The connect-with-port test ignored the configured port.
- phpunit.xml.dist used PHPUnit 9 attributes.

Remove the obsolete Travis configuration.
The first CI run was the first time the memcached tests ran since the
models and exceptions moved into namespaces:

- ActiveRecordCacheTest::setUp() called parent::set_up(), which the
  snake_case __call() helper turned back into setUp() on the same class.
  The infinite recursion hit the stack limit on PHP 8.3+ after minutes
  and segfaulted PHP 8.2. Call parent::setUp() directly.
- CacheModelTest reflected on the bare class names "Author" and
  "Publisher", which no longer exist; use the namespaced classes. The
  model cache key now carries the namespace, so expect that too.
- lib/cache/Memcached.php threw ActiveRecord\CacheException, which no
  longer exists, so a failed connection surfaced as a class-not-found
  Error instead of ActiveRecord\Exceptions\CacheException.
The OCI adapter was never completed, has no CI coverage, and its tests
have been skipped everywhere for years. Drop the adapter, its tests and
SQL fixtures, the Oracle-only branches in Model and the test helpers,
and the mentions in the documentation.
The base datetime format appended PHP's time zone offset. MySQL before
8.0.19, MariaDB and SQLite do not understand it, and MySQL 8.0.19+ makes
it worse by converting the value to the session time zone, so a value
only round-tripped when that happened to match PHP's zone; with PHP in
Europe/Madrid and a UTC server, 18:00 came back as 16:00.

Use 'Y-m-d H:i:s' everywhere, drop the per-version MySQL override and
the now identical SQLite override, and add a round-trip test that runs
with PHP in a non-UTC zone on every adapter.
Table checked `$this->conn instanceof PgsqlAdapter` without importing
ActiveRecord\Adapters\PgsqlAdapter, so since the PSR-4 move the check
was always false, the table name was quoted, and the pg_catalog lookup
matched nothing. Models on PostgreSQL therefore had no columns: no type
casting, no defaults, no primary key detection. The adapter tests never
noticed because they query the connection directly; the new datetime
round-trip test is the first model-level test that runs on PostgreSQL.
Remove every require/require_once from lib: SQLBuilder and Model pulled
in Expressions and Validations by hand, Cache loaded its backend from a
file path, and Connection and Table still carried commented-out ones.

Move the memcached backend to lib/Cache/Memcached.php as
ActiveRecord\Cache\Memcached, where PSR-4 finds it; Cache::initialize()
now resolves the backend class by scheme and throws CacheException for
an unknown one instead of failing on a missing file.

Add the imports that Reflections and Serialization were missing for
their exception classes since the move to ActiveRecord\Exceptions, and
fix three tests importing DatabaseException from its old location.

The has-many-through test models in the foo\bar\biz namespace move from
an included helper file to test/models/biz with an autoload-dev entry.
The jpfuentes2GH-101 test needs models whose namespace differs from the configured
model namespace, so that relationships must resolve related classes
through the owning model's namespace rather than the Config fallback.
A sub-namespace of TestModels satisfies that and is already covered by
the existing autoload-dev rule, so the extra foo\bar\biz entry goes.
The test bootstrap now reads PHPAR_ADAPTER (mysql, pgsql or sqlite) to
pick the default connection, replacing the --adapter switch that
PHPUnit 11 rejects, so the model-level tests can run against every
adapter. PHPAR_SLOW_TESTS=true replaces --slow-tests.

Running the suite with PostgreSQL as the default exposed these library
bugs, which MySQL hid:

- Expressions rendered a null hash condition as "IS ?" with a bound
  NULL. MySQL's emulated prepares interpolate that into IS NULL;
  PostgreSQL rejects a parameter after IS. Render the literal instead.
- Writes and hash conditions used the inflected attribute name as the
  column name (some_date for some_Date). MySQL matches column names
  case-insensitively; PostgreSQL does not once the name is quoted.
  Table::column_name_for() resolves the real column name, and eager
  loading uses it for the related table's keys.
- find() with a non-numeric value for an integer primary key raised a
  type error on PostgreSQL, where MySQL coerced it to 0 and found
  nothing. Throw RecordNotFound up front on every adapter.

Test suite adjustments for engine differences:

- PostgreSQL and SQLite schemas made events.host_id NOT NULL while
  MySQL allowed null; the belongs-to-null test needs the null.
- Two tests used MySQL-only SQL (double-quoted string literal,
  backtick quoting); use standard quotes and quote_name().
- update_all row counts: MySQL reports rows changed, PostgreSQL and
  SQLite rows matched, so the expectation depends on the adapter.
- The SQL builder test expects IS NULL and no bound null; eager-loading
  assertions read the real column name; the connection-manager test
  uses the configured default instead of "mysql"; the date-format test
  reloads by id because first() without an order is arbitrary after an
  update on PostgreSQL.
The three PostgreSQL rows (14, 16, 18) now set PHPAR_ADAPTER=pgsql so
the model-level tests run against each supported PostgreSQL version.
The remaining rows keep MySQL as the default, so MySQL coverage is
unchanged. Document PHPAR_ADAPTER in CONTRIBUTING.md.

Bump actions/checkout from v4 to v7: v4 runs on Node 20, which the
hosted runners are retiring. shivammathur/setup-php stays on v2, its
current major.
MariaDB reaches the library through the MySQL adapter and is what
Debian, Ubuntu and RHEL ship as their MySQL, yet it diverges from
MySQL 8 where the adapter is sensitive: no time zone offsets in
datetime literals, integer display widths still reported, a looser
default sql_mode and a different version string.

Three rows cover 10.11 (Debian 12, Ubuntu 24.04, RHEL 9/10), 11.8
(Debian 13) and the current 12.3 LTS. They reuse the MySQL service
definition: the image name comes from the matrix, and the health check
uses the image's healthcheck.sh because the 12.x image no longer ships
mysqladmin. compose.yaml gets the same three servers for local runs.
Mechanical style update, no behaviour change:

- array() becomes [] everywhere, in code and in docblock examples,
  README and the examples directory. Three docblock examples had a
  missing parenthesis in the original text and were fixed by hand.
- list() destructuring becomes [...].
- Function aliases join, sizeof and is_integer become implode, count
  and is_int.
- Static properties that relied on the implicit public get it spelled
  out.
- A commented-out duplicate has_one definition in the Author test model
  is removed.

phpcs.xml enforces the new state: Generic.Arrays.DisallowLongArraySyntax,
Generic.PHP.ForbiddenFunctions with the alias table, and the PSR-12
property visibility rule is no longer excluded. The examples directory
joins lib and test in the checked paths.
Add a "Coding standard" job that runs vendor/bin/phpcs with the rules
from phpcs.xml, so a pull request that breaks the standard fails CI.

Trigger the workflow on pushes to master and on pull requests only. With
both events unfiltered, every push to a branch with an open pull request
started two identical runs. A concurrency group cancels a run that a
newer push to the same branch or pull request has superseded.
@twoixter
twoixter merged commit 6ae52f8 into master Sep 10, 2026
13 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