odbc_fast 3.6.0
odbc_fast: ^3.6.0 copied to clipboard
Enterprise-grade ODBC data platform for Dart with a Rust native engine, streaming queries, pooling, and structured diagnostics.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased #
3.6.0 - 2026-05-02 #
Added #
- Async diagnostics parity: the worker-isolate backend now exposes per-connection structured error retrieval, aligning async error diagnostics more closely with the sync/native backend.
- Regression coverage: added Dart and Rust tests for repeated named placeholders, parameterized execution with more than five parameters, parameterized multi-result execution, NULL-heavy parameter binding, async statement metadata invalidation, and async streaming fallback behavior.
- Test coverage expansion: unit/component coverage was extended in Dart for
parser, prepared-statement, async-wrapper, repository-gap, driver capability,
library loader, telemetry, and stress-oriented paths; Rust and opt-in E2E
coverage were expanded for
>5parameters, repeated named-parameter flows, NULL scenarios, and batch / multi-result execution paths. - Examples:
example/named_parameters_demo.dartnow demonstrates repeated named placeholders and>5named parameters;example/multi_result_demo.dartnow includesexecuteQueryMultiParams;example/README.mdlists additional demos that were previously omitted from the index.
Fixed #
- Parameterized execution limit: removed the artificial runtime cap of 5
parameters across direct execution, prepared statements, named-parameter
execution (
executeQueryNamed,prepareNamed,executePreparedNamed), parameterized multi-result execution, and batch execution. Named parameters benefit from the same fix because they are expanded into the positional runtime pipeline before execution. The effective limits now come from the package protocol safety cap and the underlying driver/database. - Named parameter semantics: repeated placeholders such as
@id/:idnow preserve occurrence order and correctly reuse the same input value at every positional expansion. - Named parameter parsing robustness: placeholder rewriting now skips SQL string literals, identifier quotes, line comments, nested block comments, and PostgreSQL-style dollar-quoted strings, avoiding accidental rewrites inside SQL text.
- Typed NULL binding and inference: Rust parameter binding now uses typed
input parameters instead of coercing everything through strings, improving
correctness for
NULL, binary values, mixed integer/BigInt families, and metadata-driven parameter descriptions in direct, prepared, directed, and batch paths. - Async statement invalidation:
clearAllStatements,disconnect, and reconnect flows now clear Dart-side prepared-statement metadata so stale statement IDs do not survive after the native layer invalidates them. - Async multi-result fallback:
streamQueryMultinow degrades gracefully toexecuteQueryMultiFullwhen the async worker cannot start a streaming multi-result session, matching the sync/native fallback behavior on older binaries. - Native library loading during development: the native asset hook now prefers local workspace builds before the version cache, reducing the chance of running tests against stale native binaries.
Changed #
- Runtime performance and internals: execution and batch hot paths now use dynamic ODBC parameter binding collections, reuse shared column-description helpers, and reduce repeated plugin-lock work during row-shape discovery.
- Public docs/comments: Dart and Rust API comments were updated to remove the obsolete "up to 5 parameters" wording and to document repeated named placeholder support.
- README / API docs:
README.mdanddoc/API_SURFACE.mdnow describe dynamic parameter counts, repeated named placeholder behavior, and the current async XA limitation more explicitly.
3.5.4 - 2026-04-24 #
Added #
- Dart API surface: public exports now include driver capabilities,
driver feature helpers, and pool option types from
package:odbc_fast/odbc_fast.dart. - High-level native controls: repository/service layers now expose advanced
pool creation with
PoolOptions,poolSetSize, liveDbmsInfointrospection,setLogLevel, andclearAllStatements.
Fixed #
- Rust FFI safety: telemetry, columnar decompression, async requests, streaming, pool, transaction, and query entry points are hardened so panics do not cross FFI boundaries and disconnected resources are cleaned up more consistently.
- Protocol robustness: row, multi-result, parameter, columnar, and decompression paths now reject malformed or oversized payloads with explicit errors instead of relying on truncating casts or unbounded decode paths.
- ODBC execution correctness: batch execution reuses prepared statements for
parameterized batches, preserves SQL Server
FOR JSONrow shapes, and avoids silent re-execution after pending result expiry. - XA state handling: runtime
unwrap()paths in XA state transitions were replaced with error propagation. - E2E Docker stack: the test-runner image now supports IBM Db2 CLI packages
that ship
libdb2.so.1withoutlibdb2o.so.1by creating a compatibility alias during image build.
Changed #
- Performance: streaming, row encoding, columnar conversion, compression, metrics, async runtime usage, and parallel bulk insert hot paths reduce avoidable allocation, copying, and lock contention.
- Tests: added focused Rust regression coverage for FFI/protocol safety and Dart tests covering the newly public API exports and service/repository delegation paths.
3.5.3 - 2026-04-24 #
Fixed #
- Rust doctests:
native/odbc_engine/src/ffi/guard.rsno longer usesignoreRust code fences for illustrative FFI snippets. Those blocks are nowtext, socargo test --include-ignored/cargo test --docdo not try to compile non–self-contained examples. - MSDTC / XA regression smokes:
xa_dtc_sqlserver_*tests requireENABLE_MSDTC_XA_TESTS=1in addition toENABLE_E2E_TESTSand a SQL Server DSN. Without it the tests return early (pass) instead of failing onSQL_ATTR_ENLIST_IN_DTCwhen a DSN is present but MSDTC enlist is unavailable. Helper:should_run_msdtc_xa_tests()innative/odbc_engine/tests/helpers/e2e.rs.
Changed #
- Docs:
doc/development/msdtc-recovery.mddocumentsENABLE_MSDTC_XA_TESTSand updates the local PowerShell runbook.
3.5.2 - 2026-04-24 #
Fixed #
- CI / clippy (Linux):
output_aware_paramsimportedsize_ofunconditionally while only using it on Windows code paths. The import is now#[cfg(windows)], fixingclippy -D warnings(unused import) on Linux runners.
3.5.1 - 2026-04-24 #
Fixed #
- CI / Linux build (Rust): fixed
output_aware_paramstext boxing so input text uses owned buffers consistently (TextBox), resolvingE0308on Linux runners (expected VarCell<Box<[u8]>, Text>, found VarCell<&[u8], Text>). - CI / rustfmt: applied formatting normalization in the DRT1 execution path
and related regression files so
cargo fmt --all -- --checkpasses again.
3.5.0 - 2026-04-24 #
Fixed #
- DRT1 /
OUT1(Dart):BinaryProtocolParsernow compares the trailer to the little-endian u32 ofb"OUT1"(same on-wire four bytes asRowBufferEncoder::append_output_footerinnative/odbc_engine). The previous constant (0x4F555431) was the u32 ofb"1TUO"; native results with realOUT/INOUTvalues no longer arrive with emptyQueryResult.outputParamValues. - DRT1 + multi-result (Rust): the directed OUT engine path
(
execute_query_with_bound_params_and_timeout) no longer silently discards extra result sets fromSQLMoreResults. When the drain is empty (single result set — the common case) the wire format is unchanged (ODBC magic +OUT1). When drain has items, the engine emits a MULT envelope (same v2 framing asexecute_multi_result) followed byOUT1, so stored procedures that also perform DML or return multipleSELECTresult sets now deliver all items to the caller. - DRT1 + MULT RowCount-first (Rust): when a DML-first stored procedure
starts with an
INSERT/UPDATE/DELETE(no initial cursor), the engine previously discarded the affected-row count (let _rc = ...) and emitted a spurious emptyResultSetas the first MULT item. The row count is now captured and emitted asMultiResultItem::RowCount(n), so the on-wire item order faithfully mirrors the logical execution order. - DRT1 + MULT RowCount-first (Dart):
OdbcRepositoryImpl._parseMultiDirectedBufferpreviously always mapped item[0] toQueryResult.columns/rows/rowCount, silently discarding it when item[0] was aRowCount. Now, when item[0] is aRowCount, the primary fields remain empty and all items (including item[0]) are surfaced inQueryResult.additionalResults, preserving order and preventing data loss. - E2E SQL Server directed OUT test:
test/e2e/mssql_directed_out_multi_rset_test.dartwas using#dummy_e2e_multi(a connection-scoped temp table created insetUpAll) inside the stored procedure. Since the procedure executes on a different connection the temp table was invisible, causing the test to fail with an "invalid object name" error. The procedure now uses aDECLARE @t TABLE(table variable) which is fully scoped per call and requires no external setup.
Added #
- DRT1 + multi-result (Dart parser):
MultiResultParsergainsparseMultiWithOutputswhich decodes a MULT v2 envelope + trailingOUT1in one call.OdbcRepositoryImpl._parseBufferToQueryResultnow detects the MULT magic and routes to the new decoder branch; single-RS callers are unaffected. QueryResult.additionalResults: new optional field (default []) exposing tail items from a directed multi-result response asDirectedResultItem/DirectedRowCountItem(both extend the sealedDirectedMultiItem). Existing code that only readsrows/outputParamValuesrequires no changes.- Regression tests — D1:
native/odbc_engine/tests/regression/d1_drt1_multi_result_wire.rs(9 pure-protocol Rust unit tests) pins the on-wire contract: drain-empty path is byte-for-byte identical to legacy; drain-non-empty ResultSet-first and RowCount-first paths start with MULT and haveOUT1after the multi frame; RowCount → ResultSet → RowCount → OUT1 round-trip verified. - Regression tests — Dart (parser):
test/infrastructure/native/protocol/multi_result_parser_multi_out_test.dart(11 tests) coversparseMultiWithOutputsincluding RowCount-first and RowCount → ResultSet → RowCount → OUT1 round-trips. - Regression tests — Dart (repository):
test/infrastructure/repositories/odbc_repository_directed_rowcount_first_test.dart(3 tests) validates the repository mapping for RowCount-first MULT buffers: primary fields empty, all items inadditionalResults, and ResultSet-first backwards-compatibility unchanged. - E2E opt-in (SQL Server multi-result + OUT):
test/e2e/mssql_directed_out_multi_rset_test.dart— setE2E_MSSQL_DIRECTED_OUT_MULTI=1+ODBC_TEST_DSN(SQL Server DSN, ODBC Driver 17+) to run a proc that returns twoSELECTresult sets and anINT OUTPUT; validatesadditionalResultsandoutputParamValues. - Test suite stability:
RUST_TEST_THREADS=1set in.cargo/config.tomlto keepffi::testsstable without needing to pass-- --test-threads=1manually.ENABLE_SLOW_E2E_TESTS=1now gates long-running stress / benchmark tests (e2e_bulk_transaction_stress_test, pool stress, 50 k-row streaming, BCP 100 k, bulk compare benchmark). should_run_slow_e2e_tests()helper innative/odbc_engine/tests/helpers/e2e.rs.- Documentation (pendências / maturação): TYPE_MAPPING
— tabela de certificação Oracle ref cursor (preencimento manual), texto
alinhado ao path omit-
?+SQLMoreResults; columnar_protocol_sketch — secção Criterion benches (columnar_v1_v2_encode,columnar_v2_placeholder); PENDING / ROADMAP_PENDENTES — CI MSDTC live como ad hoc, checklist release OCI XA, scope TVP /SqlDataType; REF_CURSOR_ORACLE_ROADMAP — edge backlog. Columnar decode DX: mensagensFormatExceptionmais explícitas quandoodbc_columnar_decompressfalha (buildodbc_engine, algoritmos,library_loader). - Oracle DRT1 +
RefCursorOut(motor): strip de?eParamValuefiltrada (ref_cursor_oracle); prepare + execute +SQLMoreResults→RowBufferv1 por cursor +RowBufferEncoder::append_ref_cursor_footerapósOUT1(lógica Oracle Database ODBC — omissão de ref cursor no call). ErroDIRECTED_PARAM|ref_cursor_out_oracle_only:…fora do plugin Oracle. Teste integration opt-in ignorede2e_oracle_ref_cursor_test(E2E_ORACLE_REFCURSOR=1). - Roadmap / Oracle REF CURSOR (documentation):
doc/notes/ROADMAP_PENDENTES.mdorders open epics;doc/notes/REF_CURSOR_ORACLE_ROADMAP.mdis the spike and integration plan forSYS_REFCURSORbind+fetch+RC1(motor ainda a devolverref_cursor_out_bind_not_enabled); PENDING, TYPE_MAPPING §3.1.1, andmsdtc-recoverylink from the new index. Comment inoutput_aware_params.rspoints to the roadmap. - MSDTC DX (Windows /
xa-dtc): Local runbook indoc/development/msdtc-recovery.md(env,regression_test+--ignored,ENABLE_E2E_TESTS); PENDING §1.1 anddocker-test-stacklink to it; code comments inxa_dtc.rs/xa_dtc_test.rsaligned. Optional workflow.github/workflows/windows_xa_dtc_build.ymlalso runscargo test --liband compiles (--no-run) integration tests (no live MSDTC). - MSDTC E2E (segundo smoke):
regression_testganhaxa_dtc_sqlserver_prepare_commit_smoke(prepare →commit); o existente mantém rollback. Runbook e PENDING alinhados;Xiddistinto. - Directed / OUTPUT observability:
output_aware_paramsValidationErrorstrings for unsupported DRT1 shapes now use the stableDIRECTED_PARAM|…prefix and slugs (e.g.binary_out_inout_not_implemented);doc/notes/TYPE_MAPPING.md§3.1 documents the table by engine and §3.1.1 the REF CURSOR design only. - Columnar A/B bench: Criterion
columnar_v1_v2_encode
compares v1
RowBufferEncodervs v2ColumnarEncoder(nocompress + zstd). - Columnar decode DX:
isColumnarNativeDecompressAvailableand richerFormatExceptionwhen decompression returns null. - MSDTC scope: msdtc-recovery.md states explicitly that Reenlist is not implemented in-crate; PENDING 1.1/§2 updated accordingly.
- Columnar v2 golden (zstd): committed
test/fixtures/columnar_v2_int32_zstd.golden(RustColumnarEncoderwith per-column zstd); sync test columnar_v2_zstd_golden_file.rs; Dartcolumnar_v2_zstd_golden_test.dartparses the file whenodbc_columnar_decompressis loadable. - Directed params (Dart slug match):
validateDirectedOutInOutruns onserializeDirectedParamswith the sameDIRECTED_PARAM|…slugs asoutput_aware_params(fast fail before FFI). - Ref cursor wire (v1, Oracle prep):
ParamValuetag6(ParamValue::RefCursorOut/ParamValueRefCursorOuton Dart);RC1\0trailer (materialized v1 blobs) +QueryResult.refCursorResultsdecoded on the client;RowBufferEncoder::append_ref_cursor_footeron the native encoder. O happy path Oracle usa o plugin + strip de?SQLMoreResults; uma chamada defensiva abound_to_slotscomRefCursorOutfora desse path continua a devolverref_cursor_out_bind_not_enabled.
- MSDTC ops doc: Application-facing enlist/unenlist guidance in msdtc-recovery.md (log full message, do not reuse a failed enlisted handle without recycling).
- E2E PostgreSQL directed
OUT:test/e2e/postgres_directed_out_test.dart—CREATE PROCEDUREwith twoOUT(integer + text),CALLover DRT1; opt-in withE2E_PG_DIRECTED_OUT=1andODBC_TEST_DSN(host with Dart + PG ODBC; not run byscripts/docker_e2e). Notes in docker-test-stack.md. - E2E SQL Server directed
OUT(DRT1): test/e2e/mssql_directed_out_test.dart — opt-in withE2E_MSSQL_DIRECTED_OUT=1,ODBC_TEST_DSNto a SQL Server DSN, and a database login that mayCREATE/DROPthe proc indbo(see docker-test-stack §Optional / SQL Server directedOUT).
Changed #
-
odbc_engine (DRT1 +
OUT): the directed path uses preallocate +SQLExecDirect(same shape asodbc_api::Connection::execute), thenExecutionEngine::drive_more_resultsbefore reading output bind buffers, so SQL Server and similar drivers populateOUTPUTafterSQLMoreResults(aligned with the multi-result and Oracle ref-cursor paths). -
SqlDataType(30-kind roadmap):geometry(SQL Server planar WKT, same wire asgeography);intervalYearToMonth(String,[years, months], orMapwith0..11month field →INTERVAL 'y-m' YEAR TO MONTH); the third slot isjsonwithvalidate: true(kindjson_validated, already present). -
Output / INOUT (MVP): DRT1 request buffer (
serializeDirectedParams, Rustbound_param);IOdbcRepository.executeQueryParamBufferandIOdbcService.executeQueryDirectedParams;OUT1result footer; Rust engine output-aware binding (integer and string /DecimalOUT/INOUTwith wide or narrowVar*Char); andQueryResult.outputParamValues. The legacyparamValuesFromDirectedlist remains in-only (throws for non-input).BinaryProtocolParser.parseWithOutputs/QueryResultdocs indoc/notes/TYPE_MAPPING.md§3.1;example/output_param_directions_demo.dartshows DRT1 + a live directed query whenODBC_TEST_DSNis set. -
Columnar v2 (Dart + native):
BinaryProtocolParserdecodes v2; when a column is compressed, it calls the native FFIodbc_columnar_decompress/odbc_columnar_decompress_free(sameCompressionTypeasodbc_engine: 1 = zstd, 2 = lz4). Uncompressed column blocks are unchanged. Optional Cargocolumnar-v2anchors remain.columnar_v2_flags.dartanddoc/notes/columnar_protocol_sketch.mdupdated to match. -
MSDTC hardening (docs/CI):
doc/development/msdtc-recovery.md(Reenlist / scenarios), optionalwindows_xa_dtc_build.yml(workflow_dispatchforxa-dtcon Windows), and PENDING/TYPE_MAPPING follow-ups. -
Docker test-runner: IBM Db2 ODBC/CLI from IBM’s public DHE tarball (
IBM_ODBC_CLI_VERSIONinDockerfile.test-runner);IBM DB2 ODBC DRIVERin/etc/odbcinst.ini. -
CI (
e2e_docker_stack.yml):db2matrix (test_multi_db_*onTESTDB);scripts/docker_e2e.*support-Engine db2with a longerdocker_db_upwait.
Changed #
- Backlog documentation:
doc/Features/PENDING_IMPLEMENTATIONS.md,doc/Features/PENDING_IMPLEMENTATIONS.md,doc/notes/TYPE_MAPPING.md, the columnar sketch,doc/CAPABILITIES_v3.md, andREADME.md(MSDTC row) updated to match shipped scope.
Fixed #
odbc_engine(unit test,--features xa-dtc):prepared_xa_commit_rejects_wrong_statenow initialisesPreparedXa::dtc_branchon Windows so the suite compiles.
3.4.3 - 2026-04-19 #
Fixed #
- pub.dev publish: removed top-level
docs/(Pub expects singulardoc/); moveddocs/Features/*todoc/Features/. Updated backlog cross-links. .github/workflows/publish.yml:dart pub publish/--dry-runnow pass--ignore-warningsso the client-side hint about skipping versions after the last published 1.2.1 does not fail CI (server still enforces its own rules).
3.4.2 - 2026-04-19 #
Dart XA helpers (runWithStart / runWithStartOnePhase), Docker E2E
hardening for multi-engine matrices, and optional docker_e2e -Quick /
--quick for faster local runs.
Added #
scripts/docker_e2e.ps1 -Quick/scripts/docker_e2e.sh --quick— runscargo testwithout--include-ignoredso long#[ignore]cases (e.g. bulk transaction stress) stay skipped; default behaviour remains full CI parity with--include-ignored.XaTransactionHandle.runWithStart<T>— exception-safe helper that drives the full Two-Phase Commit lifecycle around a user-supplied closure. Mirrors theTransactionHandle.runWithBeginconvention shipped for local transactions in v3.1.0:- On normal completion: emits
xa_end→xa_prepare→xa_commit_prepared. Each step's failure is surfaced as aStateErrorwith a diagnostic message so the caller can distinguish "commit failed" from "user closure failed". - On any thrown exception (or runtime error): inspects the
branch state, emits
xa_endif stillActive(the engine refusesxa_rollbackon an attached branch), thenxa_rollback_prepared(Prepared) orxa_rollback(Idle/Failed) depending on where the throw landed in the lifecycle. The original cause is rethrown sotry / catchcomposes naturally. - Engine-aware: tolerates Oracle's
XA_RDONLY=3on read-only branches (the underlying Rustapply_xa_preparealready accepts it as success), so the helper completes normally even when the user's closure ran no DML.
- On normal completion: emits
XaTransactionHandle.runWithStartOnePhase<T>— 1RM optimisation variant: collapsesxa_prepare+xa_commitintoxa_commit_one_phasefor the case where this RM is the sole participant in the global transaction. Same exception-safety contract asrunWithStart.- 11 new Dart unit tests in
test/infrastructure/native/wrappers/xa_transaction_handle_test.dartcover the full state-machine matrix without touching FFI: a counter-based_FakeXasubclass overrides every state-mutating method so the helpers are exercised in isolation.- happy path of both helpers (counter assertions)
- throw-while-Active → end + rollback path
- throw-while-Prepared → rollback_prepared path
startFnreturningnull→StateErrorwith hint- per-step failure (
end,prepare,commit_prepared,commit_one_phase) →StateErrorwith the failing-step name surfaced
Changed #
example/xa_2pc_demo.dartgains a fifth section showing the helper end-to-end: commits one branch via the helper, then triggers an in-closure throw to demonstrate the rollback path catching at the surroundingtry / on Exception. Existing four sections (full 2PC, 1RM, crash-recovery, DML-inside-branch) remain untouched.example/README.mdentry for the demo updated to mention the v3.4.2 helper section.
Migration notes #
- Pure Dart-side addition — no FFI / Rust / ABI changes; the
helpers compose existing methods (
xaStart,end,prepare,commitPrepared, etc.) so the underlying engine surface is unchanged. - Existing manual 2PC code keeps working unmodified; the helpers are an opt-in convenience.
Fixed #
- Docker multi-engine E2E: FFI tests that use T-SQL only (
WAITFOR,INSERT … OUTPUT,IF OBJECT_ID) now skip unlessODBC_TEST_DSNtargets SQL Server.cell_reader_testlikewise runs only when the resolved E2E engine is SQL Server, soscripts/docker_e2e.ps1with PostgreSQL / MySQL / MariaDB / Oracle no longer fails on SQL Server–specific SQL. - E2E on non–SQL Server:
test_catalog_list_columns(dbo /IF OBJECT_ID),test_driver_capabilities_detect(pinned ODBC defaults), andtest_execution_engine_plugin_optimization(SELECT TOP) skip unless the live DSN is SQL Server. e2e_savepoint_test: forSavepointDialect::Sql92, runDROP TABLE IF EXISTSbeforeCREATEso PostgreSQL / MySQL runs do not fail whensp_test/sp_rel_testalready exist from a prior run.
3.4.1 Oracle XA / 2PC via DBMS_XA (Sprint 4.3c Phase 2) #
Added #
- Sprint 4.3c Phase 2 — Oracle XA via
DBMS_XAPL/SQL package. Production wiring for X/Open XA on Oracle 10g+ closes the last remaining engine in the cross-vendorapply_xa_*matrix fromengine::xa_transaction. The path goes through ordinary callable SQL (SYS.DBMS_XA.XA_START / XA_END / XA_PREPARE / XA_COMMIT / XA_ROLLBACK) so it works through any Oracle ODBC driver without needing access to the underlyingOCIServer*handle (whichodbc-apidoes not expose).Xid::encode_oracle_components()/decode_oracle_components()convert between the cross-vendorXidand the(formatid, RAW(64), RAW(64))triple thatSYS.DBMS_XA_XIDexpects. Hex is upper-case to round-trip with Oracle'sRAWTOHEXoutput inDBA_PENDING_TRANSACTIONS; decode is case-insensitive so future driver changes don't break recovery.oracle_xa_block(call, allow_rcs)PL/SQL helper wraps eachDBMS_XA.*call in an exception-translatingBEGIN ... END;that converts non-zero return codes intoORA-20100. ToleratesXA_RDONLY(rc=3) onXA_PREPARE(Oracle auto-completes branches that did no DML) andXAER_NOTA(rc=-4) on the follow-upXA_COMMIT(FALSE)so the read-only path is a no-op at the cross-vendorXaTransactionlayer.apply_xa_recoverfor Oracle readsDBA_PENDING_TRANSACTIONSviaRAWTOHEX(GLOBALID)/RAWTOHEX(BRANCHID)so prepared XIDs round-trip with ourHEXTORAWliterals onXA_START.
- OCI shim retained, status reframed.
engine::xa_oci(behind--features xa-oci) keeps the dynamic-loading scaffolding +OciXaBranch/recover_oci_xidsAPI as documented OCI ABI bindings and a possible future option, but is no longer a "Phase 2 wiring TODO" — theDBMS_XApath is the production integration. See module doc-header for the rationale. - Public re-export of
engine::SharedHandleManagerso tests / downstreams that hold anXaTransaction::startarg across calls don't have to reach into the privatecrate::handlesmodule. - 4 new E2E tests in
tests/e2e_xa_transaction_test.rsvalidate the Oracle path against Oracle XE 21 in the dockertest-runner-oracleprofile:test_e2e_xa_oracle_full_2pc_commit_path— full lifecycle (start → INSERT → end → prepare → recover lists xid → commit → recover empty → row visible).test_e2e_xa_oracle_rollback_prepared_path— rollback after prepare; verifiesDBA_PENDING_TRANSACTIONSclears and the INSERT was discarded.test_e2e_xa_oracle_one_phase_commit_shortcut—TMONEPHASEfast path withoutXA_PREPARE.test_e2e_xa_oracle_resume_prepared_after_disconnect— XID survives session loss; second connection recovers + commits viaresume_prepared.
- 5 new unit tests in
engine::xa_transaction::tests: Oracle component round-trip (upper-case hex), case-insensitive decode,oracle_xid_literalshape pinned,oracle_xa_blockrc-guard structure pinned. Total xa_transaction unit tests: 22 → 27.
Changed #
- Engine matrix in
engine::xa_transactiondoc-header reclassifies Oracle from "stub —UnsupportedFeaturewith TODO" to "implemented (10g+) viaDBMS_XA".unsupported_oracle()helper removed;unsupported_other()lists Oracle as supported. hex_decode/hex_nibblenow accept upper-case A–F so the same helper handles MySQL's lower-case hex and Oracle's upper-caseRAWTOHEXoutput.hex_encode_upperadded for the Oracle emit path.engine::xa_ocidoc-header rewritten to reflect the new status: dynamic-loading shim retained as documented OCI ABI; production Oracle XA flows throughDBMS_XA; OCI wiring deferred until/unlessodbc-apiexposes the underlying handle.
Required Oracle privileges #
The connection user needs EXECUTE on SYS.DBMS_XA (default for
SYSTEM), FORCE [ANY] TRANSACTION (for crash-recovery on
prepared XIDs from other sessions), and SELECT on
DBA_PENDING_TRANSACTIONS. The Oracle XE 21 image used in CI ships
with these enabled out of the box for SYSTEM.
Migration notes #
- Existing builds calling Oracle through
XaTransaction::start/recover_prepared_xidsno longer getUnsupportedFeature— they execute againstDBMS_XA. No source changes required; the failure surface narrows. --features xa-ocino longer changes the runtime behaviour of Oracle XA (it kept the OCI shim built but the shim was never wired). The feature flag still compiles cleanly and is kept for future opt-in OCI integration.
3.4.0 Transaction control Sprint 4 #
Added #
- Sprint 4.3b / 4.3c — XA / 2PC scaffolding for SQL Server (MSDTC)
and Oracle (OCI), Phase 1 of 2. Two new opt-in Cargo features
add the COM / OCI plumbing that the cross-vendor
apply_xa_*matrix in [engine::xa_transaction] needs to integrate SQL Server and Oracle into the existing 2PC lifecycle.- Honest status disclaimer: Phase 1 lands the dependency
bindings, the COM ceremony / dynamic-loading shim, and a
self-contained handle type with state-machine guards. Live
runtime behaviour against MSDTC and Oracle has not been
validated end-to-end — the dev box that produced this commit
did not have either dependency installed. Phase 2 wires the new
handles into
apply_xa_*and adds gated E2E tests against real MSDTC + Oracle hosts. Both phases are tracked underPENDING_IMPLEMENTATIONS.md§1.1 / §1.2. - Sprint 4.3b —
engine::xa_dtc(Windows-only, behind--features xa-dtc):- Pulls the
windows0.59 crate (high-level COM bindings —windows-sysdoesn't generate COM interface code). ensure_com_initialised()— caches the per-threadCoInitializeEx(COINIT_MULTITHREADED)result so the cost is paid once.acquire_transaction_dispenser()— calls the documentedDtcGetTransactionManagerExAentry point, builds a typedITransactionDispenserwrapper from the raw*mut c_voidviaInterface::from_raw.begin_msdtc_transaction()—ITransactionDispenser::BeginTransactionwithISOLATIONLEVEL_READCOMMITTED(SQL Server's MSDTC default).DtcXaBranchowned handle withcommit()/abort()callingITransaction::Commit/ITransaction::Abort.Dropaborts a still-active branch best-effort, recognisingXACT_E_NOTRANSACTION(0x8004D00B) as "already finalised — silent success".- The
apply_xa_*matrix now has a feature-awareunsupported_sqlserver()that distinguishes "feature missing" from "feature enabled, Phase 2 wiring pending" so callers can tell the difference.
- Pulls the
- Sprint 4.3c —
engine::xa_oci(cross-platform, behind--features xa-oci):- Pulls
libloading 0.8for runtime resolution of the OCI shared library (libclntsh.so/libclntsh.dylib/oci.dll). Fallback search list per platform; first-match wins. OciXidrepr(C)struct mirrors the X/Openxid_tlayout fromoraxa.h(format_id+gtrid_length+bqual_length- 128-byte concatenated payload). Pinned by a layout-asserting unit test.
- Symbol-table struct
OciXaSymbolsresolves the eight XA entry points (xaosw,xaocl,xaostart,xaoend,xaoprep,xaocommit,xaoroll,xaorecover) viaLibrary::get. Cached in aOnceLockso subsequent calls are O(1). OciXaBranchowned handle:prepare()(xa_end(TMSUCCESS)xa_prepare),commit()/rollback()(Phase 2),commit_one_phase()(xa_end+xa_commit(TMONEPHASE)).Droprolls back + closes a still-active branch best-effort.
recover_oci_xids()— Phase-2-recovery scan viaxa_recover, filters out malformed XIDs from foreign clients (length violations).- The
apply_xa_*matrix now has a feature-awareunsupported_oracle()mirroring the SQL Server pattern.
- Pulls
- Tests: 7 new Rust unit tests across the two modules:
OciXidlayout pinning + packing edge cases (empty bqual, max-size 64+64, gtrid-then-bqual ordering), XA flag constants matchingoraxa.h, the load-error path returningUnsupportedFeaturewith actionable wording, and the always-onDtcXaBranchreachability probe. Live MSDTC / Oracle behaviour is covered by the (unwritten) Phase 2 integration tests. - Build matrix: default build is byte-identical to today.
--features xa-dtcaddswindows0.59 (Windows targets only).--features xa-ociaddslibloading0.8 (every target). Both can be enabled simultaneously.
- Honest status disclaimer: Phase 1 lands the dependency
bindings, the COM ceremony / dynamic-loading shim, and a
self-contained handle type with state-machine guards. Live
runtime behaviour against MSDTC and Oracle has not been
validated end-to-end — the dev box that produced this commit
did not have either dependency installed. Phase 2 wires the new
handles into
- Sprint 4.3 — XA / 2PC distributed transactions. First-class
X/Open XA support with full Phase 1 / Phase 2 lifecycle and
recovery, exposed end-to-end (Rust core → FFI → Dart bindings →
high-level
XaTransactionHandle). Closes the Sprint 4 backlog.-
Rust core — new module
engine::xa_transaction:- [
Xid] value type (X/Openformat_id+gtrid1..64 bytes +bqual0..64 bytes), with validating constructors and engine-specific encoders (encode_postgres,encode_mysql_components). - [
XaTransaction] state machine:Active→Idle(viaxa_end) →Prepared(viaxa_prepare) →Committed/RolledBack(viaxa_commit_prepared/xa_rollback_prepared). - [
PreparingXa] / [PreparedXa] handles enforce the per-state contract at compile time — there is no way to callcommit_preparedon anActivebranch. - commit_one_phase — 1RM shortcut that fuses prepare + commit when this RM is the sole participant.
- [
recover_prepared_xids] / [resume_prepared] — crash-recovery flow that rebuilds aPreparedXahandle from the engine's prepared-transaction catalog. Dropimpl auto-rolls back anyActive/Idlebranch that escapes scope without explicit commit/rollback.
- [
-
Engine matrix (
apply_xa_*):Engine Mechanism Status PostgreSQL BEGIN+PREPARE TRANSACTION+pg_prepared_xacts✅ MySQL / MariaDB XA START / END / PREPARE / COMMIT / ROLLBACK+XA RECOVER✅ DB2 same SQL grammar as MySQL ✅ SQL Server requires MSDTC enlistment via Windows COM ( SQL_ATTR_ENLIST_IN_DTC+ITransaction*) — stub returnsUnsupportedFeaturewith a TODO pointing at a follow-up sprint⚠️ Oracle requires OCI XA library ( oraxa.h,xaoSvcCtx) — stub returnsUnsupportedFeaturewith a TODO⚠️ SQLite / Snowflake / others no 2PC support — rejected with UnsupportedFeature❌ -
XID encoding is hex-based on every engine to keep the SQL ASCII-clean regardless of the byte content (X/Open allows arbitrary binary). PostgreSQL canonicalises as
'<format_id>_<gtrid_hex>_<bqual_hex>'; MySQL/MariaDB/DB2 use the native 3-argument grammar with hex-encoded components. -
FFI — 10 new exports under the
odbc_xa_*family:odbc_xa_start,_end,_prepare,_commit_prepared,_rollback_prepared,_commit_one_phase,_rollback_active,_recover_count,_recover_get,_resume_prepared. The recovery flow uses a thread-local cache (XA_RECOVER_CACHE) to sidestep variable-length-output marshaling at the FFI boundary. -
Dart:
- [
Xid] value class inlib/domain/entities/xid.dartwith the same validation rules as Rust. - [
XaTransactionHandle] inlib/infrastructure/native/wrappers/xa_transaction_handle.dartmirrors the Rust state machine. OdbcBindings.odbc_xa_*(10 wrappers +supportsXagetter with graceful fallback throwingUnsupportedErroron pre-Sprint-4.3 binaries).OdbcNative.xa*ergonomic wrappers includingxaRecoverGetthat handles the FFI memory ceremony.NativeOdbcConnection.xaStart/xaRecover/xaResumePreparedreturn a typedXaTransactionHandle.
- [
-
Verification: 19 new Rust unit tests in
engine::xa_transaction::tests(XID validation + length limits, PostgreSQL encoding round-trip, MySQL component encoding round- trip, hex helper edge cases, error-message wording for the SQL Server / Oracle stubs, prepared-state guard checks). 17 new Dart unit tests intest/domain/entities/xid_test.dart(validation, defensive copy, fromStrings convenience, equality/hashCode, toString). 9 new gated E2E tests intests/e2e_xa_transaction_test.rscovering the full PostgreSQL and MySQL 2PC lifecycle (full commit, prepared rollback, 1RM shortcut, resume-after-disconnect withpg_prepared_xactsround- trip). E2E tests gracefully skip viaIM002driver-not-found when the matching engine isn't installed locally.
-
SqlDataTypeengine-specific kinds. Seven additional typed kinds for engine-native types that don't have a portable cross-vendor equivalent. Brings theSqlDataTypesurface from 20/30 → 27/30 of the TYPE_MAPPING.md roadmap. Wire-compatible with existingParamValue*primitives (the value is the type-discipline at the call site plus per-kind validation).- PostgreSQL
range— accepts the standard PG range literal ('[1,10)','(1,5]','[2020-01-01,2020-12-31)','empty'). Concrete subtype (int4range/tsrange/daterange...) is resolved by the server from the column definition. - PostgreSQL
cidr/inet— accepts IPv4 and IPv6 with optional/prefixmask. Validated structurally (not via a single mega-regex) so compressed IPv6 forms (2001:db8::1,::1) round trip correctly while triple-colon typos (fe80:::1) are rejected early. Mask range (/0..32for IPv4,/0..128for IPv6) is enforced. - PostgreSQL
tsvector— accepts the standard tsvector literal ('fat:1A cat:2B sat:3'). No client-side validation; PostgreSQL'sto_tsvector/ cast is the real validator. - SQL Server
hierarchyId— accepts the canonical'/'-rooted,'/'-terminated path ('/','/1/','/1/2/3.5/') with/-separated decimal segments, each optionally with a.fraction(used to insert nodes between siblings without renumbering). Caller wraps inCAST(? AS hierarchyid)in the SQL — the type is not directly bindable as a parameter. - SQL Server
geography— accepts WKT ('POINT(-122.349 47.651)','POLYGON((...))','LINESTRING(...)', etc.). Caller wraps ingeography::STGeomFromText(?, 4326)in the SQL (replace the SRID with whatever's appropriate). For binary WKB use [SqlDataType.varBinary] withgeography::STGeomFromWKB. TheList<int>path is rejected with an actionable error pointing at varBinary instead. - Oracle
raw— acceptsList<int>. Idiomatic alias for [SqlDataType.varBinary]; wire-equality pinned by an explicitserialize()test. - Oracle
bfile— accepts aStringcontaining a fully-formedBFILENAME(...)invocation. BFILE is unusual: it's a pointer to an external file, not the content. The more common pattern is twovarCharparameters fed intoBFILENAME(?, ?)in SQL; this kind is for the rarer case of binding a complete textual snippet. - Tests: 20 new Dart unit tests covering accepted shapes,
rejected typos (with structural IPv6 edge cases), wire-equality
(
rawvsvarBinary), and the cross-kind rejection messages (geographyrejectingList<int>with a hint atvarBinary).
- PostgreSQL
SqlDataTypeextras (final batch):tinyInt,bit,text,xml,interval. Five additional typed kinds inlib/infrastructure/native/protocol/param_value.dart. Together with the previous batch this brings theSqlDataTypesurface from 10/30 → 20/30 of the TYPE_MAPPING.md roadmap. Same contract as before: non-breaking, no FFI changes, no wire changes, no existing call site has to be touched.SqlDataType.tinyInt— acceptsint, validates against[0, 255](SQL Server / Sybase ASE / Sybase ASA convention; the broadest interoperable contract). Serialises asParamValueInt32. For MySQL/MariaDB signedTINYINTuse [SqlDataType.smallInt] instead — its range comfortably covers the signed-tinyint domain.SqlDataType.bit— acceptsbool(mapped to 1/0) orint(must be exactly 0 or 1). Serialises asParamValueInt32. Idiomatic for columns whose type name isBIT; semantically distinct from [SqlDataType.boolAsInt32] (which rejectsint).SqlDataType.text— long-form character data (TEXT/NTEXT/CLOB). AcceptsStringonly; no length cap. Wire-compatible with [SqlDataType.varChar] / [SqlDataType.nVarChar] — the distinction is purely semantic.SqlDataType.xml({validate})— acceptsString. Default is pass-through (engine validates at execute-time).validate: trueruns a cheap structural sanity check (must start with<and contain a closing>after trimming) — catches obvious mistakes without paying the cost of a real XML parser.SqlDataType.interval— acceptsDuration(formatted as'<n> seconds', the broadest portable spelling: PostgreSQLINTERVAL, MySQLINTERVAL, OracleNUMTODSINTERVAL(n, 'SECOND'), Db2<n> SECONDSall accept it directly) orString(passed through verbatim, for engines whose preferred syntax differs — e.g. OracleINTERVAL '1' DAY). Sub-second precision is preserved by emitting a 3-digit decimal so values round-trip back to the sameDuration.- Tests: 22 new Dart unit tests in
test/infrastructure/native/protocol/param_value_test.dartcovering the full unsigned-tinyint range, thebitint/bool duality with strict 0/1 enforcement, multi-line/Unicode TEXT payloads, the XML validate-flag opt-in, and theDuration→ "seconds" formatter (whole, sub-second, zero, negative, pre-formatted String passthrough).
SqlDataTypeextras:smallInt,bigInt,json,uuid,money. Five new typed kinds inlib/infrastructure/native/protocol/param_value.dart, bringing the total to 15/30 from theTYPE_MAPPING.mdroadmap. Every kind is non-breaking — no existing call site changes, no FFI changes, no wire-format changes. They run on top of the existingParamValue*primitives.SqlDataType.smallInt— acceptsint, validates against[-32768, 32767], serialises asParamValueInt32(the int16 distinction lives in the validation; the wire is shared).SqlDataType.bigInt— idiomatic alias for [SqlDataType.int64]. Acceptsint, serialises asParamValueInt64. Wire-compatible withint64(pinned by an explicit equality test).SqlDataType.json({validate})— acceptsString(passed through verbatim),Map<String, dynamic>orList<dynamic>(encoded viadart:convert::jsonEncode).validate: trueround-trips the payload throughjsonDecodeto catch syntactic mistakes early. Defaultfalseto avoid paying parse cost on multi-KB payloads in production.SqlDataType.uuid— accepts the canonical 8-4-4-4-12 form, the bare 32-hex form, and either wrapped in{...}(for .NET- flavoured tooling). Folds to lowercase canonical so the engine sees a normalised value regardless of the caller's formatting. Rejects malformed input with an actionable error.SqlDataType.money— fixed monetary scale of 4 fractional digits (SQL Server MONEY/PostgreSQL money/DECIMAL(15,4)convention). Acceptsnum(formatted withtoStringAsFixed(4)) orString(passed through verbatim).NaN/Infinityrejected with the same wording as the implicitdouble → decimalpath so error messages stay consistent.- Tests: 24 new Dart unit tests in
test/infrastructure/native/protocol/param_value_test.dartcovering valid inputs, range validation, format validation, canonicalisation, NaN/Infinity rejection, and thebigint/int64wire-compatibility contract.
- Sprint 4.2 — Per-transaction
LockTimeout. Transactions can now cap how long a statement waits for a lock without the caller having to emit rawSETthemselves.- Rust core: new
engine::LockTimeouttyped wrapper (u32ms, with0= engine default).Transaction::begin_with_lock_timeoutis the new full-control entry point;begin_with_access_mode/begin_with_dialect/beginkeep their signatures and forward to it withLockTimeout::engine_default().Transaction::lock_timeout()getter exposes the resolved value.OdbcConnection::begin_transaction_with_lock_timeout(...).Transaction::execute_with_lock_timeout(...)mirror.Transaction::for_test_with_lock_timeout(...)test-only constructor. - Engine matrix (
apply_lock_timeout): SQL Server emitsSET LOCK_TIMEOUT <ms>; PostgreSQL usesSET LOCAL lock_timeout = '<ms>ms'(auto-resets on commit/rollback); MySQL/MariaDB useSET SESSION innodb_lock_wait_timeout = <s>with sub-second values rounded UP to 1 second so we never silently relax the caller's bound; DB2 usesSET CURRENT LOCK TIMEOUT <s>with the same rounding; SQLite usesPRAGMA busy_timeout = <ms>; Oracle / Snowflake / Sybase / Redshift / BigQuery / unknown silently no-op (logged at debug).LockTimeout::engine_default()is the universal default and emits noSETso the connection's session log stays clean. - FFI: new export
odbc_transaction_begin_v3(conn_id, isolation, savepoint_dialect, access_mode, lock_timeout_ms). v2 delegates to v3 withlock_timeout_ms = 0; v1 still delegates to v2. All three ABIs are preserved byte-for-byte. - Dart:
Duration? lockTimeoutthreaded throughOdbcBindings(newodbc_transaction_begin_v3+ typedef +supportsTransactionLockTimeoutgetter),OdbcNative.transactionBegin(newlockTimeoutMsnamed arg, smart routing v1/v2/v3 to minimise binary surface area when the caller is on defaults),NativeOdbcConnection.beginTransaction,AsyncNativeOdbcConnection.beginTransaction,BeginTransactionRequest(new field, default0),IOdbcRepository.beginTransaction(new optional named arg — convertsDuration→ ms at the FFI boundary, with sub-ms positive durations rounding UP to 1 ms to mirror Rust-side semantics),IOdbcService.beginTransaction,OdbcService.runInTransaction, andTelemetryOdbcServiceDecorator. Existing call sites keep working unchanged because every new parameter defaults tonull(engine default) / wire0. - Graceful fallback: when an older native library predates
Sprint 4.2,
OdbcBindings.odbc_transaction_begin_v3silently delegates to v2 (or v1 if v2 is also missing) andlockTimeoutMsis ignored — the transaction uses the engine default.
- Rust core: new
- Sprint 4.4 —
IOdbcService.runInTransaction<T>(...)helper. Captures thebegin → action → commit/rollbackdance behind a single Service-layer call so application code never has to manage thetxnIdlifecycle by hand.- Returns
Failureon any combination ofbeginTransactionfailure,actionreturningFailure,actionthrowing (which is caught and converted to aQueryErrorwith the original type/message preserved), orcommitfailure. - Rollback runs automatically on any non-happy path; rollback failure is swallowed so a noisy rollback never overwrites the original error the caller is debugging.
- Threads through every
beginTransactionknob (isolation, savepoint dialect, access mode, lock timeout) with the same defaults asIOdbcService.beginTransaction. - Implementation in
OdbcServiceplus a tracing wrapper inTelemetryOdbcServiceDecoratorthat emits a singleODBC.runInTransactionspan around the whole unit of work.
- Returns
- Sprint 4.1 —
TransactionAccessMode(READ ONLY/READ WRITE). Transactions can now opt into the SQL-92 access-mode hint without having to emit rawSET TRANSACTIONthemselves.- Rust core: new
engine::TransactionAccessMode { ReadWrite, ReadOnly }.Transaction::begin_with_access_mode(handles, conn_id, isolation, savepoint_dialect, access_mode)is the new full-control entry point;begin_with_dialectandbeginkeep their existing signatures and default toReadWrite.Transaction::access_mode()getter exposes the resolved value.OdbcConnectiongainsbegin_transaction_with_access_mode(...). TheTransaction::execute*family gainsexecute_with_access_mode. - Engine matrix (
apply_access_mode): PostgreSQL / MySQL / MariaDB / DB2 / Oracle emitSET TRANSACTION READ ONLYafter isolation. SQL Server / SQLite / Snowflake / Sybase / Redshift / BigQuery / unknown silently treatReadOnlyas a no-op (logged at debug) so callers can program against the abstraction unconditionally.ReadWriteis the engine default everywhere, so we do not emit a redundantSETfor it on any engine — the connection's session log stays clean. - FFI: new export
odbc_transaction_begin_v2(conn_id, isolation, savepoint_dialect, access_mode). The legacyodbc_transaction_begindelegates to v2 withaccess_mode = 0(ReadWrite) so the v1 ABI is preserved byte-for-byte. - Dart: new
TransactionAccessMode { readWrite, readOnly }enum inlib/domain/entities/transaction_access_mode.dart. Threaded throughOdbcBindings(newodbc_transaction_begin_v2+ typedef +supportsTransactionAccessModegetter that reflects whether the loaded native library exports v2),OdbcNative.transactionBegin,NativeOdbcConnection.beginTransaction,AsyncNativeOdbcConnection.beginTransaction,BeginTransactionRequest(newaccessModefield, default0),IOdbcRepository.beginTransaction(new optional named arg),IOdbcService.beginTransaction(new optional named arg),TelemetryOdbcServiceDecorator. Existing call sites keep working unchanged because every new parameter defaults to theReadWrite/ wire0value. - Graceful fallback: when an older native library predates
Sprint 4.1,
OdbcBindings.odbc_transaction_begin_v2silently delegates to v1 and theaccessModeargument is ignored — the transaction is alwaysREAD WRITE. Callers that need the distinction gate onsupportsTransactionAccessMode.
- Rust core: new
Fixed #
test_ffi_get_structured_errorflaky in parallel runs (seeTYPE_MAPPING§3.1 and backlog). The previous implementation triggered the structured error viatrigger_structured_cancel_unsupported_error(), released the global state lock, and only then called the publicodbc_get_structured_errorFFI. Any parallel test that touched a function callingset_error()(which clearsstate.last_structured_erroras a side-effect) could clobber the injected value in that window — surfacing as the recurringassertion 'left == right' failed: Should succeed left:1 right:0.#[serial]alone wasn't enough because it only serialises against other#[serial]tests, not the broader set of FFI tests that callset_errorindirectly. The fix collapses inject + read into a single critical section by holding the lock across both operations and inlining the same algorithmodbc_get_structured_erroruses. Verified by 5 consecutivecargo test --libruns with 0 failures.
Tests #
- Sprint 4.1: 8 new lib unit tests under
engine::transaction::tests::*(TransactionAccessModefrom-u32mapping, SQL keyword formatting,is_read_onlypredicate, default value attached to theTransactionstruct,for_test_with_access_modeconstructor).tests/e2e_transaction_access_mode_test.rs— 4 new E2E tests gated byshould_run_e2e_tests(), verified against a live SQL Server (defaultReadWritepreserves v1 behaviour,ReadOnlyis a silent no-op on SQL Server, v1 path defaults toReadWrite, Postgres/MySQL/Oracle native-hint placeholder). - Sprint 4.2: 12 new lib unit tests under
engine::transaction::tests::lock_timeout_*(from_millis(0)collapses to engine-default; sub-ms positive durations round up to 1 ms;from_durationclamps atu32::MAXms;millis_as_seconds_rounded_uppolicy for MySQL/DB2; SQL formatting per engine; default attached toTransaction;for_test_with_lock_timeoutconstructor).tests/e2e_transaction_lock_timeout_test.rs— 4 new E2E tests verified against SQL Server (engine_default is a pure no-op,SET LOCK_TIMEOUT 2500is accepted, sub-ms round-up survives the driver, the Sprint 4.1 entry point still defaults to engine-default). - Sprint 4.4: 9 new Dart unit tests in
test/application/services/odbc_service_run_in_transaction_test.dartcovering the full state machine (happy path, actionFailure, action throw,beginfailure,commitfailure, rollback failure swallowing, parameter threading, defaults, async-await ordering).
Migration #
- 100% backwards compatible across all three sub-features.
- Every new parameter is optional with a sensible default
(
ReadWrite/engine_default/null lockTimeout/ etc.). - Wire-level:
odbc_transaction_begin(v1) still ships and now delegates to_v2withaccess_mode = 0;_v2delegates to_v3withlock_timeout_ms = 0. All three ABIs are preserved. - When an older native library is loaded, the higher-level Dart
layer detects the missing FFI symbols (via the
supports*getters onOdbcBindings) and silently falls back to the closest older entry point. The new parameters become no-ops in that case rather than producing errors.
- Every new parameter is optional with a sensible default
(
Notes #
- GitHub issues #1 and #2 are resolved by v3.3.0 (released as part of
the streaming multi-result + UTF-16 wide-text decoding work):
- #1 — Chinese Character Encoding Issue with SQL Server NVARCHAR Fields is closed by
the switch from
SQLGetData(SQL_C_CHAR)toSQLGetData(SQL_C_WCHAR)inengine/cell_reader.rsplus the Dart_decodeTexthardening (U+FFFD substitution instead of silent Latin-1 fallback). Verified bytests/e2e_sqlserver_test.rs::test_e2e_sqlserver_unicode_chinese_round_tripagainst a real SQL Server (CJK + emoji + RTL all round-trip). - #2 — JSON Truncation in odbc_fast with SQL Server FOR JSON Queries is closed by
engine::sqlserver_json::coalesce_for_json_rows, which detects the reservedJSON_F52E2B61-…column name SQL Server emits for FOR JSON payloads and concatenates the per-row chunks into a single logical cell before encoding. Verified bytests/e2e_sqlserver_test.rs::test_e2e_sqlserver_for_json_path_returns_complete_payload(200 rows ≈ 19 KB reassembled across ~10 chunk boundaries). Both issues should be closed on GitHub with a reference to v3.3.0.
- #1 — Chinese Character Encoding Issue with SQL Server NVARCHAR Fields is closed by
the switch from
3.3.0 Streaming multi-result (M8) #
Added #
- M8 — Streaming multi-result. New end-to-end stack that surfaces every multi-result item incrementally instead of materialising the whole batch in memory. Closes the only multi-result item that was deferred from v3.2.0.
- Engine (
native/odbc_engine/src/engine/streaming.rs):start_multi_batched_stream(handles, conn_id, sql, chunk_size)— spawns a worker that drivesStatement::more_resultsraw + usescursor.into_stmt()to consume cursors without triggeringSQLCloseCursor(which would discard pending result sets, same trick used for the M1 fix in v3.2.0).start_multi_async_stream(...)— async variant returningAsyncStreamingState(poll + fetch).- Each worker batch carries one frame-encoded multi-result item:
[tag: u8][len: u32 LE][payload].tag = 0payload is abinary_protocolrow-buffer;tag = 1payload isi64 LErow count. - Constants
MULTI_STREAM_ITEM_TAG_RESULT_SET = 0andMULTI_STREAM_ITEM_TAG_ROW_COUNT = 1.
- FFI — 2 new exports:
odbc_stream_multi_start_batched(conn_id, sql, chunk_size)odbc_stream_multi_start_async(conn_id, sql, chunk_size)- Both return
stream_idand reuse the existingodbc_stream_fetch,odbc_stream_cancel,odbc_stream_closeandodbc_stream_poll_asyncFFIs, so no other surface has to change.
- Dart —
MultiResultStreamDecoder(lib/infrastructure/native/protocol) reassembles partial frames intoMultiResultItems as bytes accumulate. Bindings:OdbcBindings.odbc_stream_multi_start_batched / _async,OdbcNative.streamMultiStartBatched / _Async,NativeOdbcConnection.streamMultiStartBatched / _Async,AsyncNativeOdbcConnection.streamMultiStartBatched / _Async(also exposesstreamFetch/streamCloseso the high-level API can drive the stream lifecycle), worker isolate handlers (StreamMultiStartBatchedRequest,StreamMultiStartAsyncRequest). - High-level Dart API —
IOdbcService.streamQueryMulti(connId, sql)returnsStream<Result<QueryResultMultiItem>>. Each item is emitted as soon as the Rust worker produces it.OdbcRepositoryImpl.streamQueryMultigracefully falls back toexecuteQueryMultiFullwhen the loaded native library predates v3.3.0. supportsStreamQueryMultigetters onOdbcBindings,OdbcNativeandNativeOdbcConnectionso callers can detect the capability without catching exceptions.
Tests #
tests/regression/m8_streaming_multi_result.rs— 3 E2E tests (#[ignore], gated byENABLE_E2E_TESTS=1+ODBC_TEST_DSN) covering the 3 batch shapes that M1 already covered for the materialising path. All 3 pass against a real SQL Server target.test/infrastructure/native/protocol/multi_result_stream_decoder_test.dart— 8 unit tests for the Dart frame decoder (full chunk, split-across, multi-frame chunk, malformed tag/len, exhaustion checks).
Internal #
streaming.rsexposes a small helper (drive_multi_result_stream) that shares the cursor / row-count traversal logic withExecutionEngine::collect_multi_results. Both call paths use the same no-SQLCloseCursordiscipline.MockOdbcRepository(test helper) now implementsstreamQueryMultiviaexecuteQueryMultiFullso existing tests keep compiling.
Migration #
- 100% backwards compatible.
executeQueryMulti / executeQueryMultiFull / executeQueryMultiParamscontinue to work unchanged. UsestreamQueryMultiwhenever the batch result sets are large enough that 3× memory cost is meaningful (e.g. wide analytics joins). - Loading an older native library only loses the
streamQueryMultifast path;OdbcRepositoryImplautomatically falls back toexecuteQueryMultiFulland replays the items as a stream so the API contract is preserved.
Validation #
cargo test --lib --include-ignored: 857 passed / 0 failed (was 846).cargo test --test regression_test: 78 passed / 0 failed / 7 ignored (3 new M8 streaming + 4 M1 batch shapes — all 7 pass withENABLE_E2E_TESTS=1).cargo clippy --all-targets --all-features -- -D warnings: 0 warnings.dart analyze lib test example: No issues found.dart test test/{application,domain,infrastructure,core,helpers}: 430 passed / 0 failed / 3 skipped (was 418, +12 from the new decoder unit tests + mock helpers).
3.2.0 Multi-result hardening #
Fixed #
- M1 —
execute_multi_resultcollected only the first item in 2 of the 4 batch shapes. The pre-v3.2 implementation took anif had_cursor { … } else { row_count }shape that silently dropped every result set produced after the first one whenever the batch mixed cursors and row-counts. Worked forcursor → cursor → cursorandrow-count → row-count(kind of — only first), broken forrow-count → cursorandcursor → row-count. v3.2.0 introducescollect_multi_resultswhich walks the full chain via rawStatement::more_results(SQLMoreResults), rebuilding aCursorImplwhenevernum_result_cols > 0. Crucially, cursors are consumed viacursor.into_stmt()instead of being dropped, soSQLCloseCursordoes not discard pending result sets. Covered by 4 new E2E regression tests undertests/regression/m1_multi_result_batch_shapes.rs. - M2 —
odbc_exec_query_multiignored pooled connection IDs. Same bug class as M2 forodbc_exec_queryin v3.1.1, fixed the same way: fall back tostate.pooled_connectionswhen the id is not instate.connections. - M7 —
MultiResultParser.getFirstResultSetandQueryResultMulti.firstResultSetreturned a fake empty buffer when the batch produced no cursors at all. Callers had no way to tell "0 rows" from "no result set".getFirstResultSetnow returnsParsedRowBuffer?.QueryResultMulti.firstResultSetis deprecated; preferfirstResultSetOrNull.
Added #
- M3 —
MultiResultItem(Dart) is now a sealed class. Two variants:MultiResultItemResultSet(value)andMultiResultItemRowCount(value). Pattern-match with Dart 3switch/sealed exhaustiveness:
The legacy 2-field constructor (switch (item) { case MultiResultItemResultSet(:final value): ... case MultiResultItemRowCount(:final value): ... }MultiResultItem(resultSet:..., rowCount:...)) is preserved as a deprecated factory for one minor cycle so existing code keeps compiling. - M4 — Multi-result wire format v2 with magic + version. Layout:
[magic = 0x4D554C54 ("MULT")][version: u16 = 2][reserved: u16 = 0][count: u32].decode_multi(Rust) andMultiResultParser.parse(Dart) auto-detect v1 (no magic) and v2 (magic + version) framings, so old buffers in any storage / cache continue to round-trip without a breaking change.encode_multialways emits v2 since v3.2.0.- New constants:
MULTI_RESULT_MAGIC,MULTI_RESULT_VERSION(Rust),multiResultMagic,multiResultVersionV2(Dart). - Legacy
encode_multi_v1retained for compatibility tests.
- New constants:
- M5 — Parameterised multi-result batches. New end-to-end stack:
- Engine:
execute_multi_result_with_params(conn, sql, &[ParamValue]). - FFI:
odbc_exec_query_multi_params(conn_id, sql, params, params_len, ...). - Dart:
OdbcNative.execQueryMultiParams,NativeOdbcConnection.executeQueryMultiParams,AsyncNativeOdbcConnection.executeQueryMultiParams,IOdbcRepository.executeQueryMultiParams,IOdbcService.executeQueryMultiParams,TelemetryOdbcServiceDecorator.executeQueryMultiParams,ExecuteQueryMultiParamsRequestworker message. Up to 5 positional?parameters are supported (same arity ceiling as the existingexecuteQueryParams). Both connection IDs and pooled IDs are accepted.
- Engine:
- M6 ergonomics —
OdbcRepositoryImpl.executeQueryMulti(single) now unwraps the first result set viafirstResultSetOrNull, returning a truly emptyQueryResultonly when the batch had zero cursors.
Internal #
ExecutionEngine::encode_cursornow takes&mut Cinstead of consuming the cursor, so the multi-result paths can callcursor.into_stmt()afterwards to preserve pending result sets.- 6 new lib unit tests in
protocol::multi_result::tests(v2 framing round-trip, legacy v1 acceptance, version rejection, truncated header).
Migration notes #
- 100% backwards compatible at the source level. Existing callers that
built
MultiResultItem(resultSet: ..., rowCount: ...)directly keep compiling thanks to the deprecated factory. - Wire-level: any pre-v3.2 buffer (v1 framing) still decodes; v3.2 emits v2 framing which includes a magic word and a version byte. Storage / cache schemes that round-trip the buffer through e.g. Redis are unaffected.
- Sealed-class migration path: callers using the runtime checks
(
item.resultSet != null) still work via the backward-compatible accessors. Dart 3 callers are encouraged to migrate to pattern matching with the new variants for compile-time exhaustiveness.
Tests #
- Lib: 846 passed (was 842) / 0 failed / 16 ignored.
- regression_test: 78 passed / 0 failed / 4 ignored (the new
m1_multi_result_batch_shapestests are gated byENABLE_E2E_TESTS=1). - Dart unit (
test/{application,domain,infrastructure,core,helpers}): 418 passed / 0 failed / 3 skipped. cargo clippy --all-targets --all-features -- -D warnings: 0 warnings.dart analyze lib test: No issues found.
3.1.1 E2E test stability fixes #
Fixed #
odbc_exec_queryignored pooled connection IDs. The function only looked upstate.connectionsand returnedInvalid connection IDfor any id handed out byodbc_pool_get_connection. Brought the function in line withodbc_exec_query_params,odbc_prepareand the other paths that already accept both kinds of id (B added in v3.1.1).test_ffi_pool_release_raii_rollback_autocommitcould not exercise the RAII path on SQL Server. It tried to dirty the connection withodbc_exec_query("BEGIN TRANSACTION")which SQL Server rejects with SQLSTATE 25000 / native error 266 ("mismatching number of BEGIN and COMMIT statements") becauseSQLExecuteruns in autocommit-on mode by default. The test now flipsset_autocommit(false)directly on the live pooledConnection(the same pathTransaction::beginuses) and asserts that the next checkout observes a clean connection thanks toPoolAutocommitCustomizer.on_acquire.test_ffi_execute_retry_after_buffer_too_small_does_not_reexecute_side_effect_sqlused a SQL Server local temp table (#name). Local temp tables are scoped per physical session, and the ODBC Driver Manager may multiplex several physical sessions over a single logicalConnection, so the temp table was missing on the second statement. Switched to a permanent table namedffi_exec_retry_guard_<pid>plus anINSERT … OUTPUT REPLICATE('X', 6000)that returns a single result set (soodbc_exec_queryactually sees the 6000-byte payload) while still proving the no-re-execute property via PRIMARY KEY constraint.tests/helpers/env.rsgot 4 broken assertions whenODBC_TEST_DSNpointed at SQL Server.get_postgresql_test_dsn/_mysql/_oracle/_sybaseall fall back to the globalODBC_TEST_DSN, but the tests asserted that the returned string contained the corresponding driver name (e.g."MySQL"). When the developer only exports a singleODBC_TEST_DSNfor SQL Server (the typical setup), all four asserts failed. They now skip gracefully when the available DSN points at a different engine, and only run for real when a per-engine env var is configured (or a multi-DB CI matrix is in place).
Tests #
- Lib: 858 passed / 0 failed / 0 ignored (was 856 / 2 / 0 with
--include-ignored). - regression_test: 78 passed.
- cell_reader_test: 32 passed (was 28 / 4).
- transaction_test: 16 passed.
- ffi_compatibility_test: 14 passed.
cargo clippy --all-targets --all-features -- -D warnings: 0 warnings.
3.1.0 Transaction control hardening #
Fixed #
- B1 / closes A1 regression via FFI —
odbc_savepoint_create,odbc_savepoint_rollbackandodbc_savepoint_releaseno longer build SQL withformat!("SAVEPOINT {}", name). They now route throughTransaction::savepoint_create / _rollback_to / _release, which runvalidate_identifier+quote_identifierfor the active dialect. A savepoint name like"sp; DROP TABLE x--"arriving over the FFI is now rejected withValidationErrorinstead of being executed. - B2 — Dart could not reach the SQL Server savepoint dialect.
OdbcNative.transactionBeginnow exposessavepointDialect(default0=SavepointDialect.auto); the dialect propagates throughAsyncNativeOdbcConnection,BeginTransactionRequest,OdbcRepositoryImpl,IOdbcService.beginTransactionandTelemetryOdbcServiceDecorator. - B4 —
Transaction::begin_with_dialectno longer firesSET TRANSACTION ISOLATION LEVEL <X>blindly. The newIsolationStrategy::for_enginedispatches perengine_id:- SQL-92 dialect →
SET TRANSACTION ISOLATION LEVEL <X>(SQL Server, PostgreSQL, MySQL, MariaDB, Sybase, Redshift, …). - SQLite →
PRAGMA read_uncommitted = 0|1. - Db2 →
SET CURRENT ISOLATION = UR|CS|RS|RR. - Oracle → only
READ COMMITTEDandSERIALIZABLE; the other two now returnValidationErrorinstead of erroring at the driver. - Snowflake → silent skip (engine has no per-tx isolation).
- SQL-92 dialect →
- B7 —
Transaction::commitandrollbackalways attemptset_autocommit(true), even when the underlying commit/rollback fails. Connections can no longer be returned to the caller stuck inautocommit=off.
Added #
SavepointDialect::Auto(Rust) andSavepointDialect.auto(Dart) — resolved atTransaction::beginviaDbmsInfo::detect_for_conn_id(SQLGetInfo). SQL Server resolves toSqlServer; everything else (PostgreSQL, MySQL, MariaDB, Oracle, SQLite, Db2, Snowflake, …) toSql92. Wire mapping (stable):0→Auto(default, recommended)1→SqlServer2→Sql92
Transaction::savepoint_create / savepoint_rollback_to / savepoint_release— new public Rust methods that validate the name and emit the right SQL for the transaction's dialect (including theRELEASEno-op on SQL Server).Savepoint::create / rollback_to / releaseare now thin shims over them.TransactionHandle.runWithBegin(beginFn, action)(Dart) — static helper that opens a transaction, runsaction, commits on success and rolls back on any thrown exception. MirrorsTransaction::executeon the Rust side and is the recommended way to write leak-proof transaction code in Dart.TransactionHandle.withSavepoint(name, action)(Dart) — runsactioninside a named savepoint, releasing on success and rolling back to the savepoint on exception (transaction stays active).TransactionHandle.createSavepoint / rollbackToSavepoint / releaseSavepoint(Dart) — the wrapper now exposes the full savepoint surface so callers do not need to skip down toOdbcService.TransactionHandle implements Finalizable(Dart) — best-effortNativeFinalizerreclaims the small token allocated for tracking when the Dart object is GC'd without explicit commit/rollback. The transaction itself is rolled back by the engine inodbc_disconnect.Transaction::for_test_no_conn(Rust,#[doc(hidden)]) — convenience constructor for integration tests that exercise validation paths without a real connection.
New tests #
tests/regression/a1_ffi_savepoint_injection.rs— 6 new tests covering every malicious-name case across both dialects, plus theAutodefault.- 4 new lib unit tests in
engine::transaction::testscovering the new Db2 keyword, the SqlServer no-oprelease, thefrom_u32Auto default and identifier validation through the new methods.
Documentation #
example/transaction_helpers_demo.dart— NEW demo showcasingrunWithBegin,withSavepointand theSavepointDialectwire codes.example/savepoint_demo.dart— updated to reference v3.1 helpers and point to the new demo.example/README.md— new entry under "Transactions / savepoints".
Migration notes #
- 100% backwards compatible at the source level. Existing callers that pass
no
savepointDialectkeep working: they now useAutoinstead ofSql92, which produces identical SQL on every engine except SQL Server (where the new behaviour is the correct one). - Wire-level: the FFI default for the third argument of
odbc_transaction_beginchanged fromSql92toAuto. C callers passing the explicit literal1(=SqlServer) keep working unchanged. Callers that previously relied on the default value0to meanSql92should pass2if they need the explicit pre-v3.1 behaviour, but typically just benefit from the new auto-detection.
Added (v3.0.0) #
- Seven new capability traits (SOLID design, opt-in by plugin):
BulkLoader— native bulk insert path per engine.Upsertable— dialect-specific INSERT-OR-UPDATE SQL builder.Returnable— append RETURNING / OUTPUT clause to DML.TypeCatalog— extended type mapping using DBMSTYPE_NAME.IdentifierQuoter— per-driver identifier quoting style.CatalogProvider— driver-specific schema introspection SQL.SessionInitializer— post-connect setup statements.- Lives in
plugins/capabilities/.
- Four new driver plugins:
SqlitePlugin—ON CONFLICT,RETURNING, PRAGMA setup, sqlite_master catalog.Db2Plugin—MERGE,FROM FINAL TABLE, SYSCAT catalog, FETCH FIRST n ROWS.SnowflakePlugin—MERGE,RETURNING, VARIANT/OBJECT/ARRAY type mapping, QUERY_TAG.MariaDbPlugin—RETURNING(MariaDB-only), backtick quoting, UUID type.
- Twelve new
OdbcTypevariants:NVarchar,TimestampWithTz,DatetimeOffset,Time,SmallInt,Boolean,Float,Double,Json,Uuid,Money,Interval. - Three new FFI entry points:
odbc_build_upsert_sql(conn_str, table, payload_json, ...)odbc_append_returning_sql(conn_str, sql, verb, columns_csv, ...)odbc_get_session_init_sql(conn_str, options_json, ...)
- Dart bindings:
OdbcDriverFeatures(inlib/infrastructure/native/driver_capabilities_v3.dart) with typedbuildUpsertSql,appendReturningClause,getSessionInitSql, plusDmlVerbenum andSessionOptionsclass. - New regression suites under
native/odbc_engine/tests/regression/:v30_capabilities,v30_upsert_dialects,v30_returning_dialects,v30_session_init. - Documentation:
doc/CAPABILITIES_v3.mdwith the full capability × engine matrix.
Changed (v3.0.0) #
PluginRegistry::detect_drivernow usesDriverCapabilities::detect_from_connection_stringto map the connection string to a canonical engine id, then to a registered plugin id. MariaDB now has its own dedicated plugin instead of falling back tomysql.from_odbc_sql_typerecognises additional SQL_* type codes (SQL_TYPE_TIME=92,SQL_TYPE_DATE=91,SQL_GUID=−11,SQL_WCHAR/WVARCHAR/WLONGVARCHAR=−8/−9/−10,SQL_BIT=−7,SQL_REAL=7,SQL_FLOAT/SQL_DOUBLE=6/8,SQL_TINYINT=−6,NUMERIC=2).
Added (v2.1.0 — included in this release) #
- Live DBMS detection via
SQLGetInfo(resolves the v2.0 limitation whereDriverCapabilities::detect(_conn)returneddefault()):- New
engine::DbmsInfostruct withdbms_name, canonicalengineid,max_*_name_len,current_catalogand embeddedDriverCapabilities. - New
OdbcConnection::dbms_info()andOdbcConnection::driver_capabilities()helpers that consult the live driver instead of parsing the connection string. - New FFI
odbc_get_connection_dbms_info(conn_id, buffer, buffer_len, out_written)returning JSON with the live DBMS information. DriverCapabilities::detect(conn)now actually queries the driver viadatabase_management_system_name()and populatesengineplus the server-reporteddriver_name.
- New
- Canonical engine ids (
engine::core::ENGINE_*constants):sqlserver,postgres,mysql,mariadb,oracle,sybase_ase,sybase_asa,sqlite,db2,snowflake,redshift,bigquery,mongodb,unknown. Stable across releases; exposed in JSON payloads under the newenginefield. PluginRegistry::plugin_id_for_dbms_name,PluginRegistry::get_for_dbms_nameandPluginRegistry::get_for_live_connectionresolve plugins from the server-reported DBMS name (or the live connection itself) — MariaDB correctly falls back to the MySQL plugin.DriverCapabilities::from_driver_namenow recognises:Microsoft SQL Server(full Windows DBMS name)MariaDB(distinct from MySQL)Adaptive Server AnywhereandAdaptive Server Enterprise(distinct Sybase variants)IBM Db2,Snowflake,Amazon Redshift,Google BigQuery- All
ENGINE_*canonical ids round-trip
- Dart side:
DatabaseEngineIdsconstants matching the Rust ids.DatabaseType.fromEngineId(id)(preferred overfromDriverNamewhen the canonical id is available).- New enum values
DatabaseType.{mariadb, sybaseAse, sybaseAsa, db2, snowflake, redshift, bigquery, mongodb}. The legacyDatabaseType.sybaseis kept as a deprecated alias forsybaseAse. DbmsInfotyped wrapper for the new FFI JSON payload.OdbcDriverCapabilities.getDbmsInfoForConnection(connId)consumes the new FFI.- Raw
odbc_get_connection_dbms_infobinding inlib/infrastructure/native/bindings/odbc_bindings.dart.
Changed #
enginefield is now part of everyDriverCapabilitiesJSON payload produced byodbc_get_driver_capabilities. Old clients ignore the extra field; new clients read it for accurate engine identification.PluginRegistry::detect_driverkeeps its connection-string heuristic but is no longer the sole detection path — preferget_for_live_connection(conn)once the connection is open.
Removed #
- None
Fixed #
- The audit gap "DSN-only connection strings always classified as
Unknown" is resolved on the live-connection path:odbc_get_connection_dbms_infoconsultsSQL_DBMS_NAMEdirectly, which is populated by the Driver Manager for DSN-only strings. MariaDBis no longer silently classified asMySQL.Adaptive Server AnywhereandAdaptive Server Enterpriseare no longer conflated.
2.0.0 - 2026-04-18 #
Hardening release driven by a full security and reliability audit. All audited critical and high-severity findings are addressed. The Dart FFI ABI is preserved (no client-side rebuilds required); only internal Rust APIs have breaking adjustments.
Added #
ffi::guardmodule withcall_int/call_ptr/call_id/call_sizehelpers andffi_guard_int!/ffi_guard_id!/ffi_guard_ptr!macros. Wrap anyextern "C"body in these helpers so panics never unwind across the FFI boundary (resolves audit C1).engine::identifiermodule withvalidate_identifier,quote_identifier,quote_identifier_default,quote_qualified_defaultandIdentifierQuotingenum. Used bySavepoint/ArrayBindingto defeat SQL injection vectors (resolves A1, A2).observability::SpanGuardRAII helper; spans are now finished even on early?returns or panics (resolves A3).observability::sanitize_sql_for_logmasks SQL literals before logging. SetODBC_FAST_LOG_RAW_SQL=1to opt into raw logging in dev (A8).protocol::bulk_insert::is_null_strictplus length validation inparse_bulk_insert_payload. Truncated null bitmaps are now rejected as malformed payloads instead of being silently treated as "not null" (C9).protocol::bulk_insert::MAX_BULK_COLUMNS,MAX_BULK_ROWS,MAX_BULK_CELL_LENresource caps to bound memory on hostile payloads (M2).engine::core::ParallelModeenum withIndependentandPerChunkTransactionalvariants forParallelBulkInsert. Per-chunk atomicity option (C8).OdbcErrorvariantsNoMoreResults,MalformedPayload,RollbackFailed,ResourceLimitReached,Cancelled,WorkerCrashedandBulkPartialFailure { rows_inserted_before_failure, failed_chunks, detail }for structured error reporting.SecureBuffer::with_byteszeroises the buffer after the closure runs (resolves C5).SecretManager::with_secretborrows secret bytes without cloning (M12).PluginRegistry::is_supportedintrospection helper.PoolOptions::connection_timeoutfield for configurable acquire timeout (resolves A9 baseline).- Pool now installs a
PoolAutocommitCustomizerthat forcesset_autocommit(true)on every checkout regardless oftest_on_check_out(resolves A14). bench_baselines/v1.2.1.txtplaceholder for benchmark comparisons.- New regression test suite under
native/odbc_engine/tests/regression/covering the new safety helpers, identifier validation, span lifecycle, and bitmap corruption.
Changed #
OdbcError::sqlstateis now used for structured "no more results" detection instead of substring matching one.to_string()(resolves A13).Savepoint::create/rollback_to/releasenow validate and quote the savepoint name usingquote_identifier(resolves A1).ArrayBinding::bulk_insert_*methods now quote table and column names viaquote_qualified_default/quote_identifier_default(resolves A2).Transaction::DropandTransaction::executenow log rollback failures vialog::error!with conn id and source error context instead of using silentlet _ = ...(resolves M3).DiskSpillStreamgains animpl Dropthat removes orphan temp files, preventing leaks on panic or early return (resolves M4).StreamingStateFileBacked::fetch_next_chunknow usesread_exactinstead of a singleread, so partial reads on Windows do not silently truncate chunks (resolves A6).BatchedStreamingState/AsyncStreamingState::fetch_next_chunk: receiver disconnect is now reported asOdbcError::WorkerCrashedinstead of being treated as a clean EOF (resolves A5).odbc_pool_get_connectionno longer holds the global state lock while callingr2d2::Pool::get(); theArc<ConnectionPool>is cloned and the lock released before the blocking acquire, eliminating up to a 30-second global stall per checkout (resolves C3).odbc_pool_closedrains live checkouts before removing the pool entry, avoiding a deadlock when other code paths drop their wrappers after the map has been mutated (resolves C4).odbc_stream_fetchno longer panics withexpect("pending stream chunk exists")when a pending chunk vanishes between length check and removal; returns-1with a structured error message instead (part of C1 hardening).PluginRegistry::get_for_connectionnow logs a warning whendetect_driverresolves a name that is not registered (e.g.mongodb,sqlite), instead of silently returningNone(resolves A7).PluginRegistry::defaultnow logs registration failures vialog::error!instead of usingunwrap_or_defaultto swallow them (M15).security::sanitize_connection_stringnow respects ODBC{...}quoting and recognises additional secret keys:secret,token,apikey,api_key,accesstoken,access_token,authorization,auth,sas,sastoken,sas_token,connectionstring,primarykey,secondarykey(resolves M10).protocol::bulk_insert::serialize_bulk_insert_payloadnow usestry_intofor length conversions and emitsOdbcError::MalformedPayloadon overflow instead of silentas u32truncation (resolves M8).versioning::ApiVersion::currentnow readsenv!("CARGO_PKG_VERSION")instead of hardcoded0.1.0(resolves M17).- Bumped Rust crate
odbc_engineand Dart packageodbc_fastfrom 1.x → 2.0.0.
Deprecated #
SecureBuffer::into_vecis deprecated. The returnedVec<u8>is no longer zeroised on drop. PreferSecureBuffer::with_bytesfor short-lived consumers (resolves C5).
Fixed #
- C1 —
odbc_stream_fetchexpect/unwrapno longer crosses FFI. - C3 — Global mutex no longer held during
r2d2.get()blocking call. - C4 —
odbc_pool_closedrains checkouts before removing the pool entry. - C5 —
SecureBufferexposes a zeroising consumer API. - C6 —
execute_multi_resultnow uses structured SQLSTATE detection for end-of-results (full row-count → multi-result handling deferred to v2.1 with a refactored statement adapter). - C9 — Truncated null bitmaps in bulk-insert payloads are now rejected.
- A1, A2 — Identifier interpolation in dynamic SQL is whitelisted + quoted.
- A3 — Span lifecycle bound to RAII guard, no leaks on early returns.
- A5 — Streaming receiver disconnect is now an explicit error.
- A6 — Disk-spill reads use
read_exactto avoid short reads. - A7 — Driver detection consistency surfaced via warning + new
is_supportedhelper. - A8 — SQL literals are masked in logs by default.
- A9 —
PoolOptions::connection_timeoutexposes acquire timeout. - A13 — Structured
02000SQLSTATE check replaces substring detection. - A14 —
PoolAutocommitCustomizerforcesautocommit(true)per checkout. - M3 — Transaction rollback failures are logged with context.
- M4 — Disk-spill orphan files cleaned up on drop.
- M8 — Wire-format length casts return errors on overflow.
- M10 — Connection-string sanitiser handles
{...}and more keys. - M12 — Secret retrieve dedup helper avoids extra heap copy.
- M15 — Registry default logs (rather than swallows) registration errors.
- M17/M18 —
ApiVersion::currentreads fromCargo.toml.
Notes #
- The pre-existing flaky test
ffi::tests::test_ffi_get_structured_error(race in global state across tests) was not introduced by this release but should be fixed in v2.1 as part of the granular-locks rework. - True chunk-by-chunk streaming (audit C7) and full row-count → multi- result handling (full C6) require a deeper refactor of the streaming worker and a new statement-adapter abstraction; tracked for v2.1.
1.2.1 - 2026-03-10 #
Fixed #
- FFI buffer-retry reliability hardening:
- preserved stream chunks across
-2retries inodbc_stream_fetch - preserved async payloads across
-2retries inodbc_async_get_result - avoided re-execution for
-2retries by serving pending payloads in:odbc_exec_query,odbc_exec_query_params,odbc_exec_query_multi, andodbc_execute - fixed
odbc_get_driver_capabilitiesto return-2(instead of truncating JSON with success)
- preserved stream chunks across
- Added regression coverage for retry semantics in stream, async, and execute paths (including side-effect safety check for prepared execute retry).
- Removed CI flakiness in async invalid-request tests by avoiding ID collision
between
TEST_INVALID_IDand generated invalid test IDs.
1.2.0 - 2026-03-10 #
Added #
- Schema reflection API for primary keys, foreign keys, and indexes:
catalogPrimaryKeys(connectionId, table)- Lists primary keys for a tablecatalogForeignKeys(connectionId, table)- Lists foreign keys for a tablecatalogIndexes(connectionId, table)- Lists indexes for a table (PRIMARY KEY and UNIQUE constraints)
- FFI exports:
odbc_catalog_primary_keys,odbc_catalog_foreign_keys,odbc_catalog_indexes - Full implementation from Rust engine -> FFI -> Dart bindings -> Repository -> Service
- Type mapping documentation consolidated:
- Added "Type Mapping" section to README with implemented vs planned status
doc/notes/TYPE_MAPPING.mdupdated with verified implementation statuscolumnar_protocol.dartmarked as experimental/not used
- Example:
example/catalog_reflection_demo.dart - Experimental typed parameter prototype:
SqlDataType,SqlTypedValue, andtypedParam(...)
- Protocol performance benchmark suite:
test/performance/protocol_performance_test.dart
Changed #
- Reliability/performance hardening completed:
- fail-fast nullability and per-type validation in
BulkInsertBuilder.addRow() - text validation by character and UTF-8 byte length
- canonical
doublemapping to fixed-scale decimal string DateTimeyear range validation (1..9999)- complex unsupported-type error message construction via
StringBuffer
- fail-fast nullability and per-type validation in
- Documentation cleanup:
- removed completed execution plans from
doc/notes/ - added
Validation examplessection in rootREADME.md
- removed completed execution plans from
Removed #
- Orphaned
native/telemetry/directory (not compiled in workspace; actual implementation is innative/odbc_engine/src/observability/telemetry/)
Fixed #
- Streaming integration stability and cleanup:
- unique dynamic test tables and safer assertions
- CI reliability:
- Rust fmt alignment and test thread safety adjustments
1.1.2 - 2026-03-03 #
Added #
workflow_dispatchsupport in publish workflow for manual pub.dev publishing
1.1.0 - 2026-02-19 #
Added #
- Statement cancellation API exposed at high-level service/repository layers:
cancelStatement(connectionId, stmtId) UnsupportedFeatureErrorin Dart domain errors for explicit unsupported capability reporting
Changed #
- Statement cancellation contract standardized as explicit unsupported at runtime
(Option B path), with structured native error SQLSTATE
0A000 - Sync and async cancellation paths now aligned with equivalent behavior and consistent unsupported semantics
- Canonical docs aligned for cancellation status and workaround guidance:
README.md,doc/TROUBLESHOOTING.md,example/README.md
Fixed #
- Removed ambiguity between exposed cancellation entrypoints and current runtime capability by returning explicit unsupported contract instead of implicit behavior
1.0.3 - 2026-02-16 #
Added #
- New canonical type mapping documentation:
doc/TYPE_MAPPING.md - New implementation checklists:
doc/notes/TYPE_MAPPING_IMPLEMENTATION_CHECKLIST.mddoc/notes/STATEMENT_CANCELLATION_IMPLEMENTATION_CHECKLIST.mddoc/notes/NULL_HANDLING_RELIABILITY_PERFORMANCE_PLAN.md
- New/updated example coverage docs and demo files for advanced/service/telemetry scenarios
Changed #
- Root and docs indexes now reference canonical type-mapping documentation
- Master gaps plan now tracks open execution checklists for remaining gaps
Fixed #
- Documentation consistency across root README,
doc/README.md, and notes references
1.0.2 - 2026-02-15 #
1.0.0 - 2026-02-15 #
Added #
- Async API request timeout:
AsyncNativeOdbcConnection(requestTimeout: Duration?)— optional timeout per request; default 30s;Duration.zeroornulldisables - AsyncError new codes:
requestTimeout(worker did not respond in time),workerTerminated(disposed or crashed) - Parallel bulk insert (pool-based) end-to-end: Rust FFI
odbc_bulk_insert_parallelnow implemented and exposed in Dart sync/async service/repository stack - Bulk insert comparative benchmark: new ignored Rust E2E benchmark test
e2e_bulk_compare_benchmark_testforArrayBindingvsParallelBulkInsert
Changed #
- Async dispose: Pending requests now complete with
AsyncError(workerTerminated) instead of hanging whendispose()is called - Worker crash handling: When the worker isolate dies, pending requests complete with error instead of hanging
- BinaryProtocolParser: Truncated buffers now throw
FormatException('Buffer too small for payload')instead ofRangeError
Fixed #
- Array binding tail chunk panic: fixed
copy_from_slicelength mismatch when the final bulk-insert chunk is smaller than configured batch size
0.3.1 - 2026-01-29 #
Changed #
- Improved download experience: Native library download now includes retry logic with exponential backoff (up to 3 attempts)
- Better error messages: Download failures now show detailed troubleshooting steps and clearly explain what went wrong
- HTTP 404 handling: When GitHub release doesn't exist, provides clear instructions for production vs development scenarios
- Connection timeout: Added 30-second timeout to HTTP client to prevent hanging on slow connections
- Download feedback: Shows file size after successful download
- CI/pub.dev detection: Skip download in CI environments to avoid analysis timeout, with clear logging
Fixed #
- pub.dev analysis timeout: Hook now detects CI/pub.dev environment and skips external download, allowing pub.dev to analyze the package correctly
0.3.0 - 2026-01-29 #
Added #
- Configurable result buffer size:
ConnectionOptions.maxResultBufferBytes(optional). When set at connect time, caps the size of query result buffers for that connection; when null, the package default (16 MB) is used. Use for large result sets to avoid "Buffer too small" errors. ConstantdefaultMaxResultBufferBytesis exported for reference.
0.2.9 - 2026-01-29 #
Fixed #
- Async API "QueryError: No error": when executing queries with no parameters, the Dart FFI was passing
nullfor the params buffer toodbc_exec_query_params, which caused invalid arguments and led to failures reported as "No error". The native bindings now always pass a valid buffer (e.g.Uint8List(0)) instead ofnull, so both sync and async (worker) paths work correctly for parameterless queries.
0.2.8 - 2026-01-29 #
Added #
scripts/copy_odbc_dll.ps1: copiesodbc_engine.dllfrom package (pub cache) to project root and Flutter runner folders (Debug/Release) for consumers who need the DLL manually
Changed #
- Publish
hook/andscripts/in the package (removed from.pubignore): Native Assets hook runs for consumers so the DLL can be downloaded/cached automatically; scriptcopy_odbc_dll.ps1is available in the package - Minimum SDK constraint raised to
>=3.6.0(required by pub.dev when publishing packages with build hooks)
Fixed #
- Async API (worker isolate): empty result (DDL/DML, SELECT with no rows) is now returned as
Result.ok(QueryResult(columns: [], rows: [], rowCount: 0))instead ofResult.err(QueryError("No error", ...))(fixes "No error" when executing CREATE TABLE, INSERT, ALTER, etc.)
0.2.7 - 2026-01-29 #
Fixed #
- Native DLL cache now keyed by package version (
~/.cache/odbc_fast/<version>/) to avoid loading an older DLL when upgrading the package (fixes symbol lookup error 127 for new symbols e.g.odbc_savepoint_create)
0.2.6 - 2026-01-29 #
0.2.5 - 2026-01-29 #
Added #
- Database type detection in tests:
detectDatabaseType(),skipIfDatabase(),skipUnlessDatabase() - Test helpers for conditional execution by database (SQL Server, PostgreSQL, MySQL, Oracle)
test/helpers/README.mdwith usage and examples
Changed #
- Dart tests run sequentially (
--concurrency=1) to avoid resource contention (ServiceLocator, worker isolates) - Savepoint release test skipped on SQL Server (RELEASE SAVEPOINT not supported)
Fixed #
- Rust FFI E2E:
ffi_test_dsn()loads.envand checksENABLE_E2E_TESTS; invalid stream ID race in tests - Dart integration test timeouts when running in parallel
0.2.4 - 2026-01-27 #
0.2.3 - 2026-01-27 #
Changed #
- CI: run only unit tests that do not require real ODBC connection (domain, protocol, errors)
- CI: exclude stress, integration/e2e, and native-dependent tests from publish pipeline
0.2.1 - 2026-01-27 #
Fixed #
- Fixed Native Assets hook to read package version from correct pubspec.yaml
- Fixed test helper to properly handle empty environment variables
- Fixed GitHub Actions cache paths and key format
Changed #
- Improved CI workflow: now builds Rust library before running tests
- Split unit and integration tests in CI for better organization
- Enhanced GitHub Actions workflows with proper dependency installation
0.2.0 - 2026-01-27 #
Added #
- Savepoints (nested transaction markers)
- Automatic retry with exponential backoff for transient errors
- Connection timeouts (login/connection timeout configuration)
- Connection String Builder (fluent API)
- Backpressure control in streaming queries
Changed #
- Async API with worker isolate for non-blocking operations
- Comprehensive E2E Rust tests with coverage reporting
- Improved documentation and troubleshooting guides
Fixed #
- Various lint issues (very_good_analysis compliance)
- Code formatting and cleanup