Living plan. Ordered by dependency / ROI. Acceptance criteria spelled out so each item can be closed independently.
| Component | Today (v1.0.x) | Target |
|---|---|---|
| DuckDB | v1.5.3 (pinned in CI and build scripts); v1.5.2–v1.5.6 all validated (v1.5.6 on 2026-09-29, full suite 21/21 test files, local matrix scripts/build_matrix.ps1) |
v1.5.x + Stable C ABI when StorageExtension lands in it |
| Firebird server | 3.0, 4.0, 5.0 all tested live (CI matrix) | same |
Firebird client (libfbclient) |
4.0+ recommended (INT128 / TZ / DECFLOAT) | 4.0+ |
| GizmoSQL | works against the local build; air-gapped recipe documented | bundled docker image |
| Platforms | Linux x64, Windows x64 | + macOS arm64, + Linux arm64 |
v1.5.6 (released 2026-09-28) is a bug-fix line release: no change in the
extension API surface we use (extension_loader, CreateTableFunctionInfo,
StorageExtension::Register), and it carries the C-API v1 symbol-versioning
unification plus additive backports of DuckDB 2.0 API spellings. The build
pin intentionally stays v1.5.3 until the DuckDB Community Extensions catalog
moves its own target (currently v1.5.5); see
docs/pt/duckdb_1_5_compatibility_plan.md
for the validation record.
The XSQLDA codes for SQL_INT128 / SQL_TIMESTAMP_TZ / SQL_TIME_TZ / SQL_DEC16 / SQL_DEC34 are wired in firebird_types.cpp and verified
live against Firebird 4+ servers. The historical FB4+ gap was DECFLOAT
NULL-on-fetch; v0.6.0 fixes it with a lossless VARCHAR fallback via
server-side CAST(... AS VARCHAR(64)) (no OO API needed for this
fallback). A native
decimal-float decoder or a lossy numeric fast path remains future work.
Without these the rest of the milestone is unverifiable.
- Add
scripts/setup_test_firebird_v4.shandsetup_test_firebird_v5.shmirroring the existing v3 script. Usefirebirdsql/firebird:v4andfirebirdsql/firebird:v5images (or build fromC:\tmp\firebird-srcfor v5 RC1 if no image exists yet). - Add a
biz4.fdbfixture exercising every new FB4+ type:INT128,NUMERIC(38,…),DECFLOAT(16),DECFLOAT(34),TIMESTAMP WITH TIME ZONE,TIME WITH TIME ZONE. - New CI workflow
build-linux-fb4.yml(matrix: fb-version ∈ {3, 4, 5}) replays both sqllogictest files plus a newfirebird_fb4_types.test.
Acceptance: green CI on FB3 / FB4 / FB5 with all 11 existing types + the 5 new FB4 types.
Verified against Firebird 5.0.4 on Windows (May 2026). All three map correctly:
- INT128 max/min (
±170141183460469231731687303715884105727) → HUGEINT round-trip exact. - DECIMAL(38, 5) backed by INT128 → DECIMAL(38, 5) exact.
- TIMESTAMP_TZ with IANA zone (
America/Sao_Paulo) → TIMESTAMP WITH TIME ZONE; the IANA zone is not preserved (DuckDB's type is offset-only), but the UTC instant matches. - TIME_TZ → TIME WITH TIME ZONE, same offset-only behaviour.
A bug found during this verification has been fixed (LoadTableSchema
was missing the FB4 RDB$FIELD_TYPE codes 24–31 — see test_report.md
"Firebird 5 live coverage" section).
Historical gap: SQL_DEC16/SQL_DEC34 used to surface as DOUBLE but fetch
NULL, because DuckDB has no native IEEE decimal-floating-point type and the
legacy ISC API path does not decode Decimal64/Decimal128 directly.
v0.6 resolves this with the conservative, lossless path documented below:
server-side CAST(col AS VARCHAR(64)), surfaced as DuckDB VARCHAR. Native
Decimal64/Decimal128 decoding or an explicit lossy numeric fast path remain
future work only if real workloads justify them.
Firebird 5.0 RC1 changelog flags a few items worth verifying:
- #7682 ParallelWorkers default from
firebird.conf— server-side parallelism. The existingpartitions=Nnamed param still controls client-side parallelism; we should document the interaction (usepartitions=1on FB5 with highParallelWorkersto avoid 2x parallelism). - #7707 Better IN-list optimization — verify our
IN (…)pushdown still wins over per-element OR when the list has 1000+ items. RDB$RELATION_TYPEvalues — confirm types 1 (view), 2 (external), 4 (GTT preserve), 5 (GTT delete) still match in FB5; spot-checkRDB$INDICES,RDB$RELATION_CONSTRAINTS,RDB$INDEX_SEGMENTSfor the PK probe path didn't add required columns.- DSQL_unprepare on close — confirm the legacy ISC API path
(
isc_dsql_prepare/isc_dsql_fetch) still works on FB5; the modern OO API (Attachment::prepare) is preferred upstream but switching is a v1.0 task.
Acceptance: the existing test suite passes unchanged against FB5; README adds a "Firebird 5 + ParallelWorkers" note.
Two scripts now in scripts/:
gizmosql_aircache.sh— downloadsicu+spatialfor${DUCKDB_VERSION:-v1.5.3}/${PLATFORM:-linux_amd64}and populates~/.duckdb/extensions/…. Bind-mount that dir into GizmoSQL on air-gapped hosts.gizmosql_smoke.sh— brings up Firebird 5 + GizmoSQL containers, loads the extension, applies the biz4 fixture, and asserts a row count + dtype over Arrow Flight SQL via the Python ADBC driver.
Acceptance (still open): a CI job that runs gizmosql_smoke.sh
on every push to main. The matrix workflow build-linux-fb-matrix.yml
is the obvious host for it; deferred until the GitHub Actions billing
hold on this repo lifts.
Investigation against DuckDB v1.5.3 source shows TableFunction has
no pushdown_limit hook and LogicalGet has no limit field; a SQL
LIMIT N lands in a LogicalLimit above the scan, so the planner
never tells our scanner about it. The optimiser stops calling the
scan once N rows are emitted (cheap on a small N), so the missing
piece only matters when the cost of opening the cursor over a huge
result set is itself prohibitive.
Workaround in place: firebird_scan(…, row_limit=N) named param
already emits ROWS N in the Firebird SQL — the user just has to
write it explicitly when they know the limit at query time. The
firebird_query.cpp builder fully supports it; no further change
needed for the manual path.
Re-evaluate when DuckDB exposes a pushdown_limit hook on
TableFunction (tracked upstream — no fixed milestone). At that
point: wire the hook, set partitions = 1 whenever a limit is
present (avoid per-partition N×partitions over-fetch), drop this
deferred note.
pushdown_complex_filter hook now lifts BoundFunctionExpression of
the ~~ (LIKE) scalar function with a constant RHS, requires at
least one literal character before any %/_, and emits
col LIKE 'pattern' ESCAPE '\' into the Firebird WHERE. Patterns
starting with %/_, non-LIKE expressions, and any shape we don't
recognise stay in filters so DuckDB re-applies them above the scan.
Observation from live testing on FB5: DuckDB v1.5.3 already
decomposes the simple LIKE 'prefix%' case into col >= 'prefix' AND col < 'prefiq' before the hook runs, so the normal TableFilter
pushdown path consumes those. Our hook catches the patterns DuckDB
does not decompose — embedded %/_ (e.g. 'S%Textil',
'C_racao%') and any future LIKE variant that doesn't fit the
range-rewrite. The two paths cooperate.
Single-quote escaping in the pattern is handled by SqlLiteral;
backslash escape semantics by the ESCAPE '\' clause.
PM review on v0.4.4 set the v0.5 scope: paging, prepared
statements with bind variables, and predicate-pushdown gap fill.
Arrow RecordBatch produced directly by the scanner was explicitly
moved to v1.x — that is a different product/API, not part of v0.5.
The current Arrow integration (DuckDB converts Vector / DataChunk
to Arrow at the Flight SQL boundary) is already correct.
Items are listed in delivery order; each has a clear acceptance test.
Shipped in the v0.5 series and released in v0.5.0+.
What landed:
- New
row_offset=Mnamed param onfirebird_scan(...)alongside the existingrow_limit=N.FirebirdBindData.offset_overridecarries it through the bind path. FirebirdQueryBuilder::Buildaccepts both and emitsROWS M+1 TO M+Nwhen both are set (Firebird is 1-based, inclusive); falls back to the existingROWS Nwhen only limit is set.- Bind-time guards keep the API honest without breaking the v0.4
row_limit=Nsyntax:row_offsetwithoutrow_limitraises aBinderException(skip-then-drain is expensive and surprising).row_limit/row_offsetcombined withpartitions > 1(the user explicitly opted into parallel scan and paging) raises — those two are incompatible. The implicit case (nopartitions=argument, default 0) is silently coerced to serial so the v0.4firebird_scan(..., row_limit=100)spelling keeps working.row_offset + row_limitoverflow guard (sum > idx_t::max raises at bind time so the scanner never builds malformed SQL).
row_offset is per-scan only — deliberately not surfaced as an
ATTACH option, because paging is a per-query decision rather
than a catalog-wide policy.
Caveat documented in the README and the named-parameter table:
physical paging is unstable under concurrent writes — rows at
positions [M, M+N) can shift between calls. Recommend keyset
pagination over a sortable PK for live workloads.
Two-part item. Part (a) — input XSQLDA bind variables — is DONE in the v0.5 series. Part (b) — LRU statement cache per connection — is deferred until there's a benchmark showing the reuse-gain justifies the lifetime-management complexity under the ATTACH connection pool.
FirebirdStatement gained a parametrised constructor that runs
isc_dsql_describe_bind, allocates the input XSQLDA + per-column
buffers, encodes each Value positionally, and passes the input
sqlda to isc_dsql_execute. The builder's TranslateFilter
emits ? placeholders for bindable types and accumulates the
matching values into Result.params; the scanner threads them
through FirebirdConnection::OpenCursor(sql, params).
Encoding covers SQL_TEXT / SQL_VARYING (length-prefixed), SHORT / LONG / INT64, FLOAT / DOUBLE, BOOLEAN, DATE, TIME, TIMESTAMP. NULL is signalled through the indicator slot.
Strings always parametrise (the security gain is unconditional —
a user-supplied apostrophe can't break the SQL). HUGEINT /
DECIMAL still ship as inline literals via SafeLiteralInline
(numeric, no quoting hazard). Unsigned ints (UINTEGER, UBIGINT, …)
and TZ types stay residual — Firebird has no unsigned counterpart
on the bind path, and FB4's TZ-literal syntax varies enough that
inlining would regress.
Skipped in v0.5. Bind variables already deliver the security/correctness win that motivated the item; the prepare-per-cursor cost is bounded by the ATTACH connection pool's reuse, and the LRU would have to reason about cursor lifetime under that pool to be safe. Revisit when there is a concrete workload + microbenchmark showing prepare time is the bottleneck.
Risk to track when we do it: parameter XSQLDA encoding for FB4
types (INT128, TIMESTAMP_TZ, DECFLOAT) differs from the
output side. Each needs a live test on FB4 + FB5 before being
claimed.
Acceptance (when we do it): a benchmark issuing 10 000
sequential point lookups fb.main.CUSTOMER WHERE CUST_ID = $1
against a remote Firebird returns end-to-end at ≤ 1.2× the cost
of the equivalent direct-isql parameterised loop.
Shipped in v0.5. The pushdown_complex_filter hook now lifts
BoundOperatorExpression(COMPARE_NOT_IN) and
BoundOperatorExpression(OPERATOR_NOT) of a boolean column ref
in addition to the existing LIKE 'prefix%' translation.
BETWEEN doesn't need a hook — DuckDB already decomposes it into
col >= a AND col <= b and the TableFilterSet path handles
both sides. Safe CAST(...) rewrites are intentionally still
residual; DuckDB's expression simplifier folds the no-op cases
and the rest don't move the needle on the workloads we care
about today (revisit when there is one that does).
All new pushdowns honour the IsNoneTextColumnGated invariant —
NONE-text columns under non-strict transcoding stay residual,
matching the v0.4.2 contract.
Original v0.5 list still open (revisit only if measured):
Plug the holes that pushdown_complex_filter still leaves on the
DuckDB-side FILTER node. Each addition is conditional on the
NONE-text gate already in place (IsNoneTextColumnGated): a
predicate that touches a transcoded NONE column never pushes.
NOT IN (...): structurally identical toIN; emitcol NOT IN (...)and reuse the same literal-formatting path.- Boolean
NOT expr: detectBoundOperatorExpression(NOT)with a translatable child; emitNOT (...). COL BETWEEN a AND bwhen DuckDB hasn't decomposed it: detect the function shape inpushdown_complex_filterand emitcol BETWEEN a AND bdirectly.CAST(col AS X) op literalfor safeX(INTEGER, BIGINT, DECIMAL, DATE) — only when Firebird'sCAST(...)shares semantics with DuckDB's.LIKE 'suffix'/LIKE '%mid%'— measured benchmark first. On indexed columns these will already use a seq scan on Firebird's side, so the pushdown may not move the needle; document the finding and only ship if the benchmark is clearly positive.
Invariant: a predicate only goes to Firebird if we can prove it is bit-for-bit equivalent to DuckDB's evaluation. Anything else stays residual.
Acceptance: EXPLAIN shows the new predicates on the
FIREBIRD_SCAN's Filters: line; existing 245 sqllogictest
assertions stay green and new ones (one per added predicate)
cover the happy path + the NONE-text-gate residual path.
Hold this until the upstream DuckDB API exposes a real
pushdown_limit hook on TableFunction or a stable
OptimizerExtension entry point — neither is in v1.5.3. The
existing row_limit=N named param remains the documented path
in the meantime; the README already labels it as the supported
v0.4.x mechanism.
If we ship a custom OptimizerExtension rewrite (the
duckdb-postgres approach) without an upstream commitment, we
inherit the maintenance burden of tracking DuckDB's planner
changes release by release. The cost-benefit only favours that
when there is no near-term upstream alternative; revisit each
DuckDB minor.
Acceptance (when we do it): EXPLAIN SELECT * FROM fb.main.BIG_T LIMIT 100 shows ROWS 100 on the FIREBIRD_SCAN
node; same query without an explicit row_limit= runs in ms not
seconds on a 1 M-row remote table.
Shipped in v0.5. test/sql/firebird_metadata.test (79 assertions)
covers SHOW TABLES FROM fb, DESCRIBE fb.main.TAB,
information_schema.tables, information_schema.columns,
case-insensitive table lookup (fb.main.employee ==
fb.main."EMPLOYEE"), schema-not-found errors, and the read-only
enforce on INSERT / DROP TABLE / CREATE TABLE against the
ATTACH catalog. Documented gaps the suite intentionally pins
the current behaviour for:
is_nullablenow reflectsRDB$NULL_FLAGfor table columns. Domain / per-column precedence handled byCOALESCE(rf.RDB$NULL_FLAG, f.RDB$NULL_FLAG, 0)inLoadTableSchema; declaredNOT NULLcolumns surface asNOand get aNotNullConstraintin the ATTACHCreateTableInfo.- Views still appear as
table_type='BASE TABLE'ininformation_schema.tables. The catalog layer doesn't yet differentiateRDB$RELATION_TYPE = 1from regular tables. Deferred past v0.5 (needsViewCatalogEntrypath).
firebird_tables(...) + the ATTACH (TYPE firebird) catalog
already populate the entries DuckDB clients query. Lock that in
with explicit sqllogictest coverage so a future refactor cannot
break BI / dbt-style introspection:
SHOW TABLES FROM fb;DESCRIBE fb.main.TAB1;SELECT * FROM information_schema.tables WHERE table_catalog = 'fb';SELECT * FROM information_schema.columns WHERE table_catalog = 'fb' ORDER BY ordinal_position;
Acceptance: a new test/sql/firebird_metadata.test exercises
each form against the EMPLOYEE + FB4_TYPES fixtures and matches
the expected name/type rows verbatim.
Phase 1 (v0.5.2 / v0.5.3) and Phase 2 (v0.5.4 candidate) cover the operational surface a federated reader needs to be honest with its users: what SQL was sent, what it cost, and which connection ran it.
Two zero-arg table functions scoped to the current ClientContext:
firebird_last_query()— last attempted remote query (1 row max).firebird_query_log()— ring buffer, opt-in viaSET firebird_query_log_size = N.
15 columns shared between both: remote_sql, redacted binds,
table_name, projected_columns, pushed_filters,
residual_filters, rows_read, firebird_time_us,
total_time_us, connection_id, connection_reused,
parallel_scan, partitions, captured_at, sanitized
error_message. Text and BLOB binds surface as <text:redacted>;
password=... and scheme://user:pass@host patterns are stripped
from error text.
Acceptance: test/sql/firebird_observability.test covers
defaults, projection + predicate pushdown surfacing, no-leak
sentinel cross-check, ATTACH path, ring-buffer rotation + disable.
FirebirdConnectionPool grows three opt-in knobs read at ATTACH
time and validated up front:
SET firebird_pool_enabled = true|false— defaulttrue. Whenfalse, everyAcquireopens a fresh connection andReleasedestroys it.SET firebird_pool_max_size = N— caps the idle queue (not active leases).0= unlimited (default).SET firebird_pool_idle_timeout_ms = MS— discards connections that sat in the idle queue longer thanMS. Clock starts atRelease().0= no expiry (default).
Connection identity surfaces through the observability slot:
connection_id is a process-wide monotonic id assigned by the
extension (not a Firebird attachment id); connection_reused is
true when the lease came from the idle queue.
Settings are read at the ATTACH that creates the pool. A later
SET does not retune an existing pool; users must
DETACH + ATTACH to apply a change.
By design, the direct firebird_scan(...) table function does
not go through any pool — each call builds its own one-shot
connection. That keeps the no-ATTACH path simple and avoids
forcing pool semantics onto callers that don't want a catalog.
Acceptance: test/sql/firebird_pool.test covers defaults,
negative-value rejection, ATTACH reuse, opt-out fresh-every-time,
and direct-scan id behaviour. Pool defaults reproduce pre-Phase-2
behaviour (unlimited LIFO, no expiry, enabled) so existing
deployments observe no change.
v0.5.4 shipped a vcpkg.json declaring libfbclient as a
build-time dependency, but the upstream vcpkg registry used by
duckdb/community-extensions does not carry a libfbclient port,
so the community CI failed before CMake on every platform of
PR #1980. v0.5.5 removes the build-time dependency entirely.
What changed:
vcpkg.jsonremoved; descriptor dropsrequires_toolchains: vcpkg.CMakeLists.txtno longer linkslibfbclient. Headers ship vendored underthird_party/firebird/include/(Interbase Public License;THIRD_PARTY_NOTICES.md).- New
src/firebird_client_loader.{hpp,cpp}resolve the 16 ISC entry points the extension actually calls (isc_attach_database…fb_interpret, includingisc_open_blob2) viadlopen/LoadLibraryon the firstfirebird_*call. Lazy + mutex-guarded; once-per-process. DUCKDB_FIREBIRD_CLIENT_LIBRARYenvironment variable lets callers pin a specific library; the override is authoritative (failed load throws instead of falling back).firebird_client.cppdefines TU-local macros that redirect every ISC entry-point name to the loader table - no call-site edits, error-context strings unaffected (preprocessor does not expand inside"...").- README + CONTRIBUTING +
docs/pt/function_manual.mddocument the runtime requirement and the env-override knob.
Acceptance: build green on Windows local without linking
fbclient; runtime smoke against the EMPLOYEE fixture green;
DUCKDB_FIREBIRD_CLIENT_LIBRARY with a bad path raises the
documented IO Error: DUCKDB_FIREBIRD_CLIENT_LIBRARY is set to '...' but that file could not be loaded; full regression sweep
(8 firebird_* sqllogic suites) reports 398 assertions, 0 fail.
Once the v0.5.5 tag is published, duckdb/community-extensions
PR #1980 must be re-pointed at repo.ref: v0.5.5 so the
community build matrix can pick up the runtime-loader path.
v0.5.5 removed vcpkg.json entirely, but the community CI
always invokes the build with -DVCPKG_BUILD=1 -DVCPKG_MANIFEST_DIR=/duckdb_build_dir/. The vcpkg toolchain
needs a manifest file in that directory before CMake configures
the project, so the missing file moved the failure mode from
"libfbclient port not found" to "manifest not found". v0.5.6
restores vcpkg.json with empty dependencies:
{
"name": "duckdb-firebird",
"version-string": "0.5.6",
"description": "DuckDB extension that scans Firebird databases via runtime-loaded libfbclient",
"dependencies": []
}The runtime loader, the vendored Firebird headers, the
requires_toolchains: vcpkg removal, and every behavioural rule
shipped in v0.5.5 stay exactly as published. v0.5.6 is a
packaging-only follow-up: the toolchain has a manifest to read,
and the manifest declares no Firebird dependency.
Once the v0.5.6 tag is published, duckdb/community-extensions
PR #1980 should be re-pointed at repo.ref: v0.5.6 and taken
out of draft.
firebird_generate_dbt_sources(catalog_name VARCHAR) walks an
attached Firebird catalog's RDB$RELATIONS / RDB$RELATION_FIELDS
and emits a deterministic dbt sources.yml as a single VARCHAR
row.
Surface:
- Argument is the alias of an existing
ATTACH ... (TYPE firebird); the directfirebird_scan(...)path is out of scope by design. - Output is one row, one column
yaml VARCHAR. Pipe throughCOPY (SELECT yaml FROM ...) TO 'sources.yml'to write to disk. - The function name, schema (always
"main"), and connection alias appear in the YAML; the Firebird connection string and credentials never do.
Behaviour:
- Catalog validation raises
BinderExceptionwith the function name whencatalog_nameis NULL, names a non-attached catalog, or names an attached catalog whoseGetCatalogType()is not"firebird". - Metadata is read once per call through a
FirebirdMetadataLeaseRAII helper friended onFirebirdCatalog, so the catalog's pool is reused without exposingconn_info_. - Tables are sorted by
RDB$RELATION_NAME; columns byRDB$FIELD_POSITION. Views (RDB$RELATION_TYPE = 1) appear as tables with notests:block. data_typeis the DuckDBLogicalType::ToString()mapping of the column - the same string a user sees when querying throughATTACH.- Single-column primary keys emit
tests: [not_null, unique]. Composite primary keys deliberately emit notests:block (aCASE WHEN COUNT(*) = 1gate on the index segments returns NULL); marking any single member of a composite key asuniquewould be semantically wrong. Users who need multi-column uniqueness should adddbt-utils.unique_combination_of_columnsby hand. - All identifier and free-text strings emitted into the YAML
pass through a
YamlQuotehelper (escape of\\,\",\n,\r,\t, controls via\xNN); regression tests assert that the connection-string sentinel literalsSYSDBA,masterkey,firebird://,.fdb,passwordnever appear in the output.
Acceptance: test/sql/firebird_dbt_sources.test covers the
21 contracts above (argument validation, YAML header, source +
schema, table ordering, column ordering, data_type for INTEGER /
DECIMAL(18,2) / DATE / BOOLEAN, exactly three - not_null
markers, composite-PK block carries no tests, view block carries
no tests, no-leak sentinel). A TPK_COMPOSITE table is added to
both scripts/setup_test_firebird.sh and
scripts/smoke_fixture.sql, and test/sql/firebird_metadata.test
is updated in lockstep (4 → 5 tables, 16 → 19 columns) so the
existing fixture-count assertions stay green. Public manual entry:
docs/pt/function_manual.md, Nível 3 - Banco anexado.
Out of scope for this milestone (tracked for later):
- Named parameters (
source_name,target_schema,include_views). RDB$DESCRIPTIONBLOB capture for table and column comments (currently emitted as"").- Composite-PK
tests:block - deliberately empty for the reason above.
The duckdb-firebird extension is one component of a larger analytics stack. v0.5 docs make the boundary explicit so reviewers and integrators stop conflating responsibilities:
| Layer | Responsibility | Owned by |
|---|---|---|
| Source | OLTP data, RDB$ catalog | Firebird server (3 / 4 / 5) |
| Bridge | Firebird ⇄ DuckDB types, pushdown, paging | duckdb-firebird (this repo) |
| Engine | Vectorised execute, optimiser, Parquet, S3 IO | DuckDB v1.5.3 |
| Server (remote) | Arrow Flight SQL over the wire | GizmoSQL (DuckDB embedded) |
| Object store | Bronze / silver / gold artefacts | MinIO / S3 / R2 / GCS |
| Transformation | Medallion modelling, contract testing | dbt-duckdb |
| Consumption | Dashboards / ad-hoc | Power BI (Parquet import primary; ADBC Flight SQL for tooling that supports it) |
The MVP flow against a legacy ERP database:
LOAD firebird;
INSTALL httpfs; LOAD httpfs;
ATTACH 'C:/legacy/erp.fdb' AS fb
(TYPE firebird, user 'SYSDBA', password 'masterkey');
CREATE SCHEMA IF NOT EXISTS bronze;
CREATE SCHEMA IF NOT EXISTS silver;
CREATE SCHEMA IF NOT EXISTS gold;
CREATE OR REPLACE VIEW bronze.vendas AS
SELECT * FROM fb.main.TABENTRADASAIDA;
CREATE OR REPLACE TABLE silver.vendas AS
SELECT *
FROM bronze.vendas
WHERE DATAMOVIMENTO >= DATE '2024-01-01';
-- MinIO / S3-compatible: use CREATE SECRET, NOT the legacy
-- SET s3_* keys (those still work but DuckDB now recommends
-- the secret-based path).
CREATE SECRET IF NOT EXISTS s3_minio (
TYPE s3,
KEY_ID 'minioadmin',
SECRET 'minioadmin',
ENDPOINT 'minio.local:9000',
URL_STYLE 'path',
USE_SSL false);
COPY (
SELECT *,
year(DATAMOVIMENTO) AS ano,
month(DATAMOVIMENTO) AS mes
FROM silver.vendas
) TO 's3://lake/erp/vendas'
(FORMAT parquet, PARTITION_BY (ano, mes));For an MVP commercial release:
- Primary: Power BI imports the Parquet produced by the DuckDB pipeline above. Stable, supported, no driver questions.
- Secondary: ODBC / JDBC gateway against DuckDB. Validate in the target environment; behaviour varies by Power BI release.
- Advanced: Arrow Flight SQL via GizmoSQL — useful for Python / JDBC / ADBC tooling and for Power BI integrations that ship ADBC support; do not sell this as first-class universal Power BI today. Microsoft's ADBC story is still ramping (currently anchored on specific connectors like Databricks); avoid overclaim.
Strategic decision: the next phase is not firebird_materialize().
Materialization is not part of the core roadmap. DuckDB already owns
materialization, local tables, Parquet export, and lakehouse handoff well.
If a thin convenience helper is ever considered, it is an improvement
suggestion outside the core extension roadmap and must be re-approved by
the PM/HUMAN against the ACTION_GUIDE.
Materialization is DuckDB's strength. Firebird-native diagnostics is this extension's differentiator.
Delivery order for v0.6:
firebird_profile_table()- Heavy-view diagnostics
- Pushdown report / explainability
firebird_pool_stats()- DECFLOAT fallback
- Conservative aggregate pushdown
- Adaptive parallel scan recommendations
v0.6.0 release status: published. The remaining release work is the community-extension submission path for PR #1980 and any post-release documentation alignment.
Status: released in v0.6.0. See docs/en/function_manual.md for the
delivered behavior and current limitations.
Shape:
SELECT *
FROM firebird_profile_table('fb.main.TABELA');The analyzer returns (delivered in the first version unless noted):
- Object type:
TABLE/VIEW. (delivered) - Whether a primary key exists. (delivered)
- Primary-key columns. (delivered)
- Existing indexes. (delivered)
- Candidate watermark columns. (delivered, by type)
- Good filter columns. (delivered, indexed + cheap-type)
- Row estimate. (delivered: PK-range upper bound by default; opt-in exact
COUNT(*)viaexact_row_count=true) - Full-scan risk. (delivered)
- Recommended
partitions=N. (delivered, advisory, from PK range) - Required-filter recommendations. (delivered as structured alerts
filter_before_scan/materialize_before_scan)
Acceptance is diagnostic quality, not query execution speed. The function should help users decide whether to scan live, filter harder, partition, or materialize through DuckDB/dbt/Parquet.
Status: released in v0.6.0 (folded into firebird_profile_table() via the
warnings column). See docs/en/function_manual.md.
Detect and explain (delivered unless noted):
- View without a primary key. (delivered)
- View with joins. (delivered, token detection of
JOIN) - View with aggregation. (delivered,
GROUP BY/ aggregate functions) - View without a selective filter. (delivered, no
WHEREin definition) - Cases where DuckDB/dbt/Parquet materialization is the safer workflow. (delivered, surfaced as warnings)
The recommendation path is explicit: heavy Firebird views should often be materialized by DuckDB, dbt, or Parquet pipelines instead of hidden behind an extension-specific materialization wrapper.
Delivered as conservative token-level inspection of RDB$VIEW_SOURCE, not
a SQL parser or plan analyzer. The view source text is never returned;
comma-joins are not detected; unreadable definitions emit an explicit
view definition not inspected warning. No new function or schema change —
the existing 10-column firebird_profile_table() shape and its warnings
column carry the diagnostics.
Status: released in v0.6.0. Delivered by EXPANDING the existing
firebird_last_query() / firebird_query_log() telemetry (no new
function) — schema grew from 15 to 18 columns. See
docs/en/observability.md.
Expose what the connector sent to Firebird and what DuckDB still had to apply locally. Built on the existing remote-query telemetry instead of a disconnected diagnostics surface.
The report makes these decisions visible (delivered unless noted):
- projected columns (already in telemetry)
- pushed filters (already in telemetry)
- pushed paging / explicit row limits (delivered —
limit_pushed/offset_pushed,NULLwhen no paging pushed) - serial vs. parallel scan (already in telemetry)
- partition count (already in telemetry)
- predicates or operators not pushed (already in telemetry —
residual_filters) - why an operator stayed local (delivered —
not_pushed_reasons, coarse:NONE_CHARSET/UNSUPPORTED_OP/ROWID_OR_INVALID_COLUMN/UNSUPPORTED_PROJECTION_MAPPING)
Delivered as structured fields in the existing telemetry path, kept
factual, compact, and testable — not a cost-based optimizer, advisor, or
planner replacement. Reasons are coarse tags captured at the existing
pushdown-decision points; TranslateFilter was not broadly refactored.
Status: released in v0.6.0. See docs/en/function_manual.md.
Shape: firebird_pool_stats('fb') — explicit ATTACH alias, one row per
call. It does NOT enumerate catalogs (no no-argument form), reads only
counters the pool already tracks, and never leases a connection.
Expose pool state so the numbers have context (delivered unless noted):
- configured maximum idle size (delivered —
max_idle_size) - configured idle timeout (delivered —
idle_timeout_ms) - pool enabled flag (delivered —
pool_enabled) - idle connections (delivered —
idle_connections) - lifetime created / reused / discarded (delivered —
total_created/total_reused/total_discarded) - in-use / active connections (deferred — needs a new lease counter on the pool; reported before implementing per scope)
- last error, if tracked safely (deferred — no pool-level error history yet)
This closes most of the Phase 2 observability debt without making pool metrics the lead feature of v0.6, and without new pool instrumentation: the first version surfaces only what the pool already counts.
Status: released in v0.6.0. See docs/en/function_manual.md.
Resolve the remaining Firebird 4+ gap with a conservative fallback.
Delivered as lossless text: DECFLOAT(16) / DECFLOAT(34) are projected
server-side as CAST(col AS VARCHAR(64)) and surfaced as VARCHAR, ending
the previous silent-NULL behavior (the column was typed DOUBLE but always
fetched NULL). No local Decimal64/Decimal128 decoder, no DOUBLE default.
A lossy numeric fast path (opt-in) remains possible future work when
precision loss is acceptable.
Debt: the DECFLOAT test fixture is dedicated (scripts/fixture_decfloat.sql
via FIREBIRD_DECFLOAT_DB) and skips in CI. Promoting it into the main
setup_test_firebird.sh fixture — with the coordinated metadata /
dbt-sources test updates that a new relation forces — is a deliberate
future step.
Status: deferred (investigated during v0.6.0, not implemented). Decision recorded so the same path is not reopened blindly.
Investigation finding (DuckDB v1.5.3): there is no extension-facing
aggregate-pushdown hook on TableFunction. The clean path for a
COUNT(*) shortcut is get_partition_stats — the optimizer
(StatisticsPropagator::TryExecuteAggregates) already folds COUNT(*)
with no GROUP BY into a constant when the scan returns a
PartitionStatistics with an exact count. The blocker: GetPartitionStatsInput
exposes only {table_function, bind_data} and does not give the
callback access to table_filters. The optimizer applies the filter
after calling the callback. So inside get_partition_stats we cannot
tell a bare COUNT(*) from a COUNT(*) ... WHERE .... Implementing it
anyway would run a remote SELECT COUNT(*) for a filtered query, only for
the optimizer to bail on the filter and fall back to a normal scan — an
invisible extra round-trip and misleading telemetry on a filtered query.
We do not accept that surprise cost on production Firebird.
Deferred until DuckDB exposes table filters to the partition-stats
callback, or another clean hook appears, or we explicitly accept the
filtered-query round-trip tradeoff. bind_operator / bind_replace are
not viable here: they run before the regular bind and cannot see the
LogicalAggregate above the scan.
When eventually unblocked, the first version stays deliberately small.
Initially allowed:
COUNT(*)MIN/MAX- controlled
SUM - simple
GROUP BY
Out of scope:
- joins
- complex
HAVING - calculated expressions
BLOB/TEXT- problematic charset
NONE - heavy views
- uncertain overflow
The analyzer should gate this work: push aggregation only when the Firebird object is simple enough and the result semantics are boring.
Status: released in v0.6.0. See docs/en/function_manual.md.
Diagnostic / recommendation-only — not autotuning. firebird_scan is
unchanged: nothing is parallelized automatically, and no performance gain is
promised. The work refines the existing firebird_profile_table()
recommended_partitions and warnings (10-column schema unchanged):
- recommend
partitions=Nfrom the single-column numeric PKMIN/MAXrange width only (no row count, noCOUNT(*), no full scan), capped at 8 (delivered) - recommend
partitions=1even for a numeric PK when the range is small, with an explicit note (delivered) - recommend
partitions=1with a warning for views, no-PK, composite, and non-numeric PK (delivered) - emit a server-side parallelism caveat when
partitions > 1is suggested, pointing at Firebird 5ParallelWorkers— a generic caveat, since the server's parallel setting is not probed (delivered)
Do not start with fully automatic tuning. Automatic parallelism can surprise production Firebird systems, especially when server-side parallelism is already configured. Automatic adaptation stays out of scope until the recommendation path is observable, benchmarked, and boring across Firebird 3/4/5.
Limitation: the recommended_partitions > 1 branch is not exercised by a
CI fixture — a wide-range numeric-PK table would force a new relation into
the main fixture and cascade metadata / dbt-sources test updates. The
span heuristic is deterministic and covered by code; the = 1 paths and
the warnings are covered by firebird_profile_table.test.
v0.6.0 is published for real production use. The release gate completed: Windows build, GitHub Linux/Windows checks, FB 3/4/5 matrix, local Docker community-path simulation, and Docker smoke. After v0.6.0, the roadmap accepts only core stabilization and community-submission work unless PM/HUMAN explicitly opens a new milestone:
- release-blocking build, portability, packaging, or CI defects;
- regressions in Firebird scan, ATTACH, type mapping, pushdown, telemetry, pool behavior, or diagnostics;
- documentation required for users to run the released extension safely;
- community-extension metadata required to update PR #1980.
Known non-blocking technical debt stays recorded instead of delaying v0.6:
- row estimates and structured required-filter recommendations in
firebird_profile_table(); (delivered post-v1.0.2) - active/in-use count and
last_errorinfirebird_pool_stats(); (delivered post-v1.0.2) - promoting the DECFLOAT fixture into the main CI fixture; (delivered
post-v1.0.2 as main-suite execution:
firebird_decfloat.testruns in the canonical suite step on every FB4/FB5 leg and in local bootstraps on Firebird 4+; DECVALS deliberately keeps its own database because the main fixture's relation lists are pinned by static cross-version expectations) - exercising
recommended_partitions > 1in CI; (delivered post-v1.0.2:firebird_partitions_scan.test, dedicated sparse-wide-PK fixture, all three Firebird legs) - aggregate pushdown, blocked by DuckDB v1.5.3 API limitations.
Completed for v0.6.0 and required again before future publication:
- HUMAN final acceptance test for the complete v0.6 feature set.
- Windows native build and Firebird sqllogictest group green.
- Docker/Linux gcc community-path simulation green from a fresh recursive clone of the committed HEAD.
- Optional but recommended: Docker smoke test that loads the extension and runs at least one synthetic Firebird query end-to-end.
git statusclean on the release branch.- PT/EN docs parity checked for every user-visible change.
- Changelog / release notes prepared.
- Version and tag approved by HUMAN.
community-extensions/description.ymlupdated to the approved tag/ref.- Explicit handling plan for
duckdb/community-extensions#1980.
Release-test notes:
- The extension must not link
libfbclientat build time; it must continue to load the client library at runtime. - Tests against
C:\Athenas\restaurado.fdbmay read metadata and aggregate counts only. Do not save, export, materialize, log, commit, or push real data from that database. - New release artifacts, Docker scratch files, and local simulation files must stay ignored unless they are intentionally part of the public source.
A firebird_arrow_scan function that emits arrow::RecordBatch
directly from the XSQLDA fetch loop — bypassing the
Vector / DataChunk step DuckDB normally drives. Only worth
building when there's a concrete consumer that benefits measurably
over the current DuckDB→Arrow conversion path. v0.5 deliberately
stays out of this and keeps the README's "Arrow note" honest.
community-extensions/description.yml already exists. Process:
- Tag the approved v0.6 release on this repo.
- Fork
duckdb/community-extensions. - Copy
description.ymltoextensions/firebird/description.yml, setrepo.refto the approved v0.6 tag. - Open PR; their CI builds Linux/Windows/macOS binaries from the tag.
- If PR #1980 already exists, update it explicitly to the new v0.6 description/ref instead of opening a conflicting submission.
Acceptance: INSTALL firebird FROM community; LOAD firebird;
works on a vanilla DuckDB CLI on three platforms.
The DuckDB Stable C ABI (duckdb_extension.h) currently does not
support StorageExtension. Migrating today would mean dropping
ATTACH 'firebird://…' AS fb (TYPE firebird). Watch
duckdb/duckdb#… for the
catalog C API; when it lands, port firebird_storage.cpp to it. Until
then, ship per-DuckDB-minor-version binaries via community-extensions.
- Writes —
INSERT / UPDATE / DELETE / CREATE TABLEagainstfb.main.…. The use case is "lift Firebird OLTP data into DuckDB for analytics", not "use Firebird through DuckDB". Adding writes changes the transaction profile (isc_tpb_readis hard-wired) and the risk envelope (a bug deletes user data). - DDL emission —
firebird_attach_sql()already covers the view- based path; replicating Firebird's DDL inside DuckDB is a much bigger surface than the wins justify. - Replacement scan (so
SELECT * FROM 'file.fdb'.EMPLOYEEJust Works) — neat trick butATTACHis the right surface for a multi-table source.
Additional non-core suggestions kept outside the roadmap:
firebird_materialize()as a roadmap item - DuckDB already owns materialization, local tables, Parquet export, and lakehouse handoff. A future convenience wrapper may be discussed as a separate improvement suggestion, but it is not part of the core extension roadmap.- CDC / ETL / orchestration / governance features - these belong to dbt, Airflow, Dagster, dlt, Meltano, CDC tools, or data-governance systems. The extension stays focused on safe, observable Firebird access from DuckDB.