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() emit WITH 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 NULL on 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/createOrFirst now work under race conditions (via UniqueConstraintViolationException).
  • Configurable constraint/index naming (index_names): define PK_{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()): emit COMPUTED BY (always virtual on Firebird).
  • Temporary tables ($table->temporary()): emit GLOBAL TEMPORARY TABLE ... ON COMMIT PRESERVE ROWS (previously threw).
  • json() as BLOB SUB_TYPE TEXT (instead of a truncating VARCHAR(8191)).

What we implemented

CategoryItemType
BugInteger bind into DECIMAL/NUMERIC (scale)Fix
BugdropAllTables() with uppercase/mixed namesFix
Bugdate cast conversion error (-413)Fix
Bugexists() over UNION queriesFix
VersionIdentity columns (FB3+) with generator+trigger fallback (FB2.5)Feature
VersionWITH TIME ZONE types (FB4+)Feature
Version63-char identifiers (FB4+) + anti-collision hashFeature
VersionVersion-aware change() nullabilityFeature
VersiongroupLimit via ROW_NUMBER() (FB3+)Feature
VersionServer version / feature detectionFeature
NewReal-constraint insertOrIgnore (any unique)Feature
NewConfigurable naming (index_names)Feature
NewuniqueIndex() / dropUniqueIndex()Feature
NewCOMMENT ON (table/column)Feature
Newauto_increment in introspectionFeature
NewIN chunking > 1499 itemsFeature
NewAutomatic reconnect (lost connection)Feature
NewUniqueConstraintViolationException (firstOrCreate)Feature
NewSerializesFirebirdDates trait (date cast without time)Feature
NewComputed columns (virtualAs/storedAs → COMPUTED BY)Feature
NewTemporary tables (temporary() → GTT)Feature
Newjson() as BLOB SUB_TYPE TEXTImprovement

Before × After (parity per feature)

Legend: ✅ full · ⚠️ partial · ❌ missing/broken · 🚫 Firebird’s own limitation

FeatureBeforeAfter
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.

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading...

Leave a Reply