jeffersongoncalves/pest-plugin-mysql-compat

MySQL compatibility shims (UDFs) for running MySQL-flavored Pest/PHPUnit test suites against SQLite
1
Install
composer require jeffersongoncalves/pest-plugin-mysql-compat
PHP:^8.2
License:MIT
Last Updated:Sep 8, 2026
Links: GitHub  ·  Packagist
Maintainer: jeffersongoncalves

Pest Plugin MySQL Compat

Pest Plugin MySQL Compat

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads License

Run a MySQL-flavored Laravel test suite against SQLite in memory without changing a single line of production SQL. The production query is the truth; the test database is what adapts.

A Laravel project with a legacy MySQL database usually runs its test suite on SQLite for speed and isolation. Every query that uses a MySQL-only function then dies in the test, and the usual fixes are all bad: rewriting the query to a SQLite-compatible subset (now the test covers SQL that isn't the production SQL), marking the test ->todo() (the business rule goes uncovered), or requiring a real MySQL for the suite (losing isolation and speed). This package installs the missing/divergent MySQL functions as SQLite user-defined functions (UDFs), with fidelity to NULL propagation, byte-length semantics, and legacy zero-dates — so the query itself never has to change.

Installation

You can install the package via composer:

composer require jeffersongoncalves/pest-plugin-mysql-compat --dev

Usage

Pest

// tests/Pest.php
use JeffersonGoncalves\PestPluginMysqlCompat\Pest\Plugin;

Plugin::boot();

This is shorthand for:

// tests/Pest.php
use JeffersonGoncalves\PestPluginMysqlCompat\MysqlCompat;

pest()->beforeEach(fn () => MysqlCompat::install());

Alternative, without the Pest-specific helper — bind your own base TestCase instead:

// tests/Pest.php
uses(Tests\TestCase::class)->in('Feature', 'Unit');
// tests/TestCase.php
use JeffersonGoncalves\PestPluginMysqlCompat\MysqlCompat;

abstract class TestCase extends BaseTestCase
{
    protected function setUp(): void
    {
        MysqlCompat::install();

        parent::setUp();
    }
}

PHPUnit / plain Laravel

abstract class TestCase extends BaseTestCase
{
    protected function setUp(): void
    {
        MysqlCompat::install();

        parent::setUp();
    }
}

MysqlCompat::install() must run before the app resolves its first database connection — it registers a connection resolver, and resolvers only affect connections created afterwards. That is why it is called before parent::setUp(), not after.

Granular configuration

MysqlCompat::install(
    functions: ['DATE_FORMAT', 'IF', 'CONCAT'],   // default: all
    rewriters: MysqlCompat::REWRITE_ALL,           // reserved for Phase 2, no-op today
    strict: true,                                  // reserved for Phase 3, no-op today
);

Function catalog

Missing from SQLite — installed as UDFs

Function Signature Fidelity note
IF 3 args SQLite has native IIF; registering IF works
JSON_UNQUOTE 1 arg see below — not a blind identity
CONCAT variadic propagates NULL
SUBSTRING 2–3 args 1-indexed, like MySQL
DATE_FORMAT 2 args treats '0000-00-00' as NULL
LPAD 3 args
LEFT 2 args
RIGHT 2 args
SUBSTRING_INDEX 3 args negative count counts from the end
PERIOD_ADD 2 args returns the same type it received
LAST_DAY 1 arg
NOW 0 args

Native in SQLite, but with divergent semantics — overridden

Function SQLite native MySQL Action
LENGTH LENGTH('ção')3 (characters) 5 (bytes) overridden with strlen
ROUND ROUND(1234.5, -2)1235.0 1200 overridden for negative precision

LENGTH is the most treacherous divergence: on a column of digits (CPF, CNPJ, CEP) both agree and the test passes; on accented text they silently diverge.

JSON_EXTRACT was measured to diverge too (JSON_EXTRACT returns a bare scalar in SQLite, a quoted JSON value in MySQL) but is not overridden — JSON_UNQUOTE is adapted instead to strip the surrounding quotes only when they are present, so it works over either shape.

Known limitations

  • WHERE of UPDATE/DELETE with a float parameter is not rewritten — the (Phase 2) float-binding rewriter only targets select()/cursor(). No real case found yet; documented instead of covered speculatively.
  • Rewriting will be textual (Phase 2). SQL assembled at runtime by string concatenation with a fragment coming from data is risky territory; the lexer will refuse when unsure, and refusing means the original error comes back, not corrupted SQL.
  • GROUP_CONCAT exists in both engines with the same default separator (,), but ordering and DISTINCT diverge. It belongs in a future catalog addition with a parity test, not as "already works".
  • Text collation/ordering is not covered. ORDER BY on accented text differs between the two engines — that is a collation problem, not a function problem.
  • The LENGTH override changes behavior for anyone who was already writing SQL expecting SQLite's native character-count LENGTH. This is a breaking change for that consumer.

Roadmap

  • Phase 1 — what pays for the package. FunctionRegistry + the 12 missing functions + LENGTH and ROUND overrides. Pest and PHPUnit integration. Unit tests.
  • Phase 2 — the differentiator. Lexer + FloatBindingRewriter (highest measured payoff) + IsNullRewriter + IntervalRewriter.
  • Phase 3 — confidence. scan diagnostic command, strict mode, a MySQL-parity suite that runs against a real MySQL service in CI.
  • Phase 4 — reach. The demand-driven catalog (IFNULL, CONCAT_WS, RPAD, DATEDIFF, DAY/MONTH/YEAR, ADDDATE, PERIOD_DIFF, STR_TO_DATE, TIMESTAMPDIFF, GROUP_CONCAT, FIND_IN_SET, FIELD, GREATEST, LEAST, …) and an adapter for consumers not on Laravel (Doctrine, plain PDO).

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email the author instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

Related Packages

antennaio/laravel-codeception-dbdump

Create database dumps ready to be used with Codeception

1,515 6
orottier/laravel-migration-squasher

Aggregate your incremental Laravel migration files into single migration for eac...

95 7
repat/laravel-medoo

Laravel Facade and ServiceProvider for The Lightest PHP database framework to ac...

248 5
cytracom/squasher

Aggregate your incremental Laravel migration files into single migration for eac...

307 26