Database drivers: native and firebirdsql¶
fbadmin talks to Firebird servers through one of two Python drivers. By default it picks one itself (Automatic): the faster native driver when it can be used with a Firebird 4 or newer client library, otherwise the pure-Python one. You can also choose one for the whole fbadmin instance.
| native (used by Automatic when possible) | firebirdsql (pure Python) | |
|---|---|---|
| What it is | firebird-driver, the official Python driver, a ctypes binding over the Firebird client library (libfbclient.so / fbclient.dll / libfbclient.dylib) |
firebirdsql, a pure-Python implementation of the Firebird wire protocol, plus fbadmin's fixes for Firebird 4+ types (driver_patches.py, fb4types.py) |
| Install | firebird-driver is a dependency of fbadmin (Python 3.11+). The client library is not on PyPI: it comes with the desktop app and the Docker image, or from a Firebird installation / your distribution (see Which client library) |
Nothing: it is a dependency of fbadmin |
| Runs on | Platforms with a Firebird client build (Linux x64/arm64, Windows, macOS) | Anything that runs Python |
| Strengths | Much faster for anything that moves data (see Benchmarks), above all with wire encryption, which firebirdsql does in Python; the client Firebird itself ships, so new protocol features arrive with the library |
No native code; same behaviour everywhere; no library to deploy |
| Weaknesses | One more native dependency; a process can load only one libfbclient; a Firebird 3 client cannot decode Firebird 4+ types | Slow at bulk data with wire encryption on (the Firebird default) |
Local (embedded) databases of the desktop app always use the native driver with the bundled Firebird 5 engine, whatever is selected here (see EMBEDDED.md). The setting on this page is about connections to Firebird servers.
Automatic selection (the default)¶
With the setting Automatic (auto, the default when nothing is saved and FBADMIN_DRIVER
is not set), fbadmin uses the native driver when all of these hold, and firebirdsql
otherwise:
- the
firebird-driverpackage can be imported (it is a dependency on Python 3.11+; on Python 3.10 it is not installed: "firebird-driver is not installed"); - a client library loads ("No Firebird client library found ..." or the load error);
- it is a Firebird 4 or newer client. A Firebird 3 client would get INT128, DECFLOAT and the time zone types only as legacy types, which the pure driver reads exactly ("The client library is Firebird 3: ...");
- that client can load ICU, which it needs for time zone names. Without ICU a Firebird
client reads
TIMESTAMP '... Europe/Prague'without its zone and cannot send one at all ("... cannot load ICU ..."). Linux distributions and Firebird's own packages install ICU; minimal containers may not.
The client library is looked for in this order: the embedded engine's file set when present
(the desktop app, or FBADMIN_EMBEDDED_DIR; one process can load only one libfbclient, and
local databases need that one), then the configured path (FBADMIN_FBCLIENT or the setting
in the panel; if it does not exist, Automatic uses firebirdsql and says so), then the system
library search path (firebird-driver's default lookup: libfbclient.so via ldconfig,
fbclient.dll on the PATH, the Firebird framework on macOS).
The decision is made once, when it is first needed, and again when the setting changes; the
log says which driver is in use and why at startup (INFO, logger fbadmin.drivers):
INFO fbadmin.drivers: Database driver: native (setting auto, default). Firebird 5.0 (LI-V6.3.4.1812) client library: libfbclient.so.2
Safety net at run time. If a native connection attempt fails for a reason that is not a
Firebird error, Automatic makes that connection with firebirdsql instead, logs a warning
once, and the panel shows "Native driver failed, using firebirdsql: ...". Such failures are:
the client library or firebird-driver cannot be loaded, the library raises an OSError or a
ctypes error, or it reports a problem with its own installation (a plugin, provider, ICU or
libtomcrypt it cannot load). When the library itself does not load, Automatic stops trying it
until the setting changes. Firebird errors are never a reason to fall back: a wrong
password, an unknown database, a refused connection or a server-side error are reported as
they are, whichever driver is in use. An explicitly selected driver never falls back either.
Choosing and configuring¶
In the UI: open the user menu (in desktop mode the Security button in the top bar) → Database driver…. In server mode only administrators see it. The panel offers:
- Automatic (recommended): native when available, otherwise pure Python;
- Native (firebird-driver): always the native driver. It is disabled, and the API refuses it, when the native driver cannot be used: the package is missing, the library is not found or does not load, or it is older than Firebird 3;
- Pure Python (firebirdsql): always the pure-Python driver.
It shows the selected setting and where it comes from (environment, saved setting or
default), the driver in use and the reason (for Automatic: which rule decided), a
run-time fallback if one happened, whether firebird-driver is installed and which version,
which libfbclient is loaded (or would be), its version, whether it can load ICU, and the
client library path (empty: the embedded engine's library when present, else the system
search path).
The choice is saved in driver.json in the data directory ({"driver": "native", "fbclient":
"/opt/firebird/lib/libfbclient.so"}). The API is GET /api/driver and PUT /api/driver
({"driver": "auto" | "native" | "firebirdsql", "fbclient": "<path>"}), for administrators
only in server mode. GET also returns effective (native or firebirdsql), reason and
fallback.
Environment variables win over the saved setting, and the panel then shows "set by environment" and keeps those fields read-only:
FBADMIN_DRIVER=auto # or native, firebirdsql
FBADMIN_FBCLIENT=/opt/firebird/lib/libfbclient.so # the client library
What happens when you switch:
- When the driver in use changes, the pooled metadata connections (object explorer, data grid, dashboards) are closed, so new ones use it. The panel says how many were closed.
- Open Query Tool tabs keep their connection, and therefore their driver, until they are closed. Closing them would throw away uncommitted transactions; a session that keeps running on the old driver is harmless. New Query Tool tabs use the new driver.
- The client library is loaded once per process. If a different library is already loaded, the panel says so; the new path takes effect after a restart.
- With the embedded engine's file set present (the desktop app, or
FBADMIN_EMBEDDED_DIR), its Firebird 5 client library serves remote servers too.
Which client library¶
Use a Firebird 4 or 5 client library; it works with Firebird 3, 4, 5 and 6 servers, and Automatic uses the native driver with it. Where to get one:
- Desktop app and Docker image: included (Firebird 5.0.4), nothing to install.
- A Firebird 4 or 5 server installation on the same machine (the official installers and tarballs install the client library too).
- Linux distributions: the
libfbclient2package. It is a Firebird 4 client in Debian 13 "trixie" (4.0.5) and newer, and in Ubuntu 25.04 (4.0.5), 25.10 and 26.04 (4.0.6). Debian 12 "bookworm" and Ubuntu 24.04 LTS and older ship a Firebird 3 client (3.0.11), with which Automatic keeps the pure-Python driver. A Firebird 4 client works with Firebird 5 servers, but it cannot send service options that are new in Firebird 5, such as parallel workers for backup, restore and sweep: fbadmin then offers only the options both the client library and the server understand, and says why. Use a Firebird 5 client (or the pure-Python driver) to get those options. - Elsewhere: extract the client from the official release archive, e.g.
python packaging/embedded/fetch-embedded.py --client-only --dest /opt/firebird(Linux; needslibtommath1and ICU from the system) and pointFBADMIN_FBCLIENTat/opt/firebird/lib/libfbclient.so.2.
With no client library, or a Firebird 3 one, fbadmin works exactly as before with the pure-Python driver.
If you select Native explicitly with a Firebird 3 client, fbadmin binds the Firebird 4+ types
to legacy types on Firebird 4+ servers (SET BIND OF INT128 / DECFLOAT / TIME ZONE TO LEGACY):
values arrive as BIGINT, DOUBLE PRECISION and timestamps without a time zone, and the panel
shows a warning.
Wire encryption: the client file sets fbadmin ships (desktop, Docker, fetch-embedded.py)
include Firebird's ChaCha plugin, so the native driver negotiates ChaCha64 with Firebird 4+
servers, like a normal Firebird client (checked with
RDB$GET_CONTEXT('SYSTEM', 'WIRE_CRYPT_PLUGIN') against Firebird 4, 5 and the 6 snapshot).
Before 2.1 the file set had no ChaCha plugin and the native driver used Arc4. Firebird 3
servers only offer Arc4.
pip, desktop app and Docker¶
- pip (
pip install firebird-admin): installsfirebird-driveron Python 3.11+. If a Firebird 4+ client library is installed (see above), Automatic uses the native driver; otherwise the pure-Python one, with nothing else to do.pip install "firebird-admin[native]"still works; the extra is now empty. - Desktop app: it bundles the Firebird 5 client with the embedded engine, so Automatic uses the native driver for remote servers too, through the same library local databases use (one libfbclient per process).
- Docker image: it contains the Firebird 5.0.4 client for its architecture (amd64 and
arm64):
libfbclient, the ChaCha plugin and libtomcrypt from the pinned, SHA-256-checked official release (fetch-embedded.py --client-only, about 4.7 MB), plus Debian'slibtommath1and ICU (libicu76, about 37 MB, needed for time zone names). The library is on the system library path, so Automatic finds it and uses the native driver.FBADMIN_DRIVER=firebirdsqlselects the pure-Python driver. Compared with 2.0 the image is about 49 MB larger uncompressed (19 MB compressed), most of it ICU.
What each feature uses¶
With the native driver in use, every connection to a registered server goes through
firebird-driver. backend/fbadmin/native.py adapts it to the small DB-API subset fbadmin
uses. Errors become the same firebirdsql exception classes with the same message format.
Values are made to match (CHAR without trailing blanks, INT128 without a scale as int). The
only exception is the event monitor.
| Feature | firebirdsql | native | How (native) |
|---|---|---|---|
| Object explorer, properties, DDL, schema tools, data grid, dashboards, monitoring (MON$), overview | ✅ | ✅ | NativeConnection / NativeCursor |
| Query Tool: transactions, isolation levels, read-only, lock timeout | ✅ | ✅ | The raw TPB fbadmin builds becomes firebird-driver's default_tpb |
| Query Tool: statement timeout, cancel | ✅ | ✅ | SET STATEMENT TIMEOUT (Firebird 4+). Cancel is DELETE FROM MON$STATEMENTS from another attachment, as with firebirdsql. The native call releases the GIL, so the cancel request runs at once |
| Query Tool: plans and explained plans | ✅ | ✅ | Statement.plan / Statement.detailed_plan (firebirdsql: isc_info_sql_get_plan / explain_plan) |
| Query Tool: parameters, fetch more, statistics, column types | ✅ | ✅ | |
| Data import, data generator, copy data, data compare | ✅ | ✅ | cursor.prep() is emulated with a prepared Statement |
| PSQL debugger (trace, run, tests) and profiler | ✅ | ✅ | firebirdsql uses wire-level fetches so it keeps the rows sent before an error. The native client hands those rows out before raising the error, so plain cursor fetches suffice |
| Create and drop database | ✅ | ✅ | create_database with page size and default character set in the DPB / drop_database |
| Services: backup/restore (server file, streamed download, upload restore), nbackup, gstat, validation, sweep, limbo, shutdown/online, properties, server info, server log | ✅ | ✅ | NativeServices: firebird-driver's service manager. The three wire primitives fbadmin's service code uses (_op_service_start, _op_service_info, _op_response) map to IService.start / IService.query, which take the same buffers. The same class serves local databases |
| Firebird users (Services API), trace sessions, replication status | ✅ | ✅ | Same NativeServices |
| Wire encryption (per server) | ✅ | ✅ | WireCrypt = Enabled / Disabled in the connection's configuration (isc_dpb_config / isc_spb_config) |
| Wire compression (per server) | ✅ | ✅ | WireCompression = true. The client needs zlib, which Linux systems have |
| Authentication plugin (per server) | ✅ | ✅ | AuthClient. Srp256 means "Srp256, then Srp", because firebirdsql also falls back to Srp when the server lacks Srp256 (a Firebird 3 default) |
| Connect timeout | ✅ | ✅ | ConnectionTimeout = 30 |
| INT128, DECFLOAT, NUMERIC(38), TIME/TIMESTAMP WITH TIME ZONE | ✅ | ✅ | With a Firebird 4+ client that can load ICU; a Firebird 3 client gets legacy types (see above) |
| Event monitor (POST_EVENT listener) | ✅ | firebirdsql | It speaks the auxiliary-port protocol itself (events/wire.py) on top of firebirdsql's socket, so api/events.py opens its connection with driver="firebirdsql" |
fbadmin command line (backup, restore, sql, ...) |
✅ | ✅ | Follows FBADMIN_DRIVER / the saved setting (Automatic by default) |
| Local (embedded) databases | — | always | The embedded engine runs in-process, which only libfbclient can do |
Correctness: the two drivers compared¶
backend/tests/test_driver_equivalence.py runs the same queries and API calls through both
drivers and requires identical serialized results, the JSON the browser receives. It
covers:
- Types: every type, with NULLs and extremes. That includes INT128 at both limits, NUMERIC(38,6), DECFLOAT(16/34) up to 1E+100 and down to 1E-6143, NaN, ±Infinity and -0, TIME/TIMESTAMP WITH TIME ZONE by region and offset, and year 1 / 9999 dates.
- Character sets and BLOBs: UTF8 (emoji, CJK), WIN1252, NONE and OCTETS columns, text BLOBs in UTF8 and WIN1252, and a 256 KB binary BLOB.
- Parameters: values bound as parameters, including a 100,000-character text BLOB and the Firebird 4+ types.
- Query Tool behaviour: column metadata, fetch more on an open cursor, error messages and positions for unknown tables, syntax errors and constraint violations, and plans.
- Metadata endpoints: object lists, table DDL, the data grid and row counts.
It runs in the backend-native CI job against Firebird 3, 4, 5 and 6. Writing it found
four differences, all fixed in the adapter so the native driver matches firebirdsql:
- Error messages.
fb_interpretputs a-in front of every continuation line of an error; firebirdsql does not. The adapter strips it. - CHAR padding. firebirdsql returns CHAR values without trailing blanks; firebird-driver keeps them. The adapter strips them.
- INT128 without a scale. firebird-driver returns a
Decimal(for example the result ofBIGINT / 2on Firebird 4+); firebirdsql returns anint. The adapter converts it. - Exception classes. The adapter now raises the classes firebirdsql raises for the same
status codes:
IntegrityErrorfor constraint violations,DataErrorfor arithmetic errors, otherwiseOperationalError.test_error_classes_match_firebirdsqlchecks this.
Two differences remain on the Firebird 6 snapshot, both bugs on the firebirdsql side:
- WIN1252 text BLOBs: firebirdsql receives them untransliterated and fails to decode them ("'utf-8' codec can't decode byte 0xe9").
- WIN1252 parameters: a value firebirdsql binds for a WIN1252 column is stored without transliteration, so non-ASCII characters come back garbled.
The native driver gets both right. The equivalence test avoids these two cases on Firebird 6 and says why. Firebird 3, 4 and 5 show no difference.
scripts/driver-compare.py also compares one value set per type group outside the test
suite; see the "Correctness" line under each benchmark table.
The benchmark found one more firebirdsql limitation:
- Unclosed cursors on Firebird 5. firebirdsql keeps a statement allocated on the server until its cursor is closed or the connection ends. Firebird 5 shares compiled statements and refuses a 1001st concurrent execution of the same request. So one connection that runs the same query more than 1000 times without closing its cursors fails with "Too many concurrent executions of the same request".
- Reproduced with a plain loop: it fails at iteration 1001 on Firebird 5, and passes
with
cursor.close()or on Firebird 4. - Where it shows in fbadmin: the catalog code does not close its cursors, so a long metadata job on one connection can hit this (loading the 352-object schema below a few times did).
- The native driver is not affected: the adapter frees a cursor's statement when the cursor goes away.
- The benchmark therefore opens a fresh connection per metadata run. Closing the cursors
in
Catalog.rows()is a separate fix.
Benchmarks¶
How it was measured¶
scripts/driver-compare.py measures both drivers against each server. It uses fbadmin's own
connection layer (fbadmin.connections.connect), the same code path the app uses, so the
native numbers include fbadmin's adapter. Each test runs once as a warm-up, then 5 times
(3 times for a test whose warm-up took longer than 5 s). The tables give the median and, in
brackets, the fastest and slowest run. CPU is the client process's CPU time, which shows
how much of the wall time Python spends on the client side.
| Test | What it does |
|---|---|
| connect + attach + detach | Open and close one connection (10 runs) |
| round trip ×1000 | SELECT 1 FROM RDB$DATABASE 1000 times |
| fetch 10,000 / 100,000 rows | SELECT * from a table of INTEGER, BIGINT, VARCHAR(50) UTF8 (non-ASCII), NUMERIC(18,4), DOUBLE, TIMESTAMP, DATE and BOOLEAN; on Firebird 4+ also INT128, DECFLOAT(34) and TIMESTAMP WITH TIME ZONE |
| fetch 100,000 rows, firebird-driver without fbadmin's adapter | The same fetch on a plain firebird.driver connection, to show the adapter's own cost |
| insert 5,000 rows (params) | One prepared INSERT with 9 parameters, executed 5,000 times, then a commit |
| BLOB write / read 1 MB, 10 MB | Random bytes into a BLOB SUB_TYPE BINARY as a parameter, and reading them back |
| metadata load | What the object tree and the properties panes query (counts, the list of every object type, columns, constraints, indices, triggers and DDL of every table) for a generated schema of 352 objects: 100 tables with a primary key, a foreign key and an index each, 50 views, 50 procedures, 50 triggers, a domain and a sequence. Each run opens a fresh connection (see the cursor note under Correctness) |
| backup stream to local file | A gbak backup through the Services API streamed to stdout (backup_stream, used for "download a backup") and written to a local file. The backup holds the 100,000-row table, 9 to 13 MB |
To reproduce (servers as in DEVELOPMENT.md; takes about two hours, mostly
the firebirdsql runs; --quick gives a smoke test in a few minutes):
scripts/embedded-test-setup.sh # Firebird 5 client library → build/firebird-embedded
.venv/bin/python scripts/driver-compare.py --ports 3053,3054,3050,3056 \
--fbclient build/firebird-embedded/lib/libfbclient.so.2 --json driver-compare.json --md driver-compare.md
What the numbers say¶
- Wire encryption dominates for firebirdsql. Firebird encrypts the wire by default
(
WireCrypt = Requiredin the Docker images used here), and firebirdsql encrypts and decrypts in pure Python. Profiling firebirdsql 1.5.0 reading a 1 MB BLOB and 20,000 rows on Firebird 5 put about 95% of the client time infirebirdsql/chacha.py, nearly all of it computing the ChaCha20 key stream (the quarter rounds), not XOR-ing it with the data. The firebirdsql CPU column is almost equal to its wall time in the bulk tests, so the client CPU is the bottleneck, not the server or the loopback network. That explains the extremes for anything that moves bytes: BLOBs are 36 to 390 times faster with the native driver, and backup streaming 12 to 49 times. - ChaCha is the costlier cipher. On Firebird 4+ firebirdsql negotiates ChaCha64, and the gap is largest there. On Firebird 3 both drivers use Arc4, and firebirdsql is "only" 36 to 140 times slower for BLOBs.
- The two drivers did not use the same cipher on Firebird 4+ in these runs. The native driver used Arc4 there too, because the client library used for the runs has no ChaCha plugin. The file sets fbadmin ships include it (see Which client library), so there the native driver negotiates ChaCha64 like firebirdsql. In C that costs about the same: writing and reading a 10 MB BLOB on Firebird 5 took 443 / 116 ms with ChaCha64 against 436 / 139 ms with Arc4 (median of 5, same machine, within the run-to-run spread).
- Row fetches are 2.3 to 4 times faster natively (3 to 5 times with firebirdsql 1.4). Both drivers are CPU-bound in Python here, the native one in firebird-driver's ctypes value conversion. fbadmin's adapter costs little: the "without fbadmin's adapter" rows are 2 to 8% faster.
- Small round trips are 7 to 13 times faster natively. About 0.23 to 0.31 ms instead of 1.7 to 3.2 ms per query.
- Connects cost the same (20 to 32 ms, 52 ms on the Firebird 6 snapshot), dominated by the server's authentication (SRP).
- Parameterized inserts gain least (the same to 2 times). Each execute is a server round trip either way; firebird-driver's input conversion is not cheap.
- Metadata load is 1.3 to 4.5 times faster natively. It is many small queries, so it sits between round trips and fetches.
firebirdsql 1.5.0 compared with 1.4.7¶
firebirdsql 1.5.0 brought performance work
(pyfirebirdsql#143): rows are parsed from
a read-ahead buffer without copying, the ChaCha20 XOR works in place, and executemany uses
the Firebird 4+ batch API (fbadmin does not call executemany). The benchmark was run again
with 1.5.0 the day after the 1.4.7 run, on the same machine and servers. Time saved by 1.5.0
(positive = faster; bold = more than the ±15% run-to-run spread):
| Test | Firebird 3 | Firebird 4 | Firebird 5 | Firebird 6 |
|---|---|---|---|---|
| connect + attach + detach | +1% | −7% | +9% | −2% |
| round trip ×1000 | −22% | −9% | −19% | +5% |
| fetch 10,000 rows | +19% | +5% | +9% | +11% |
| fetch 100,000 rows | +17% | +8% | +14% | +5% |
| insert 5,000 rows (params) | +14% | −12% | +14% | +6% |
| BLOB write 1 MB | −18% | −1% | +9% | −1% |
| BLOB read 1 MB | −1% | −12% | +6% | +1% |
| BLOB write 10 MB | −15% | −1% | +5% | +7% |
| BLOB read 10 MB | +3% | −5% | −7% | +1% |
| metadata load (352 objects) | +14% | −1% | −7% | +5% |
| backup stream to local file | −5% | +21% | +2% | +20% |
- Row fetches are 5 to 19% faster, the clearest gain, and also the one a back-to-back A/B run of both versions (plain driver, no fbadmin code) confirmed: 15 to 20% on Firebird 5.
- BLOBs, round trips and metadata did not change beyond the noise: the A/B run showed no consistent difference, so the larger swings above (round trips on Firebird 3 and 5, BLOB writes on Firebird 3) are run-to-run variation. With encryption on, BLOB and backup speed is set by the Python ChaCha20/Arc4 key stream, which 1.5.0 did not change.
- Backup streaming was about 20% faster on Firebird 4 and 6, not on 3 and 5.
- The gap to the native driver is therefore about the same as with 1.4.7, a little smaller for row fetches.
fbadmin works with both. 1.5.0 needed three adjustments in fbadmin, all guarded so 1.4.x keeps working:
driver_patches.py: the INT128 / time zone parameter patch recognises 1.5.0's new null check (array support) inparams_to_blr.driver_patches.py: 1.5.0 asserts the wrong bytes when the description of a statement's parameters continues in a second info request (assert buf2[:2] == bytes([0x08, 0x07]), where the server sendsisc_info_sql_bind, isc_info_sql_describe_vars, i.e.[5, 7]). Any statement with about 19 or more parameters failed withAssertionError, for example "Copy data" into a wide table. fbadmin corrects the assert until the driver does.security/wirecompat.py: fbadmin's replacement of the driver's packet reader now reads 1.5.0's read-ahead buffer first (otherwise an error after a fetch, such as a cancelled query, blocked), and the new fetch loop gets the same guard against waiting inselect()while decompressed bytes are already buffered (otherwise wire compression with a connection timeout stalled).
Caveats¶
- One machine, loopback. Everything ran in one container (4 CPUs) with the servers in Docker on the same host, over loopback. Network latency, which would add the same per round trip to both drivers, is absent. The client and the servers shared the CPUs.
- Indicative only. Spreads were mostly within ±15%. Ratios above about 1.2× are meaningful; the connect differences are not.
- Encryption on, compression off. These were the defaults. With
WireCrypt = Disabledon the server and encryption off in the registration, firebirdsql's bulk numbers would improve a lot. This was not measured: the test servers require encryption. - The Firebird 6 numbers are from a pre-release snapshot.
- The numbers are single runs of the script (1.4.7 on 2026-10-24, 1.5.0 on 2026-10-25), not a statistical study.
Results¶
Environment: Python 3.13.16, Linux-6.18.44-fc-v153-x86_64-with-glibc2.39, 4 CPUs; firebirdsql 1.5.0; firebird-driver 2.0.3 with libfbclient Firebird 5.0 (LI-V6.3.4.1812). Median of 5 runs after 1 warm-up (min–max in brackets); times in ms, lower is better. ratio = firebirdsql time / native time.
Firebird 3.0.14 (port 3053)¶
| Test | firebirdsql | native | ratio | firebirdsql CPU | native CPU |
|---|---|---|---|---|---|
| connect + attach + detach | 25.0 (22.4–25.9) | 23.3 (21.4–29.1) | 1.07× | 5.0 | 4.7 |
| round trip ×1000 | 1,738 (1,620–1,816) | 244 (217–287) | 7.12× | 850 | 102 |
| fetch 10,000 rows | 786 (635–905) | 312 (292–329) | 2.52× | 763 | 309 |
| fetch 100,000 rows | 7,711 (7,437–8,302) | 3,355 (3,321–3,612) | 2.30× | 7,463 | 3,329 |
| fetch 100,000 rows, firebird-driver without fbadmin's adapter | 3,243 (3,088–3,323) | 3,211 | |||
| insert 5,000 rows (params) | 3,387 (3,244–3,892) | 4,007 (3,680–4,500) | 0.85× | 1,714 | 2,409 |
| BLOB write 1 MB | 798 (762–952) | 22.2 (20.7–25.9) | 35.94× | 725 | 7.0 |
| BLOB read 1 MB | 917 (860–987) | 13.0 (11.8–14.8) | 70.80× | 654 | 7.2 |
| BLOB write 10 MB | 8,249 (8,078–8,401) | 188 (170–231) | 43.88× | 7,719 | 60.7 |
| BLOB read 10 MB | 14,083 (14,047–14,370) | 99.2 (97.4–110) | 141.91× | 10,926 | 60.3 |
| metadata load (352 objects) | 3,367 (3,257–3,598) | 2,615 (2,463–2,722) | 1.29× | 1,766 | 1,291 |
| backup stream to local file | 9,640 (9,147–9,767) | 782 (757–870) | 12.33× | 8,816 | 78.6 |
Backup size 9.2 MB. Wire: firebirdsql: encrypted=TRUE, compressed=FALSE; native: encrypted=TRUE, compressed=FALSE.
Correctness: 8 of 8 type groups serialize identically.
Firebird 4.0.7 (port 3054)¶
| Test | firebirdsql | native | ratio | firebirdsql CPU | native CPU |
|---|---|---|---|---|---|
| connect + attach + detach | 23.8 (23.1–29.5) | 31.6 (28.1–40.5) | 0.75× | 5.7 | 5.6 |
| round trip ×1000 | 3,208 (2,750–3,339) | 308 (262–340) | 10.40× | 2,003 | 116 |
| fetch 10,000 rows | 3,667 (3,283–3,715) | 1,163 (961–1,288) | 3.15× | 3,605 | 1,150 |
| fetch 100,000 rows | 35,153 (34,061–35,559) | 9,556 (9,190–9,934) | 3.68× | 34,622 | 9,471 |
| fetch 100,000 rows, firebird-driver without fbadmin's adapter | 8,802 (8,389–9,263) | 8,535 | |||
| insert 5,000 rows (params) | 7,330 (6,809–7,531) | 4,049 (3,766–4,269) | 1.81× | 4,722 | 2,389 |
| BLOB write 1 MB | 2,295 (2,225–2,496) | 25.3 (23.3–26.1) | 90.58× | 2,217 | 7.5 |
| BLOB read 1 MB | 3,022 (2,881–3,230) | 12.3 (11.8–13.9) | 246.21× | 2,630 | 6.2 |
| BLOB write 10 MB | 23,003 (22,459–23,071) | 190 (185–254) | 120.85× | 22,292 | 63.7 |
| BLOB read 10 MB | 36,342 (36,052–37,683) | 106 (98.3–108) | 342.37× | 31,921 | 64.7 |
| metadata load (352 objects) | 7,948 (7,648–8,075) | 2,383 (2,208–2,607) | 3.34× | 5,870 | 1,190 |
| backup stream to local file | 29,028 (28,767–30,487) | 997 (970–1,069) | 29.11× | 27,654 | 105 |
Backup size 13.2 MB. Wire: firebirdsql: encrypted=TRUE (ChaCha64), compressed=FALSE; native: encrypted=TRUE (Arc4), compressed=FALSE.
Correctness: 12 of 12 type groups serialize identically.
Firebird 5.0.4 (port 3050)¶
| Test | firebirdsql | native | ratio | firebirdsql CPU | native CPU |
|---|---|---|---|---|---|
| connect + attach + detach | 20.1 (18.5–21.4) | 21.4 (18.9–24.2) | 0.94× | 4.7 | 4.0 |
| round trip ×1000 | 2,917 (2,491–3,035) | 233 (220–371) | 12.52× | 1,919 | 94.4 |
| fetch 10,000 rows | 3,352 (3,248–3,426) | 995 (933–1,349) | 3.37× | 3,292 | 974 |
| fetch 100,000 rows | 34,691 (34,083–34,831) | 9,422 (9,366–9,498) | 3.68× | 34,130 | 9,260 |
| fetch 100,000 rows, firebird-driver without fbadmin's adapter | 9,224 (8,824–9,473) | 8,953 | |||
| insert 5,000 rows (params) | 6,528 (6,351–6,767) | 3,803 (3,521–4,058) | 1.72× | 4,215 | 2,243 |
| BLOB write 1 MB | 2,252 (2,085–2,360) | 22.7 (20.1–31.1) | 99.23× | 2,194 | 7.1 |
| BLOB read 1 MB | 2,872 (2,796–3,013) | 12.4 (10.8–13.4) | 232.11× | 2,480 | 5.4 |
| BLOB write 10 MB | 23,232 (23,141–25,345) | 188 (180–223) | 123.71× | 22,529 | 61.8 |
| BLOB read 10 MB | 39,124 (38,679–39,323) | 99.4 (95.1–110) | 393.45× | 34,056 | 61.0 |
| metadata load (352 objects) | 8,861 (8,718–9,045) | 1,969 (1,887–2,059) | 4.50× | 6,650 | 1,170 |
| backup stream to local file | 38,857 (31,852–41,786) | 802 (786–837) | 48.47× | 37,304 | 115 |
Backup size 13.1 MB. Wire: firebirdsql: encrypted=TRUE (ChaCha64), compressed=FALSE; native: encrypted=TRUE (Arc4), compressed=FALSE.
Correctness: 12 of 12 type groups serialize identically.
Firebird 6.0.0 (port 3056)¶
| Test | firebirdsql | native | ratio | firebirdsql CPU | native CPU |
|---|---|---|---|---|---|
| connect + attach + detach | 52.1 (46.8–65.3) | 52.2 (45.1–66.8) | 1.00× | 5.0 | 4.1 |
| round trip ×1000 | 3,142 (2,934–3,279) | 241 (234–269) | 13.05× | 2,026 | 98.7 |
| fetch 10,000 rows | 3,583 (3,288–3,959) | 970 (854–1,027) | 3.69× | 3,534 | 952 |
| fetch 100,000 rows | 38,220 (38,157–39,719) | 9,431 (9,274–10,079) | 4.05× | 37,628 | 9,352 |
| fetch 100,000 rows, firebird-driver without fbadmin's adapter | 8,908 (8,857–9,789) | 8,818 | |||
| insert 5,000 rows (params) | 7,435 (7,399–7,600) | 3,642 (3,443–4,212) | 2.04× | 4,671 | 2,239 |
| BLOB write 1 MB | 2,490 (2,297–2,605) | 24.1 (21.0–25.5) | 103.23× | 2,398 | 7.5 |
| BLOB read 1 MB | 2,999 (2,911–3,101) | 11.4 (9.9–13.8) | 263.18× | 2,598 | 6.3 |
| BLOB write 10 MB | 22,846 (22,420–24,857) | 185 (155–283) | 123.26× | 22,101 | 61.0 |
| BLOB read 10 MB | 35,699 (34,411–36,388) | 100 (92.8–102) | 356.04× | 31,501 | 62.8 |
| metadata load (352 objects) | 8,069 (7,794–8,542) | 2,526 (2,439–2,632) | 3.19× | 5,648 | 1,168 |
| backup stream to local file | 29,603 (27,847–31,560) | 717 (672–742) | 41.27× | 28,465 | 98.8 |
Backup size 13.1 MB. Wire: firebirdsql: encrypted=TRUE (ChaCha64), compressed=FALSE; native: encrypted=TRUE (Arc4), compressed=FALSE.
Correctness: 12 of 12 type groups serialize identically.
Tests and CI¶
- Locally: see DEVELOPMENT.md. In short:
FBADMIN_FBCLIENT=build/firebird-embedded/lib/libfbclient.so.2 python -m pytest -qruns the whole backend suite with the default setting, which then resolves to the native driver;FBADMIN_DRIVER=firebirdsqlforces the pure-Python one. - CI: the plain
backendjobs run the whole suite with the pure-Python driver (FBADMIN_DRIVER=firebirdsql). Their runner image (Ubuntu 26.04) shipslibfbclient2, a Firebird 4 client, so Automatic itself resolves to native there, as it does for users on Ubuntu 25.04+ and Debian 13;test_driver_auto.pyasserts it (FBADMIN_TEST_EXPECT_DRIVER=native). One more job runs the native driver with that system Firebird 4 client against a Firebird 5 server. Thebackend-nativejob of.github/workflows/ci.ymlruns the full suite withFBADMIN_DRIVER=nativeagainst Firebird 3, 4, 5 and the 6 snapshot, and checks that Automatic resolves to native with that client (FBADMIN_TEST_EXPECT_DRIVER=native) and negotiates ChaCha64. Firebird 6 is reported but does not fail the build. The client library is the Firebird 5 one from the pinned, SHA-256-checked embedded file set (scripts/embedded-test-setup.sh); the release archive is cached between runs. Thee2ejob runs the Playwright suite with the default setting (Automatic, which resolves to native with the embedded file set's client) and withFBADMIN_DRIVER=firebirdsql.e2e/tests/driver.spec.tscovers the settings panel, switching to each driver and back, and an axe audit. - Automatic selection:
backend/tests/test_driver_auto.pycovers the resolution rules (package missing, no library, Firebird 3 client, Firebird 4+ client, no ICU, environment and saved settings, explicit native refused when unavailable) and the run-time fallback (a client-library failure falls back, a Firebird error such as a wrong password does not), also against a real server with a simulatedOSErrorfrom the client library. - Driver-specific tests:
backend/tests/test_native_driver.pycovers the setting and its API, the environment override, switching at run time (pools closed, open Query Tool sessions kept), Query Tool sessions, services, wire options, error classes, 1 and 10 MB BLOBs, Firebird 4+ types (or legacy binding with a Firebird 3 client), and the event monitor staying on firebirdsql.