Skip to content

Round Trips Optimisation + Service API + Column Descriptions - #196

Open
mreis1 wants to merge 4 commits into
fernandobatels:masterfrom
mreis1:qdt/describe
Open

mreis1 wants to merge 4 commits into
fernandobatels:masterfrom
mreis1:qdt/describe

Conversation

@mreis1

@mreis1 mreis1 commented Oct 20, 2026

Copy link
Copy Markdown

No description provided.

…eads, 64 KiB segments, protocol 19 inline blobs

Over a remote link the pure-rust client was round-trip-bound: every BLOB
cell cost op_open_blob + N × op_get_segment (1 KiB each!) + op_close_blob,
each a full round trip, done serially per cell. A metadata scan of a few
thousand objects whose sources live in BLOBs took many minutes.

Protocol negotiation
- Offer protocol versions 10–16 and 19 (was 10–13). Firebird 3 answers
  15, Firebird 4 16, Firebird 5.0.2+ 19. 17/18 are not offered: nothing
  we send uses op_batch_sync / op_fetch_scroll, and the server picks the
  highest *offered* version it supports. FB_MAX_PROTOCOL=13 restores the
  previous negotiation.
- op_execute / op_execute2 carry the version-gated trailing fields of
  P_SQLDATA: p_sqldata_timeout (>= 16), p_sqldata_cursor_flags (>= 18),
  p_sqldata_inline_blob_size (>= 19). A server at >= 16 blocks reading
  them, so they must follow the negotiated version exactly.
- The attach DPB match covers the new versions (13–19 attach alike); the
  arm also no longer uses an `if let` match guard, which is not stable
  Rust and kept the crate from building on a stable toolchain.

Blob reads
- `resolve_blobs` completes the BLOB columns of a whole fetch batch in a
  few round trips: opens for 32 blobs in one write, then one op_get_segment
  per open blob per round (share doubling for blobs that keep going, 32
  requests in flight max), closes written without waiting (their responses
  are consumed lazily, like op_free_statement — the server never defers a
  close response). Responses arrive strictly in request order, so a FIFO
  keeps the stream in sync; on a server error every outstanding response
  is still drained so the connection stays usable.
- op_get_segment asks for 65535 bytes (the USHORT maximum) instead of 1024,
  so a blob under 64 KiB is one segment round trip.
- Protocol 19+: the client announces an inline blob size (default 65535,
  fbclient's default; FB_INLINE_BLOB_SIZE=0 disables) and consumes the
  op_inline_blob packets the server sends ahead of op_fetch_response /
  op_sql_response, decoding those blob cells on the spot. Small blobs then
  cost zero extra round trips.

Describe
- op_prepare_statement / op_info_sql offer a 64 KiB info buffer (was 1 KiB),
  so the describe of a wide table no longer comes back truncated and
  re-requested one round trip at a time.
wire.rs (pure packet tests): protocol offers follow FB_MAX_PROTOCOL with
ascending weights; op_execute/op_execute2 trailing fields appear exactly
from 16/18/19 and the packet head never moves; op_get_segment caps at the
USHORT maximum; segment unframing; op_inline_blob layout; a row decode
completes inlined blob columns (text, binary, NULL) and leaves the rest
pending.

client.rs (pipeline_tests): a scripted Firebird-shaped peer on a loopback
socket answers op_fetch / open / get_segment / close / commit strictly in
request order, like the server. Covers text, binary, NULL, empty,
multi-segment, exactly-one-buffer and multi-round blobs across two batches,
lazy-close responses being skipped ahead of a commit, protocol 19 inlining
(zero blob ops for small blobs, pipeline for the big one), and a server
error in the window (invalid BLOB ID) leaving the connection usable.
The service manager is now reachable without fbclient: op_service_attach
after the usual connect/SRP/wire-crypt handshake (the connect packet names
the follow-up operation and 'service_mgr'), op_service_start,
op_service_info and op_service_detach, with a v2 attach SPB that carries
the same auth material as the DPB (plaintext on protocol 10, hashed on
11/12, the SRP proof on 13+ when cont_auth did not verify it).

The byte formats — option structs, backup/restore start requests, the
isc_info_svc_line / server-version replies — move to
rsfbclient_core::services so both clients share them; the native code in
rsfbclient::services now delegates to them, and ServiceManager::builder()
picks the native client when compiled in and the pure-rust one otherwise
(.pure_rust() forces it, .expected_db() names the database for servers
with per-database security). start()/query() are public for raw actions.

Tests: request/reply byte layouts in core, and a scripted loopback service
manager in rsfbclient-rust checking attach, start, line draining, server
version and detach.
…alias and field

A prepared statement's description now reaches callers: ColumnDesc (core)
carries the result-set name, the underlying field, the relation, the
context alias it has in the statement (FROM clients c -> C), the owner and
the type as the server reported it before coercion.

Pure-rust client: isc_info_sql_relation_alias joins the describe items, so
the whole description is parsed from the prepare response and
describe_statement answers without a round trip. Native client: the XSQLDA
names and types are captured at prepare; the context aliases are asked
from the library once, on request, and kept with the statement.

Facade: Queryable::describe(sql) prepares (through the statement cache,
so the query that follows reuses it) and describes without executing;
Statement::describe() for an already prepared statement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant