Laravel-Firebird (Benson Fork)
TL;DR
We started from an already working, mature Firebird driver for Laravel and brought it to real parity with the first-party drivers (MySQL, PostgreSQL, SQL Server). The result: from an estimated ~51% to ~96% functional parity, 216 tests running against Firebird 3, 4 and 5 (dialects 1 and 3), ~87% line coverage, and a green CI across the whole matrix (PHP 8.3/8.4 × Firebird 3/4/5).
This write-up is for people who live and breathe Firebird — the details below are engine-specific.
The codebase it builds on
benson/laravel-firebird is a fork of harrygulliford/laravel-firebird, which itself descends from the pioneering jacquestvanzuydam/laravel-firebird. Much of the foundation (query grammar, schema grammar, Eloquent integration, FIRST/SKIP pagination, INSERT ... RETURNING, MERGE-based upsert, join mutations via RDB$DB_KEY) already came from that prior work. What we describe here is the refinement and parity layer we built on top of that base.
Why it matters
Firebird has quirks no generic ORM handles on its own: case-sensitive identifiers when quoted, DATE with no time part in dialect 3, NUMERIC/DECIMAL stored as a scaled integer, no lastInsertId(), dialect 1 without delimited identifiers… each one can become a silent production bug. We tackled them head-on.
Real, Firebird-specific bug fixes
1. Scale loss on DECIMAL/NUMERIC (the nastiest one).
Inserting the PHP integer 40 into a DECIMAL(5,2) column stored 0.40 — off by 100×. Cause: Laravel binds integers with PDO::PARAM_INT, and pdo_firebird treats them as the column’s raw scaled value. We now bind integers as PARAM_STR, and Firebird converts the literal to the column type with the correct scale. Strings, floats, where clauses and booleans stay untouched.
2. dropAllTables() with legacy uppercase names.
Tables created without quotes live in UPPERCASE in the catalog (AUDITORIA). Introspection returned the name lowercased, and DROP TABLE "auditoria" failed with -607 Table does not exist. Dropping now uses the real catalog name, working for lowercase (quoted), UPPERCASE (legacy) and mixed-case tables in the same database.
3. date cast triggering conversion errors.
In dialect 3, DATE has no time. Laravel formats every date as Y-m-d H:i:s, and Firebird rejected it with -413 conversion error from string. We ship a SerializesFirebirdDates trait (and a base model) that stores date columns without the time component while keeping datetime intact — using the cast you already declare.
4. exists() over UNION queries.FIRST 1 was applied only to the first union branch. We now wrap the query in a derived table before limiting.
Server-version aware features
We added version detection (server_version, overridable via config) that enables:
- Identity columns on Firebird 3+: auto-increment via
GENERATED BY DEFAULT AS IDENTITY— no generator + trigger pair. On 2.5 servers we keep the legacy path automatically. - Time-zone types on Firebird 4+:
timestampTz(),timeTz(),dateTimeTz()emitWITH TIME ZONE. - 63-character identifiers (FB4+) vs 31 (FB3-), with a collision-proof hash suffix for long names.
- Nullability ALTER COLUMN adapted:
SET/DROP NOT NULLon FB3+, system-table update on FB2.5. - groupLimit (Eloquent eager-load limits) via
ROW_NUMBER()on FB3+, with a clear error where window functions are unavailable.
New capabilities
- Correct insertOrIgnore: drops the key/id column heuristic and ignores violations of any unique constraint;
firstOrCreate/createOrFirstnow work under race conditions (viaUniqueConstraintViolationException). - Configurable constraint/index naming (
index_names): definePK_{table},FK_{table}_{columns}, etc. — applied even to the inline PK of identity columns. Great for standardizing legacy schemas. - uniqueIndex() / dropUniqueIndex(): a unique index without a constraint (
CREATE UNIQUE INDEX). - COMMENT ON for tables and columns.
- auto_increment in introspection (reads
rdb$identity_type). - IN lists > 1499 items split automatically (FB < 5 limit).
- Automatic reconnect: we recognize Firebird’s lost-connection messages, enabling Laravel’s retry.
- Computed columns (
virtualAs()/storedAs()): emitCOMPUTED BY(always virtual on Firebird). - Temporary tables (
$table->temporary()): emitGLOBAL TEMPORARY TABLE ... ON COMMIT PRESERVE ROWS(previously threw). - json() as BLOB SUB_TYPE TEXT (instead of a truncating
VARCHAR(8191)).
What we implemented
| Category | Item | Type |
|---|---|---|
| Bug | Integer bind into DECIMAL/NUMERIC (scale) | Fix |
| Bug | dropAllTables() with uppercase/mixed names | Fix |
| Bug | date cast conversion error (-413) | Fix |
| Bug | exists() over UNION queries | Fix |
| Version | Identity columns (FB3+) with generator+trigger fallback (FB2.5) | Feature |
| Version | WITH TIME ZONE types (FB4+) | Feature |
| Version | 63-char identifiers (FB4+) + anti-collision hash | Feature |
| Version | Version-aware change() nullability | Feature |
| Version | groupLimit via ROW_NUMBER() (FB3+) | Feature |
| Version | Server version / feature detection | Feature |
| New | Real-constraint insertOrIgnore (any unique) | Feature |
| New | Configurable naming (index_names) | Feature |
| New | uniqueIndex() / dropUniqueIndex() | Feature |
| New | COMMENT ON (table/column) | Feature |
| New | auto_increment in introspection | Feature |
| New | IN chunking > 1499 items | Feature |
| New | Automatic reconnect (lost connection) | Feature |
| New | UniqueConstraintViolationException (firstOrCreate) | Feature |
| New | SerializesFirebirdDates trait (date cast without time) | Feature |
| New | Computed columns (virtualAs/storedAs → COMPUTED BY) | Feature |
| New | Temporary tables (temporary() → GTT) | Feature |
| New | json() as BLOB SUB_TYPE TEXT | Improvement |
Before × After (parity per feature)
Legend: ✅ full · ⚠️ partial · ❌ missing/broken · 🚫 Firebird’s own limitation
| Feature | Before | After |
|---|---|---|
| Query builder core (select/where/join/group/order/union) | ✅ | ✅ |
| Pagination (limit/offset, paginate, cursor) | ✅ | ✅ |
| whereDate/Time/… | ✅ | ✅ |
| Locks forUpdate | ✅ | ✅ |
| Stored procedures | ✅ | ✅ |
| insertGetId / insertUsing | ✅ | ✅ |
| upsert (MERGE) | ✅ | ✅ |
| Update/Delete with join | ✅ | ✅ |
| truncate | ✅ | ✅ |
| Schema CRUD (columns/indexes/FK) | ✅ | ✅ |
| Introspection (tables/columns/indexes/FKs/views) | ✅ | ✅ |
| Transactions / savepoints | ✅ | ✅ |
| exists over union | ⚠️ | ✅ |
| insertOrIgnore (any unique) | ⚠️ | ✅ |
| Identity columns (FB3+) | ❌ | ✅ |
| 63-char identifiers + anti-collision | ⚠️ | ✅ |
| change() nullability by version | ⚠️ | ✅ |
| Time-zone types (FB4+) | ❌ | ✅ |
| Comments (table/column) | ❌ | ✅ |
| whereLike(caseSensitive) | ❌ | ⚠️ |
| whereIn > 1499 | ❌ | ✅ |
| groupLimit (eager load) | ❌ | ✅ |
| json() storage | ⚠️ | ⚠️ |
| Reconnect / lost connection | ❌ | ✅ |
| UniqueConstraintViolationException | ❌ | ✅ |
| date cast without time | ❌ | ✅ |
| Integer bind into DECIMAL | ❌ | ✅ |
| dropAllTables (mixed case) | ⚠️ | ✅ |
| auto_increment in introspection | ❌ | ✅ |
| server_version / feature detection | ❌ | ✅ |
| Custom naming (index_names) | ❌ | ✅ |
| uniqueIndex without constraint | ❌ | ✅ |
| Computed columns (COMPUTED BY) | ❌ | ✅ |
| Temporary tables (GTT) | ❌ | ✅ |
| JSON ops / full-text / spatial / rename table | 🚫 | 🚫 |
Test coverage
- 216 tests, run against real Firebird on dialects 1 and 3.
- ~87% line coverage (measured on dialect 3; combined with dialect 1 it’s higher).
- CI matrix: Firebird 3/4/5 × PHP 8.3/8.4 — 6 jobs, all green.
- We cover cache/insert/upsert through the database, and assert the presence of the correct value (not just the absence of the wrong one) — that’s how we caught the DECIMAL scale bug.
Functional coverage (parity with the official drivers)
| Estimated parity | |
|---|---|
| Before | ~51% |
| After | ~96% |
The remaining ~4% are Firebird’s own limitations (JSON operations, full-text, spatial types, table rename) — nothing a driver can do; we surface them with clear errors.
Requirements
PHP 8.2+ · Laravel 12/13 · Firebird 2.5 → 5.0 (dialects 1 and 3).
Closing
The goal was to make Firebird a first-class citizen in Laravel — not a “mostly works”. If you run Firebird with PHP, feedback and contributions are very welcome.
Open source runs on community
This driver — like so many tools we use every day — only exists because people chose to share their work openly. Every fix, every test and every line of docs comes from hours someone donated so the next developer would have an easier path. Firebird itself is proof of that: a mature, free, community-maintained database that’s been around for decades.
Keeping an open source project alive is real work: triaging issues, testing across versions, writing docs, ensuring compatibility. If this driver saved you time, consider giving back — in any of these ways, all valuable:
- ⭐ Star the repository and share it with fellow Firebird users.
- 🐛 Open issues with real-world cases and send pull requests.
- 📝 Improve the docs and help other developers.
- 💜 Sponsor the development: github.com/sponsors/bensonsbc
Sponsorship isn’t only about money — it’s about making the time spent keeping the Firebird + PHP ecosystem healthy and evolving sustainable. Any support, of any size, makes a real difference.
Créditos / Credits
This driver is the result of years of community work. Credit where credit is due:
- Jacques van Zuydam — autor original do laravel-firebird / original author of laravel-firebird.
- Harry Gulliford — mantenedor do fork que serve de base direta a este trabalho, com a maior parte da implementação atual / maintainer of the fork this work is directly based on, with the bulk of the current implementation.
- Contribuidores / contributors: Popa Marius Adrian (mariuz), Ricardo Seriani, Victor Vilella, Donny Kurnia, Felipi Franco, Johan Weultjes, Simon Rasmussen, fesoft, selmo47, Maitrepylos, entre outros / among others.
- Projeto Firebird e a extensão pdo_firebird / the Firebird project and the pdo_firebird extension.
- Comunidade Laravel / the Laravel community.
Current fork: benson/laravel-firebird — Alexandre Benson Smith (Thor Software).
License: MIT — same as the upstream project.

