Skip to main content

type_bridge_typedb_runtime/
lib.rs

1//! Shared TypeDB driver runtime.
2//!
3//! This crate owns all direct TypeDB driver interaction so higher-level crates
4//! can share version gating, gRPC fallback, database lifecycle, transactions,
5//! and JSON conversion without depending on each other.
6
7#![deny(missing_docs)]
8
9#[cfg(not(any(feature = "band8", feature = "band9")))]
10compile_error!(
11    "type-bridge-typedb-runtime requires at least one band feature; enable `band8` and/or `band9` (both are default)"
12);
13
14use std::future::Future;
15use std::path::PathBuf;
16use std::pin::Pin;
17use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering as AtomicOrdering};
18use std::sync::{Arc, Mutex};
19use std::time::{Duration, Instant};
20
21use futures::TryStreamExt;
22use serde::{Deserialize, Serialize};
23use tokio::sync::watch;
24use type_bridge_core_lib::version as core_version;
25use type_bridge_core_lib::version::DEFAULT_HTTP_PORT;
26
27#[cfg(feature = "band8")]
28use type_bridge_typedb_driver_b8 as driver_b8;
29#[cfg(feature = "band8")]
30use type_bridge_typedb_driver_b8::answer::QueryAnswer as B8QueryAnswer;
31#[cfg(feature = "band8")]
32use type_bridge_typedb_driver_b8::{
33    Addresses, Credentials as B8Credentials, DriverOptions, DriverTlsConfig,
34    TransactionOptions as B8TransactionOptions, TransactionType as B8TransactionType,
35    TypeDBDriver as B8Driver,
36};
37
38#[cfg(feature = "band9")]
39use typedb_driver as driver_b9;
40#[cfg(feature = "band9")]
41use typedb_driver::answer::QueryAnswer as B9QueryAnswer;
42#[cfg(feature = "band9")]
43use typedb_driver::{
44    Addresses as B9Addresses, Credentials as B9Credentials, DriverOptions as B9DriverOptions,
45    DriverTlsConfig as B9DriverTlsConfig, TransactionOptions as B9TransactionOptions,
46    TransactionType as B9TransactionType, TypeDBDriver as B9Driver,
47};
48
49/// Boxed future returned by async runtime methods.
50pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
51
52/// How confidently a failed commit can be classified without observing server state.
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum CommitFailureCertainty {
55    /// The server rejected the commit and the transaction did not commit.
56    DefinitelyAborted,
57    /// The client cannot determine whether the transaction committed.
58    Unknown,
59}
60
61/// Runtime error type for direct TypeDB driver operations.
62#[derive(Debug, thiserror::Error)]
63pub enum RuntimeError {
64    /// The detected TypeDB server version lies outside the supported window or
65    /// its protocol band is not compiled into this runtime.
66    #[error("Unsupported version: {0}")]
67    UnsupportedVersion(#[from] core_version::VersionError),
68
69    /// Connection to TypeDB failed.
70    #[error("Connection error: {0}")]
71    Connection(String),
72
73    /// Query execution failed.
74    #[error("Query execution error: {0}")]
75    QueryExecution(String),
76
77    /// Transaction lifecycle failed.
78    #[error("Transaction error: {0}")]
79    Transaction(String),
80
81    /// A bounded answer ceiling was exceeded while reading the driver stream.
82    #[error("Resource limit [{code}]: {message}")]
83    ResourceLimit {
84        /// Stable resource code.
85        code: &'static str,
86        /// Human-readable diagnostic.
87        message: &'static str,
88    },
89
90    /// The higher-level answer decoder rejected one streamed item.
91    #[error("Answer consumer rejected a streamed provider item")]
92    AnswerConsumer,
93}
94
95/// A classified result available to recovery-aware internal commit callers.
96///
97/// [`RuntimeTransaction::commit`] retains the released [`RuntimeError`]
98/// surface. Callers that need durability certainty can opt into
99/// [`RuntimeTransaction::commit_classified`] without adding a variant to that
100/// released, exhaustively matchable enum.
101#[derive(Debug, thiserror::Error)]
102pub enum RuntimeCommitError {
103    /// A transaction lifecycle error before the driver attempted a commit.
104    #[error(transparent)]
105    Runtime(#[from] RuntimeError),
106    /// A driver commit failure with explicit durability certainty.
107    #[error("Transaction error: Commit failed: {message}")]
108    Driver {
109        /// Whether the failed response proves that the commit was aborted.
110        certainty: CommitFailureCertainty,
111        /// The original driver error text.
112        message: String,
113    },
114}
115
116impl RuntimeCommitError {
117    /// Convert to the released transaction-error surface.
118    #[must_use]
119    pub fn into_runtime_error(self) -> RuntimeError {
120        match self {
121            Self::Runtime(error) => error,
122            Self::Driver { message, .. } => {
123                RuntimeError::Transaction(format!("Commit failed: {message}"))
124            }
125        }
126    }
127}
128
129fn commit_failure(
130    certainty: CommitFailureCertainty,
131    message: impl Into<String>,
132) -> RuntimeCommitError {
133    RuntimeCommitError::Driver {
134        certainty,
135        message: message.into(),
136    }
137}
138
139#[cfg(feature = "band8")]
140fn band8_commit_failure(error: driver_b8::Error) -> RuntimeCommitError {
141    let certainty = if matches!(&error, driver_b8::Error::Server(_)) {
142        CommitFailureCertainty::DefinitelyAborted
143    } else {
144        CommitFailureCertainty::Unknown
145    };
146    commit_failure(certainty, error.to_string())
147}
148
149#[cfg(feature = "band9")]
150fn band9_commit_failure(error: driver_b9::Error) -> RuntimeCommitError {
151    let certainty = if matches!(&error, driver_b9::Error::Server(_)) {
152        CommitFailureCertainty::DefinitelyAborted
153    } else {
154        CommitFailureCertainty::Unknown
155    };
156    commit_failure(certainty, error.to_string())
157}
158
159#[cfg(test)]
160mod commit_failure_tests {
161    use super::*;
162
163    #[test]
164    fn typed_commit_failure_preserves_the_legacy_display() {
165        for certainty in [
166            CommitFailureCertainty::DefinitelyAborted,
167            CommitFailureCertainty::Unknown,
168        ] {
169            let error = commit_failure(certainty, "driver response");
170            assert_eq!(
171                error.to_string(),
172                "Transaction error: Commit failed: driver response"
173            );
174            assert!(matches!(
175                &error,
176                RuntimeCommitError::Driver {
177                    certainty: actual,
178                    ..
179                } if *actual == certainty
180            ));
181            assert!(matches!(
182                error.into_runtime_error(),
183                RuntimeError::Transaction(message)
184                    if message == "Commit failed: driver response"
185            ));
186        }
187    }
188
189    #[cfg(feature = "band8")]
190    #[test]
191    fn band8_opaque_commit_failure_is_unknown() {
192        let error = band8_commit_failure(driver_b8::Error::Other("transport".into()));
193        assert!(matches!(
194            error,
195            RuntimeCommitError::Driver {
196                certainty: CommitFailureCertainty::Unknown,
197                ..
198            }
199        ));
200    }
201
202    #[cfg(feature = "band9")]
203    #[test]
204    fn band9_opaque_commit_failure_is_unknown() {
205        let error = band9_commit_failure(driver_b9::Error::Other("transport".into()));
206        assert!(matches!(
207            error,
208            RuntimeCommitError::Driver {
209                certainty: CommitFailureCertainty::Unknown,
210                ..
211            }
212        ));
213    }
214}
215
216/// Convenience result alias for runtime operations.
217pub type Result<T> = std::result::Result<T, RuntimeError>;
218
219/// Result of a query execution after conversion from TypeDB stream types.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub enum QueryResult {
222    /// No data returned.
223    Ok,
224    /// Document results from a fetch clause.
225    Documents(Vec<serde_json::Value>),
226    /// Row results from a match/reduce/concept query.
227    Rows(Vec<serde_json::Value>),
228}
229
230/// One item read from a TypeDB concept answer stream.
231#[derive(Debug, Clone, PartialEq)]
232pub enum RuntimeAnswerItem {
233    /// Concept row converted to JSON.
234    Row(serde_json::Value),
235    /// Concept document converted to JSON.
236    Document(serde_json::Value),
237}
238
239/// Provider answer kind, retained even for an empty stream.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum RuntimeAnswerKind {
242    /// Statement returned no data stream.
243    Ok,
244    /// Concept-row stream.
245    Rows,
246    /// Concept-document stream.
247    Documents,
248}
249
250/// Whether the answer consumer needs another driver item.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum RuntimeAnswerControl {
253    /// Continue reading.
254    Continue,
255    /// Stop before polling the stream again.
256    Stop,
257}
258
259/// Cooperative cancellation shared with the ORM answer policy.
260#[derive(Debug, Clone)]
261pub struct RuntimeAnswerCancellation {
262    cancelled: watch::Sender<bool>,
263}
264
265impl Default for RuntimeAnswerCancellation {
266    fn default() -> Self {
267        let (cancelled, _) = watch::channel(false);
268        Self { cancelled }
269    }
270}
271
272impl RuntimeAnswerCancellation {
273    /// Construct from a shared wakeable cancellation state.
274    pub fn from_shared(cancelled: watch::Sender<bool>) -> Self {
275        Self { cancelled }
276    }
277
278    /// Request cancellation.
279    pub fn cancel(&self) {
280        self.cancelled.send_replace(true);
281    }
282
283    /// Return whether cancellation was requested.
284    pub fn is_cancelled(&self) -> bool {
285        *self.cancelled.borrow()
286    }
287
288    async fn cancelled(&self) {
289        let mut receiver = self.cancelled.subscribe();
290        if *receiver.borrow_and_update() {
291            return;
292        }
293        while receiver.changed().await.is_ok() {
294            if *receiver.borrow_and_update() {
295                return;
296            }
297        }
298    }
299}
300
301/// Dependency-neutral limits and interruption state for one secure connection.
302///
303/// This type is doc-hidden because binding-neutral policy construction belongs
304/// to the ORM. The runtime receives only the applicable connection dimensions:
305/// admitted items, retained nonsecret version bytes, provider/probe dispatch
306/// statements, one absolute deadline, and one wakeable cancellation owner.
307#[doc(hidden)]
308#[derive(Debug, Clone)]
309pub struct RuntimeConnectionControl {
310    max_items: u64,
311    max_bytes: u64,
312    max_statements: u32,
313    deadline: Instant,
314    cancellation: RuntimeAnswerCancellation,
315}
316
317impl RuntimeConnectionControl {
318    /// Construct one connection control from already-tightened common limits.
319    #[doc(hidden)]
320    #[must_use]
321    pub const fn new(
322        max_items: u64,
323        max_bytes: u64,
324        max_statements: u32,
325        deadline: Instant,
326        cancellation: RuntimeAnswerCancellation,
327    ) -> Self {
328        Self {
329            max_items,
330            max_bytes,
331            max_statements,
332            deadline,
333            cancellation,
334        }
335    }
336}
337
338/// Answers the server may stream ahead of a bounded consumer per batch.
339///
340/// Keep this at two or more. When a transaction closes while a query stream
341/// is paused, TypeDB 3.11/3.12 interrupts the query worker, re-arms one batch
342/// of `prefetch_size` messages, then waits for the worker to exit. The
343/// worker's queue to that batch holds `prefetch_size` messages, and after
344/// the interrupt it may still owe one in-flight answer plus a final status
345/// message. With a prefetch of one, the second of those blocks forever
346/// whenever the client's latency estimate rounds to 0 ms (the batch then
347/// ends without a latency window): the close never completes, the read
348/// transaction never times out, and the database stays "in use" until the
349/// server restarts. Two leaves room for both trailing messages.
350const BOUNDED_QUERY_PREFETCH_SIZE: u64 = 2;
351const _: () = assert!(BOUNDED_QUERY_PREFETCH_SIZE >= 2);
352
353/// Hard limits checked while polling a real driver answer stream.
354#[derive(Debug, Clone)]
355pub struct RuntimeAnswerLimits {
356    /// Maximum processed rows/documents.
357    pub max_items: u64,
358    /// Maximum encoded JSON response bytes.
359    pub max_bytes: u64,
360    /// Optional transaction deadline.
361    pub deadline: Option<Instant>,
362    /// Cooperative cancellation signal.
363    pub cancellation: RuntimeAnswerCancellation,
364}
365
366impl RuntimeAnswerLimits {
367    fn unbounded() -> Self {
368        Self {
369            max_items: u64::MAX,
370            max_bytes: u64::MAX,
371            deadline: None,
372            cancellation: RuntimeAnswerCancellation::default(),
373        }
374    }
375
376    fn is_unbounded(&self) -> bool {
377        self.max_items == u64::MAX && self.max_bytes == u64::MAX && self.deadline.is_none()
378    }
379}
380
381/// V2-only runtime limits layered over the released answer-limit shape.
382///
383/// The wrapper keeps [`RuntimeAnswerLimits`] exhaustively constructible by
384/// released 1.5.x callers while carrying the additive document-list budget.
385#[derive(Debug, Clone)]
386pub struct QueryV2RuntimeAnswerLimits {
387    /// Released item, byte, deadline, and cancellation limits.
388    pub answer: RuntimeAnswerLimits,
389    /// Maximum aggregate list members converted across fetched documents.
390    pub max_collection_members: u64,
391}
392
393impl Default for QueryV2RuntimeAnswerLimits {
394    fn default() -> Self {
395        Self {
396            answer: RuntimeAnswerLimits {
397                max_items: 100_000,
398                max_bytes: 64 * 1024 * 1024,
399                deadline: None,
400                cancellation: RuntimeAnswerCancellation::default(),
401            },
402            max_collection_members: 65_536,
403        }
404    }
405}
406
407/// Bounded stream counters returned without materializing all items.
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub struct RuntimeAnswerStats {
410    /// Answer kind, including empty streams.
411    pub kind: RuntimeAnswerKind,
412    /// Items processed.
413    pub processed_items: u64,
414    /// Encoded response bytes processed.
415    pub response_bytes: u64,
416    /// Whether the consumer stopped early.
417    pub stopped_early: bool,
418}
419
420impl RuntimeAnswerStats {
421    fn new(kind: RuntimeAnswerKind) -> Self {
422        Self {
423            kind,
424            processed_items: 0,
425            response_bytes: 0,
426            stopped_early: false,
427        }
428    }
429}
430
431/// Transaction type for TypeDB operations.
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
433pub enum TxType {
434    /// Read-only transaction.
435    Read,
436    /// Read-write transaction.
437    Write,
438    /// Schema transaction.
439    Schema,
440}
441
442/// A single value bound to a `given` variable, band-agnostic and serializable.
443///
444/// Released temporal variants retain their ISO-8601 text representation.
445/// Additive V2 variants carry exact components when text alone would erase
446/// provider semantics.
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448pub enum GivenValue {
449    /// Explicit absence for an optional `given` cell.
450    Empty,
451    /// TypeQL `boolean`.
452    Boolean(bool),
453    /// TypeQL `integer`.
454    Integer(i64),
455    /// TypeQL `double`.
456    Double(f64),
457    /// TypeQL `string`.
458    String(String),
459    /// TypeQL `date`, ISO-8601 (`YYYY-MM-DD`).
460    Date(String),
461    /// TypeQL `datetime`, ISO-8601 without offset.
462    Datetime(String),
463    /// TypeQL `datetime-tz`, with a fixed offset.
464    DatetimeTz(String),
465    /// Exact V2 TypeQL `datetime-tz`, retaining an optional authored IANA zone
466    /// and its already-validated effective offset.
467    DatetimeTzExact {
468        /// Canonical local datetime.
469        local: String,
470        /// Authored IANA name, or `None` for UTC/fixed-offset values.
471        named_zone: Option<String>,
472        /// Effective offset selected for this local value.
473        effective_offset_seconds: i32,
474    },
475    /// TypeQL fixed-point `decimal`.
476    Decimal(String),
477    /// Exact non-negative TypeDB duration components.
478    Duration {
479        /// Calendar months.
480        months: u32,
481        /// Calendar days.
482        days: u32,
483        /// Absolute nanoseconds.
484        nanos: u64,
485    },
486}
487
488/// Input rows for a `given`-stage query: a variable header plus value rows.
489///
490/// Every row must have exactly one entry per header variable, in header
491/// order. Rows travel through the driver API, never the query string.
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493pub struct GivenRowsSpec {
494    /// Variable names, without the `$` sigil, in column order.
495    pub variables: Vec<String>,
496    /// Value rows; each inner vec is one input row in header order.
497    pub rows: Vec<Vec<GivenValue>>,
498}
499
500/// The compile-pinned band-8 driver fork version resolved in `Cargo.lock`.
501///
502/// This constant records the exact driver version this runtime crate was built
503/// against.  It is the factual counterpart to the policy window declared in
504/// `type_bridge_core_lib::version`: the window says which versions are allowed;
505/// this constant says which version is currently in use.
506///
507/// When `type-bridge-typedb-driver-b8` is refreshed, update this constant to
508/// match the new `Cargo.lock` entry — the `tests::cargo_lock_pin`
509/// test will catch any divergence.
510pub const PINNED_DRIVER_VERSION: &str = "3.11.5";
511
512/// The compile-pinned version of the upstream band-9 `typedb-driver` crate
513/// this runtime crate was built against.
514///
515/// Mirrors [`PINNED_DRIVER_VERSION`] for the band-8 driver fork.  When
516/// the band-9 dependency is refreshed, update this constant to match the new
517/// `Cargo.lock` entry — the `tests::cargo_lock_pin_b9` test will
518/// catch any divergence.
519pub const PINNED_DRIVER_VERSION_B9: &str = "3.12.3";
520
521/// Protocol bands this build embeds a driver for, derived from the compiled-in
522/// band features.  This is the `embedded_bands` argument to
523/// [`core_version::check_server_supported`] — the embedded-runtime gate accepts
524/// any in-window server whose band is in this set.
525///
526/// Each element is individually cfg-gated so the set reflects exactly the bands
527/// the build can construct; there is no hardcoded band-set literal (master-plan
528/// I6).  The default build compiles both retained bands, so the set is `{8, 9}`.
529const EMBEDDED_BANDS: &[u8] = &[
530    #[cfg(feature = "band8")]
531    8,
532    #[cfg(feature = "band9")]
533    9,
534];
535
536/// Return the driver versions compiled into this build, keyed by protocol band.
537///
538/// Each entry is `(band, version_string)` and is individually cfg-gated so only
539/// compiled-in bands appear in the slice (master-plan I6 — no hardcoded
540/// band-set literal).  The default build embeds both retained bands and returns
541/// `[(8, "3.11.5"), (9, "3.12.3")]`.
542///
543/// This is the Rust-side source of truth for the Python `embedded_driver_versions()`
544/// binding — `crates/python/src/version.rs` wraps this function and exposes it
545/// as a Python dict `{int: str}`.
546pub fn embedded_driver_versions() -> &'static [(u8, &'static str)] {
547    &[
548        #[cfg(feature = "band8")]
549        (8, PINNED_DRIVER_VERSION),
550        #[cfg(feature = "band9")]
551        (9, PINNED_DRIVER_VERSION_B9),
552    ]
553}
554
555/// Connection-time options for the version-gated TypeDB runtime.
556///
557/// Callers that accept the defaults can pass `ConnectOptions::default()`.
558#[derive(Debug, Clone, Copy)]
559pub struct ConnectOptions {
560    /// Port of the TypeDB HTTP API used for the connect-time version probe.
561    pub http_port: u16,
562    /// Whether to use TLS for the HTTP version probe.
563    pub tls: bool,
564    /// Exact server version supplied by the caller.
565    ///
566    /// When set, the connect gate validates this version and skips the HTTP
567    /// `/v1/version` probe. This is the supported path for gRPC-only TypeDB
568    /// deployments where the HTTP API is disabled or unreachable.
569    pub server_version: Option<core_version::Version>,
570}
571
572impl Default for ConnectOptions {
573    fn default() -> Self {
574        Self {
575            http_port: DEFAULT_HTTP_PORT,
576            tls: false,
577            server_version: None,
578        }
579    }
580}
581
582/// Canonical TLS policy for secure TypeDB connections.
583///
584/// The released [`ConnectOptions::tls`] Boolean is retained as a compatibility
585/// adapter.  New callers use this type so a custom root can never be confused
586/// with either plaintext or native-root TLS.
587#[derive(Debug, Clone, PartialEq, Eq, Default)]
588pub enum TlsMode {
589    /// Connect over plaintext HTTP and gRPC.
590    #[default]
591    Disabled,
592    /// Connect over TLS using the operating system's native trust roots.
593    NativeRoots,
594    /// Connect over TLS using only the PEM certificate bundle at this path.
595    CustomRootCa(PathBuf),
596}
597
598impl TlsMode {
599    /// Return whether this mode requires TLS on every transport.
600    #[must_use]
601    pub const fn is_enabled(&self) -> bool {
602        !matches!(self, Self::Disabled)
603    }
604}
605
606/// Connection options for the typed TLS transport surface.
607#[derive(Debug, Clone, PartialEq, Eq)]
608pub struct SecureConnectOptions {
609    /// Port of the TypeDB HTTP API used for the connect-time version probe.
610    pub http_port: u16,
611    /// TLS policy shared by the HTTP probe and every gRPC driver attempt.
612    pub tls_mode: TlsMode,
613    /// Exact server version supplied by the caller.
614    ///
615    /// When set, the HTTP probe is skipped, but `tls_mode` still applies to
616    /// the selected gRPC driver.
617    pub server_version: Option<core_version::Version>,
618}
619
620impl Default for SecureConnectOptions {
621    fn default() -> Self {
622        Self {
623            http_port: DEFAULT_HTTP_PORT,
624            tls_mode: TlsMode::Disabled,
625            server_version: None,
626        }
627    }
628}
629
630impl SecureConnectOptions {
631    /// Validate and lower this transport policy without constructing a host.
632    ///
633    /// This runs the exact shared custom-root and compiled-driver-band
634    /// validation used by every secure connect/lifecycle entry point, then
635    /// discards the provider options. It performs no HTTP request, gRPC host
636    /// construction, credential use, or database operation, so orchestration
637    /// layers can fail malformed trust material before resolving secrets.
638    pub fn validate_transport(&self) -> SecureResult<()> {
639        self.prepare_transport().map(|_| ())
640    }
641
642    /// Resolve and retain this transport policy for several lifecycle calls.
643    ///
644    /// This hidden additive seam exists for orchestrators that must finish all
645    /// trust-material I/O before resolving credentials. Clones share the same
646    /// captured custom-root snapshot and already-lowered band configurations;
647    /// they never reopen [`TlsMode::CustomRootCa`]'s configured path.
648    #[doc(hidden)]
649    pub fn prepare_transport(&self) -> SecureResult<PreparedSecureConnectOptions> {
650        Ok(PreparedSecureConnectOptions {
651            http_port: self.http_port,
652            server_version: self.server_version,
653            required_server_version: None,
654            resolved_tls: ResolvedTlsMode::from_configured_path(self.tls_mode.clone())?,
655            connection_control: None,
656        })
657    }
658
659    /// Prepare a path that already carries workspace/server confinement
660    /// provenance without following any component alias.
661    ///
662    /// Only confinement authorities may use this hidden entry point. Ordinary
663    /// callers must use [`Self::prepare_transport`], which accepts normal OS
664    /// path aliases before capturing the physical file.
665    #[doc(hidden)]
666    pub fn prepare_transport_from_validated_physical_path(
667        &self,
668    ) -> SecureResult<PreparedSecureConnectOptions> {
669        Ok(PreparedSecureConnectOptions {
670            http_port: self.http_port,
671            server_version: self.server_version,
672            required_server_version: None,
673            resolved_tls: ResolvedTlsMode::from_validated_physical_path(self.tls_mode.clone())?,
674            connection_control: None,
675        })
676    }
677
678    /// Prepare custom-root bytes already captured through a configuration
679    /// authority without reopening the diagnostic [`TlsMode`] path.
680    ///
681    /// Only confinement authorities may use this hidden entry point. The
682    /// captured bytes are still parsed and lowered by the shared TLS engine,
683    /// and every driver band receives the resulting private snapshot.
684    #[doc(hidden)]
685    pub fn prepare_transport_from_captured_custom_root(
686        &self,
687        bytes: Arc<[u8]>,
688    ) -> SecureResult<PreparedSecureConnectOptions> {
689        Ok(PreparedSecureConnectOptions {
690            http_port: self.http_port,
691            server_version: self.server_version,
692            required_server_version: None,
693            resolved_tls: ResolvedTlsMode::from_captured_custom_root(self.tls_mode.clone(), bytes)?,
694            connection_control: None,
695        })
696    }
697}
698
699impl From<ConnectOptions> for SecureConnectOptions {
700    fn from(value: ConnectOptions) -> Self {
701        Self {
702            http_port: value.http_port,
703            tls_mode: if value.tls {
704                TlsMode::NativeRoots
705            } else {
706                TlsMode::Disabled
707            },
708            server_version: value.server_version,
709        }
710    }
711}
712
713/// Error returned by typed secure connection and lifecycle entry points.
714///
715/// This additive type keeps TLS configuration failures matchable without
716/// adding a variant to the released, exhaustively matchable [`RuntimeError`].
717#[derive(Debug, thiserror::Error)]
718#[non_exhaustive]
719pub enum SecureConnectError {
720    /// Custom or native trust material failed validation before network I/O.
721    #[error(transparent)]
722    TlsConfiguration(#[from] core_version::TlsConfigurationError),
723    /// A driver band could not lower the already validated TLS policy.
724    #[error(
725        "TLS configuration error [tls_driver_lowering_failed]: TypeDB driver band {band} rejected the TLS policy"
726    )]
727    DriverTlsConfiguration {
728        /// Driver protocol band that rejected the configuration.
729        band: u8,
730    },
731    /// The connection reached the ordinary version gate or driver runtime.
732    #[error(transparent)]
733    Runtime(#[from] RuntimeError),
734}
735
736impl SecureConnectError {
737    /// Return the stable TLS configuration code, or `None` for an ordinary
738    /// version/connection/runtime failure.
739    #[must_use]
740    pub fn configuration_code(&self) -> Option<&'static str> {
741        match self {
742            Self::TlsConfiguration(error) => Some(error.code()),
743            Self::DriverTlsConfiguration { .. } => Some("tls_driver_lowering_failed"),
744            Self::Runtime(_) => None,
745        }
746    }
747
748    /// Return a diagnostic that is safe to expose after credentials have
749    /// been resolved.
750    ///
751    /// TLS diagnostics are reconstructed from typed codes without retaining
752    /// configured paths. Version diagnostics are retained only for variants
753    /// whose fields are closed version/band data. Probe, parse, connection,
754    /// query, and transaction failures may contain provider-controlled text
755    /// and therefore return `None`.
756    #[must_use]
757    pub fn credential_safe_diagnostic(&self) -> Option<String> {
758        match self {
759            Self::TlsConfiguration(error) => Some(format!(
760                "TLS policy preparation failed [{}]; inspect the configured trust material",
761                error.code()
762            )),
763            Self::DriverTlsConfiguration { band } => Some(format!(
764                "TLS policy lowering failed [tls_driver_lowering_failed] for TypeDB driver band {band}"
765            )),
766            Self::Runtime(RuntimeError::UnsupportedVersion(
767                error @ (core_version::VersionError::Unsupported { .. }
768                | core_version::VersionError::BandMismatch { .. }
769                | core_version::VersionError::EmbeddedUnavailable { .. }
770                | core_version::VersionError::FeatureUnsupported { .. }),
771            )) => Some(format!("Unsupported version: {error}")),
772            Self::Runtime(
773                RuntimeError::UnsupportedVersion(
774                    core_version::VersionError::Probe(_) | core_version::VersionError::Parse(_),
775                )
776                | RuntimeError::Connection(_)
777                | RuntimeError::QueryExecution(_)
778                | RuntimeError::Transaction(_)
779                | RuntimeError::ResourceLimit { .. }
780                | RuntimeError::AnswerConsumer,
781            ) => None,
782        }
783    }
784
785    /// Collapse an additive secure error onto the released runtime surface.
786    #[must_use]
787    pub fn into_runtime_error(self) -> RuntimeError {
788        match self {
789            Self::Runtime(error) => error,
790            other => RuntimeError::Connection(other.to_string()),
791        }
792    }
793}
794
795const TRACE_CODE_DRIVER_DROP_CLOSE_FAILED: &str = "typedb_runtime_driver_drop_close_failed";
796const TRACE_CODE_VERSION_GATE_PASSED: &str = "typedb_runtime_version_gate_passed";
797#[cfg(all(feature = "band8", feature = "band9"))]
798const TRACE_CODE_BAND9_UPGRADE_SUCCEEDED: &str = "typedb_runtime_band9_upgrade_succeeded";
799#[cfg(all(feature = "band8", feature = "band9"))]
800const TRACE_CODE_BAND9_UPGRADE_FAILED: &str = "typedb_runtime_band9_upgrade_failed";
801#[cfg(feature = "band8")]
802const TRACE_CODE_BAND8_FALLBACK_CONNECTED: &str = "typedb_runtime_band8_fallback_connected";
803const TRACE_CODE_CONNECTED: &str = "typedb_runtime_connected";
804
805fn runtime_trace_failure_code(error: &RuntimeError) -> &'static str {
806    match error {
807        RuntimeError::UnsupportedVersion(_) => "typedb_runtime_unsupported_version",
808        RuntimeError::Connection(_) => "typedb_runtime_connection_failed",
809        RuntimeError::QueryExecution(_) => "typedb_runtime_query_failed",
810        RuntimeError::Transaction(_) => "typedb_runtime_transaction_failed",
811        RuntimeError::ResourceLimit { .. } => "typedb_runtime_resource_limit",
812        RuntimeError::AnswerConsumer => "typedb_runtime_answer_consumer_failed",
813    }
814}
815
816#[cfg(all(feature = "band8", feature = "band9"))]
817fn secure_trace_failure_code(error: &SecureConnectError) -> &'static str {
818    match error {
819        SecureConnectError::TlsConfiguration(error) => error.code(),
820        SecureConnectError::DriverTlsConfiguration { .. } => "tls_driver_lowering_failed",
821        SecureConnectError::Runtime(error) => runtime_trace_failure_code(error),
822    }
823}
824
825fn trace_driver_drop_close_failure(error: &RuntimeError) {
826    tracing::warn!(
827        code = TRACE_CODE_DRIVER_DROP_CLOSE_FAILED,
828        failure_code = runtime_trace_failure_code(error),
829        "TypeDB driver cleanup failed during final runtime drop"
830    );
831}
832
833fn trace_version_gate_passed(_address: &str, band: u8, server_version: core_version::Version) {
834    tracing::debug!(
835        code = TRACE_CODE_VERSION_GATE_PASSED,
836        driver_band = band,
837        %server_version,
838        "TypeDB runtime version gate passed"
839    );
840}
841
842#[cfg(all(feature = "band8", feature = "band9"))]
843fn trace_band9_upgrade_succeeded(_address: &str, server_version: core_version::Version) {
844    tracing::debug!(
845        code = TRACE_CODE_BAND9_UPGRADE_SUCCEEDED,
846        driver_band = 9_u8,
847        %server_version,
848        "TypeDB runtime upgraded its validated fallback connection"
849    );
850}
851
852#[cfg(all(feature = "band8", feature = "band9"))]
853fn trace_band9_upgrade_failed(
854    _address: &str,
855    server_version: core_version::Version,
856    error: &SecureConnectError,
857) {
858    tracing::warn!(
859        code = TRACE_CODE_BAND9_UPGRADE_FAILED,
860        failure_code = secure_trace_failure_code(error),
861        driver_band = 9_u8,
862        %server_version,
863        "TypeDB runtime could not upgrade its validated fallback connection"
864    );
865}
866
867#[cfg(feature = "band8")]
868fn trace_band8_fallback_connected(_address: &str, server_version: core_version::Version) {
869    tracing::debug!(
870        code = TRACE_CODE_BAND8_FALLBACK_CONNECTED,
871        driver_band = 8_u8,
872        %server_version,
873        "TypeDB runtime retained its validated fallback connection"
874    );
875}
876
877fn trace_runtime_connected(
878    _address: &str,
879    driver_band: u8,
880    server_version: Option<core_version::Version>,
881) {
882    match server_version {
883        Some(server_version) => tracing::info!(
884            code = TRACE_CODE_CONNECTED,
885            driver_band,
886            server_version_known = true,
887            %server_version,
888            "TypeDB runtime connected"
889        ),
890        None => tracing::info!(
891            code = TRACE_CODE_CONNECTED,
892            driver_band,
893            server_version_known = false,
894            "TypeDB runtime connected"
895        ),
896    }
897}
898
899/// Result alias for typed secure connection and lifecycle operations.
900pub type SecureResult<T> = std::result::Result<T, SecureConnectError>;
901
902/// TLS policy lowered eagerly for every compiled TypeDB driver band.
903///
904/// Constructing this value reads and validates a custom root exactly once
905/// before the HTTP probe or any gRPC host is created. Every band is then
906/// lowered from the retained file material, and HTTP receives the captured
907/// bytes rather than the caller-controlled path. Every fallback receives this
908/// same value, so an enabled request has no plaintext reconstruction path.
909#[derive(Clone, Debug)]
910struct ResolvedTlsMode {
911    probe_mode: ResolvedTlsProbeMode,
912    #[cfg(feature = "band8")]
913    band8: DriverTlsConfig,
914    #[cfg(feature = "band9")]
915    band9: B9DriverTlsConfig,
916}
917
918#[derive(Clone, Debug)]
919enum ResolvedTlsProbeMode {
920    Disabled,
921    NativeRoots,
922    CustomRootCa(core_version::RetainedCustomRootCa),
923}
924
925/// A reusable, fully resolved secure transport policy.
926///
927/// This type is doc-hidden because ordinary callers should keep using
928/// [`SecureConnectOptions`]. It is public only so orchestration crates can
929/// prepare transport material once before credential resolution and reuse the
930/// same immutable snapshot across database lifecycle and connection calls.
931#[doc(hidden)]
932#[derive(Clone, Debug)]
933pub struct PreparedSecureConnectOptions {
934    http_port: u16,
935    server_version: Option<core_version::Version>,
936    required_server_version: Option<core_version::Version>,
937    resolved_tls: ResolvedTlsMode,
938    connection_control: Option<RuntimeConnectionControl>,
939}
940
941impl PreparedSecureConnectOptions {
942    /// Attach one invocation's common control after trust preparation.
943    ///
944    /// Cloning the returned value shares cancellation and the exact absolute
945    /// deadline, but each connection invocation owns independent counters.
946    #[doc(hidden)]
947    #[must_use]
948    pub fn with_connection_control(mut self, control: RuntimeConnectionControl) -> Self {
949        self.connection_control = Some(control);
950        self
951    }
952
953    /// Require the exact provider version owned by the current generated
954    /// package policy.
955    ///
956    /// This is deliberately not an arbitrary version-taking lane. It clears
957    /// the released caller-version shortcut so the authoritative HTTP probe
958    /// always runs before any driver or credential construction.
959    #[doc(hidden)]
960    #[must_use]
961    pub fn with_generated_3_12_3_requirement(mut self) -> Self {
962        self.server_version = None;
963        self.required_server_version = Some(core_version::Version::new(3, 12, 3));
964        self
965    }
966}
967
968#[derive(Debug)]
969struct ConnectionMeter {
970    control: Option<RuntimeConnectionControl>,
971    statements: u32,
972    response_bytes: u64,
973    admitted_items: u64,
974}
975
976impl ConnectionMeter {
977    fn new(control: RuntimeConnectionControl) -> Self {
978        Self {
979            control: Some(control),
980            statements: 0,
981            response_bytes: 0,
982            admitted_items: 0,
983        }
984    }
985
986    fn legacy() -> Self {
987        Self {
988            control: None,
989            statements: 0,
990            response_bytes: 0,
991            admitted_items: 0,
992        }
993    }
994
995    fn control(&self) -> Option<&RuntimeConnectionControl> {
996        self.control.as_ref()
997    }
998
999    fn check_interruption(&self) -> Result<()> {
1000        let Some(control) = &self.control else {
1001            return Ok(());
1002        };
1003        if control.cancellation.is_cancelled() {
1004            return Err(RuntimeError::ResourceLimit {
1005                code: "provider_cancelled",
1006                message: "provider connection was cancelled",
1007            });
1008        }
1009        if tokio::time::Instant::now() >= tokio::time::Instant::from_std(control.deadline) {
1010            return Err(RuntimeError::ResourceLimit {
1011                code: "transaction_deadline_exceeded",
1012                message: "provider connection deadline expired",
1013            });
1014        }
1015        Ok(())
1016    }
1017
1018    fn require_possible_success(&self) -> Result<()> {
1019        self.check_interruption()?;
1020        if self
1021            .control
1022            .as_ref()
1023            .is_some_and(|control| control.max_items == 0)
1024        {
1025            return Err(RuntimeError::ResourceLimit {
1026                code: "processed_item_limit",
1027                message: "provider connection item limit exceeded",
1028            });
1029        }
1030        Ok(())
1031    }
1032
1033    fn charge_statement(&mut self) -> Result<()> {
1034        self.check_interruption()?;
1035        let Some(control) = &self.control else {
1036            return Ok(());
1037        };
1038        let next = self
1039            .statements
1040            .checked_add(1)
1041            .ok_or(RuntimeError::ResourceLimit {
1042                code: "provider_statement_counter_overflow",
1043                message: "provider connection statement counter overflowed",
1044            })?;
1045        if next > control.max_statements {
1046            return Err(RuntimeError::ResourceLimit {
1047                code: "provider_statement_limit",
1048                message: "provider connection statement limit exceeded",
1049            });
1050        }
1051        self.statements = next;
1052        Ok(())
1053    }
1054
1055    fn charge_version_evidence(&mut self, version: core_version::Version) -> Result<()> {
1056        let Some(control) = &self.control else {
1057            return Ok(());
1058        };
1059        // Only the canonical version retained by TypeDBRuntime is public
1060        // connection evidence. Provider errors, raw response framing, hosts,
1061        // credentials, and trust material are deliberately never measured or
1062        // reflected into a diagnostic.
1063        let bytes =
1064            u64::try_from(version.to_string().len()).map_err(|_| RuntimeError::ResourceLimit {
1065                code: "response_byte_counter_overflow",
1066                message: "provider connection response byte counter overflowed",
1067            })?;
1068        let next = self
1069            .response_bytes
1070            .checked_add(bytes)
1071            .ok_or(RuntimeError::ResourceLimit {
1072                code: "response_byte_counter_overflow",
1073                message: "provider connection response byte counter overflowed",
1074            })?;
1075        if next > control.max_bytes {
1076            return Err(RuntimeError::ResourceLimit {
1077                code: "response_byte_limit",
1078                message: "provider connection response byte limit exceeded",
1079            });
1080        }
1081        self.response_bytes = next;
1082        Ok(())
1083    }
1084
1085    fn admit_connection(&mut self) -> Result<()> {
1086        let Some(control) = &self.control else {
1087            return Ok(());
1088        };
1089        let next = self
1090            .admitted_items
1091            .checked_add(1)
1092            .ok_or(RuntimeError::ResourceLimit {
1093                code: "processed_item_counter_overflow",
1094                message: "provider connection item counter overflowed",
1095            })?;
1096        if next > control.max_items {
1097            return Err(RuntimeError::ResourceLimit {
1098                code: "processed_item_limit",
1099                message: "provider connection item limit exceeded",
1100            });
1101        }
1102        self.admitted_items = next;
1103        Ok(())
1104    }
1105}
1106
1107async fn await_connection_work<T>(
1108    future: impl Future<Output = T>,
1109    control: Option<&RuntimeConnectionControl>,
1110) -> Result<T> {
1111    let Some(control) = control else {
1112        return Ok(future.await);
1113    };
1114    tokio::pin!(future);
1115    let cancellation = control.cancellation.cancelled();
1116    tokio::pin!(cancellation);
1117    let deadline = tokio::time::sleep_until(tokio::time::Instant::from_std(control.deadline));
1118    tokio::pin!(deadline);
1119
1120    tokio::select! {
1121        biased;
1122        output = &mut future => Ok(output),
1123        () = &mut cancellation => Err(RuntimeError::ResourceLimit {
1124            code: "provider_cancelled",
1125            message: "provider connection was cancelled",
1126        }),
1127        () = &mut deadline => Err(RuntimeError::ResourceLimit {
1128            code: "transaction_deadline_exceeded",
1129            message: "provider connection deadline expired",
1130        }),
1131    }
1132}
1133
1134fn controlled_connection_error(error: SecureConnectError) -> SecureConnectError {
1135    match error {
1136        SecureConnectError::TlsConfiguration(
1137            core_version::TlsConfigurationError::CustomRootCaNotFile { .. }
1138            | core_version::TlsConfigurationError::CustomRootCaUnreadable { .. }
1139            | core_version::TlsConfigurationError::CustomRootCaTooLarge { .. }
1140            | core_version::TlsConfigurationError::CustomRootCaInvalidPem { .. },
1141        ) => SecureConnectError::TlsConfiguration(
1142            core_version::TlsConfigurationError::ClientConfiguration,
1143        ),
1144        SecureConnectError::Runtime(RuntimeError::UnsupportedVersion(
1145            core_version::VersionError::Probe(_) | core_version::VersionError::Parse(_),
1146        )) => SecureConnectError::Runtime(RuntimeError::Connection(
1147            "TypeDB version discovery failed".to_owned(),
1148        )),
1149        SecureConnectError::Runtime(RuntimeError::Connection(_)) => SecureConnectError::Runtime(
1150            RuntimeError::Connection("TypeDB connection failed".to_owned()),
1151        ),
1152        SecureConnectError::Runtime(RuntimeError::QueryExecution(_)) => {
1153            SecureConnectError::Runtime(RuntimeError::QueryExecution(
1154                "TypeDB connection query failed".to_owned(),
1155            ))
1156        }
1157        SecureConnectError::Runtime(RuntimeError::Transaction(_)) => SecureConnectError::Runtime(
1158            RuntimeError::Transaction("TypeDB connection transaction failed".to_owned()),
1159        ),
1160        other => other,
1161    }
1162}
1163
1164#[cfg(feature = "band8")]
1165fn is_connection_control_error(error: &SecureConnectError) -> bool {
1166    matches!(
1167        error,
1168        SecureConnectError::Runtime(RuntimeError::ResourceLimit { .. })
1169    )
1170}
1171
1172fn close_driver_preserving(driver: &DriverHandle, error: SecureConnectError) -> SecureConnectError {
1173    if let Err(close_error) = driver.force_close() {
1174        trace_driver_drop_close_failure(&close_error);
1175    }
1176    error
1177}
1178
1179impl ResolvedTlsProbeMode {
1180    #[cfg(test)]
1181    const fn is_enabled(&self) -> bool {
1182        !matches!(self, Self::Disabled)
1183    }
1184}
1185
1186impl ResolvedTlsMode {
1187    fn from_captured_custom_root(mode: TlsMode, bytes: Arc<[u8]>) -> SecureResult<Self> {
1188        let material = match &mode {
1189            TlsMode::CustomRootCa(path) => {
1190                core_version::RetainedCustomRootCa::load_captured_bytes(path, bytes)?
1191            }
1192            TlsMode::Disabled | TlsMode::NativeRoots => {
1193                return Err(core_version::TlsConfigurationError::ClientConfiguration.into());
1194            }
1195        };
1196        Self::lower(mode, ResolvedTlsProbeMode::CustomRootCa(material))
1197    }
1198
1199    fn from_validated_physical_path(mode: TlsMode) -> SecureResult<Self> {
1200        let probe_mode = match &mode {
1201            TlsMode::Disabled => ResolvedTlsProbeMode::Disabled,
1202            TlsMode::NativeRoots => ResolvedTlsProbeMode::NativeRoots,
1203            TlsMode::CustomRootCa(path) => {
1204                ResolvedTlsProbeMode::CustomRootCa(core_version::RetainedCustomRootCa::load(path)?)
1205            }
1206        };
1207        Self::lower(mode, probe_mode)
1208    }
1209
1210    fn from_configured_path(mode: TlsMode) -> SecureResult<Self> {
1211        let probe_mode = match &mode {
1212            TlsMode::Disabled => ResolvedTlsProbeMode::Disabled,
1213            TlsMode::NativeRoots => ResolvedTlsProbeMode::NativeRoots,
1214            TlsMode::CustomRootCa(path) => ResolvedTlsProbeMode::CustomRootCa(
1215                core_version::RetainedCustomRootCa::load_configured_alias(path)?,
1216            ),
1217        };
1218        Self::lower(mode, probe_mode)
1219    }
1220
1221    fn lower(mode: TlsMode, probe_mode: ResolvedTlsProbeMode) -> SecureResult<Self> {
1222        let custom_root = match &probe_mode {
1223            ResolvedTlsProbeMode::CustomRootCa(material) => Some(material),
1224            ResolvedTlsProbeMode::Disabled | ResolvedTlsProbeMode::NativeRoots => None,
1225        };
1226
1227        if matches!(mode, TlsMode::CustomRootCa(_)) != custom_root.is_some() {
1228            return Err(core_version::TlsConfigurationError::ClientConfiguration.into());
1229        }
1230
1231        #[cfg(feature = "band8")]
1232        let band8 = match &mode {
1233            TlsMode::Disabled => DriverTlsConfig::disabled(),
1234            TlsMode::NativeRoots => DriverTlsConfig::enabled_with_native_root_ca(),
1235            TlsMode::CustomRootCa(_) => custom_root
1236                .ok_or(core_version::TlsConfigurationError::ClientConfiguration)?
1237                .with_driver_root_path(DriverTlsConfig::enabled_with_root_ca)?
1238                .map_err(|_| SecureConnectError::DriverTlsConfiguration { band: 8 })?,
1239        };
1240
1241        #[cfg(feature = "band9")]
1242        let band9 = match &mode {
1243            TlsMode::Disabled => B9DriverTlsConfig::disabled(),
1244            TlsMode::NativeRoots => B9DriverTlsConfig::enabled_with_native_root_ca(),
1245            TlsMode::CustomRootCa(_) => custom_root
1246                .ok_or(core_version::TlsConfigurationError::ClientConfiguration)?
1247                .with_driver_root_path(B9DriverTlsConfig::enabled_with_root_ca)?
1248                .map_err(|_| SecureConnectError::DriverTlsConfiguration { band: 9 })?,
1249        };
1250
1251        Ok(Self {
1252            probe_mode,
1253            #[cfg(feature = "band8")]
1254            band8,
1255            #[cfg(feature = "band9")]
1256            band9,
1257        })
1258    }
1259}
1260
1261/// Band-tagged driver state. Crate-private; no driver type escapes the crate.
1262enum DriverHandleInner {
1263    #[cfg(feature = "band8")]
1264    B8(B8Driver),
1265    #[cfg(feature = "band9")]
1266    B9(B9Driver),
1267}
1268
1269fn contain_driver_shutdown(shutdown: impl FnOnce() -> Result<()>) -> Result<()> {
1270    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(shutdown)) {
1271        Ok(result) => result,
1272        Err(_) => Err(RuntimeError::Connection(
1273            "Driver close failed: upstream driver panicked during shutdown".to_owned(),
1274        )),
1275    }
1276}
1277
1278#[cfg(test)]
1279fn run_driver_shutdown(
1280    state: &AtomicU8,
1281    close_lock: &Mutex<()>,
1282    shutdown: impl FnOnce() -> Result<()>,
1283) -> Result<()> {
1284    // Serialize explicit binding close, cleanup retries, and final-lease drop.
1285    // Recovering a poisoned guard is preferable to leaking the driver's
1286    // background runtime if an unrelated close caller previously panicked.
1287    let _close_guard = match close_lock.lock() {
1288        Ok(close_guard) => close_guard,
1289        Err(poisoned) => poisoned.into_inner(),
1290    };
1291
1292    run_driver_shutdown_locked(state, shutdown)
1293}
1294
1295fn run_driver_shutdown_locked(
1296    state: &AtomicU8,
1297    shutdown: impl FnOnce() -> Result<()>,
1298) -> Result<()> {
1299    match state.load(AtomicOrdering::Acquire) {
1300        DRIVER_CLOSED => return Ok(()),
1301        DRIVER_OPEN => {
1302            // Publish the monotonic transition before invoking upstream
1303            // shutdown. New operations now fail immediately; active
1304            // operations are cancelled rather than drained, because waiting
1305            // for them can recreate the terminal-close liveness defect
1306            // tracked by #196.
1307            state.store(DRIVER_CLOSING, AtomicOrdering::Release);
1308        }
1309        DRIVER_CLOSING => {
1310            // A prior shutdown attempt failed or panicked. Keep admission
1311            // terminal, but retry cleanup: the upstream 3.12 line can return before
1312            // closing its background runtime when server cleanup fails.
1313        }
1314        _ => unreachable!("driver shutdown state is internal and validated"),
1315    }
1316
1317    let result = contain_driver_shutdown(shutdown);
1318    if result.is_ok() {
1319        state.store(DRIVER_CLOSED, AtomicOrdering::Release);
1320    }
1321    result
1322}
1323
1324fn retain_driver_transaction(
1325    state: &AtomicU8,
1326    active_transactions: &AtomicUsize,
1327    close_lock: &Mutex<()>,
1328) -> Result<()> {
1329    let _close_guard = match close_lock.lock() {
1330        Ok(close_guard) => close_guard,
1331        Err(poisoned) => poisoned.into_inner(),
1332    };
1333    if state.load(AtomicOrdering::Acquire) != DRIVER_OPEN {
1334        return Err(RuntimeError::Connection(
1335            "TypeDB driver connection is closed".to_owned(),
1336        ));
1337    }
1338    active_transactions.fetch_add(1, AtomicOrdering::AcqRel);
1339    Ok(())
1340}
1341
1342/// Shared driver lifetime retained by every open transaction.
1343///
1344/// Automatic shutdown happens only when the final runtime/transaction lease is
1345/// released. Explicit connection close rejects while a transaction is active,
1346/// preserving child usability and making parent/child close order observable.
1347pub(crate) struct DriverHandle {
1348    inner: DriverHandleInner,
1349    state: AtomicU8,
1350    active_transactions: AtomicUsize,
1351    close_lock: Mutex<()>,
1352}
1353
1354const DRIVER_OPEN: u8 = 0;
1355const DRIVER_CLOSING: u8 = 1;
1356const DRIVER_CLOSED: u8 = 2;
1357
1358impl DriverHandle {
1359    #[cfg(feature = "band8")]
1360    fn band8(driver: B8Driver) -> Self {
1361        Self {
1362            inner: DriverHandleInner::B8(driver),
1363            state: AtomicU8::new(DRIVER_OPEN),
1364            active_transactions: AtomicUsize::new(0),
1365            close_lock: Mutex::new(()),
1366        }
1367    }
1368
1369    #[cfg(feature = "band9")]
1370    fn band9(driver: B9Driver) -> Self {
1371        Self {
1372            inner: DriverHandleInner::B9(driver),
1373            state: AtomicU8::new(DRIVER_OPEN),
1374            active_transactions: AtomicUsize::new(0),
1375            close_lock: Mutex::new(()),
1376        }
1377    }
1378
1379    fn inner(&self) -> &DriverHandleInner {
1380        &self.inner
1381    }
1382
1383    fn band(&self) -> u8 {
1384        match &self.inner {
1385            #[cfg(feature = "band8")]
1386            DriverHandleInner::B8(_) => 8,
1387            #[cfg(feature = "band9")]
1388            DriverHandleInner::B9(_) => 9,
1389        }
1390    }
1391
1392    fn ensure_open(&self) -> Result<()> {
1393        if self.state.load(AtomicOrdering::Acquire) == DRIVER_OPEN {
1394            Ok(())
1395        } else {
1396            Err(RuntimeError::Connection(
1397                "TypeDB driver connection is closed".to_owned(),
1398            ))
1399        }
1400    }
1401
1402    fn shutdown_started(&self) -> bool {
1403        self.state.load(AtomicOrdering::Acquire) != DRIVER_OPEN
1404    }
1405
1406    /// Make the selected driver terminal and dispatch upstream shutdown.
1407    ///
1408    /// A failed attempt leaves admission closed and permits a later cleanup
1409    /// retry; after successful dispatch, subsequent calls are no-ops. The
1410    /// upstream callback worker is joined only when the final driver lease is
1411    /// dropped.
1412    fn force_close(&self) -> Result<()> {
1413        let _close_guard = match self.close_lock.lock() {
1414            Ok(close_guard) => close_guard,
1415            Err(poisoned) => poisoned.into_inner(),
1416        };
1417        if self.active_transactions.load(AtomicOrdering::Acquire) != 0 {
1418            return Err(RuntimeError::ResourceLimit {
1419                code: "resource_in_use",
1420                message: "connection cannot close while a transaction is active",
1421            });
1422        }
1423        run_driver_shutdown_locked(&self.state, || match &self.inner {
1424            #[cfg(feature = "band8")]
1425            DriverHandleInner::B8(driver) => driver
1426                .force_close()
1427                .map_err(|error| RuntimeError::Connection(format!("Driver close failed: {error}"))),
1428            #[cfg(feature = "band9")]
1429            DriverHandleInner::B9(driver) => driver
1430                .force_close()
1431                .map_err(|error| RuntimeError::Connection(format!("Driver close failed: {error}"))),
1432        })
1433    }
1434}
1435
1436impl Drop for DriverHandle {
1437    fn drop(&mut self) {
1438        // Temporary downstream lifecycle workaround and removal gate:
1439        // https://github.com/ds1sqe/type-bridge/issues/196
1440        //
1441        // The official driver exposes explicit shutdown because ordinary field
1442        // drop can leave background transaction workers alive long enough to
1443        // poison later driver lifecycles. The final shared lease closes while
1444        // every driver field is intact, without invalidating a transaction that
1445        // outlives its originating database handle.
1446        if let Err(error) = self.force_close() {
1447            trace_driver_drop_close_failure(&error);
1448        }
1449    }
1450}
1451
1452/// Real TypeDB backend wrapping an internal band-tagged driver handle.
1453pub struct TypeDBRuntime {
1454    driver: Arc<DriverHandle>,
1455    /// The server version the connect gate detected, when it could.
1456    ///
1457    /// `Some` whenever the exact-version, HTTP-probe, or gRPC path produced an
1458    /// authoritative identity. When it is `None`, the negotiated driver band
1459    /// remains the authority for legacy-fallback classification.
1460    server_version: Option<core_version::Version>,
1461}
1462
1463impl TypeDBRuntime {
1464    /// Make the selected driver terminal and dispatch upstream shutdown.
1465    ///
1466    /// Bindings call this at their public connection-close boundary instead of
1467    /// relying on field drop ordering. While any transaction handle is retained,
1468    /// close returns `resource_in_use` and leaves the connection available.
1469    /// After those handles are released, the first call prevents new operations,
1470    /// invalidates in-flight work through upstream shutdown, and
1471    /// leaves the runtime permanently unavailable even if shutdown reports an
1472    /// error. A later call retries incomplete upstream dispatch without
1473    /// reopening the connection; calls after successful dispatch are harmless.
1474    /// Final callback-worker release remains lease-aware and occurs when the
1475    /// last driver handle drops.
1476    pub fn force_close(&self) -> Result<()> {
1477        self.driver.force_close()
1478    }
1479}
1480
1481/// Released Boolean-adapter gate with an injectable probe.
1482#[cfg(test)]
1483pub(crate) async fn gated_driver_with_probe<F>(
1484    address: &str,
1485    username: &str,
1486    password: &str,
1487    options: ConnectOptions,
1488    probe: F,
1489) -> Result<(DriverHandle, Option<core_version::Version>)>
1490where
1491    F: FnOnce(
1492            &str,
1493            u16,
1494            bool,
1495        ) -> std::result::Result<core_version::Version, core_version::VersionError>
1496        + Send
1497        + 'static,
1498{
1499    gated_driver_secure_with_probe(
1500        address,
1501        username,
1502        password,
1503        options.into(),
1504        move |probe_address, http_port, tls_mode| {
1505            probe(probe_address, http_port, tls_mode.is_enabled())
1506                .map_err(core_version::VersionProbeError::Probe)
1507        },
1508    )
1509    .await
1510    .map_err(SecureConnectError::into_runtime_error)
1511}
1512
1513/// Version-gated secure driver constructor with an injectable typed probe.
1514///
1515/// Custom roots and all compiled driver TLS configurations are validated
1516/// before the exact-version fast path, HTTP probe, or any gRPC construction.
1517/// If the HTTP request itself fails, the same [`ResolvedTlsMode`] is retained
1518/// by every fallback and upgrade attempt.  TLS configuration failures are
1519/// terminal and never trigger a transport fallback.
1520async fn gated_driver_secure_with_probe<F>(
1521    address: &str,
1522    username: &str,
1523    password: &str,
1524    options: SecureConnectOptions,
1525    probe: F,
1526) -> SecureResult<(DriverHandle, Option<core_version::Version>)>
1527where
1528    F: FnOnce(
1529            &str,
1530            u16,
1531            &ResolvedTlsProbeMode,
1532        )
1533            -> std::result::Result<core_version::Version, core_version::VersionProbeError>
1534        + Send
1535        + 'static,
1536{
1537    let prepared = options.prepare_transport()?;
1538    gated_driver_prepared_with_probe(address, username, password, prepared, probe).await
1539}
1540
1541async fn gated_driver_prepared_with_probe<F>(
1542    address: &str,
1543    username: &str,
1544    password: &str,
1545    options: PreparedSecureConnectOptions,
1546    probe: F,
1547) -> SecureResult<(DriverHandle, Option<core_version::Version>)>
1548where
1549    F: FnOnce(
1550            &str,
1551            u16,
1552            &ResolvedTlsProbeMode,
1553        )
1554            -> std::result::Result<core_version::Version, core_version::VersionProbeError>
1555        + Send
1556        + 'static,
1557{
1558    let controlled = options.connection_control.is_some();
1559    let result = async {
1560        let PreparedSecureConnectOptions {
1561            http_port,
1562            server_version,
1563            required_server_version,
1564            resolved_tls,
1565            connection_control,
1566        } = options;
1567        let mut meter = connection_control
1568            .map(ConnectionMeter::new)
1569            .unwrap_or_else(ConnectionMeter::legacy);
1570        // A zero item ceiling cannot admit the only public connection result.
1571        // Reject it before a probe, address parse, credential construction, or
1572        // driver host is attempted.
1573        meter.require_possible_success()?;
1574
1575        let connected = if let Some(server_version) = server_version {
1576            let driver = driver_for_server_version(
1577                address,
1578                username,
1579                password,
1580                server_version,
1581                &resolved_tls,
1582                &mut meter,
1583            )
1584            .await?;
1585            (driver, Some(server_version))
1586        } else {
1587            // The HTTP client is blocking, so dispatch it on the blocking pool
1588            // and race its join handle against the same wakeable invocation
1589            // control used for asynchronous driver construction.
1590            meter.charge_statement()?;
1591            let address_owned = address.to_string();
1592            let probe_mode = resolved_tls.probe_mode.clone();
1593            let probe_task =
1594                tokio::task::spawn_blocking(move || probe(&address_owned, http_port, &probe_mode));
1595            let probe_result = await_connection_work(probe_task, meter.control())
1596                .await?
1597                .map_err(|error| {
1598                    RuntimeError::Connection(if controlled {
1599                        "Version probe task failed".to_owned()
1600                    } else {
1601                        format!("Version probe task panicked: {error}")
1602                    })
1603                })?;
1604
1605            match probe_result {
1606                Ok(server_version) => {
1607                    meter.charge_version_evidence(server_version)?;
1608                    require_generated_server_version(server_version, required_server_version)?;
1609                    let driver = driver_for_server_version(
1610                        address,
1611                        username,
1612                        password,
1613                        server_version,
1614                        &resolved_tls,
1615                        &mut meter,
1616                    )
1617                    .await?;
1618                    (driver, Some(server_version))
1619                }
1620                Err(core_version::VersionProbeError::Probe(http_error)) => {
1621                    grpc_fallback_driver(
1622                        address,
1623                        username,
1624                        password,
1625                        http_error,
1626                        &resolved_tls,
1627                        &mut meter,
1628                        required_server_version,
1629                    )
1630                    .await?
1631                }
1632                Err(core_version::VersionProbeError::TlsConfiguration(error)) => {
1633                    return Err(SecureConnectError::TlsConfiguration(error));
1634                }
1635            }
1636        };
1637
1638        if let Err(error) = meter.admit_connection() {
1639            return Err(close_driver_preserving(&connected.0, error.into()));
1640        }
1641        Ok(connected)
1642    }
1643    .await;
1644
1645    if controlled {
1646        result.map_err(controlled_connection_error)
1647    } else {
1648        result
1649    }
1650}
1651
1652fn require_generated_server_version(
1653    observed: core_version::Version,
1654    required: Option<core_version::Version>,
1655) -> Result<()> {
1656    let Some(required) = required else {
1657        return Ok(());
1658    };
1659    if observed == required {
1660        return Ok(());
1661    }
1662    Err(RuntimeError::UnsupportedVersion(
1663        core_version::VersionError::FeatureUnsupported {
1664            feature: "generated-package exact provider compatibility",
1665            server: observed,
1666            required,
1667            remediation: "connect the generated package to its exact required TypeDB release",
1668        },
1669    ))
1670}
1671
1672fn validate_server_band(server_version: &core_version::Version) -> Result<u8> {
1673    // Embedded-runtime gate: accept the server when it is in-window and any
1674    // band it accepts is one this build embedded.  Unlike the installed-driver
1675    // gate (check_supported), the embedded runtime carries every compiled-in
1676    // band. A confirmed 3.12 server normally negotiates native band 9; its
1677    // band-8 acceptance remains
1678    // available to discovery/fallback paths. After this passes, negotiation
1679    // over EMBEDDED_BANDS is guaranteed to yield a band.
1680    core_version::check_server_supported(server_version, EMBEDDED_BANDS)
1681        .map_err(RuntimeError::UnsupportedVersion)?;
1682
1683    Ok(
1684        core_version::negotiate_server_band(server_version, EMBEDDED_BANDS)
1685            .expect("check_server_supported accepted a server with no negotiable band"),
1686    )
1687}
1688
1689async fn driver_for_server_version(
1690    address: &str,
1691    username: &str,
1692    password: &str,
1693    server_version: core_version::Version,
1694    tls: &ResolvedTlsMode,
1695    meter: &mut ConnectionMeter,
1696) -> SecureResult<DriverHandle> {
1697    let band = validate_server_band(&server_version)?;
1698
1699    trace_version_gate_passed(address, band, server_version);
1700
1701    #[cfg(feature = "band8")]
1702    if band == 8 {
1703        return connect_band8_driver(address, username, password, tls, meter)
1704            .await
1705            .map(DriverHandle::band8);
1706    }
1707
1708    #[cfg(feature = "band9")]
1709    if band == 9 {
1710        return connect_band9_driver(address, username, password, tls, meter)
1711            .await
1712            .map(DriverHandle::band9);
1713    }
1714
1715    // Unreachable by invariant: the gate already rejected any server whose
1716    // accepted-band set does not intersect EMBEDDED_BANDS, and negotiation
1717    // only yields embedded bands, each of which returned above.  The arm
1718    // exists only to keep the tail total; it is never entered.
1719    Err(RuntimeError::Connection(format!(
1720        "No compiled driver band supports the negotiated server band ({band})"
1721    ))
1722    .into())
1723}
1724
1725#[cfg(feature = "band8")]
1726async fn connect_band8_driver(
1727    address: &str,
1728    username: &str,
1729    password: &str,
1730    tls: &ResolvedTlsMode,
1731    meter: &mut ConnectionMeter,
1732) -> SecureResult<B8Driver> {
1733    let addresses = Addresses::try_from_address_str(address)
1734        .map_err(|e| RuntimeError::Connection(format!("Invalid TypeDB address {address}: {e}")))?;
1735    meter.charge_statement()?;
1736    let result = await_connection_work(
1737        B8Driver::new(
1738            addresses,
1739            B8Credentials::new(username, password),
1740            DriverOptions::new(tls.band8.clone()),
1741        ),
1742        meter.control(),
1743    )
1744    .await?;
1745    result.map_err(|e| {
1746        RuntimeError::Connection(format!("Failed to connect to {address}: {e}")).into()
1747    })
1748}
1749
1750#[cfg(feature = "band9")]
1751async fn connect_band9_driver(
1752    address: &str,
1753    username: &str,
1754    password: &str,
1755    tls: &ResolvedTlsMode,
1756    meter: &mut ConnectionMeter,
1757) -> SecureResult<B9Driver> {
1758    let addresses = B9Addresses::try_from_address_str(address)
1759        .map_err(|e| RuntimeError::Connection(format!("Invalid TypeDB address {address}: {e}")))?;
1760    meter.charge_statement()?;
1761    let result = await_connection_work(
1762        B9Driver::new(
1763            addresses,
1764            B9Credentials::new(username, password),
1765            B9DriverOptions::new(tls.band9.clone()),
1766        ),
1767        meter.control(),
1768    )
1769    .await?;
1770    result.map_err(|e| {
1771        RuntimeError::Connection(format!("Failed to connect to {address}: {e}")).into()
1772    })
1773}
1774
1775async fn grpc_fallback_driver(
1776    address: &str,
1777    username: &str,
1778    password: &str,
1779    http_error: core_version::VersionError,
1780    tls: &ResolvedTlsMode,
1781    meter: &mut ConnectionMeter,
1782    required_server_version: Option<core_version::Version>,
1783) -> SecureResult<(DriverHandle, Option<core_version::Version>)> {
1784    #[cfg(not(feature = "band8"))]
1785    let _ = (address, username, password, tls, meter);
1786    let mut failures = vec![format!("HTTP version probe failed: {http_error}")];
1787
1788    // Band 9 is deliberately NOT probed blindly: a band-9 connection attempt
1789    // against a 3.11 server crashes the server outright (measured live on
1790    // 3.11.5).  The fallback therefore discovers the server through band 8 —
1791    // accepted by every band-{8,9} server — and only upgrades to the native
1792    // band-9 protocol once the reported version proves the server accepts it.
1793    #[cfg(feature = "band8")]
1794    {
1795        match connect_band8_driver(address, username, password, tls, meter).await {
1796            Ok(driver) => {
1797                // Wrap the discovery candidate immediately so every error,
1798                // retry, and native-band upgrade path deterministically closes
1799                // it rather than relying on the upstream field-drop path.
1800                let driver = DriverHandle::band8(driver);
1801                let reported = match driver.inner() {
1802                    DriverHandleInner::B8(driver) => {
1803                        meter.charge_statement()?;
1804                        await_connection_work(driver.server_version(), meter.control())
1805                            .await?
1806                            .map(|reported| reported.version().to_owned())
1807                            .map_err(|error| error.to_string())
1808                    }
1809                    #[cfg(feature = "band9")]
1810                    _ => {
1811                        return Err(RuntimeError::Connection(
1812                            "Internal TypeDB driver-band mismatch".to_owned(),
1813                        )
1814                        .into());
1815                    }
1816                };
1817                let classification = match classify_band8_grpc_version(address, reported) {
1818                    Ok(classification) => classification,
1819                    Err(error) => {
1820                        driver.force_close().map_err(SecureConnectError::Runtime)?;
1821                        return Err(SecureConnectError::Runtime(error));
1822                    }
1823                };
1824                match classification {
1825                    Band8GrpcVersion::Validated(server_version) => {
1826                        if let Err(error) = meter.charge_version_evidence(server_version) {
1827                            return Err(close_driver_preserving(&driver, error.into()));
1828                        }
1829                        if let Err(error) = require_generated_server_version(
1830                            server_version,
1831                            required_server_version,
1832                        ) {
1833                            return Err(close_driver_preserving(&driver, error.into()));
1834                        }
1835                        // Prefer the server's native band when this build embeds
1836                        // it. The authoritative band-8 version round trip makes a
1837                        // band-9 attempt safe; probing band 9 before this point can
1838                        // crash a 3.11 server.
1839                        #[cfg(feature = "band9")]
1840                        if core_version::negotiate_server_band(&server_version, EMBEDDED_BANDS)
1841                            == Some(9)
1842                        {
1843                            // The frozen common statement maximum is three.
1844                            // HTTP failure, band-8 construction, and band-8
1845                            // version discovery consume those three charges;
1846                            // a controlled band-9 upgrade is therefore a real
1847                            // fourth dispatch and fails closed at the meter.
1848                            // The legacy path has no attached meter and keeps
1849                            // its released upgrade behavior unchanged.
1850                            match connect_band9_driver(address, username, password, tls, meter)
1851                                .await
1852                            {
1853                                Ok(b9_driver) => {
1854                                    let b9_driver = DriverHandle::band9(b9_driver);
1855                                    driver.force_close().map_err(SecureConnectError::Runtime)?;
1856                                    trace_band9_upgrade_succeeded(address, server_version);
1857                                    return Ok((b9_driver, Some(server_version)));
1858                                }
1859                                Err(error) => {
1860                                    if is_connection_control_error(&error) {
1861                                        return Err(close_driver_preserving(&driver, error));
1862                                    }
1863                                    trace_band9_upgrade_failed(address, server_version, &error);
1864                                }
1865                            }
1866                        }
1867                        trace_band8_fallback_connected(address, server_version);
1868                        return Ok((driver, Some(server_version)));
1869                    }
1870                    Band8GrpcVersion::RetryableFailure(failure) => {
1871                        driver.force_close().map_err(SecureConnectError::Runtime)?;
1872                        failures.push(failure);
1873                    }
1874                }
1875            }
1876            Err(error) if is_connection_control_error(&error) => return Err(error),
1877            Err(error) => failures.push(format!("band-8 gRPC attempt failed: {error}")),
1878        }
1879    }
1880
1881    #[cfg(not(feature = "band8"))]
1882    failures.push("band-8 gRPC attempt skipped: band8 feature is not compiled in".to_string());
1883
1884    Err(
1885        RuntimeError::UnsupportedVersion(core_version::VersionError::Probe(format!(
1886            "HTTP version probe and gRPC fallback both failed: {}",
1887            failures.join("; ")
1888        )))
1889        .into(),
1890    )
1891}
1892
1893#[cfg(feature = "band8")]
1894#[derive(Debug)]
1895enum Band8GrpcVersion {
1896    Validated(core_version::Version),
1897    RetryableFailure(String),
1898}
1899
1900#[cfg(feature = "band8")]
1901fn classify_band8_grpc_version(
1902    address: &str,
1903    reported: std::result::Result<String, String>,
1904) -> Result<Band8GrpcVersion> {
1905    let reported = match reported {
1906        Ok(reported) => reported,
1907        Err(error) => {
1908            // TypeDB's band-8 Driver::new is lazy, so it can return Ok before
1909            // server_version performs the first protocol round trip. Preserve
1910            // that failure as fallback evidence without accepting an
1911            // unvalidated connection.
1912            return Ok(Band8GrpcVersion::RetryableFailure(format!(
1913                "band-8 gRPC version validation failed after connect to {address}: {error}"
1914            )));
1915        }
1916    };
1917
1918    // Once the server has authoritatively reported a version, parse and gate
1919    // failures are terminal. Falling through to band 7 here could silently
1920    // accept an unsupported server and discard the exact rejection evidence.
1921    let server_version = reported
1922        .parse::<core_version::Version>()
1923        .map_err(RuntimeError::UnsupportedVersion)?;
1924    validate_server_band(&server_version)?;
1925    if !core_version::server_accepted_bands(&server_version).contains(&8) {
1926        return Err(RuntimeError::UnsupportedVersion(
1927            core_version::VersionError::Probe(format!(
1928                "band-8 gRPC connection reported server version {server_version}, \
1929                 which does not accept band 8"
1930            )),
1931        ));
1932    }
1933
1934    Ok(Band8GrpcVersion::Validated(server_version))
1935}
1936
1937fn probe_server_version_for_tls_mode(
1938    address: &str,
1939    http_port: u16,
1940    tls_mode: &ResolvedTlsProbeMode,
1941) -> std::result::Result<core_version::Version, core_version::VersionProbeError> {
1942    match tls_mode {
1943        ResolvedTlsProbeMode::Disabled => {
1944            core_version::server_version_plaintext(address, http_port)
1945        }
1946        ResolvedTlsProbeMode::NativeRoots => {
1947            core_version::server_version_native_roots(address, http_port)
1948        }
1949        ResolvedTlsProbeMode::CustomRootCa(material) => {
1950            core_version::server_version_retained_custom_root_ca(address, http_port, material)
1951        }
1952    }
1953}
1954
1955/// Version-gated released [`ConnectOptions`] adapter.
1956async fn gated_driver(
1957    address: &str,
1958    username: &str,
1959    password: &str,
1960    options: ConnectOptions,
1961) -> Result<(DriverHandle, Option<core_version::Version>)> {
1962    gated_driver_secure(address, username, password, options.into())
1963        .await
1964        .map_err(SecureConnectError::into_runtime_error)
1965}
1966
1967/// Version-gated typed TLS constructor.
1968async fn gated_driver_secure(
1969    address: &str,
1970    username: &str,
1971    password: &str,
1972    options: SecureConnectOptions,
1973) -> SecureResult<(DriverHandle, Option<core_version::Version>)> {
1974    gated_driver_secure_with_probe(
1975        address,
1976        username,
1977        password,
1978        options,
1979        probe_server_version_for_tls_mode,
1980    )
1981    .await
1982}
1983
1984async fn gated_driver_prepared_secure(
1985    address: &str,
1986    username: &str,
1987    password: &str,
1988    options: PreparedSecureConnectOptions,
1989) -> SecureResult<(DriverHandle, Option<core_version::Version>)> {
1990    gated_driver_prepared_with_probe(
1991        address,
1992        username,
1993        password,
1994        options,
1995        probe_server_version_for_tls_mode,
1996    )
1997    .await
1998}
1999
2000impl TypeDBRuntime {
2001    /// Connect to a TypeDB server.
2002    ///
2003    /// Validates the supplied server version, or probes via HTTP before opening
2004    /// any gRPC connection when no version is supplied.
2005    /// Returns [`RuntimeError::UnsupportedVersion`] when the server is outside the
2006    /// supported window or on a different protocol band than the driver.
2007    pub async fn connect(
2008        address: &str,
2009        username: &str,
2010        password: &str,
2011        options: ConnectOptions,
2012    ) -> Result<Self> {
2013        let (driver, server_version) = gated_driver(address, username, password, options).await?;
2014        trace_runtime_connected(address, driver.band(), server_version);
2015        Ok(Self {
2016            driver: Arc::new(driver),
2017            server_version,
2018        })
2019    }
2020
2021    /// Connect using an explicit typed TLS policy.
2022    ///
2023    /// Custom trust material is validated before the HTTP version probe or
2024    /// any gRPC host is constructed.  An enabled mode remains enabled through
2025    /// HTTP failure, driver-band discovery, and native-band upgrade.
2026    pub async fn connect_secure(
2027        address: &str,
2028        username: &str,
2029        password: &str,
2030        options: SecureConnectOptions,
2031    ) -> SecureResult<Self> {
2032        let (driver, server_version) =
2033            gated_driver_secure(address, username, password, options).await?;
2034        trace_runtime_connected(address, driver.band(), server_version);
2035        Ok(Self {
2036            driver: Arc::new(driver),
2037            server_version,
2038        })
2039    }
2040
2041    /// Connect with a transport policy that was already fully resolved.
2042    ///
2043    /// This hidden orchestration seam performs no trust-material path I/O;
2044    /// clones of `options` share the exact prepared custom-root snapshot.
2045    #[doc(hidden)]
2046    pub async fn connect_prepared_secure(
2047        address: &str,
2048        username: &str,
2049        password: &str,
2050        options: PreparedSecureConnectOptions,
2051    ) -> SecureResult<Self> {
2052        let (driver, server_version) =
2053            gated_driver_prepared_secure(address, username, password, options).await?;
2054        trace_runtime_connected(address, driver.band(), server_version);
2055        Ok(Self {
2056            driver: Arc::new(driver),
2057            server_version,
2058        })
2059    }
2060
2061    /// The server version detected by the connect gate, when known.
2062    ///
2063    /// When this is `None`, callers must use the negotiated driver band rather
2064    /// than inferring a server version from a failed HTTP probe.
2065    pub fn server_version(&self) -> Option<core_version::Version> {
2066        self.server_version
2067    }
2068
2069    /// Return whether the connection actually negotiated a driver that can
2070    /// transport `given` input rows.
2071    ///
2072    /// This is deliberately separate from the reported server version. A
2073    /// 3.12 server may accept the band-8 discovery connection when a later
2074    /// band-9 upgrade fails; advertising `given` in that state would select an
2075    /// execution path the active driver cannot carry.
2076    pub fn supports_given_rows(&self) -> bool {
2077        match self.driver.inner() {
2078            #[cfg(feature = "band8")]
2079            DriverHandleInner::B8(_) => false,
2080            #[cfg(feature = "band9")]
2081            DriverHandleInner::B9(_) => true,
2082        }
2083    }
2084}
2085
2086fn runtime_error_diagnostic(error: RuntimeError) -> String {
2087    match error {
2088        RuntimeError::Connection(message) => message,
2089        other => other.to_string(),
2090    }
2091}
2092
2093fn finish_one_shot_database_operation<T>(operation: Result<T>, close: Result<()>) -> Result<T> {
2094    match (operation, close) {
2095        (Ok(value), Ok(())) => Ok(value),
2096        (Ok(_), Err(close_error)) => Err(close_error),
2097        (Err(operation_error), Ok(())) => Err(operation_error),
2098        (Err(operation_error), Err(close_error)) => Err(RuntimeError::Connection(format!(
2099            "{}; additionally, TypeDB driver cleanup failed: {}",
2100            runtime_error_diagnostic(operation_error),
2101            runtime_error_diagnostic(close_error),
2102        ))),
2103    }
2104}
2105
2106/// Ensure a TypeDB database exists, creating it if absent.
2107///
2108/// Validates the supplied server version, or probes via HTTP before opening any
2109/// gRPC connection when no version is supplied.
2110/// Returns [`RuntimeError::UnsupportedVersion`] when the server is outside the
2111/// supported window or on a different protocol band than the driver.
2112///
2113/// On all other TypeDB failures (including unreachable server) the error is
2114/// returned so callers can treat it as a hard failure rather than silently
2115/// skipping.
2116pub async fn ensure_database_exists(
2117    address: &str,
2118    database: &str,
2119    username: &str,
2120    password: &str,
2121    options: ConnectOptions,
2122) -> Result<()> {
2123    ensure_database_exists_secure(address, database, username, password, options.into())
2124        .await
2125        .map_err(SecureConnectError::into_runtime_error)
2126}
2127
2128/// Ensure a TypeDB database exists using an explicit typed TLS policy.
2129///
2130/// The same resolved policy is used for version discovery, gRPC fallback, and
2131/// the database lookup/create operations on the resulting driver.
2132pub async fn ensure_database_exists_secure(
2133    address: &str,
2134    database: &str,
2135    username: &str,
2136    password: &str,
2137    options: SecureConnectOptions,
2138) -> SecureResult<()> {
2139    ensure_database_exists_prepared_secure(
2140        address,
2141        database,
2142        username,
2143        password,
2144        options.prepare_transport()?,
2145    )
2146    .await
2147}
2148
2149/// Ensure a database exists using an already resolved transport snapshot.
2150#[doc(hidden)]
2151pub async fn ensure_database_exists_prepared_secure(
2152    address: &str,
2153    database: &str,
2154    username: &str,
2155    password: &str,
2156    options: PreparedSecureConnectOptions,
2157) -> SecureResult<()> {
2158    let (driver, server_version) =
2159        gated_driver_prepared_secure(address, username, password, options).await?;
2160    let runtime = TypeDBRuntime {
2161        driver: Arc::new(driver),
2162        server_version,
2163    };
2164    let operation = async {
2165        if !runtime.database_exists(database).await? {
2166            runtime.create_database(database).await?;
2167        }
2168        Ok(())
2169    }
2170    .await;
2171    let close = runtime.force_close();
2172    finish_one_shot_database_operation(operation, close).map_err(SecureConnectError::Runtime)
2173}
2174
2175/// Check whether a TypeDB database exists without creating it.
2176///
2177/// Same version gating as [`ensure_database_exists`]; the lookup itself is
2178/// read-only and never mutates server state.
2179pub async fn database_exists(
2180    address: &str,
2181    database: &str,
2182    username: &str,
2183    password: &str,
2184    options: ConnectOptions,
2185) -> Result<bool> {
2186    database_exists_secure(address, database, username, password, options.into())
2187        .await
2188        .map_err(SecureConnectError::into_runtime_error)
2189}
2190
2191/// Check whether a TypeDB database exists using an explicit typed TLS policy.
2192pub async fn database_exists_secure(
2193    address: &str,
2194    database: &str,
2195    username: &str,
2196    password: &str,
2197    options: SecureConnectOptions,
2198) -> SecureResult<bool> {
2199    database_exists_prepared_secure(
2200        address,
2201        database,
2202        username,
2203        password,
2204        options.prepare_transport()?,
2205    )
2206    .await
2207}
2208
2209/// Check for a database using an already resolved transport snapshot.
2210#[doc(hidden)]
2211pub async fn database_exists_prepared_secure(
2212    address: &str,
2213    database: &str,
2214    username: &str,
2215    password: &str,
2216    options: PreparedSecureConnectOptions,
2217) -> SecureResult<bool> {
2218    let (driver, server_version) =
2219        gated_driver_prepared_secure(address, username, password, options).await?;
2220    let runtime = TypeDBRuntime {
2221        driver: Arc::new(driver),
2222        server_version,
2223    };
2224    let operation = runtime.database_exists(database).await;
2225    let close = runtime.force_close();
2226    finish_one_shot_database_operation(operation, close).map_err(SecureConnectError::Runtime)
2227}
2228
2229/// Connect securely and delete a TypeDB database.
2230///
2231/// This lifecycle entry point exists for callers that do not otherwise need
2232/// to retain a [`TypeDBRuntime`].  Deleting through an already connected
2233/// runtime remains available as [`TypeDBRuntime::delete_database`].
2234pub async fn delete_database_secure(
2235    address: &str,
2236    database: &str,
2237    username: &str,
2238    password: &str,
2239    options: SecureConnectOptions,
2240) -> SecureResult<()> {
2241    delete_database_prepared_secure(
2242        address,
2243        database,
2244        username,
2245        password,
2246        options.prepare_transport()?,
2247    )
2248    .await
2249}
2250
2251/// Delete a database using an already resolved transport snapshot.
2252#[doc(hidden)]
2253pub async fn delete_database_prepared_secure(
2254    address: &str,
2255    database: &str,
2256    username: &str,
2257    password: &str,
2258    options: PreparedSecureConnectOptions,
2259) -> SecureResult<()> {
2260    let runtime =
2261        TypeDBRuntime::connect_prepared_secure(address, username, password, options).await?;
2262    let operation = async {
2263        if runtime.database_exists(database).await? {
2264            runtime.delete_database(database).await?;
2265        }
2266        Ok(())
2267    }
2268    .await;
2269    let close = runtime.force_close();
2270    finish_one_shot_database_operation(operation, close).map_err(SecureConnectError::Runtime)
2271}
2272
2273impl TypeDBRuntime {
2274    /// Open a TypeDB transaction against `database`.
2275    pub fn open_transaction(
2276        &self,
2277        database: &str,
2278        tx_type: TxType,
2279    ) -> BoxFuture<'_, Result<RuntimeTransaction>> {
2280        self.open_transaction_with_optional_timeout(database, tx_type, None)
2281    }
2282
2283    /// Open a TypeDB transaction with an explicit server-side lifetime bound.
2284    ///
2285    /// Higher layers use this for transactions that hold schema exclusion: a
2286    /// lost client must not leave the server-side fence unbounded while the
2287    /// transport is still detecting terminal closure.
2288    pub fn open_transaction_with_timeout(
2289        &self,
2290        database: &str,
2291        tx_type: TxType,
2292        timeout: Duration,
2293    ) -> BoxFuture<'_, Result<RuntimeTransaction>> {
2294        self.open_transaction_with_optional_timeout(database, tx_type, Some(timeout))
2295    }
2296
2297    fn open_transaction_with_optional_timeout(
2298        &self,
2299        database: &str,
2300        tx_type: TxType,
2301        timeout: Option<Duration>,
2302    ) -> BoxFuture<'_, Result<RuntimeTransaction>> {
2303        let db = database.to_string();
2304        let driver_lease = Arc::clone(&self.driver);
2305        Box::pin(async move {
2306            driver_lease.ensure_open()?;
2307            match driver_lease.inner() {
2308                #[cfg(feature = "band8")]
2309                DriverHandleInner::B8(d) => {
2310                    let typedb_tx_type = match tx_type {
2311                        TxType::Read => B8TransactionType::Read,
2312                        TxType::Write => B8TransactionType::Write,
2313                        TxType::Schema => B8TransactionType::Schema,
2314                    };
2315                    let mut options = B8TransactionOptions::new();
2316                    if let Some(timeout) = timeout {
2317                        options = options
2318                            .transaction_timeout(timeout)
2319                            .schema_lock_acquire_timeout(timeout);
2320                    }
2321                    let transaction = d
2322                        .transaction_with_options(&db, typedb_tx_type, options)
2323                        .await
2324                        .map_err(|e| {
2325                            RuntimeError::Transaction(format!("Failed to open transaction: {e}"))
2326                        })?;
2327                    retain_driver_transaction(
2328                        &driver_lease.state,
2329                        &driver_lease.active_transactions,
2330                        &driver_lease.close_lock,
2331                    )?;
2332                    Ok(RuntimeTransaction {
2333                        inner: RuntimeTransactionInner::B8(Some(transaction)),
2334                        driver_lease: Some(driver_lease),
2335                    })
2336                }
2337                #[cfg(feature = "band9")]
2338                DriverHandleInner::B9(d) => {
2339                    let typedb_tx_type = match tx_type {
2340                        TxType::Read => B9TransactionType::Read,
2341                        TxType::Write => B9TransactionType::Write,
2342                        TxType::Schema => B9TransactionType::Schema,
2343                    };
2344                    let mut options = B9TransactionOptions::new();
2345                    if let Some(timeout) = timeout {
2346                        options = options
2347                            .transaction_timeout(timeout)
2348                            .schema_lock_acquire_timeout(timeout);
2349                    }
2350                    let transaction = d
2351                        .transaction_with_options(&db, typedb_tx_type, options)
2352                        .await
2353                        .map_err(|e| {
2354                            RuntimeError::Transaction(format!("Failed to open transaction: {e}"))
2355                        })?;
2356                    retain_driver_transaction(
2357                        &driver_lease.state,
2358                        &driver_lease.active_transactions,
2359                        &driver_lease.close_lock,
2360                    )?;
2361                    Ok(RuntimeTransaction {
2362                        inner: RuntimeTransactionInner::B9(Some(transaction)),
2363                        driver_lease: Some(driver_lease),
2364                    })
2365                }
2366            }
2367        })
2368    }
2369
2370    /// Check whether the underlying driver is open.
2371    pub fn is_open(&self) -> bool {
2372        if self.driver.shutdown_started() {
2373            return false;
2374        }
2375        match self.driver.inner() {
2376            #[cfg(feature = "band8")]
2377            DriverHandleInner::B8(d) => d.is_open(),
2378            #[cfg(feature = "band9")]
2379            DriverHandleInner::B9(d) => d.is_open(),
2380        }
2381    }
2382
2383    /// Check whether a database exists.
2384    pub fn database_exists(&self, database: &str) -> BoxFuture<'_, Result<bool>> {
2385        let database = database.to_string();
2386        Box::pin(async move {
2387            self.driver.ensure_open()?;
2388            match self.driver.inner() {
2389                #[cfg(feature = "band8")]
2390                DriverHandleInner::B8(d) => {
2391                    d.databases().contains(database).await.map_err(|e| {
2392                        RuntimeError::Connection(format!("Database lookup failed: {e}"))
2393                    })
2394                }
2395                #[cfg(feature = "band9")]
2396                DriverHandleInner::B9(d) => {
2397                    d.databases().contains(database).await.map_err(|e| {
2398                        RuntimeError::Connection(format!("Database lookup failed: {e}"))
2399                    })
2400                }
2401            }
2402        })
2403    }
2404
2405    /// Create a database.
2406    pub fn create_database(&self, database: &str) -> BoxFuture<'_, Result<()>> {
2407        let database = database.to_string();
2408        Box::pin(async move {
2409            self.driver.ensure_open()?;
2410            match self.driver.inner() {
2411                #[cfg(feature = "band8")]
2412                DriverHandleInner::B8(d) => {
2413                    d.databases().create(database).await.map_err(|e| {
2414                        RuntimeError::Connection(format!("Database create failed: {e}"))
2415                    })
2416                }
2417                #[cfg(feature = "band9")]
2418                DriverHandleInner::B9(d) => {
2419                    d.databases().create(database).await.map_err(|e| {
2420                        RuntimeError::Connection(format!("Database create failed: {e}"))
2421                    })
2422                }
2423            }
2424        })
2425    }
2426
2427    /// Delete a database.
2428    pub fn delete_database(&self, database: &str) -> BoxFuture<'_, Result<()>> {
2429        let database = database.to_string();
2430        Box::pin(async move {
2431            self.driver.ensure_open()?;
2432            match self.driver.inner() {
2433                #[cfg(feature = "band8")]
2434                DriverHandleInner::B8(d) => {
2435                    let db = d.databases().get(database).await.map_err(|e| {
2436                        RuntimeError::Connection(format!("Database lookup failed: {e}"))
2437                    })?;
2438                    db.delete().await.map_err(|e| {
2439                        RuntimeError::Connection(format!("Database delete failed: {e}"))
2440                    })
2441                }
2442                #[cfg(feature = "band9")]
2443                DriverHandleInner::B9(d) => {
2444                    let db = d.databases().get(database).await.map_err(|e| {
2445                        RuntimeError::Connection(format!("Database lookup failed: {e}"))
2446                    })?;
2447                    db.delete().await.map_err(|e| {
2448                        RuntimeError::Connection(format!("Database delete failed: {e}"))
2449                    })
2450                }
2451            }
2452        })
2453    }
2454
2455    /// Export a database schema as TypeQL text.
2456    pub fn schema_text(&self, database: &str) -> BoxFuture<'_, Result<String>> {
2457        let database = database.to_string();
2458        Box::pin(async move {
2459            self.driver.ensure_open()?;
2460            match self.driver.inner() {
2461                #[cfg(feature = "band8")]
2462                DriverHandleInner::B8(d) => {
2463                    let db = d.databases().get(&database).await.map_err(|e| {
2464                        RuntimeError::Connection(format!("Database lookup failed: {e}"))
2465                    })?;
2466                    db.schema()
2467                        .await
2468                        .map_err(|e| RuntimeError::Connection(format!("Schema export failed: {e}")))
2469                }
2470                #[cfg(feature = "band9")]
2471                DriverHandleInner::B9(d) => {
2472                    let db = d.databases().get(&database).await.map_err(|e| {
2473                        RuntimeError::Connection(format!("Database lookup failed: {e}"))
2474                    })?;
2475                    db.schema()
2476                        .await
2477                        .map_err(|e| RuntimeError::Connection(format!("Schema export failed: {e}")))
2478                }
2479            }
2480        })
2481    }
2482}
2483
2484/// Band-tagged transaction inner state.
2485///
2486/// The `Option` wrapper lets commit/close/rollback take ownership by value
2487/// (`.take()`) while the outer `&mut RuntimeTransaction` satisfies the trait's
2488/// `&mut self`.  A `None` after `.take()` marks the transaction as consumed.
2489enum RuntimeTransactionInner {
2490    #[cfg(feature = "band8")]
2491    B8(Option<type_bridge_typedb_driver_b8::Transaction>),
2492    #[cfg(feature = "band9")]
2493    B9(Option<typedb_driver::Transaction>),
2494}
2495
2496/// Open TypeDB transaction owned by the shared runtime.
2497pub struct RuntimeTransaction {
2498    inner: RuntimeTransactionInner,
2499    // Keep the selected driver alive until this transaction is released. Test
2500    // doubles use `None` because they contain no live upstream transaction.
2501    driver_lease: Option<Arc<DriverHandle>>,
2502}
2503
2504impl Drop for RuntimeTransaction {
2505    fn drop(&mut self) {
2506        if let Some(driver) = &self.driver_lease {
2507            let previous = driver
2508                .active_transactions
2509                .fetch_sub(1, AtomicOrdering::AcqRel);
2510            debug_assert!(previous != 0, "runtime transaction lease count underflow");
2511        }
2512    }
2513}
2514
2515fn runtime_cancelled_error() -> RuntimeError {
2516    RuntimeError::ResourceLimit {
2517        code: "provider_cancelled",
2518        message: "provider answer processing was cancelled",
2519    }
2520}
2521
2522fn runtime_deadline_error() -> RuntimeError {
2523    RuntimeError::ResourceLimit {
2524        code: "transaction_deadline_exceeded",
2525        message: "provider transaction deadline expired",
2526    }
2527}
2528
2529fn runtime_check(limits: &RuntimeAnswerLimits) -> Result<()> {
2530    if limits.cancellation.is_cancelled() {
2531        return Err(runtime_cancelled_error());
2532    }
2533    if limits.deadline.is_some_and(|deadline| {
2534        tokio::time::Instant::now() >= tokio::time::Instant::from_std(deadline)
2535    }) {
2536        return Err(runtime_deadline_error());
2537    }
2538    Ok(())
2539}
2540
2541async fn runtime_await<T>(
2542    future: impl Future<Output = Result<T>>,
2543    limits: &RuntimeAnswerLimits,
2544) -> Result<T> {
2545    runtime_check(limits)?;
2546    tokio::pin!(future);
2547    let cancellation = limits.cancellation.cancelled();
2548    tokio::pin!(cancellation);
2549
2550    if let Some(deadline) = limits.deadline {
2551        let deadline = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline));
2552        tokio::pin!(deadline);
2553        tokio::select! {
2554            biased;
2555            result = &mut future => result,
2556            () = &mut cancellation => Err(runtime_cancelled_error()),
2557            () = &mut deadline => Err(runtime_deadline_error()),
2558        }
2559    } else {
2560        tokio::select! {
2561            biased;
2562            result = &mut future => result,
2563            () = &mut cancellation => Err(runtime_cancelled_error()),
2564        }
2565    }
2566}
2567
2568fn runtime_accept(
2569    limits: &RuntimeAnswerLimits,
2570    stats: &mut RuntimeAnswerStats,
2571    item: RuntimeAnswerItem,
2572    consumer: &mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2573) -> Result<RuntimeAnswerControl> {
2574    runtime_check(limits)?;
2575    let next_items = stats
2576        .processed_items
2577        .checked_add(1)
2578        .ok_or(RuntimeError::ResourceLimit {
2579            code: "processed_item_counter_overflow",
2580            message: "processed provider item counter overflowed",
2581        })?;
2582    if next_items > limits.max_items {
2583        return Err(RuntimeError::ResourceLimit {
2584            code: "processed_item_limit",
2585            message: "provider answer exceeded the processed-item ceiling",
2586        });
2587    }
2588    let value = match &item {
2589        RuntimeAnswerItem::Row(value) | RuntimeAnswerItem::Document(value) => value,
2590    };
2591    let encoded = u64::try_from(
2592        serde_json::to_vec(value)
2593            .map_err(|error| RuntimeError::QueryExecution(format!("Answer encode: {error}")))?
2594            .len(),
2595    )
2596    .map_err(|_| RuntimeError::ResourceLimit {
2597        code: "answer_byte_counter_overflow",
2598        message: "encoded provider answer length exceeds the counter range",
2599    })?;
2600    let next_bytes =
2601        stats
2602            .response_bytes
2603            .checked_add(encoded)
2604            .ok_or(RuntimeError::ResourceLimit {
2605                code: "answer_byte_counter_overflow",
2606                message: "provider answer byte counter overflowed",
2607            })?;
2608    if next_bytes > limits.max_bytes {
2609        return Err(RuntimeError::ResourceLimit {
2610            code: "response_byte_limit",
2611            message: "provider answer exceeded the response-byte ceiling",
2612        });
2613    }
2614    stats.processed_items = next_items;
2615    stats.response_bytes = next_bytes;
2616    let control = consumer(item);
2617    runtime_check(limits)?;
2618    let control = control?;
2619    if control == RuntimeAnswerControl::Stop {
2620        stats.stopped_early = true;
2621    }
2622    Ok(control)
2623}
2624
2625#[cfg(any(feature = "band9", test))]
2626async fn runtime_consume_stream<S>(
2627    mut stream: S,
2628    kind: RuntimeAnswerKind,
2629    limits: &RuntimeAnswerLimits,
2630    consumer: &mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2631) -> Result<RuntimeAnswerStats>
2632where
2633    S: futures::TryStream<Ok = RuntimeAnswerItem, Error = RuntimeError> + Send + Unpin,
2634{
2635    let mut stats = RuntimeAnswerStats::new(kind);
2636    while let Some(item) = runtime_await(async { stream.try_next().await }, limits).await? {
2637        if runtime_accept(limits, &mut stats, item, consumer)? == RuntimeAnswerControl::Stop {
2638            break;
2639        }
2640    }
2641    Ok(stats)
2642}
2643
2644fn materialize_runtime_answer(
2645    stats: RuntimeAnswerStats,
2646    items: Vec<RuntimeAnswerItem>,
2647) -> QueryResult {
2648    match stats.kind {
2649        RuntimeAnswerKind::Ok => QueryResult::Ok,
2650        RuntimeAnswerKind::Rows => QueryResult::Rows(
2651            items
2652                .into_iter()
2653                .filter_map(|item| match item {
2654                    RuntimeAnswerItem::Row(value) => Some(value),
2655                    RuntimeAnswerItem::Document(_) => None,
2656                })
2657                .collect(),
2658        ),
2659        RuntimeAnswerKind::Documents => QueryResult::Documents(
2660            items
2661                .into_iter()
2662                .filter_map(|item| match item {
2663                    RuntimeAnswerItem::Document(value) => Some(value),
2664                    RuntimeAnswerItem::Row(_) => None,
2665                })
2666                .collect(),
2667        ),
2668    }
2669}
2670
2671impl RuntimeTransaction {
2672    fn ensure_driver_open(&self) -> Result<()> {
2673        match &self.driver_lease {
2674            Some(driver) => driver.ensure_open(),
2675            None => Ok(()),
2676        }
2677    }
2678
2679    fn driver_shutdown_started(driver_lease: &Option<Arc<DriverHandle>>) -> bool {
2680        driver_lease
2681            .as_ref()
2682            .is_some_and(|driver| driver.shutdown_started())
2683    }
2684
2685    /// Return whether this transaction's negotiated driver can transport
2686    /// `given` input rows.
2687    pub fn supports_given_rows(&self) -> bool {
2688        match &self.inner {
2689            #[cfg(feature = "band8")]
2690            RuntimeTransactionInner::B8(_) => false,
2691            #[cfg(feature = "band9")]
2692            RuntimeTransactionInner::B9(_) => true,
2693        }
2694    }
2695
2696    /// Execute TypeQL within this transaction, materializing for legacy callers.
2697    ///
2698    /// New match executors use [`Self::query_bounded`] directly.
2699    pub fn query(&mut self, typeql: &str) -> BoxFuture<'_, Result<QueryResult>> {
2700        let typeql = typeql.to_owned();
2701        Box::pin(async move {
2702            let mut items = Vec::new();
2703            let mut collect = |item| {
2704                items.push(item);
2705                Ok(RuntimeAnswerControl::Continue)
2706            };
2707            let stats = self
2708                .query_bounded(&typeql, RuntimeAnswerLimits::unbounded(), &mut collect)
2709                .await?;
2710            Ok(materialize_runtime_answer(stats, items))
2711        })
2712    }
2713
2714    /// Execute TypeQL and enforce bounds while polling the driver stream.
2715    pub fn query_bounded<'a>(
2716        &'a mut self,
2717        typeql: &'a str,
2718        limits: RuntimeAnswerLimits,
2719        consumer: &'a mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2720    ) -> BoxFuture<'a, Result<RuntimeAnswerStats>> {
2721        self.query_bounded_with_scalar_encoding(
2722            typeql,
2723            QueryV2RuntimeAnswerLimits {
2724                answer: limits,
2725                max_collection_members: u64::MAX,
2726            },
2727            ScalarJsonEncoding::DriverDisplay,
2728            consumer,
2729        )
2730    }
2731
2732    /// Execute V2 TypeQL with an additive document collection-member limit.
2733    pub fn query_v2_bounded<'a>(
2734        &'a mut self,
2735        typeql: &'a str,
2736        limits: QueryV2RuntimeAnswerLimits,
2737        consumer: &'a mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2738    ) -> BoxFuture<'a, Result<RuntimeAnswerStats>> {
2739        self.query_bounded_with_scalar_encoding(
2740            typeql,
2741            limits,
2742            ScalarJsonEncoding::CanonicalV2,
2743            consumer,
2744        )
2745    }
2746
2747    /// Execute V2 with canonical scalar spelling and retain the answer kind.
2748    #[doc(hidden)]
2749    pub async fn query_v2_materialized(
2750        &mut self,
2751        typeql: &str,
2752        limits: QueryV2RuntimeAnswerLimits,
2753    ) -> Result<QueryResult> {
2754        let mut items = Vec::new();
2755        let mut consumer = |item: RuntimeAnswerItem| {
2756            items.push(item);
2757            Ok(RuntimeAnswerControl::Continue)
2758        };
2759        let stats = self.query_v2_bounded(typeql, limits, &mut consumer).await?;
2760        Ok(materialize_runtime_answer(stats, items))
2761    }
2762
2763    fn query_bounded_with_scalar_encoding<'a>(
2764        &'a mut self,
2765        typeql: &'a str,
2766        limits: QueryV2RuntimeAnswerLimits,
2767        scalar_encoding: ScalarJsonEncoding,
2768        consumer: &'a mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2769    ) -> BoxFuture<'a, Result<RuntimeAnswerStats>> {
2770        let QueryV2RuntimeAnswerLimits {
2771            answer: limits,
2772            max_collection_members,
2773        } = limits;
2774        let tql = typeql.to_string();
2775        Box::pin(async move {
2776            self.ensure_driver_open()?;
2777            runtime_check(&limits)?;
2778            match &self.inner {
2779                #[cfg(feature = "band8")]
2780                RuntimeTransactionInner::B8(opt) => {
2781                    let tx = opt.as_ref().ok_or_else(|| {
2782                        RuntimeError::Transaction("Transaction already consumed".into())
2783                    })?;
2784                    let answer = runtime_await(
2785                        async {
2786                            let options = if limits.is_unbounded() {
2787                                driver_b8::QueryOptions::new()
2788                            } else {
2789                                driver_b8::QueryOptions::new()
2790                                    .prefetch_size(BOUNDED_QUERY_PREFETCH_SIZE)
2791                            };
2792                            tx.query_with_options(&tql, options)
2793                                .await
2794                                .map_err(|e| RuntimeError::QueryExecution(format!("{e}")))
2795                        },
2796                        &limits,
2797                    )
2798                    .await?;
2799                    match answer {
2800                        B8QueryAnswer::Ok(_) => Ok(RuntimeAnswerStats::new(RuntimeAnswerKind::Ok)),
2801                        B8QueryAnswer::ConceptRowStream(_, mut stream) => {
2802                            let mut stats = RuntimeAnswerStats::new(RuntimeAnswerKind::Rows);
2803                            while let Some(row) = runtime_await(
2804                                async {
2805                                    stream.try_next().await.map_err(|error| {
2806                                        RuntimeError::QueryExecution(format!("Row stream: {error}"))
2807                                    })
2808                                },
2809                                &limits,
2810                            )
2811                            .await?
2812                            {
2813                                let mut object = serde_json::Map::new();
2814                                for (index, column) in row.get_column_names().iter().enumerate() {
2815                                    let value = row
2816                                        .row
2817                                        .get(index)
2818                                        .and_then(|concept| concept.as_ref())
2819                                        .map(|concept| concept_to_json_b8(concept, scalar_encoding))
2820                                        .transpose()?
2821                                        .unwrap_or(serde_json::Value::Null);
2822                                    object.insert(column.clone(), value);
2823                                }
2824                                if runtime_accept(
2825                                    &limits,
2826                                    &mut stats,
2827                                    RuntimeAnswerItem::Row(serde_json::Value::Object(object)),
2828                                    consumer,
2829                                )? == RuntimeAnswerControl::Stop
2830                                {
2831                                    break;
2832                                }
2833                            }
2834                            Ok(stats)
2835                        }
2836                        B8QueryAnswer::ConceptDocumentStream(_, mut stream) => {
2837                            let mut stats = RuntimeAnswerStats::new(RuntimeAnswerKind::Documents);
2838                            let mut collection_members = 0_u64;
2839                            while let Some(document) = runtime_await(
2840                                async {
2841                                    stream.try_next().await.map_err(|error| {
2842                                        RuntimeError::QueryExecution(format!(
2843                                            "Document stream: {error}"
2844                                        ))
2845                                    })
2846                                },
2847                                &limits,
2848                            )
2849                            .await?
2850                            {
2851                                let value = document_to_json_b8(
2852                                    &document,
2853                                    &mut collection_members,
2854                                    max_collection_members,
2855                                    scalar_encoding,
2856                                )?;
2857                                if runtime_accept(
2858                                    &limits,
2859                                    &mut stats,
2860                                    RuntimeAnswerItem::Document(value),
2861                                    consumer,
2862                                )? == RuntimeAnswerControl::Stop
2863                                {
2864                                    break;
2865                                }
2866                            }
2867                            Ok(stats)
2868                        }
2869                    }
2870                }
2871                #[cfg(feature = "band9")]
2872                RuntimeTransactionInner::B9(opt) => {
2873                    let tx = opt.as_ref().ok_or_else(|| {
2874                        RuntimeError::Transaction("Transaction already consumed".into())
2875                    })?;
2876                    let answer = runtime_await(
2877                        async {
2878                            let options = if limits.is_unbounded() {
2879                                driver_b9::QueryOptions::new()
2880                            } else {
2881                                driver_b9::QueryOptions::new()
2882                                    .prefetch_size(BOUNDED_QUERY_PREFETCH_SIZE)
2883                            };
2884                            tx.query_with_options(&tql, options)
2885                                .await
2886                                .map_err(|e| RuntimeError::QueryExecution(format!("{e}")))
2887                        },
2888                        &limits,
2889                    )
2890                    .await?;
2891                    consume_answer_b9(
2892                        answer,
2893                        &limits,
2894                        max_collection_members,
2895                        scalar_encoding,
2896                        consumer,
2897                    )
2898                    .await
2899                }
2900            }
2901        })
2902    }
2903
2904    /// Execute TypeQL with `given`-stage input rows within this transaction.
2905    ///
2906    /// Rows travel through the driver API, not the query string, so this
2907    /// path is only available on the band-9 (TypeDB 3.12+) driver. On a
2908    /// band-8 connection this returns an actionable error —
2909    /// callers that can fall back to per-row queries should consult the
2910    /// detected server version before choosing this path.
2911    pub fn query_with_rows(
2912        &mut self,
2913        typeql: &str,
2914        rows: GivenRowsSpec,
2915    ) -> BoxFuture<'_, Result<QueryResult>> {
2916        let typeql = typeql.to_owned();
2917        Box::pin(async move {
2918            let mut items = Vec::new();
2919            let mut collect = |item| {
2920                items.push(item);
2921                Ok(RuntimeAnswerControl::Continue)
2922            };
2923            let stats = self
2924                .query_with_rows_bounded(
2925                    &typeql,
2926                    rows,
2927                    RuntimeAnswerLimits::unbounded(),
2928                    &mut collect,
2929                )
2930                .await?;
2931            Ok(materialize_runtime_answer(stats, items))
2932        })
2933    }
2934
2935    /// Execute TypeQL with `given` rows and enforce bounds while polling the
2936    /// driver answer stream.
2937    pub fn query_with_rows_bounded<'a>(
2938        &'a mut self,
2939        typeql: &'a str,
2940        rows: GivenRowsSpec,
2941        limits: RuntimeAnswerLimits,
2942        consumer: &'a mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2943    ) -> BoxFuture<'a, Result<RuntimeAnswerStats>> {
2944        self.query_with_rows_bounded_with_scalar_encoding(
2945            typeql,
2946            rows,
2947            QueryV2RuntimeAnswerLimits {
2948                answer: limits,
2949                max_collection_members: u64::MAX,
2950            },
2951            ScalarJsonEncoding::DriverDisplay,
2952            consumer,
2953        )
2954    }
2955
2956    /// Execute a V2 `given` query with an additive collection-member limit.
2957    pub fn query_v2_with_rows_bounded<'a>(
2958        &'a mut self,
2959        typeql: &'a str,
2960        rows: GivenRowsSpec,
2961        limits: QueryV2RuntimeAnswerLimits,
2962        consumer: &'a mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2963    ) -> BoxFuture<'a, Result<RuntimeAnswerStats>> {
2964        self.query_with_rows_bounded_with_scalar_encoding(
2965            typeql,
2966            rows,
2967            limits,
2968            ScalarJsonEncoding::CanonicalV2,
2969            consumer,
2970        )
2971    }
2972
2973    fn query_with_rows_bounded_with_scalar_encoding<'a>(
2974        &'a mut self,
2975        typeql: &'a str,
2976        rows: GivenRowsSpec,
2977        limits: QueryV2RuntimeAnswerLimits,
2978        scalar_encoding: ScalarJsonEncoding,
2979        consumer: &'a mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
2980    ) -> BoxFuture<'a, Result<RuntimeAnswerStats>> {
2981        let QueryV2RuntimeAnswerLimits {
2982            answer: limits,
2983            max_collection_members,
2984        } = limits;
2985        let tql = typeql.to_owned();
2986        #[cfg(not(feature = "band9"))]
2987        let _ = (
2988            &tql,
2989            &rows,
2990            max_collection_members,
2991            scalar_encoding,
2992            &consumer,
2993        );
2994        Box::pin(async move {
2995            self.ensure_driver_open()?;
2996            runtime_check(&limits)?;
2997            match &self.inner {
2998                #[cfg(feature = "band8")]
2999                RuntimeTransactionInner::B8(_) => Err(RuntimeError::QueryExecution(
3000                    "given-stage parameterized queries require the band-9 driver \
3001                     (TypeDB 3.12+); this connection negotiated band 8"
3002                        .into(),
3003                )),
3004                #[cfg(feature = "band9")]
3005                RuntimeTransactionInner::B9(opt) => {
3006                    let tx = opt.as_ref().ok_or_else(|| {
3007                        RuntimeError::Transaction("Transaction already consumed".into())
3008                    })?;
3009                    let given_rows = given_rows_b9(rows)?;
3010                    let answer = runtime_await(
3011                        async {
3012                            let options = if limits.is_unbounded() {
3013                                driver_b9::QueryOptions::new()
3014                            } else {
3015                                driver_b9::QueryOptions::new()
3016                                    .prefetch_size(BOUNDED_QUERY_PREFETCH_SIZE)
3017                            };
3018                            tx.query_with_options_and_rows(&tql, options, Some(given_rows))
3019                                .await
3020                                .map_err(|e| RuntimeError::QueryExecution(format!("{e}")))
3021                        },
3022                        &limits,
3023                    )
3024                    .await?;
3025                    consume_answer_b9(
3026                        answer,
3027                        &limits,
3028                        max_collection_members,
3029                        scalar_encoding,
3030                        consumer,
3031                    )
3032                    .await
3033                }
3034            }
3035        })
3036    }
3037
3038    /// Commit this transaction.
3039    pub fn commit(&mut self) -> BoxFuture<'_, Result<()>> {
3040        let commit = self.commit_classified();
3041        Box::pin(async move { commit.await.map_err(RuntimeCommitError::into_runtime_error) })
3042    }
3043
3044    /// Commit this transaction while retaining driver durability certainty.
3045    pub fn commit_classified(
3046        &mut self,
3047    ) -> BoxFuture<'_, std::result::Result<(), RuntimeCommitError>> {
3048        // Take ownership out of the Option so the async block can move the
3049        // transaction by value into commit(self) — both bands consume self.
3050        let driver_lease = self.driver_lease.clone();
3051        match &mut self.inner {
3052            #[cfg(feature = "band8")]
3053            RuntimeTransactionInner::B8(opt) => {
3054                let tx = opt.take();
3055                Box::pin(async move {
3056                    if let Some(driver) = &driver_lease {
3057                        driver.ensure_open().map_err(RuntimeCommitError::Runtime)?;
3058                    }
3059                    let t = tx.ok_or_else(|| {
3060                        RuntimeCommitError::Runtime(RuntimeError::Transaction(
3061                            "Transaction already consumed".into(),
3062                        ))
3063                    })?;
3064                    t.commit().await.map_err(band8_commit_failure)
3065                })
3066            }
3067            #[cfg(feature = "band9")]
3068            RuntimeTransactionInner::B9(opt) => {
3069                let tx = opt.take();
3070                Box::pin(async move {
3071                    if let Some(driver) = &driver_lease {
3072                        driver.ensure_open().map_err(RuntimeCommitError::Runtime)?;
3073                    }
3074                    let t = tx.ok_or_else(|| {
3075                        RuntimeCommitError::Runtime(RuntimeError::Transaction(
3076                            "Transaction already consumed".into(),
3077                        ))
3078                    })?;
3079                    t.commit().await.map_err(band9_commit_failure)
3080                })
3081            }
3082        }
3083    }
3084
3085    /// Roll back this transaction.
3086    pub fn rollback(&mut self) -> BoxFuture<'_, Result<()>> {
3087        // Both bands expose rollback(&self), so we can move the value out of
3088        // the Option and call rollback on the owned T (the compiler
3089        // auto-borrows through the owned value for &self methods).
3090        let driver_lease = self.driver_lease.clone();
3091        match &mut self.inner {
3092            #[cfg(feature = "band8")]
3093            RuntimeTransactionInner::B8(opt) => {
3094                let tx = opt.take();
3095                Box::pin(async move {
3096                    if let Some(driver) = &driver_lease {
3097                        driver.ensure_open()?;
3098                    }
3099                    let t = tx.ok_or_else(|| {
3100                        RuntimeError::Transaction("Transaction already consumed".into())
3101                    })?;
3102                    if !t.is_open() {
3103                        return Ok(());
3104                    }
3105                    t.rollback()
3106                        .await
3107                        .map_err(|e| RuntimeError::Transaction(format!("Rollback failed: {e}")))
3108                })
3109            }
3110            #[cfg(feature = "band9")]
3111            RuntimeTransactionInner::B9(opt) => {
3112                let tx = opt.take();
3113                Box::pin(async move {
3114                    if let Some(driver) = &driver_lease {
3115                        driver.ensure_open()?;
3116                    }
3117                    let t = tx.ok_or_else(|| {
3118                        RuntimeError::Transaction("Transaction already consumed".into())
3119                    })?;
3120                    if !t.is_open() {
3121                        return Ok(());
3122                    }
3123                    t.rollback()
3124                        .await
3125                        .map_err(|e| RuntimeError::Transaction(format!("Rollback failed: {e}")))
3126                })
3127            }
3128        }
3129    }
3130
3131    /// Close this transaction without committing.
3132    pub fn close(&mut self) -> BoxFuture<'_, Result<()>> {
3133        let driver_lease = self.driver_lease.clone();
3134        match &mut self.inner {
3135            #[cfg(feature = "band8")]
3136            RuntimeTransactionInner::B8(opt) => {
3137                let tx = opt.take();
3138                Box::pin(async move {
3139                    let Some(t) = tx else {
3140                        return Ok(());
3141                    };
3142                    if Self::driver_shutdown_started(&driver_lease) {
3143                        drop(t);
3144                        return Ok(());
3145                    }
3146                    t.close()
3147                        .await
3148                        .map_err(|e| RuntimeError::Transaction(format!("Close failed: {e}")))
3149                })
3150            }
3151            #[cfg(feature = "band9")]
3152            RuntimeTransactionInner::B9(opt) => {
3153                let tx = opt.take();
3154                Box::pin(async move {
3155                    let Some(t) = tx else {
3156                        return Ok(());
3157                    };
3158                    if Self::driver_shutdown_started(&driver_lease) {
3159                        drop(t);
3160                        return Ok(());
3161                    }
3162                    t.close()
3163                        .await
3164                        .map_err(|e| RuntimeError::Transaction(format!("Close failed: {e}")))
3165                })
3166            }
3167        }
3168    }
3169}
3170
3171fn document_type_to_json(kind: &str, label: &str) -> serde_json::Value {
3172    serde_json::Value::Object(serde_json::Map::from_iter([
3173        (
3174            "kind".to_owned(),
3175            serde_json::Value::String(kind.to_owned()),
3176        ),
3177        (
3178            "label".to_owned(),
3179            serde_json::Value::String(label.to_owned()),
3180        ),
3181    ]))
3182}
3183
3184fn document_attribute_type_to_json(label: &str, value_type: Option<&str>) -> serde_json::Value {
3185    serde_json::Value::Object(serde_json::Map::from_iter([
3186        (
3187            "kind".to_owned(),
3188            serde_json::Value::String("attribute".to_owned()),
3189        ),
3190        (
3191            "label".to_owned(),
3192            serde_json::Value::String(label.to_owned()),
3193        ),
3194        (
3195            "valueType".to_owned(),
3196            serde_json::Value::String(value_type.unwrap_or("none").to_owned()),
3197        ),
3198    ]))
3199}
3200
3201fn runtime_document_member_error(message: &'static str) -> RuntimeError {
3202    RuntimeError::ResourceLimit {
3203        code: "query_v2_document_member_limit",
3204        message,
3205    }
3206}
3207
3208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3209enum ScalarJsonEncoding {
3210    DriverDisplay,
3211    CanonicalV2,
3212}
3213
3214fn canonical_decimal_text(display: String) -> Result<String> {
3215    type_bridge_core_lib::decimal::parse_decimal(&display)
3216        .map(|decimal| decimal.canonical_string())
3217        .ok_or_else(|| {
3218            RuntimeError::QueryExecution("provider returned an unparseable decimal".to_owned())
3219        })
3220}
3221
3222fn checked_datetime_tz_local<Tz>(
3223    value: &chrono::DateTime<Tz>,
3224) -> Result<(chrono::NaiveDateTime, i32)>
3225where
3226    Tz: chrono::TimeZone,
3227    Tz::Offset: chrono::Offset,
3228{
3229    use chrono::Offset as _;
3230
3231    let offset_seconds = value.offset().fix().local_minus_utc();
3232    let local = value
3233        .naive_utc()
3234        .checked_add_signed(chrono::TimeDelta::seconds(i64::from(offset_seconds)))
3235        .ok_or_else(|| {
3236            RuntimeError::QueryExecution(
3237                "provider datetime-tz local value is outside the supported range".to_owned(),
3238            )
3239        })?;
3240    Ok((local, offset_seconds))
3241}
3242
3243fn append_exact_offset(rendered: &mut String, offset_seconds: i64) {
3244    use std::fmt::Write as _;
3245
3246    if offset_seconds == 0 {
3247        rendered.push('Z');
3248        return;
3249    }
3250    let sign = if offset_seconds < 0 { '-' } else { '+' };
3251    let absolute = offset_seconds.unsigned_abs();
3252    let hours = absolute / 3_600;
3253    let minutes = (absolute % 3_600) / 60;
3254    let seconds = absolute % 60;
3255    write!(rendered, "{sign}{hours:02}:{minutes:02}")
3256        .expect("writing an offset to String cannot fail");
3257    if seconds != 0 {
3258        write!(rendered, ":{seconds:02}").expect("writing an offset to String cannot fail");
3259    }
3260}
3261
3262fn exact_naive_datetime(value: &chrono::NaiveDateTime) -> String {
3263    use std::fmt::Write as _;
3264
3265    use chrono::{Datelike as _, Timelike as _};
3266
3267    let mut rendered = String::new();
3268    let year = value.year();
3269    match year {
3270        0..=9999 => write!(rendered, "{year:04}"),
3271        10_000.. => write!(rendered, "+{year}"),
3272        -9999..=-1 => write!(rendered, "-{:04}", -year),
3273        _ => write!(rendered, "{year}"),
3274    }
3275    .expect("writing a year to String cannot fail");
3276    write!(
3277        rendered,
3278        "-{:02}-{:02}T{:02}:{:02}:{:02}",
3279        value.month(),
3280        value.day(),
3281        value.hour(),
3282        value.minute(),
3283        value.second(),
3284    )
3285    .expect("writing datetime components to String cannot fail");
3286    let nanosecond = value.nanosecond();
3287    if nanosecond != 0 {
3288        let fraction = format!("{nanosecond:09}");
3289        write!(rendered, ".{}", fraction.trim_end_matches('0'))
3290            .expect("writing a datetime fraction to String cannot fail");
3291    }
3292    rendered
3293}
3294
3295fn exact_duration(months: u32, days: u32, nanos: u64) -> String {
3296    use std::fmt::Write as _;
3297
3298    let seconds = nanos / 1_000_000_000;
3299    let nanosecond = nanos % 1_000_000_000;
3300    let mut rendered = String::from("P");
3301    if months != 0 {
3302        write!(rendered, "{months}M").expect("writing duration months to String cannot fail");
3303    }
3304    if days != 0 {
3305        write!(rendered, "{days}D").expect("writing duration days to String cannot fail");
3306    }
3307    if seconds != 0 || nanosecond != 0 || (months == 0 && days == 0) {
3308        write!(rendered, "T{seconds}").expect("writing duration seconds to String cannot fail");
3309        if nanosecond != 0 {
3310            let fraction = format!("{nanosecond:09}");
3311            write!(rendered, ".{}", fraction.trim_end_matches('0'))
3312                .expect("writing a duration fraction to String cannot fail");
3313        }
3314        rendered.push('S');
3315    }
3316    rendered
3317}
3318
3319macro_rules! define_driver_json_conversion {
3320    (
3321        $feature:literal,
3322        $driver:ident,
3323        $document_fn:ident,
3324        $node_fn:ident,
3325        $leaf_fn:ident,
3326        $value_fn:ident
3327    ) => {
3328        #[cfg(feature = $feature)]
3329        fn $document_fn(
3330            document: &$driver::answer::ConceptDocument,
3331            collection_members: &mut u64,
3332            max_collection_members: u64,
3333            scalar_encoding: ScalarJsonEncoding,
3334        ) -> Result<serde_json::Value> {
3335            document
3336                .root
3337                .as_ref()
3338                .map(|node| {
3339                    $node_fn(
3340                        node,
3341                        collection_members,
3342                        max_collection_members,
3343                        scalar_encoding,
3344                    )
3345                })
3346                .transpose()
3347                .map(|value| value.unwrap_or(serde_json::Value::Null))
3348        }
3349
3350        #[cfg(feature = $feature)]
3351        fn $node_fn(
3352            node: &$driver::answer::concept_document::Node,
3353            collection_members: &mut u64,
3354            max_collection_members: u64,
3355            scalar_encoding: ScalarJsonEncoding,
3356        ) -> Result<serde_json::Value> {
3357            use $driver::answer::concept_document::Node;
3358
3359            match node {
3360                Node::Map(map) => map
3361                    .iter()
3362                    .map(|(name, node)| {
3363                        $node_fn(
3364                            node,
3365                            collection_members,
3366                            max_collection_members,
3367                            scalar_encoding,
3368                        )
3369                        .map(|value| (name.clone(), value))
3370                    })
3371                    .collect::<Result<serde_json::Map<String, serde_json::Value>>>()
3372                    .map(serde_json::Value::Object),
3373                Node::List(list) => {
3374                    let members = u64::try_from(list.len()).map_err(|_| {
3375                        runtime_document_member_error(
3376                            "document list member count exceeds the supported counter range",
3377                        )
3378                    })?;
3379                    let next = collection_members.checked_add(members).ok_or_else(|| {
3380                        runtime_document_member_error("document list member counter overflowed")
3381                    })?;
3382                    if next > max_collection_members {
3383                        return Err(runtime_document_member_error(
3384                            "document lists exceed the aggregate member ceiling",
3385                        ));
3386                    }
3387                    *collection_members = next;
3388                    list.iter()
3389                        .map(|node| {
3390                            $node_fn(
3391                                node,
3392                                collection_members,
3393                                max_collection_members,
3394                                scalar_encoding,
3395                            )
3396                        })
3397                        .collect::<Result<Vec<_>>>()
3398                        .map(serde_json::Value::Array)
3399                }
3400                Node::Leaf(Some(leaf)) => $leaf_fn(leaf, scalar_encoding),
3401                Node::Leaf(None) => Ok(serde_json::Value::Null),
3402            }
3403        }
3404
3405        #[cfg(feature = $feature)]
3406        fn $leaf_fn(
3407            leaf: &$driver::answer::concept_document::Leaf,
3408            scalar_encoding: ScalarJsonEncoding,
3409        ) -> Result<serde_json::Value> {
3410            use $driver::answer::concept_document::Leaf;
3411            use $driver::concept::Concept;
3412
3413            match leaf {
3414                Leaf::Empty => Ok(serde_json::Value::Null),
3415                Leaf::Concept(concept) => match concept {
3416                    Concept::EntityType(_) => {
3417                        Ok(document_type_to_json("entity", concept.get_label()))
3418                    }
3419                    Concept::RelationType(_) => {
3420                        Ok(document_type_to_json("relation", concept.get_label()))
3421                    }
3422                    Concept::RoleType(_) => {
3423                        Ok(document_type_to_json("relation:role", concept.get_label()))
3424                    }
3425                    Concept::AttributeType(_) => {
3426                        let value_type = concept.try_get_value_type();
3427                        Ok(document_attribute_type_to_json(
3428                            concept.get_label(),
3429                            value_type.as_ref().map(|value_type| value_type.name()),
3430                        ))
3431                    }
3432                    Concept::Attribute(_) | Concept::Value(_) => {
3433                        let value = concept.try_get_value().ok_or_else(|| {
3434                            RuntimeError::QueryExecution(
3435                                "document value concept did not carry a value".to_owned(),
3436                            )
3437                        })?;
3438                        $value_fn(value, scalar_encoding)
3439                    }
3440                    Concept::Entity(_) | Concept::Relation(_) => Err(RuntimeError::QueryExecution(
3441                        "document response carried an unsupported thing instance".to_owned(),
3442                    )),
3443                },
3444                Leaf::ValueType(value_type) => {
3445                    Ok(serde_json::Value::String(value_type.name().to_owned()))
3446                }
3447                Leaf::Kind(kind) => Ok(serde_json::Value::String(kind.name().to_owned())),
3448            }
3449        }
3450
3451        #[cfg(feature = $feature)]
3452        fn $value_fn(
3453            value: &$driver::concept::Value,
3454            scalar_encoding: ScalarJsonEncoding,
3455        ) -> Result<serde_json::Value> {
3456            use $driver::concept::Value;
3457
3458            let converted = match value {
3459                Value::Boolean(value) => serde_json::Value::Bool(*value),
3460                Value::Integer(value) => serde_json::Value::from(*value),
3461                Value::Double(value) => serde_json::Value::from(*value),
3462                Value::String(value) => serde_json::Value::String(value.clone()),
3463                Value::Decimal(decimal) => {
3464                    let rendered = match scalar_encoding {
3465                        ScalarJsonEncoding::DriverDisplay => decimal.to_string(),
3466                        ScalarJsonEncoding::CanonicalV2 => {
3467                            canonical_decimal_text(decimal.to_string())?
3468                        }
3469                    };
3470                    serde_json::Value::String(rendered)
3471                }
3472                Value::Date(_) => serde_json::Value::String(value.to_string()),
3473                Value::Datetime(datetime) => {
3474                    let rendered = match scalar_encoding {
3475                        ScalarJsonEncoding::DriverDisplay => value.to_string(),
3476                        ScalarJsonEncoding::CanonicalV2 => exact_naive_datetime(datetime),
3477                    };
3478                    serde_json::Value::String(rendered)
3479                }
3480                Value::Duration(duration) => {
3481                    let rendered = match scalar_encoding {
3482                        ScalarJsonEncoding::DriverDisplay => value.to_string(),
3483                        ScalarJsonEncoding::CanonicalV2 => {
3484                            exact_duration(duration.months, duration.days, duration.nanos)
3485                        }
3486                    };
3487                    serde_json::Value::String(rendered)
3488                }
3489                Value::DatetimeTZ(datetime_tz) => {
3490                    let (local, offset_seconds) = checked_datetime_tz_local(datetime_tz)?;
3491                    let rendered = match scalar_encoding {
3492                        ScalarJsonEncoding::DriverDisplay => value.to_string(),
3493                        ScalarJsonEncoding::CanonicalV2 => {
3494                            let mut rendered = exact_naive_datetime(&local);
3495                            append_exact_offset(&mut rendered, i64::from(offset_seconds));
3496                            if let $driver::concept::value::TimeZone::IANA(timezone) =
3497                                datetime_tz.timezone()
3498                            {
3499                                use std::fmt::Write as _;
3500                                write!(rendered, "[{}]", timezone.name())
3501                                    .expect("writing a timezone name to String cannot fail");
3502                            }
3503                            rendered
3504                        }
3505                    };
3506                    serde_json::Value::String(rendered)
3507                }
3508                Value::Struct(value, name) => {
3509                    let fields = value
3510                        .fields()
3511                        .iter()
3512                        .map(|(field, value)| -> Result<_> {
3513                            Ok((
3514                                field.clone(),
3515                                value
3516                                    .as_ref()
3517                                    .map(|value| $value_fn(value, scalar_encoding))
3518                                    .transpose()?
3519                                    .unwrap_or(serde_json::Value::Null),
3520                            ))
3521                        })
3522                        .collect::<Result<_>>()?;
3523                    serde_json::Value::Object(serde_json::Map::from_iter([(
3524                        name.clone(),
3525                        serde_json::Value::Object(fields),
3526                    )]))
3527                }
3528            };
3529            Ok(converted)
3530        }
3531    };
3532}
3533
3534define_driver_json_conversion!(
3535    "band8",
3536    driver_b8,
3537    document_to_json_b8,
3538    document_node_to_json_b8,
3539    document_leaf_to_json_b8,
3540    value_to_json_b8
3541);
3542define_driver_json_conversion!(
3543    "band9",
3544    driver_b9,
3545    document_to_json_b9,
3546    document_node_to_json_b9,
3547    document_leaf_to_json_b9,
3548    value_to_json_b9
3549);
3550
3551/// Convert a band-8 TypeDB concept to a JSON value.
3552#[cfg(feature = "band8")]
3553fn concept_to_json_b8(
3554    concept: &type_bridge_typedb_driver_b8::concept::Concept,
3555    scalar_encoding: ScalarJsonEncoding,
3556) -> Result<serde_json::Value> {
3557    let mut obj = serde_json::Map::new();
3558    obj.insert(
3559        "category".into(),
3560        serde_json::Value::String(concept.get_category().name().into()),
3561    );
3562    obj.insert(
3563        "label".into(),
3564        serde_json::Value::String(concept.get_label().into()),
3565    );
3566    if let Some(iid) = concept.try_get_iid() {
3567        obj.insert("iid".into(), serde_json::Value::String(iid.to_string()));
3568    }
3569    if let Some(value) = concept.try_get_value() {
3570        obj.insert("value".into(), value_to_json_b8(value, scalar_encoding)?);
3571    }
3572    if let Some(vt) = concept.try_get_value_type() {
3573        obj.insert(
3574            "value_type".into(),
3575            serde_json::Value::String(vt.name().into()),
3576        );
3577    }
3578    Ok(serde_json::Value::Object(obj))
3579}
3580
3581/// Lower a portable [`GivenRowsSpec`] onto the band-9 driver's `GivenRows`.
3582///
3583/// Fails when a row's width does not match the header or a temporal string
3584/// does not parse — both are caller mistakes reported before any wire I/O.
3585#[cfg(feature = "band9")]
3586fn given_rows_b9(spec: GivenRowsSpec) -> Result<typedb_driver::given::GivenRows> {
3587    use typedb_driver::given::GivenRows;
3588
3589    let mut given = GivenRows::new(spec.variables, spec.rows.len());
3590    for row in spec.rows {
3591        let entries = row
3592            .into_iter()
3593            .map(given_entry_b9)
3594            .collect::<Result<Vec<_>>>()?;
3595        given
3596            .push_row(entries)
3597            .map_err(|e| RuntimeError::QueryExecution(format!("Invalid given row: {e}")))?;
3598    }
3599    Ok(given)
3600}
3601
3602/// Convert one portable [`GivenValue`] to a band-9 driver row entry.
3603#[cfg(feature = "band9")]
3604fn given_entry_b9(value: GivenValue) -> Result<typedb_driver::given::GivenRowEntry> {
3605    use chrono::{NaiveDate, NaiveDateTime};
3606    use typedb_driver::concept::value::TimeZone as B9TimeZone;
3607    use typedb_driver::concept::value::{Decimal, Duration};
3608    use typedb_driver::given::GivenRowEntry;
3609
3610    Ok(match value {
3611        GivenValue::Empty => GivenRowEntry::Empty,
3612        GivenValue::Boolean(b) => GivenRowEntry::from(b),
3613        GivenValue::Integer(i) => GivenRowEntry::from(i),
3614        GivenValue::Double(d) => GivenRowEntry::from(d),
3615        GivenValue::String(s) => GivenRowEntry::from(s),
3616        GivenValue::Date(s) => {
3617            let date = s.parse::<NaiveDate>().map_err(|e| {
3618                RuntimeError::QueryExecution(format!("Invalid given date {s:?}: {e}"))
3619            })?;
3620            GivenRowEntry::from(date)
3621        }
3622        GivenValue::Datetime(s) => {
3623            let dt = s.parse::<NaiveDateTime>().map_err(|e| {
3624                RuntimeError::QueryExecution(format!("Invalid given datetime {s:?}: {e}"))
3625            })?;
3626            GivenRowEntry::from(dt)
3627        }
3628        GivenValue::DatetimeTz(s) => {
3629            let dt = parse_given_datetime_tz(&s).map_err(|e| {
3630                RuntimeError::QueryExecution(format!("Invalid given datetime-tz {s:?}: {e}"))
3631            })?;
3632            let offset = *dt.offset();
3633            GivenRowEntry::from(dt.with_timezone(&B9TimeZone::Fixed(offset)))
3634        }
3635        GivenValue::DatetimeTzExact {
3636            local,
3637            named_zone,
3638            effective_offset_seconds,
3639        } => GivenRowEntry::from(exact_given_datetime_tz_b9(
3640            &local,
3641            named_zone.as_deref(),
3642            effective_offset_seconds,
3643        )?),
3644        GivenValue::Decimal(s) => {
3645            // The upstream parser parses the unsigned magnitude into `i64`,
3646            // so the otherwise-valid decimal minimum cannot pass through it.
3647            // Preserve that one boundary explicitly; every other canonical
3648            // decimal spelling is accepted by the driver parser.
3649            let decimal = if s == "-9223372036854775808" {
3650                Decimal::MIN
3651            } else {
3652                s.parse::<Decimal>().map_err(|e| {
3653                    RuntimeError::QueryExecution(format!("Invalid given decimal {s:?}: {e}"))
3654                })?
3655            };
3656            GivenRowEntry::from(decimal)
3657        }
3658        GivenValue::Duration {
3659            months,
3660            days,
3661            nanos,
3662        } => GivenRowEntry::from(Duration::new(months, days, nanos)),
3663    })
3664}
3665
3666#[cfg(feature = "band9")]
3667fn exact_given_datetime_tz_b9(
3668    local: &str,
3669    named_zone: Option<&str>,
3670    effective_offset_seconds: i32,
3671) -> Result<chrono::DateTime<typedb_driver::concept::value::TimeZone>> {
3672    use chrono::{FixedOffset, LocalResult, NaiveDateTime, TimeZone as _};
3673    use typedb_driver::concept::value::TimeZone as B9TimeZone;
3674
3675    let local = local.parse::<NaiveDateTime>().map_err(|error| {
3676        RuntimeError::QueryExecution(format!("Invalid given exact datetime-tz local: {error}"))
3677    })?;
3678    let matches_offset = |value: &chrono::DateTime<B9TimeZone>| {
3679        value
3680            .naive_local()
3681            .signed_duration_since(value.naive_utc())
3682            .num_seconds()
3683            == i64::from(effective_offset_seconds)
3684    };
3685    if let Some(name) = named_zone {
3686        let timezone = chrono_tz::Tz::from_str_insensitive(name)
3687            .map(B9TimeZone::IANA)
3688            .map_err(|_| {
3689                RuntimeError::QueryExecution(
3690                    "Invalid given exact datetime-tz named zone".to_owned(),
3691                )
3692            })?;
3693        let selected = match timezone.from_local_datetime(&local) {
3694            LocalResult::Single(value) if matches_offset(&value) => Some(value),
3695            LocalResult::Ambiguous(earlier, later) => [earlier, later]
3696                .into_iter()
3697                .find(|value| matches_offset(value)),
3698            LocalResult::Single(_) | LocalResult::None => None,
3699        };
3700        return selected.ok_or_else(|| {
3701            RuntimeError::QueryExecution(
3702                "Invalid given exact datetime-tz named-zone resolution".to_owned(),
3703            )
3704        });
3705    }
3706
3707    let offset = FixedOffset::east_opt(effective_offset_seconds).ok_or_else(|| {
3708        RuntimeError::QueryExecution("Invalid given exact datetime-tz fixed offset".to_owned())
3709    })?;
3710    B9TimeZone::Fixed(offset)
3711        .from_local_datetime(&local)
3712        .single()
3713        .ok_or_else(|| {
3714            RuntimeError::QueryExecution(
3715                "Invalid given exact datetime-tz fixed resolution".to_owned(),
3716            )
3717        })
3718}
3719
3720/// Parse the fixed-offset spelling used by the portable V2 contract.
3721///
3722/// Chrono's RFC-3339 parser deliberately accepts only four-digit years and
3723/// minute-resolution offsets. TypeDB's scalar domain is wider, so parse the
3724/// canonical local datetime and an optional seconds component separately.
3725#[cfg(feature = "band9")]
3726fn parse_given_datetime_tz(
3727    value: &str,
3728) -> std::result::Result<chrono::DateTime<chrono::FixedOffset>, &'static str> {
3729    use chrono::{FixedOffset, NaiveDateTime, TimeZone as _};
3730
3731    let (local, offset_seconds) = if let Some(local) = value.strip_suffix('Z') {
3732        (local, 0)
3733    } else {
3734        let (local, offset) = [9_usize, 6]
3735            .into_iter()
3736            .find_map(|width| {
3737                let split = value.len().checked_sub(width)?;
3738                if !value.is_char_boundary(split) {
3739                    return None;
3740                }
3741                let (local, offset) = (&value[..split], &value[split..]);
3742                parse_fixed_offset(offset).map(|seconds| (local, seconds))
3743            })
3744            .ok_or("expected a Z or signed fixed offset")?;
3745        (local, offset)
3746    };
3747    let local = local
3748        .parse::<NaiveDateTime>()
3749        .map_err(|_| "invalid local datetime")?;
3750    let offset = FixedOffset::east_opt(offset_seconds).ok_or("fixed offset is out of range")?;
3751    offset
3752        .from_local_datetime(&local)
3753        .single()
3754        .ok_or("local datetime is out of range")
3755}
3756
3757#[cfg(feature = "band9")]
3758fn parse_fixed_offset(value: &str) -> Option<i32> {
3759    let bytes = value.as_bytes();
3760    if !matches!(
3761        bytes,
3762        [b'+' | b'-', _, _, b':', _, _] | [b'+' | b'-', _, _, b':', _, _, b':', _, _]
3763    ) || !bytes
3764        .iter()
3765        .enumerate()
3766        .filter(|(index, _)| !matches!(index, 0 | 3 | 6))
3767        .all(|(_, byte)| byte.is_ascii_digit())
3768    {
3769        return None;
3770    }
3771    let component = |left: usize, right: usize| {
3772        std::str::from_utf8(&bytes[left..right])
3773            .ok()?
3774            .parse::<i32>()
3775            .ok()
3776    };
3777    let hours = component(1, 3)?;
3778    let minutes = component(4, 6)?;
3779    let seconds = if bytes.len() == 9 {
3780        component(7, 9)?
3781    } else {
3782        0
3783    };
3784    if hours > 23 || minutes > 59 || seconds > 59 {
3785        return None;
3786    }
3787    let magnitude = hours * 3_600 + minutes * 60 + seconds;
3788    Some(if bytes[0] == b'-' {
3789        -magnitude
3790    } else {
3791        magnitude
3792    })
3793}
3794
3795/// Convert and consume a band-9 answer without materializing its stream.
3796#[cfg(feature = "band9")]
3797async fn consume_answer_b9(
3798    answer: B9QueryAnswer,
3799    limits: &RuntimeAnswerLimits,
3800    max_collection_members: u64,
3801    scalar_encoding: ScalarJsonEncoding,
3802    consumer: &mut (dyn FnMut(RuntimeAnswerItem) -> Result<RuntimeAnswerControl> + Send),
3803) -> Result<RuntimeAnswerStats> {
3804    match answer {
3805        B9QueryAnswer::Ok(_) => Ok(RuntimeAnswerStats::new(RuntimeAnswerKind::Ok)),
3806        B9QueryAnswer::ConceptRowStream(_, stream) => {
3807            let stream = stream
3808                .map_err(|error| RuntimeError::QueryExecution(format!("Row stream: {error}")))
3809                .and_then(move |row| {
3810                    let result = (|| -> Result<RuntimeAnswerItem> {
3811                        let mut obj = serde_json::Map::new();
3812                        for (i, col) in row.get_column_names().iter().enumerate() {
3813                            let value = row
3814                                .row
3815                                .get(i)
3816                                .and_then(|c| c.as_ref())
3817                                .map(|concept| concept_to_json_b9(concept, scalar_encoding))
3818                                .transpose()?
3819                                .unwrap_or(serde_json::Value::Null);
3820                            obj.insert(col.clone(), value);
3821                        }
3822                        Ok(RuntimeAnswerItem::Row(serde_json::Value::Object(obj)))
3823                    })();
3824                    futures::future::ready(result)
3825                });
3826            runtime_consume_stream(stream, RuntimeAnswerKind::Rows, limits, consumer).await
3827        }
3828        B9QueryAnswer::ConceptDocumentStream(_, stream) => {
3829            let mut collection_members = 0_u64;
3830            let stream = stream
3831                .map_err(|error| RuntimeError::QueryExecution(format!("Document stream: {error}")))
3832                .and_then(move |document| {
3833                    let result = document_to_json_b9(
3834                        &document,
3835                        &mut collection_members,
3836                        max_collection_members,
3837                        scalar_encoding,
3838                    )
3839                    .map(RuntimeAnswerItem::Document);
3840                    futures::future::ready(result)
3841                });
3842            runtime_consume_stream(stream, RuntimeAnswerKind::Documents, limits, consumer).await
3843        }
3844    }
3845}
3846
3847/// Convert a band-9 TypeDB concept to a JSON value.
3848///
3849/// Output shape is identical to [`concept_to_json_b8`] for all common concepts.
3850#[cfg(feature = "band9")]
3851fn concept_to_json_b9(
3852    concept: &typedb_driver::concept::Concept,
3853    scalar_encoding: ScalarJsonEncoding,
3854) -> Result<serde_json::Value> {
3855    let mut obj = serde_json::Map::new();
3856    obj.insert(
3857        "category".into(),
3858        serde_json::Value::String(concept.get_category().name().into()),
3859    );
3860    obj.insert(
3861        "label".into(),
3862        serde_json::Value::String(concept.get_label().into()),
3863    );
3864    if let Some(iid) = concept.try_get_iid() {
3865        obj.insert("iid".into(), serde_json::Value::String(iid.to_string()));
3866    }
3867    if let Some(value) = concept.try_get_value() {
3868        obj.insert("value".into(), value_to_json_b9(value, scalar_encoding)?);
3869    }
3870    if let Some(vt) = concept.try_get_value_type() {
3871        obj.insert(
3872            "value_type".into(),
3873            serde_json::Value::String(vt.name().into()),
3874        );
3875    }
3876    Ok(serde_json::Value::Object(obj))
3877}
3878
3879#[cfg(test)]
3880mod tests {
3881    use super::*;
3882    use std::io;
3883    use std::sync::atomic::{AtomicUsize, Ordering};
3884    use std::sync::{Arc, Mutex};
3885
3886    static NEXT_TLS_MATERIAL_TEST_ID: AtomicUsize = AtomicUsize::new(0);
3887    static TRACE_CAPTURE_LOCK: Mutex<()> = Mutex::new(());
3888
3889    #[derive(Clone, Default)]
3890    struct TraceCapture {
3891        bytes: Arc<Mutex<Vec<u8>>>,
3892    }
3893
3894    struct TraceCaptureWriter {
3895        bytes: Arc<Mutex<Vec<u8>>>,
3896    }
3897
3898    impl io::Write for TraceCaptureWriter {
3899        fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
3900            self.bytes
3901                .lock()
3902                .expect("trace capture lock")
3903                .extend_from_slice(buffer);
3904            Ok(buffer.len())
3905        }
3906
3907        fn flush(&mut self) -> io::Result<()> {
3908            Ok(())
3909        }
3910    }
3911
3912    impl<'writer> tracing_subscriber::fmt::MakeWriter<'writer> for TraceCapture {
3913        type Writer = TraceCaptureWriter;
3914
3915        fn make_writer(&'writer self) -> Self::Writer {
3916            TraceCaptureWriter {
3917                bytes: Arc::clone(&self.bytes),
3918            }
3919        }
3920    }
3921
3922    fn capture_traces(emit: impl FnOnce()) -> String {
3923        let _capture_guard = TRACE_CAPTURE_LOCK.lock().expect("trace capture suite lock");
3924        let capture = TraceCapture::default();
3925        let subscriber = tracing_subscriber::fmt()
3926            .without_time()
3927            .with_ansi(false)
3928            .with_target(false)
3929            .with_max_level(tracing::Level::TRACE)
3930            .with_writer(capture.clone())
3931            .finish();
3932        tracing::subscriber::with_default(subscriber, emit);
3933        let bytes = capture.bytes.lock().expect("trace capture lock").clone();
3934        String::from_utf8(bytes).expect("tracing formatter emits UTF-8")
3935    }
3936
3937    #[test]
3938    fn driver_drop_close_warning_drops_hostile_provider_text() {
3939        const PROVIDER_SENTINEL: &str = "TB_DROP_CLOSE_PROVIDER_SECRET";
3940        let error = RuntimeError::Connection(PROVIDER_SENTINEL.to_owned());
3941        let output = capture_traces(|| trace_driver_drop_close_failure(&error));
3942
3943        assert!(
3944            output.contains(TRACE_CODE_DRIVER_DROP_CLOSE_FAILED),
3945            "{output}"
3946        );
3947        assert!(
3948            output.contains("typedb_runtime_connection_failed"),
3949            "{output}"
3950        );
3951        assert!(!output.contains(PROVIDER_SENTINEL), "{output}");
3952    }
3953
3954    #[cfg(all(feature = "band8", feature = "band9"))]
3955    #[test]
3956    fn band9_upgrade_failure_warning_drops_hostile_provider_and_address_text() {
3957        const ADDRESS_SENTINEL: &str = "TB_BAND9_UPGRADE_ADDRESS_SECRET";
3958        const PROVIDER_SENTINEL: &str = "TB_BAND9_UPGRADE_PROVIDER_SECRET";
3959        let error =
3960            SecureConnectError::Runtime(RuntimeError::Connection(PROVIDER_SENTINEL.to_owned()));
3961        let output = capture_traces(|| {
3962            trace_band9_upgrade_failed(
3963                ADDRESS_SENTINEL,
3964                core_version::Version::new(3, 12, 1),
3965                &error,
3966            );
3967        });
3968
3969        assert!(output.contains(TRACE_CODE_BAND9_UPGRADE_FAILED), "{output}");
3970        assert!(
3971            output.contains("typedb_runtime_connection_failed"),
3972            "{output}"
3973        );
3974        assert!(output.contains("3.12.1"), "{output}");
3975        assert!(!output.contains(ADDRESS_SENTINEL), "{output}");
3976        assert!(!output.contains(PROVIDER_SENTINEL), "{output}");
3977    }
3978
3979    #[test]
3980    fn post_credential_connection_traces_drop_raw_address_identity() {
3981        const ADDRESS_SENTINEL: &str = "TB_POST_CREDENTIAL_ADDRESS_SECRET";
3982        let server_version = core_version::Version::new(3, 12, 1);
3983        let output = capture_traces(|| {
3984            trace_version_gate_passed(ADDRESS_SENTINEL, 9, server_version);
3985            #[cfg(all(feature = "band8", feature = "band9"))]
3986            trace_band9_upgrade_succeeded(ADDRESS_SENTINEL, server_version);
3987            #[cfg(feature = "band8")]
3988            trace_band8_fallback_connected(ADDRESS_SENTINEL, server_version);
3989            trace_runtime_connected(ADDRESS_SENTINEL, 9, Some(server_version));
3990        });
3991
3992        assert!(!output.contains(ADDRESS_SENTINEL), "{output}");
3993        assert!(output.contains(TRACE_CODE_VERSION_GATE_PASSED), "{output}");
3994        assert!(output.contains(TRACE_CODE_CONNECTED), "{output}");
3995        assert!(output.contains("3.12.1"), "{output}");
3996        #[cfg(all(feature = "band8", feature = "band9"))]
3997        assert!(
3998            output.contains(TRACE_CODE_BAND9_UPGRADE_SUCCEEDED),
3999            "{output}"
4000        );
4001        #[cfg(feature = "band8")]
4002        assert!(
4003            output.contains(TRACE_CODE_BAND8_FALLBACK_CONNECTED),
4004            "{output}"
4005        );
4006    }
4007
4008    #[test]
4009    fn retained_server_trace_is_warning_free() {
4010        for (driver_band, server_version) in [
4011            (8, Some(core_version::Version::new(3, 11, 5))),
4012            (9, Some(core_version::Version::new(3, 12, 1))),
4013            // A 3.12 server may retain the validated band-8 fallback after
4014            // discovery. Classification keys on the known server line, not
4015            // the negotiated driver band.
4016            (8, Some(core_version::Version::new(3, 12, 1))),
4017            (8, None),
4018            (9, None),
4019        ] {
4020            let output =
4021                capture_traces(|| trace_runtime_connected("redacted", driver_band, server_version));
4022            assert!(output.contains(TRACE_CODE_CONNECTED), "{output}");
4023            assert!(!output.contains("WARN"), "{output}");
4024            assert!(!output.contains("deprecated"), "{output}");
4025        }
4026    }
4027
4028    #[test]
4029    fn credential_safe_secure_diagnostics_drop_provider_controlled_text() {
4030        const SENTINEL: &str = "TB_POST_CREDENTIAL_PROVIDER_SECRET";
4031        for error in [
4032            SecureConnectError::Runtime(RuntimeError::Connection(SENTINEL.to_owned())),
4033            SecureConnectError::Runtime(RuntimeError::UnsupportedVersion(
4034                core_version::VersionError::Probe(SENTINEL.to_owned()),
4035            )),
4036            SecureConnectError::Runtime(RuntimeError::UnsupportedVersion(
4037                core_version::VersionError::Parse(SENTINEL.to_owned()),
4038            )),
4039        ] {
4040            assert_eq!(error.credential_safe_diagnostic(), None);
4041        }
4042
4043        let tls = SecureConnectError::TlsConfiguration(
4044            core_version::TlsConfigurationError::CustomRootCaUnreadable {
4045                path: std::path::PathBuf::from(SENTINEL),
4046            },
4047        )
4048        .credential_safe_diagnostic()
4049        .expect("typed TLS codes remain safe");
4050        assert!(tls.contains("tls_custom_root_ca_unreadable"), "{tls}");
4051        assert!(!tls.contains(SENTINEL), "{tls}");
4052    }
4053
4054    #[test]
4055    fn credential_safe_secure_diagnostics_preserve_closed_version_data() {
4056        let error = SecureConnectError::Runtime(RuntimeError::UnsupportedVersion(
4057            core_version::VersionError::Unsupported {
4058                component: "server",
4059                found: core_version::Version::new(3, 13, 0),
4060            },
4061        ));
4062        let diagnostic = error
4063            .credential_safe_diagnostic()
4064            .expect("closed version diagnostics remain actionable");
4065        assert!(diagnostic.contains("server version 3.13.0"), "{diagnostic}");
4066        assert!(diagnostic.contains("3.11.0–3.12.x"), "{diagnostic}");
4067    }
4068
4069    #[test]
4070    fn driver_shutdown_panics_are_contained_as_connection_errors() {
4071        let error = contain_driver_shutdown(|| panic!("simulated upstream shutdown panic"))
4072            .expect_err("shutdown panic must not cross the runtime boundary");
4073
4074        assert!(matches!(
4075            error,
4076            RuntimeError::Connection(message)
4077                if message == "Driver close failed: upstream driver panicked during shutdown"
4078        ));
4079    }
4080
4081    #[test]
4082    fn failed_driver_shutdown_is_retried_without_reopening_admission() {
4083        let state = AtomicU8::new(DRIVER_OPEN);
4084        let close_lock = Mutex::new(());
4085
4086        for _ in 0..2 {
4087            let error = run_driver_shutdown(&state, &close_lock, || {
4088                Err(RuntimeError::Connection(
4089                    "Driver close failed: simulated incomplete cleanup".to_owned(),
4090                ))
4091            })
4092            .expect_err("incomplete cleanup must remain observable");
4093            assert_eq!(
4094                error.to_string(),
4095                "Connection error: Driver close failed: simulated incomplete cleanup"
4096            );
4097            assert_eq!(state.load(AtomicOrdering::Acquire), DRIVER_CLOSING);
4098        }
4099
4100        run_driver_shutdown(&state, &close_lock, || Ok(()))
4101            .expect("a later close retries and completes cleanup");
4102        assert_eq!(state.load(AtomicOrdering::Acquire), DRIVER_CLOSED);
4103
4104        run_driver_shutdown(&state, &close_lock, || {
4105            panic!("successful cleanup must make later close calls no-ops")
4106        })
4107        .expect("close remains idempotent after cleanup succeeds");
4108    }
4109
4110    #[test]
4111    fn transaction_retention_and_shutdown_share_one_admission_lock() {
4112        let state = AtomicU8::new(DRIVER_OPEN);
4113        let active_transactions = AtomicUsize::new(0);
4114        let close_lock = Mutex::new(());
4115
4116        retain_driver_transaction(&state, &active_transactions, &close_lock)
4117            .expect("an open driver admits a transaction lease");
4118        assert_eq!(active_transactions.load(AtomicOrdering::Acquire), 1);
4119
4120        let _close_guard = close_lock.lock().expect("admission lock remains usable");
4121        state.store(DRIVER_CLOSING, AtomicOrdering::Release);
4122        drop(_close_guard);
4123
4124        let error = retain_driver_transaction(&state, &active_transactions, &close_lock)
4125            .expect_err("shutdown must make later transaction retention terminal");
4126        assert!(matches!(error, RuntimeError::Connection(_)));
4127        assert_eq!(active_transactions.load(AtomicOrdering::Acquire), 1);
4128    }
4129
4130    #[test]
4131    fn one_shot_database_operation_propagates_close_failure_after_success() {
4132        assert_eq!(
4133            finish_one_shot_database_operation(Ok(17_u8), Ok(())).unwrap(),
4134            17
4135        );
4136
4137        let error = finish_one_shot_database_operation(
4138            Ok(17_u8),
4139            Err(RuntimeError::Connection("close diagnosis".to_owned())),
4140        )
4141        .expect_err("a successful operation must still report driver-close failure");
4142        assert!(matches!(
4143            error,
4144            RuntimeError::Connection(message) if message == "close diagnosis"
4145        ));
4146    }
4147
4148    #[test]
4149    fn one_shot_database_operation_preserves_primary_failure_and_combines_close_failure() {
4150        let primary_only = finish_one_shot_database_operation::<()>(
4151            Err(RuntimeError::Connection("primary diagnosis".to_owned())),
4152            Ok(()),
4153        )
4154        .expect_err("the primary database-operation failure must be returned");
4155        assert!(matches!(
4156            primary_only,
4157            RuntimeError::Connection(message) if message == "primary diagnosis"
4158        ));
4159
4160        let combined = finish_one_shot_database_operation::<()>(
4161            Err(RuntimeError::Connection("primary diagnosis".to_owned())),
4162            Err(RuntimeError::Connection("close diagnosis".to_owned())),
4163        )
4164        .expect_err("both failures must produce one deterministic diagnostic");
4165        assert!(matches!(
4166            combined,
4167            RuntimeError::Connection(message)
4168                if message
4169                    == "primary diagnosis; additionally, TypeDB driver cleanup failed: close diagnosis"
4170        ));
4171    }
4172
4173    macro_rules! row_and_document_scalar_regression {
4174        ($feature:literal, $name:ident, $driver:ident, $concept_fn:ident, $node_fn:ident, $decimal:expr) => {
4175            #[cfg(feature = $feature)]
4176            #[test]
4177            fn $name() {
4178                use $driver::answer::concept_document::{Leaf, Node};
4179                use $driver::concept::{Concept, Value};
4180
4181                const LARGE_INTEGER: i64 = 9_007_199_254_740_993;
4182                let integer = Node::Leaf(Some(Leaf::Concept(Concept::Value(Value::Integer(
4183                    LARGE_INTEGER,
4184                )))));
4185                let mut collection_members = 0;
4186                assert_eq!(
4187                    $node_fn(
4188                        &integer,
4189                        &mut collection_members,
4190                        u64::MAX,
4191                        ScalarJsonEncoding::CanonicalV2,
4192                    )
4193                    .unwrap(),
4194                    serde_json::Value::from(LARGE_INTEGER),
4195                    "concept-document integers must not cross an f64 boundary"
4196                );
4197
4198                let decimal = $decimal;
4199                let scalar_text = |value: serde_json::Value| {
4200                    value
4201                        .get("value")
4202                        .and_then(serde_json::Value::as_str)
4203                        .or_else(|| value.as_str())
4204                        .map(str::to_owned)
4205                        .expect("decimal conversion must expose scalar text")
4206                };
4207                let legacy_expected = decimal.to_string();
4208                let expected = "1234.56";
4209                let decimal =
4210                    Node::Leaf(Some(Leaf::Concept(Concept::Value(Value::Decimal(decimal)))));
4211                let row_decimal = Concept::Value(Value::Decimal($decimal));
4212                assert_eq!(
4213                    scalar_text(
4214                        $node_fn(
4215                            &decimal,
4216                            &mut collection_members,
4217                            u64::MAX,
4218                            ScalarJsonEncoding::CanonicalV2,
4219                        )
4220                        .unwrap()
4221                    ),
4222                    expected,
4223                    "concept-document decimals must use canonical strings"
4224                );
4225                assert_eq!(
4226                    scalar_text(
4227                        $concept_fn(&row_decimal, ScalarJsonEncoding::CanonicalV2).unwrap()
4228                    ),
4229                    expected
4230                );
4231                assert_eq!(
4232                    scalar_text(
4233                        $concept_fn(&row_decimal, ScalarJsonEncoding::DriverDisplay).unwrap()
4234                    ),
4235                    legacy_expected.clone()
4236                );
4237                let legacy_decimal = Node::Leaf(Some(Leaf::Concept(Concept::Value(
4238                    Value::Decimal($decimal),
4239                ))));
4240                assert_eq!(
4241                    scalar_text(
4242                        $node_fn(
4243                            &legacy_decimal,
4244                            &mut collection_members,
4245                            u64::MAX,
4246                            ScalarJsonEncoding::DriverDisplay,
4247                        )
4248                        .unwrap()
4249                    ),
4250                    legacy_expected,
4251                    "legacy document decimals must retain driver spelling"
4252                );
4253            }
4254        };
4255    }
4256
4257    row_and_document_scalar_regression!(
4258        "band8",
4259        band8_rows_and_documents_preserve_lossless_scalars,
4260        driver_b8,
4261        concept_to_json_b8,
4262        document_node_to_json_b8,
4263        driver_b8::concept::value::Decimal::new(1234, 5_600_000_000_000_000_000)
4264    );
4265    row_and_document_scalar_regression!(
4266        "band9",
4267        band9_rows_and_documents_preserve_lossless_scalars,
4268        driver_b9,
4269        concept_to_json_b9,
4270        document_node_to_json_b9,
4271        driver_b9::concept::value::Decimal::from_parts(1234, 5_600_000_000_000_000_000)
4272    );
4273
4274    macro_rules! datetime_tz_evidence_regression {
4275        ($feature:literal, $name:ident, $driver:ident, $concept_fn:ident, $leaf_fn:ident) => {
4276            #[cfg(feature = $feature)]
4277            #[test]
4278            fn $name() {
4279                use chrono::{NaiveDateTime, TimeZone as _};
4280                use $driver::answer::concept_document::Leaf;
4281                use $driver::concept::value::TimeZone;
4282                use $driver::concept::{Concept, Value};
4283
4284                let london = TimeZone::IANA(
4285                    "Europe/London"
4286                        .parse()
4287                        .expect("driver timezone database contains London"),
4288                );
4289                let overlap_utc = "2024-10-27T00:30:00"
4290                    .parse::<NaiveDateTime>()
4291                    .expect("UTC datetime");
4292                let named = Value::DatetimeTZ(london.from_utc_datetime(&overlap_utc));
4293                assert_eq!(
4294                    $concept_fn(
4295                        &Concept::Value(named.clone()),
4296                        ScalarJsonEncoding::CanonicalV2,
4297                    )
4298                    .expect("row concept")["value"],
4299                    serde_json::json!("2024-10-27T01:30:00+01:00[Europe/London]"),
4300                    "row evidence preserves both the IANA identity and overlap side",
4301                );
4302                assert_eq!(
4303                    $leaf_fn(
4304                        &Leaf::Concept(Concept::Value(named.clone())),
4305                        ScalarJsonEncoding::CanonicalV2,
4306                    )
4307                    .expect("document leaf"),
4308                    serde_json::json!("2024-10-27T01:30:00+01:00[Europe/London]"),
4309                    "document evidence uses the same exact datetime-tz bridge",
4310                );
4311                assert_eq!(
4312                    $concept_fn(&Concept::Value(named), ScalarJsonEncoding::DriverDisplay,)
4313                        .expect("row concept")["value"],
4314                    serde_json::json!("2024-10-27T01:30:00.000000000 Europe/London"),
4315                    "released conversion retains the upstream driver display",
4316                );
4317
4318                let fixed = TimeZone::Fixed(
4319                    chrono::FixedOffset::east_opt(1_172).expect("second-resolution offset"),
4320                );
4321                let fixed_utc = "1900-01-01T11:40:28"
4322                    .parse::<NaiveDateTime>()
4323                    .expect("UTC datetime");
4324                let fixed = Value::DatetimeTZ(fixed.from_utc_datetime(&fixed_utc));
4325                assert_eq!(
4326                    $concept_fn(
4327                        &Concept::Value(fixed.clone()),
4328                        ScalarJsonEncoding::CanonicalV2,
4329                    )
4330                    .expect("row concept")["value"],
4331                    serde_json::json!("1900-01-01T12:00:00+00:19:32"),
4332                    "row evidence preserves fixed offset seconds",
4333                );
4334                assert_eq!(
4335                    $leaf_fn(
4336                        &Leaf::Concept(Concept::Value(fixed)),
4337                        ScalarJsonEncoding::CanonicalV2,
4338                    )
4339                    .expect("document leaf"),
4340                    serde_json::json!("1900-01-01T12:00:00+00:19:32"),
4341                    "document evidence preserves fixed offset seconds",
4342                );
4343            }
4344        };
4345    }
4346
4347    datetime_tz_evidence_regression!(
4348        "band8",
4349        band8_rows_and_documents_preserve_exact_datetime_tz_evidence,
4350        driver_b8,
4351        concept_to_json_b8,
4352        document_leaf_to_json_b8
4353    );
4354    datetime_tz_evidence_regression!(
4355        "band9",
4356        band9_rows_and_documents_preserve_exact_datetime_tz_evidence,
4357        driver_b9,
4358        concept_to_json_b9,
4359        document_leaf_to_json_b9
4360    );
4361
4362    macro_rules! datetime_tz_local_range_regression {
4363        ($feature:literal, $name:ident, $driver:ident, $concept_fn:ident, $leaf_fn:ident) => {
4364            #[cfg(feature = $feature)]
4365            #[test]
4366            fn $name() {
4367                use chrono::{FixedOffset, NaiveDateTime, TimeZone as _};
4368                use $driver::answer::concept_document::Leaf;
4369                use $driver::concept::value::TimeZone;
4370                use $driver::concept::{Concept, Value};
4371
4372                let assert_range_error = |error: RuntimeError| {
4373                    assert!(matches!(
4374                        error,
4375                        RuntimeError::QueryExecution(message)
4376                            if message
4377                                == "provider datetime-tz local value is outside the supported range"
4378                    ));
4379                };
4380                let invalid = [
4381                    (
4382                        FixedOffset::east_opt(1).expect("positive offset"),
4383                        NaiveDateTime::MAX,
4384                    ),
4385                    (
4386                        FixedOffset::west_opt(1).expect("negative offset"),
4387                        NaiveDateTime::MIN,
4388                    ),
4389                ];
4390                for (offset, utc) in invalid {
4391                    let timezone = TimeZone::Fixed(offset);
4392                    let value = Value::DatetimeTZ(timezone.from_utc_datetime(&utc));
4393                    for encoding in [
4394                        ScalarJsonEncoding::CanonicalV2,
4395                        ScalarJsonEncoding::DriverDisplay,
4396                    ] {
4397                        let row_error =
4398                            $concept_fn(&Concept::Value(value.clone()), encoding).expect_err(
4399                                "an unrepresentable provider local datetime must fail row conversion",
4400                            );
4401                        assert_range_error(row_error);
4402
4403                        let document_error =
4404                            $leaf_fn(&Leaf::Concept(Concept::Value(value.clone())), encoding)
4405                                .expect_err(
4406                                    "an unrepresentable provider local datetime must fail document conversion",
4407                                );
4408                        assert_range_error(document_error);
4409                    }
4410                }
4411
4412                let valid = [
4413                    (
4414                        FixedOffset::west_opt(1).expect("negative offset"),
4415                        NaiveDateTime::MAX,
4416                    ),
4417                    (
4418                        FixedOffset::east_opt(1).expect("positive offset"),
4419                        NaiveDateTime::MIN,
4420                    ),
4421                ];
4422                for (offset, utc) in valid {
4423                    let timezone = TimeZone::Fixed(offset);
4424                    let value = Value::DatetimeTZ(timezone.from_utc_datetime(&utc));
4425                    for encoding in [
4426                        ScalarJsonEncoding::CanonicalV2,
4427                        ScalarJsonEncoding::DriverDisplay,
4428                    ] {
4429                        let row = $concept_fn(&Concept::Value(value.clone()), encoding)
4430                            .expect("an inward offset must remain representable");
4431                        assert!(row["value"].is_string());
4432
4433                        let document =
4434                            $leaf_fn(&Leaf::Concept(Concept::Value(value.clone())), encoding)
4435                                .expect("document conversion accepts an inward offset");
4436                        assert!(document.is_string());
4437                    }
4438                }
4439            }
4440        };
4441    }
4442
4443    datetime_tz_local_range_regression!(
4444        "band8",
4445        band8_datetime_tz_local_range_errors_are_propagated,
4446        driver_b8,
4447        concept_to_json_b8,
4448        document_leaf_to_json_b8
4449    );
4450    datetime_tz_local_range_regression!(
4451        "band9",
4452        band9_datetime_tz_local_range_errors_are_propagated,
4453        driver_b9,
4454        concept_to_json_b9,
4455        document_leaf_to_json_b9
4456    );
4457
4458    macro_rules! exact_temporal_evidence_regression {
4459        ($feature:literal, $name:ident, $driver:ident, $concept_fn:ident, $leaf_fn:ident) => {
4460            #[cfg(feature = $feature)]
4461            #[test]
4462            fn $name() {
4463                use $driver::answer::concept_document::Leaf;
4464                use $driver::concept::value::Duration;
4465                use $driver::concept::{Concept, Value};
4466
4467                let duration_cases = [
4468                    (Duration::new(0, 0, 0), "PT0S"),
4469                    (Duration::new(12, 0, 0), "P12M"),
4470                    (Duration::new(0, 0, 3_660_000_000_000), "PT3660S"),
4471                    (Duration::new(0, 0, 1_500_000_000), "PT1.5S"),
4472                    (
4473                        Duration::new(u32::MAX, u32::MAX, u64::MAX),
4474                        "P4294967295M4294967295DT18446744073.709551615S",
4475                    ),
4476                ];
4477                for (duration, expected) in duration_cases {
4478                    let value = Value::Duration(duration);
4479                    assert_eq!(
4480                        $concept_fn(
4481                            &Concept::Value(value.clone()),
4482                            ScalarJsonEncoding::CanonicalV2,
4483                        )
4484                        .expect("row concept")["value"],
4485                        serde_json::Value::String(expected.to_owned()),
4486                        "row evidence uses the contract's exact duration components",
4487                    );
4488                    assert_eq!(
4489                        $leaf_fn(
4490                            &Leaf::Concept(Concept::Value(value)),
4491                            ScalarJsonEncoding::CanonicalV2,
4492                        )
4493                        .expect("document leaf"),
4494                        serde_json::Value::String(expected.to_owned()),
4495                        "document evidence uses the same exact duration bridge",
4496                    );
4497                }
4498
4499                let year = Value::Duration(Duration::new(12, 0, 0));
4500                assert_eq!(
4501                    $concept_fn(&Concept::Value(year), ScalarJsonEncoding::DriverDisplay,)
4502                        .expect("row concept")["value"],
4503                    serde_json::json!("P1Y"),
4504                    "released conversion retains the upstream duration display",
4505                );
4506
4507                let datetime = chrono::NaiveDate::from_ymd_opt(2024, 1, 2)
4508                    .expect("date")
4509                    .and_hms_nano_opt(3, 4, 5, 500_000_000)
4510                    .expect("datetime");
4511                let datetime = Value::Datetime(datetime);
4512                assert_eq!(
4513                    $concept_fn(
4514                        &Concept::Value(datetime.clone()),
4515                        ScalarJsonEncoding::CanonicalV2,
4516                    )
4517                    .expect("row concept")["value"],
4518                    serde_json::json!("2024-01-02T03:04:05.5"),
4519                    "row evidence trims only insignificant datetime zeroes",
4520                );
4521                assert_eq!(
4522                    $leaf_fn(
4523                        &Leaf::Concept(Concept::Value(datetime.clone())),
4524                        ScalarJsonEncoding::CanonicalV2,
4525                    )
4526                    .expect("document leaf"),
4527                    serde_json::json!("2024-01-02T03:04:05.5"),
4528                    "document evidence uses the same exact datetime bridge",
4529                );
4530                assert_eq!(
4531                    $concept_fn(&Concept::Value(datetime), ScalarJsonEncoding::DriverDisplay,)
4532                        .expect("row concept")["value"],
4533                    serde_json::json!("2024-01-02T03:04:05.500000000"),
4534                    "released conversion retains the upstream datetime display",
4535                );
4536            }
4537        };
4538    }
4539
4540    exact_temporal_evidence_regression!(
4541        "band8",
4542        band8_rows_and_documents_preserve_exact_datetime_and_duration_evidence,
4543        driver_b8,
4544        concept_to_json_b8,
4545        document_leaf_to_json_b8
4546    );
4547    exact_temporal_evidence_regression!(
4548        "band9",
4549        band9_rows_and_documents_preserve_exact_datetime_and_duration_evidence,
4550        driver_b9,
4551        concept_to_json_b9,
4552        document_leaf_to_json_b9
4553    );
4554
4555    #[cfg(feature = "band9")]
4556    #[test]
4557    fn document_list_limit_rejects_before_json_collection() {
4558        use driver_b9::answer::concept_document::{Leaf, Node};
4559        let document = Node::List(vec![Node::Leaf(Some(Leaf::Empty)), Node::Leaf(None)]);
4560        let mut members = 0;
4561        let error =
4562            document_node_to_json_b9(&document, &mut members, 1, ScalarJsonEncoding::CanonicalV2)
4563                .expect_err("the list exceeds its conversion budget");
4564        assert!(matches!(
4565            error,
4566            RuntimeError::ResourceLimit {
4567                code: "query_v2_document_member_limit",
4568                ..
4569            }
4570        ));
4571        assert_eq!(members, 0, "rejected lists are not partially charged");
4572    }
4573
4574    #[cfg(feature = "band9")]
4575    #[test]
4576    fn unexpected_document_thing_is_a_typed_error_not_a_panic() {
4577        use driver_b9::answer::concept_document::{Leaf, Node};
4578        use driver_b9::concept::{Concept, Entity};
4579
4580        let document = Node::Leaf(Some(Leaf::Concept(Concept::Entity(Entity {
4581            iid: vec![1_u8].into(),
4582            type_: None,
4583        }))));
4584        let mut members = 0;
4585        let error = document_node_to_json_b9(
4586            &document,
4587            &mut members,
4588            u64::MAX,
4589            ScalarJsonEncoding::CanonicalV2,
4590        )
4591        .expect_err("thing instances are not valid fetch-document leaves");
4592        assert!(matches!(
4593            error,
4594            RuntimeError::QueryExecution(message)
4595                if message == "document response carried an unsupported thing instance"
4596        ));
4597    }
4598
4599    async fn assert_pending_runtime_await_hits_deadline() {
4600        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
4601        let limits = RuntimeAnswerLimits {
4602            max_items: 10,
4603            max_bytes: 4096,
4604            deadline: Some(
4605                (tokio::time::Instant::now() + std::time::Duration::from_secs(5)).into_std(),
4606            ),
4607            cancellation: RuntimeAnswerCancellation::default(),
4608        };
4609        let execution = tokio::spawn(async move {
4610            runtime_await(
4611                async move {
4612                    let _ = started_tx.send(());
4613                    std::future::pending::<Result<()>>().await
4614                },
4615                &limits,
4616            )
4617            .await
4618        });
4619
4620        started_rx.await.unwrap();
4621        tokio::time::advance(std::time::Duration::from_secs(5)).await;
4622        tokio::task::yield_now().await;
4623        assert!(execution.is_finished());
4624        assert!(matches!(
4625            execution.await.unwrap().unwrap_err(),
4626            RuntimeError::ResourceLimit {
4627                code: "transaction_deadline_exceeded",
4628                ..
4629            }
4630        ));
4631    }
4632
4633    #[tokio::test(start_paused = true)]
4634    async fn runtime_deadline_interrupts_pending_query_await() {
4635        assert_pending_runtime_await_hits_deadline().await;
4636    }
4637
4638    #[tokio::test(start_paused = true)]
4639    async fn runtime_deadline_interrupts_pending_stream_poll() {
4640        assert_pending_runtime_await_hits_deadline().await;
4641    }
4642
4643    #[tokio::test]
4644    async fn runtime_cancellation_after_poll_start_interrupts_pending_await() {
4645        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
4646        let cancellation = RuntimeAnswerCancellation::default();
4647        let trigger = cancellation.clone();
4648        let limits = RuntimeAnswerLimits {
4649            max_items: 10,
4650            max_bytes: 4096,
4651            deadline: None,
4652            cancellation,
4653        };
4654        let execution = tokio::spawn(async move {
4655            runtime_await(
4656                async move {
4657                    let _ = started_tx.send(());
4658                    std::future::pending::<Result<()>>().await
4659                },
4660                &limits,
4661            )
4662            .await
4663        });
4664
4665        started_rx.await.unwrap();
4666        trigger.cancel();
4667        tokio::task::yield_now().await;
4668        assert!(execution.is_finished());
4669        assert!(matches!(
4670            execution.await.unwrap().unwrap_err(),
4671            RuntimeError::ResourceLimit {
4672                code: "provider_cancelled",
4673                ..
4674            }
4675        ));
4676    }
4677
4678    #[test]
4679    fn bounded_runtime_reader_stops_and_enforces_limits_before_consumer() {
4680        let limits = RuntimeAnswerLimits {
4681            max_items: 1,
4682            max_bytes: 64,
4683            deadline: None,
4684            cancellation: RuntimeAnswerCancellation::default(),
4685        };
4686        let mut stats = RuntimeAnswerStats::new(RuntimeAnswerKind::Rows);
4687        let mut accepted = 0;
4688        let mut stop = |_item| {
4689            accepted += 1;
4690            Ok(RuntimeAnswerControl::Stop)
4691        };
4692        assert_eq!(
4693            runtime_accept(
4694                &limits,
4695                &mut stats,
4696                RuntimeAnswerItem::Row(serde_json::json!({"v": 1})),
4697                &mut stop,
4698            )
4699            .unwrap(),
4700            RuntimeAnswerControl::Stop
4701        );
4702        assert_eq!(accepted, 1);
4703        assert!(stats.stopped_early);
4704
4705        let mut never_called =
4706            |_item| -> Result<RuntimeAnswerControl> { panic!("over-limit item reached consumer") };
4707        let error = runtime_accept(
4708            &limits,
4709            &mut stats,
4710            RuntimeAnswerItem::Row(serde_json::json!({"v": 2})),
4711            &mut never_called,
4712        )
4713        .unwrap_err();
4714        assert!(matches!(
4715            error,
4716            RuntimeError::ResourceLimit {
4717                code: "processed_item_limit",
4718                ..
4719            }
4720        ));
4721    }
4722
4723    #[tokio::test]
4724    async fn bounded_runtime_stream_stops_before_polling_another_item() {
4725        let polls = Arc::new(AtomicUsize::new(0));
4726        let observed_polls = Arc::clone(&polls);
4727        let mut emitted = false;
4728        let stream = futures::stream::poll_fn(move |_context| {
4729            observed_polls.fetch_add(1, Ordering::SeqCst);
4730            assert!(!emitted, "stream was polled after the consumer stopped");
4731            emitted = true;
4732            std::task::Poll::Ready(Some(Ok::<_, RuntimeError>(RuntimeAnswerItem::Row(
4733                serde_json::json!({"v": 1}),
4734            ))))
4735        });
4736        let limits = RuntimeAnswerLimits::unbounded();
4737        let mut consumer = |_item| Ok(RuntimeAnswerControl::Stop);
4738
4739        let stats = runtime_consume_stream(stream, RuntimeAnswerKind::Rows, &limits, &mut consumer)
4740            .await
4741            .unwrap();
4742
4743        assert_eq!(polls.load(Ordering::SeqCst), 1);
4744        assert_eq!(stats.processed_items, 1);
4745        assert!(stats.stopped_early);
4746    }
4747
4748    #[tokio::test]
4749    async fn bounded_runtime_stream_rejects_an_over_limit_item_before_consumer() {
4750        let stream = futures::stream::iter([
4751            Ok(RuntimeAnswerItem::Row(serde_json::json!({"v": 1}))),
4752            Ok(RuntimeAnswerItem::Row(serde_json::json!({"v": 2}))),
4753        ]);
4754        let limits = RuntimeAnswerLimits {
4755            max_items: 1,
4756            max_bytes: u64::MAX,
4757            deadline: None,
4758            cancellation: RuntimeAnswerCancellation::default(),
4759        };
4760        let mut accepted = 0;
4761        let mut consumer = |_item| {
4762            accepted += 1;
4763            Ok(RuntimeAnswerControl::Continue)
4764        };
4765
4766        let error = runtime_consume_stream(stream, RuntimeAnswerKind::Rows, &limits, &mut consumer)
4767            .await
4768            .unwrap_err();
4769
4770        assert_eq!(accepted, 1);
4771        assert!(matches!(
4772            error,
4773            RuntimeError::ResourceLimit {
4774                code: "processed_item_limit",
4775                ..
4776            }
4777        ));
4778    }
4779
4780    #[cfg(feature = "band8")]
4781    fn empty_given_rows() -> GivenRowsSpec {
4782        GivenRowsSpec {
4783            variables: vec!["value".into()],
4784            rows: Vec::new(),
4785        }
4786    }
4787
4788    #[cfg(feature = "band8")]
4789    #[tokio::test]
4790    async fn band8_transaction_rejects_bounded_given_rows_actionably() {
4791        let mut transaction = RuntimeTransaction {
4792            inner: RuntimeTransactionInner::B8(None),
4793            driver_lease: None,
4794        };
4795        let mut consumer = |_item| Ok(RuntimeAnswerControl::Continue);
4796
4797        assert!(!transaction.supports_given_rows());
4798        let error = transaction
4799            .query_with_rows_bounded(
4800                "given $value: string; match $x isa thing;",
4801                empty_given_rows(),
4802                RuntimeAnswerLimits::unbounded(),
4803                &mut consumer,
4804            )
4805            .await
4806            .unwrap_err();
4807
4808        assert!(
4809            matches!(&error, RuntimeError::QueryExecution(message) if message.contains("band-9 driver") && message.contains("band 8")),
4810            "unexpected error: {error}"
4811        );
4812    }
4813
4814    #[cfg(feature = "band9")]
4815    #[test]
4816    fn band9_transaction_reports_given_row_transport_support() {
4817        let transaction = RuntimeTransaction {
4818            inner: RuntimeTransactionInner::B9(None),
4819            driver_lease: None,
4820        };
4821
4822        assert!(transaction.supports_given_rows());
4823    }
4824
4825    #[test]
4826    fn bounded_runtime_reader_rechecks_cancellation_after_consumer_work() {
4827        let cancellation = RuntimeAnswerCancellation::default();
4828        let trigger = cancellation.clone();
4829        let limits = RuntimeAnswerLimits {
4830            max_items: 1,
4831            max_bytes: 64,
4832            deadline: None,
4833            cancellation,
4834        };
4835        let mut stats = RuntimeAnswerStats::new(RuntimeAnswerKind::Rows);
4836        let mut consumer = move |_item| {
4837            trigger.cancel();
4838            Ok(RuntimeAnswerControl::Continue)
4839        };
4840
4841        let error = runtime_accept(
4842            &limits,
4843            &mut stats,
4844            RuntimeAnswerItem::Row(serde_json::json!({"v": 1})),
4845            &mut consumer,
4846        )
4847        .unwrap_err();
4848        assert!(matches!(
4849            error,
4850            RuntimeError::ResourceLimit {
4851                code: "provider_cancelled",
4852                ..
4853            }
4854        ));
4855
4856        let cancellation = RuntimeAnswerCancellation::default();
4857        let trigger = cancellation.clone();
4858        let limits = RuntimeAnswerLimits {
4859            max_items: 1,
4860            max_bytes: 64,
4861            deadline: None,
4862            cancellation,
4863        };
4864        let mut stats = RuntimeAnswerStats::new(RuntimeAnswerKind::Rows);
4865        let mut failing_consumer = move |_item| -> Result<RuntimeAnswerControl> {
4866            trigger.cancel();
4867            Err(RuntimeError::AnswerConsumer)
4868        };
4869        let error = runtime_accept(
4870            &limits,
4871            &mut stats,
4872            RuntimeAnswerItem::Row(serde_json::json!({"v": 1})),
4873            &mut failing_consumer,
4874        )
4875        .unwrap_err();
4876        assert!(matches!(
4877            error,
4878            RuntimeError::ResourceLimit {
4879                code: "provider_cancelled",
4880                ..
4881            }
4882        ));
4883    }
4884
4885    #[test]
4886    fn connect_options_default_matches_ssot() {
4887        let options = ConnectOptions::default();
4888        assert_eq!(options.http_port, DEFAULT_HTTP_PORT);
4889        assert!(!options.tls);
4890        assert_eq!(options.server_version, None);
4891    }
4892
4893    fn test_connection_control(
4894        max_items: u64,
4895        max_bytes: u64,
4896        max_statements: u32,
4897        cancellation: RuntimeAnswerCancellation,
4898    ) -> RuntimeConnectionControl {
4899        RuntimeConnectionControl::new(
4900            max_items,
4901            max_bytes,
4902            max_statements,
4903            Instant::now() + Duration::from_secs(5),
4904            cancellation,
4905        )
4906    }
4907
4908    fn prepared_controlled_probe(
4909        control: RuntimeConnectionControl,
4910    ) -> PreparedSecureConnectOptions {
4911        SecureConnectOptions {
4912            http_port: 8123,
4913            tls_mode: TlsMode::Disabled,
4914            server_version: None,
4915        }
4916        .prepare_transport()
4917        .expect("prepare plaintext test transport")
4918        .with_connection_control(control)
4919    }
4920
4921    #[test]
4922    fn controlled_connection_meter_proves_direct_success_and_fallback_fourth_dispatch_failure() {
4923        let version = core_version::Version::new(3, 12, 1);
4924        let version_bytes = u64::try_from(version.to_string().len()).unwrap();
4925
4926        let mut direct = ConnectionMeter::new(test_connection_control(
4927            1,
4928            version_bytes,
4929            3,
4930            RuntimeAnswerCancellation::default(),
4931        ));
4932        direct.charge_statement().unwrap(); // HTTP version probe
4933        direct.charge_version_evidence(version).unwrap();
4934        direct.charge_statement().unwrap(); // authoritative band-9 constructor
4935        direct.admit_connection().unwrap();
4936        assert_eq!(direct.statements, 2);
4937        assert_eq!(direct.response_bytes, version_bytes);
4938        assert_eq!(direct.admitted_items, 1);
4939
4940        let mut fallback = ConnectionMeter::new(test_connection_control(
4941            1,
4942            version_bytes,
4943            3,
4944            RuntimeAnswerCancellation::default(),
4945        ));
4946        fallback.charge_statement().unwrap(); // failed HTTP version probe
4947        fallback.charge_statement().unwrap(); // band-8 constructor
4948        fallback.charge_statement().unwrap(); // band-8 version dispatch
4949        fallback.charge_version_evidence(version).unwrap();
4950        assert!(matches!(
4951            fallback.charge_statement(), // validated band-9 constructor
4952            Err(RuntimeError::ResourceLimit {
4953                code: "provider_statement_limit",
4954                ..
4955            })
4956        ));
4957        assert_eq!(fallback.statements, 3);
4958        assert_eq!(fallback.response_bytes, version_bytes);
4959        assert_eq!(fallback.admitted_items, 0);
4960
4961        let mut bytes = ConnectionMeter::new(test_connection_control(
4962            1,
4963            version_bytes,
4964            3,
4965            RuntimeAnswerCancellation::default(),
4966        ));
4967        bytes.charge_version_evidence(version).unwrap();
4968        assert!(matches!(
4969            bytes.charge_version_evidence(version),
4970            Err(RuntimeError::ResourceLimit {
4971                code: "response_byte_limit",
4972                ..
4973            })
4974        ));
4975
4976        let mut items = ConnectionMeter::new(test_connection_control(
4977            1,
4978            version_bytes,
4979            3,
4980            RuntimeAnswerCancellation::default(),
4981        ));
4982        items.admit_connection().unwrap();
4983        assert!(matches!(
4984            items.admit_connection(),
4985            Err(RuntimeError::ResourceLimit {
4986                code: "processed_item_limit",
4987                ..
4988            })
4989        ));
4990    }
4991
4992    #[test]
4993    fn legacy_connection_meter_is_an_exact_uncontrolled_adapter() {
4994        let mut meter = ConnectionMeter::legacy();
4995        for _ in 0..16 {
4996            meter.charge_statement().unwrap();
4997            meter
4998                .charge_version_evidence(core_version::Version::new(3, 12, 1))
4999                .unwrap();
5000            meter.admit_connection().unwrap();
5001        }
5002        assert_eq!(meter.statements, 0);
5003        assert_eq!(meter.response_bytes, 0);
5004        assert_eq!(meter.admitted_items, 0);
5005    }
5006
5007    #[tokio::test]
5008    async fn controlled_zero_item_and_statement_ceilings_reject_before_probe_dispatch() {
5009        for (control, expected_code) in [
5010            (
5011                test_connection_control(0, 32, 3, RuntimeAnswerCancellation::default()),
5012                "processed_item_limit",
5013            ),
5014            (
5015                test_connection_control(1, 32, 0, RuntimeAnswerCancellation::default()),
5016                "provider_statement_limit",
5017            ),
5018        ] {
5019            let probe_called = Arc::new(Mutex::new(false));
5020            let captured = Arc::clone(&probe_called);
5021            let error = gated_driver_prepared_with_probe(
5022                "endpoint must not be used",
5023                "credential must not be used",
5024                "credential must not be used",
5025                prepared_controlled_probe(control),
5026                move |_address, _port, _mode| {
5027                    *captured.lock().unwrap() = true;
5028                    Ok(core_version::Version::new(3, 12, 1))
5029                },
5030            )
5031            .await
5032            .err()
5033            .expect("zero applicable ceiling must reject before probe dispatch");
5034
5035            assert!(!*probe_called.lock().unwrap());
5036            assert!(matches!(
5037                error,
5038                SecureConnectError::Runtime(RuntimeError::ResourceLimit { code, .. })
5039                    if code == expected_code
5040            ));
5041        }
5042    }
5043
5044    #[tokio::test]
5045    async fn controlled_probe_bytes_are_bounded_before_driver_construction() {
5046        let probe_called = Arc::new(Mutex::new(false));
5047        let captured = Arc::clone(&probe_called);
5048        let error = gated_driver_prepared_with_probe(
5049            "endpoint must not reach a driver",
5050            "credential must not be used",
5051            "credential must not be used",
5052            prepared_controlled_probe(test_connection_control(
5053                1,
5054                5,
5055                2,
5056                RuntimeAnswerCancellation::default(),
5057            )),
5058            move |_address, _port, _mode| {
5059                *captured.lock().unwrap() = true;
5060                Ok(core_version::Version::new(3, 12, 1))
5061            },
5062        )
5063        .await
5064        .err()
5065        .expect("six retained version bytes must cross a five-byte ceiling");
5066
5067        assert!(*probe_called.lock().unwrap());
5068        assert!(matches!(
5069            error,
5070            SecureConnectError::Runtime(RuntimeError::ResourceLimit {
5071                code: "response_byte_limit",
5072                ..
5073            })
5074        ));
5075    }
5076
5077    #[tokio::test]
5078    async fn generated_exact_version_rejects_after_one_probe_before_driver_construction() {
5079        let probe_calls = Arc::new(AtomicUsize::new(0));
5080        let observed_calls = Arc::clone(&probe_calls);
5081        let result = gated_driver_prepared_with_probe(
5082            "not a provider endpoint and must not reach address parsing",
5083            "credential must not be used",
5084            "credential must not be used",
5085            prepared_controlled_probe(test_connection_control(
5086                1,
5087                32,
5088                2,
5089                RuntimeAnswerCancellation::default(),
5090            ))
5091            .with_generated_3_12_3_requirement(),
5092            move |_address, _port, _mode| {
5093                observed_calls.fetch_add(1, Ordering::AcqRel);
5094                Ok(core_version::Version::new(3, 12, 0))
5095            },
5096        )
5097        .await;
5098        let error = match result {
5099            Ok(_) => panic!("a generated package requires exact TypeDB 3.12.3"),
5100            Err(error) => error,
5101        };
5102
5103        assert_eq!(probe_calls.load(Ordering::Acquire), 1);
5104        assert!(matches!(
5105            error,
5106            SecureConnectError::Runtime(RuntimeError::UnsupportedVersion(
5107                core_version::VersionError::FeatureUnsupported {
5108                    server,
5109                    required,
5110                    ..
5111                }
5112            )) if server == core_version::Version::new(3, 12, 0)
5113                && required == core_version::Version::new(3, 12, 3)
5114        ));
5115    }
5116
5117    #[tokio::test]
5118    async fn controlled_probe_cancellation_wakes_while_blocking_work_is_in_flight() {
5119        let cancellation = RuntimeAnswerCancellation::default();
5120        let prepared =
5121            prepared_controlled_probe(test_connection_control(1, 32, 2, cancellation.clone()));
5122        let (started_sender, started_receiver) = std::sync::mpsc::channel();
5123        let (release_sender, release_receiver) = std::sync::mpsc::channel();
5124        let connection = tokio::spawn(async move {
5125            gated_driver_prepared_with_probe(
5126                "pending endpoint",
5127                "credential",
5128                "credential",
5129                prepared,
5130                move |_address, _port, _mode| {
5131                    started_sender.send(()).unwrap();
5132                    release_receiver.recv().unwrap();
5133                    Err(core_version::VersionProbeError::Probe(
5134                        core_version::VersionError::Probe("released probe".to_owned()),
5135                    ))
5136                },
5137            )
5138            .await
5139        });
5140
5141        tokio::task::spawn_blocking(move || started_receiver.recv().unwrap())
5142            .await
5143            .unwrap();
5144        cancellation.cancel();
5145        let error = tokio::time::timeout(Duration::from_secs(1), connection)
5146            .await
5147            .expect("cancellation must wake the pending connection")
5148            .unwrap()
5149            .err()
5150            .expect("cancelled connection must fail");
5151        release_sender.send(()).unwrap();
5152
5153        assert!(matches!(
5154            error,
5155            SecureConnectError::Runtime(RuntimeError::ResourceLimit {
5156                code: "provider_cancelled",
5157                ..
5158            })
5159        ));
5160    }
5161
5162    #[tokio::test]
5163    async fn completed_provider_outcome_wins_an_already_ready_cancellation_branch() {
5164        let cancellation = RuntimeAnswerCancellation::default();
5165        cancellation.cancel();
5166        let control = test_connection_control(1, 32, 1, cancellation);
5167        assert_eq!(
5168            await_connection_work(std::future::ready(17_u8), Some(&control))
5169                .await
5170                .unwrap(),
5171            17
5172        );
5173    }
5174
5175    #[tokio::test(start_paused = true)]
5176    async fn controlled_deadline_wakes_pending_provider_work() {
5177        let control = RuntimeConnectionControl::new(
5178            1,
5179            32,
5180            1,
5181            Instant::now() + Duration::from_secs(1),
5182            RuntimeAnswerCancellation::default(),
5183        );
5184        let pending = tokio::spawn(async move {
5185            await_connection_work(std::future::pending::<()>(), Some(&control)).await
5186        });
5187        tokio::time::advance(Duration::from_secs(2)).await;
5188        let error = pending
5189            .await
5190            .unwrap()
5191            .expect_err("absolute deadline must wake pending provider work");
5192        assert!(matches!(
5193            error,
5194            RuntimeError::ResourceLimit {
5195                code: "transaction_deadline_exceeded",
5196                ..
5197            }
5198        ));
5199    }
5200
5201    #[test]
5202    fn controlled_connection_errors_drop_provider_endpoint_and_parse_text() {
5203        const SENTINEL: &str = "TB_CONTROLLED_CONNECT_SECRET";
5204        for error in [
5205            SecureConnectError::Runtime(RuntimeError::Connection(SENTINEL.to_owned())),
5206            SecureConnectError::Runtime(RuntimeError::UnsupportedVersion(
5207                core_version::VersionError::Parse(SENTINEL.to_owned()),
5208            )),
5209        ] {
5210            let rendered = controlled_connection_error(error).to_string();
5211            assert!(!rendered.contains(SENTINEL), "{rendered}");
5212        }
5213    }
5214
5215    #[test]
5216    fn released_boolean_options_map_to_the_typed_tls_truth_table() {
5217        let disabled = SecureConnectOptions::from(ConnectOptions {
5218            http_port: 8123,
5219            tls: false,
5220            server_version: Some(core_version::Version::new(3, 11, 5)),
5221        });
5222        assert_eq!(disabled.http_port, 8123);
5223        assert_eq!(disabled.tls_mode, TlsMode::Disabled);
5224        assert_eq!(
5225            disabled.server_version,
5226            Some(core_version::Version::new(3, 11, 5))
5227        );
5228
5229        let native = SecureConnectOptions::from(ConnectOptions {
5230            http_port: 8124,
5231            tls: true,
5232            server_version: None,
5233        });
5234        assert_eq!(native.http_port, 8124);
5235        assert_eq!(native.tls_mode, TlsMode::NativeRoots);
5236        assert_eq!(native.server_version, None);
5237    }
5238
5239    #[test]
5240    fn every_enabled_fallback_band_lowers_to_tls_without_plaintext() {
5241        let disabled = ResolvedTlsMode::from_configured_path(TlsMode::Disabled).unwrap();
5242        assert!(!disabled.probe_mode.is_enabled());
5243        #[cfg(feature = "band8")]
5244        assert!(!disabled.band8.is_enabled());
5245        #[cfg(feature = "band9")]
5246        assert!(!disabled.band9.is_enabled());
5247
5248        let native = ResolvedTlsMode::from_configured_path(TlsMode::NativeRoots).unwrap();
5249        assert!(native.probe_mode.is_enabled());
5250        #[cfg(feature = "band8")]
5251        assert!(native.band8.is_enabled());
5252        #[cfg(feature = "band9")]
5253        assert!(native.band9.is_enabled());
5254
5255        let root_ca = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/root-ca.pem");
5256        let custom =
5257            ResolvedTlsMode::from_configured_path(TlsMode::CustomRootCa(root_ca.clone())).unwrap();
5258        assert!(custom.probe_mode.is_enabled());
5259        #[cfg(any(feature = "band8", feature = "band9"))]
5260        let expected_root = std::fs::read(&root_ca).unwrap();
5261        #[cfg(feature = "band8")]
5262        {
5263            assert!(custom.band8.is_enabled());
5264            assert_eq!(
5265                std::fs::read(custom.band8.root_ca_path().unwrap()).unwrap(),
5266                expected_root
5267            );
5268        }
5269        #[cfg(feature = "band9")]
5270        {
5271            assert!(custom.band9.is_enabled());
5272            assert_eq!(
5273                std::fs::read(custom.band9.root_ca_path().unwrap()).unwrap(),
5274                expected_root
5275            );
5276        }
5277    }
5278
5279    #[test]
5280    fn every_band_lowers_from_retained_material_after_path_replacement() {
5281        let sequence = NEXT_TLS_MATERIAL_TEST_ID.fetch_add(1, Ordering::Relaxed);
5282        let directory = std::env::temp_dir().join(format!(
5283            "type-bridge-runtime-root-replacement-{}-{sequence}",
5284            std::process::id()
5285        ));
5286        std::fs::create_dir(&directory).unwrap();
5287        // load() requires an already-physical path; the ambient temp
5288        // directory may sit behind symlinked components (macOS /var).
5289        let directory = directory.canonicalize().unwrap();
5290        let configured = directory.join("root.pem");
5291        let moved = directory.join("loaded-root.pem");
5292        let original = include_bytes!("../tests/fixtures/root-ca.pem");
5293        std::fs::write(&configured, original).unwrap();
5294
5295        let material = core_version::RetainedCustomRootCa::load(&configured).unwrap();
5296        match std::fs::rename(&configured, &moved) {
5297            Ok(()) => {
5298                std::fs::write(&configured, b"replacement is not a certificate\n").unwrap();
5299            }
5300            Err(_) => {
5301                // Windows' retained no-delete handle makes the replacement
5302                // itself fail closed. It must also prevent an in-place write.
5303                assert!(
5304                    std::fs::write(&configured, b"replacement is not a certificate\n").is_err()
5305                );
5306            }
5307        }
5308
5309        let resolved = ResolvedTlsMode::lower(
5310            TlsMode::CustomRootCa(configured),
5311            ResolvedTlsProbeMode::CustomRootCa(material),
5312        )
5313        .expect("all bands must lower the retained original material");
5314
5315        #[cfg(feature = "band8")]
5316        assert_eq!(
5317            std::fs::read(resolved.band8.root_ca_path().unwrap()).unwrap(),
5318            original
5319        );
5320        #[cfg(feature = "band9")]
5321        assert_eq!(
5322            std::fs::read(resolved.band9.root_ca_path().unwrap()).unwrap(),
5323            original
5324        );
5325
5326        drop(resolved);
5327        std::fs::remove_dir_all(directory).unwrap();
5328    }
5329
5330    #[test]
5331    fn every_band_lowers_from_retained_material_after_parent_swap() {
5332        let sequence = NEXT_TLS_MATERIAL_TEST_ID.fetch_add(1, Ordering::Relaxed);
5333        let root = std::env::temp_dir().join(format!(
5334            "type-bridge-runtime-root-parent-swap-{}-{sequence}",
5335            std::process::id()
5336        ));
5337        std::fs::create_dir(&root).unwrap();
5338        // load() requires an already-physical path; the ambient temp
5339        // directory may sit behind symlinked components (macOS /var).
5340        let root = root.canonicalize().unwrap();
5341        let configured_parent = root.join("configured-parent");
5342        let moved_parent = root.join("loaded-parent");
5343        std::fs::create_dir(&configured_parent).unwrap();
5344        let configured = configured_parent.join("root.pem");
5345        let original = include_bytes!("../tests/fixtures/root-ca.pem");
5346        std::fs::write(&configured, original).unwrap();
5347
5348        let material = core_version::RetainedCustomRootCa::load(&configured).unwrap();
5349        match std::fs::rename(&configured_parent, &moved_parent) {
5350            Ok(()) => {
5351                std::fs::create_dir(&configured_parent).unwrap();
5352                std::fs::write(&configured, b"replacement is not a certificate\n").unwrap();
5353            }
5354            Err(_) => {
5355                // Windows retains the parent without delete sharing, so a
5356                // namespace swap is rejected before any driver lowering.
5357                assert_eq!(std::fs::read(&configured).unwrap(), original);
5358            }
5359        }
5360
5361        let resolved = ResolvedTlsMode::lower(
5362            TlsMode::CustomRootCa(configured),
5363            ResolvedTlsProbeMode::CustomRootCa(material),
5364        )
5365        .expect("all bands must lower the retained parent/file material");
5366
5367        #[cfg(feature = "band8")]
5368        assert_eq!(
5369            std::fs::read(resolved.band8.root_ca_path().unwrap()).unwrap(),
5370            original
5371        );
5372        #[cfg(feature = "band9")]
5373        assert_eq!(
5374            std::fs::read(resolved.band9.root_ca_path().unwrap()).unwrap(),
5375            original
5376        );
5377
5378        drop(resolved);
5379        std::fs::remove_dir_all(root).unwrap();
5380    }
5381
5382    #[test]
5383    fn every_band_lowers_from_snapshot_after_in_place_source_overwrite() {
5384        let sequence = NEXT_TLS_MATERIAL_TEST_ID.fetch_add(1, Ordering::Relaxed);
5385        let directory = std::env::temp_dir().join(format!(
5386            "type-bridge-runtime-root-overwrite-{}-{sequence}",
5387            std::process::id()
5388        ));
5389        std::fs::create_dir(&directory).unwrap();
5390        // load() requires an already-physical path; the ambient temp
5391        // directory may sit behind symlinked components (macOS /var).
5392        let directory = directory.canonicalize().unwrap();
5393        let configured = directory.join("root.pem");
5394        let original = include_bytes!("../tests/fixtures/root-ca.pem");
5395        std::fs::write(&configured, original).unwrap();
5396
5397        let material = core_version::RetainedCustomRootCa::load(&configured).unwrap();
5398        match std::fs::write(&configured, b"overwritten source is not a certificate\n") {
5399            Ok(()) => assert_ne!(std::fs::read(&configured).unwrap(), original),
5400            Err(_) => assert_eq!(std::fs::read(&configured).unwrap(), original),
5401        }
5402
5403        let resolved = ResolvedTlsMode::lower(
5404            TlsMode::CustomRootCa(configured),
5405            ResolvedTlsProbeMode::CustomRootCa(material),
5406        )
5407        .expect("all bands must lower the captured-byte snapshot");
5408
5409        #[cfg(feature = "band8")]
5410        assert_eq!(
5411            std::fs::read(resolved.band8.root_ca_path().unwrap()).unwrap(),
5412            original
5413        );
5414        #[cfg(feature = "band9")]
5415        assert_eq!(
5416            std::fs::read(resolved.band9.root_ca_path().unwrap()).unwrap(),
5417            original
5418        );
5419
5420        drop(resolved);
5421        std::fs::remove_dir_all(directory).unwrap();
5422    }
5423
5424    #[tokio::test]
5425    async fn prepared_transport_never_rereads_mutated_custom_root_path() {
5426        let sequence = NEXT_TLS_MATERIAL_TEST_ID.fetch_add(1, Ordering::Relaxed);
5427        let directory = std::env::temp_dir().join(format!(
5428            "type-bridge-runtime-root-prepared-{}-{sequence}",
5429            std::process::id()
5430        ));
5431        std::fs::create_dir(&directory).unwrap();
5432        let configured = directory.join("root.pem");
5433        let original = include_bytes!("../tests/fixtures/root-ca.pem");
5434        std::fs::write(&configured, original).unwrap();
5435
5436        let prepared = SecureConnectOptions {
5437            http_port: 8123,
5438            tls_mode: TlsMode::CustomRootCa(configured.clone()),
5439            server_version: None,
5440        }
5441        .prepare_transport()
5442        .expect("prepare the complete custom-root transport");
5443
5444        match std::fs::write(&configured, b"mutated after preparation\n") {
5445            Ok(()) => assert_ne!(std::fs::read(&configured).unwrap(), original),
5446            Err(_) => assert_eq!(std::fs::read(&configured).unwrap(), original),
5447        }
5448
5449        #[cfg(feature = "band8")]
5450        assert_eq!(
5451            std::fs::read(prepared.resolved_tls.band8.root_ca_path().unwrap()).unwrap(),
5452            original
5453        );
5454        #[cfg(feature = "band9")]
5455        assert_eq!(
5456            std::fs::read(prepared.resolved_tls.band9.root_ca_path().unwrap()).unwrap(),
5457            original
5458        );
5459
5460        let expected = original.to_vec();
5461        let error = gated_driver_prepared_with_probe(
5462            "host must not be constructed",
5463            "credential must not be used",
5464            "credential must not be used",
5465            prepared
5466                .clone()
5467                .with_connection_control(test_connection_control(
5468                    1,
5469                    32,
5470                    1,
5471                    RuntimeAnswerCancellation::default(),
5472                )),
5473            move |_address, port, mode| {
5474                assert_eq!(port, 8123);
5475                let ResolvedTlsProbeMode::CustomRootCa(material) = mode else {
5476                    panic!("prepared probe must retain custom-root mode");
5477                };
5478                let bytes = material
5479                    .with_driver_root_path(|path| std::fs::read(path))
5480                    .unwrap()
5481                    .unwrap();
5482                assert_eq!(bytes, expected);
5483                Err(core_version::VersionProbeError::TlsConfiguration(
5484                    core_version::TlsConfigurationError::NativeRootsUnavailable,
5485                ))
5486            },
5487        )
5488        .await
5489        .err()
5490        .expect("injected terminal probe error prevents host construction");
5491        assert!(matches!(
5492            error,
5493            SecureConnectError::TlsConfiguration(
5494                core_version::TlsConfigurationError::NativeRootsUnavailable
5495            )
5496        ));
5497
5498        drop(prepared);
5499        std::fs::remove_dir_all(directory).unwrap();
5500    }
5501
5502    #[cfg(unix)]
5503    #[test]
5504    fn binding_transport_resolves_a_raw_alias_without_weakening_physical_paths() {
5505        use std::os::unix::fs::symlink;
5506
5507        let sequence = NEXT_TLS_MATERIAL_TEST_ID.fetch_add(1, Ordering::Relaxed);
5508        let directory = std::env::temp_dir().join(format!(
5509            "type-bridge-runtime-binding-alias-{}-{sequence}",
5510            std::process::id()
5511        ));
5512        std::fs::create_dir(&directory).expect("create binding-alias directory");
5513        let physical_parent = directory.join("physical");
5514        let alias_parent = directory.join("alias");
5515        std::fs::create_dir(&physical_parent).expect("create physical CA parent");
5516        std::fs::write(
5517            physical_parent.join("root.pem"),
5518            include_bytes!("../tests/fixtures/root-ca.pem"),
5519        )
5520        .expect("write binding CA");
5521        symlink(&physical_parent, &alias_parent).expect("create caller path alias");
5522        let configured = alias_parent.join("root.pem");
5523        let options = SecureConnectOptions {
5524            http_port: 8123,
5525            tls_mode: TlsMode::CustomRootCa(configured.clone()),
5526            server_version: None,
5527        };
5528
5529        assert!(matches!(
5530            options.prepare_transport_from_validated_physical_path(),
5531            Err(SecureConnectError::TlsConfiguration(
5532                core_version::TlsConfigurationError::CustomRootCaUnreadable { path }
5533            )) if path == configured
5534        ));
5535        options
5536            .prepare_transport()
5537            .expect("raw binding preparation resolves one caller alias");
5538        std::fs::remove_dir_all(directory).expect("remove binding-alias directory");
5539    }
5540
5541    #[test]
5542    fn public_transport_preflight_returns_typed_errors_without_a_host() {
5543        let missing =
5544            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/does-not-exist.pem");
5545        let options = SecureConnectOptions {
5546            http_port: 8123,
5547            tls_mode: TlsMode::CustomRootCa(missing.clone()),
5548            server_version: Some(core_version::Version::new(3, 11, 5)),
5549        };
5550
5551        let error = options
5552            .validate_transport()
5553            .expect_err("preflight must validate trust material synchronously");
5554        assert_eq!(
5555            error.configuration_code(),
5556            Some("tls_custom_root_ca_unreadable")
5557        );
5558        assert!(matches!(
5559            error,
5560            SecureConnectError::TlsConfiguration(
5561                core_version::TlsConfigurationError::CustomRootCaUnreadable { path }
5562            ) if path == missing
5563        ));
5564    }
5565
5566    #[tokio::test]
5567    async fn invalid_custom_root_is_rejected_before_exact_version_or_probe_io() {
5568        let probe_called = Arc::new(Mutex::new(false));
5569        let captured = Arc::clone(&probe_called);
5570        let missing =
5571            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/does-not-exist.pem");
5572
5573        let error = gated_driver_secure_with_probe(
5574            "invalid host that must not be constructed",
5575            "admin",
5576            "password",
5577            SecureConnectOptions {
5578                http_port: 8123,
5579                tls_mode: TlsMode::CustomRootCa(missing.clone()),
5580                server_version: Some(core_version::Version::new(3, 11, 5)),
5581            },
5582            move |_address, _port, _mode| {
5583                *captured.lock().unwrap() = true;
5584                Ok(core_version::Version::new(3, 11, 5))
5585            },
5586        )
5587        .await
5588        .err()
5589        .expect("invalid custom root must fail before connection construction");
5590
5591        assert!(!*probe_called.lock().unwrap());
5592        assert!(matches!(
5593            error,
5594            SecureConnectError::TlsConfiguration(
5595                core_version::TlsConfigurationError::CustomRootCaUnreadable { path }
5596            ) if path == missing
5597        ));
5598    }
5599
5600    #[tokio::test]
5601    async fn enabled_probe_tls_failure_is_terminal_before_grpc_fallback() {
5602        let error = gated_driver_secure_with_probe(
5603            "127.0.0.1:1",
5604            "admin",
5605            "password",
5606            SecureConnectOptions {
5607                http_port: 8123,
5608                tls_mode: TlsMode::NativeRoots,
5609                server_version: None,
5610            },
5611            |_address, _port, mode| {
5612                assert!(matches!(mode, ResolvedTlsProbeMode::NativeRoots));
5613                Err(core_version::VersionProbeError::TlsConfiguration(
5614                    core_version::TlsConfigurationError::NativeRootsUnavailable,
5615                ))
5616            },
5617        )
5618        .await
5619        .err()
5620        .expect("TLS configuration failure must be terminal");
5621
5622        assert!(matches!(
5623            error,
5624            SecureConnectError::TlsConfiguration(
5625                core_version::TlsConfigurationError::NativeRootsUnavailable
5626            )
5627        ));
5628    }
5629
5630    /// The injectable probe must receive the configured port, not a
5631    /// hardcoded 8000.  The probe returns an in-window version so the gate
5632    /// proceeds to the gRPC connection attempt, which fails with a network
5633    /// error (no server) — the assertion is about the recorded port and
5634    /// that the HTTP failure fallback path was not involved.
5635    #[tokio::test]
5636    async fn gated_driver_probe_receives_configured_port() {
5637        let recorded_port: Arc<Mutex<Option<u16>>> = Arc::new(Mutex::new(None));
5638        let captured = Arc::clone(&recorded_port);
5639
5640        let result = gated_driver_with_probe(
5641            "localhost:1729",
5642            "admin",
5643            "password",
5644            ConnectOptions {
5645                http_port: 9123,
5646                tls: false,
5647                server_version: None,
5648            },
5649            move |_addr, port, _tls| {
5650                *captured.lock().unwrap() = Some(port);
5651                Ok(core_version::Version::new(3, 11, 5))
5652            },
5653        )
5654        .await;
5655
5656        let observed = recorded_port.lock().unwrap().expect("probe was not called");
5657        assert_eq!(
5658            observed, 9123,
5659            "probe must receive the configured http_port (9123), got {observed}"
5660        );
5661
5662        if let Err(RuntimeError::UnsupportedVersion(_)) = result {
5663            panic!("expected a connection error (no server), not a version gate rejection")
5664        }
5665    }
5666
5667    /// When HTTP version detection fails, the constructor attempts the safe
5668    /// band-8 discovery path. If it cannot connect, the error should preserve
5669    /// both the discovery failure and the HTTP failure.
5670    #[tokio::test]
5671    async fn gated_driver_http_failure_reports_grpc_fallback_failures() {
5672        let result = gated_driver_with_probe(
5673            "127.0.0.1:1",
5674            "admin",
5675            "password",
5676            ConnectOptions {
5677                http_port: 9123,
5678                tls: false,
5679                server_version: None,
5680            },
5681            move |_addr, _port, _tls| {
5682                Err(core_version::VersionError::Probe(
5683                    "HTTP endpoint unavailable".to_string(),
5684                ))
5685            },
5686        )
5687        .await;
5688
5689        match result {
5690            Err(RuntimeError::UnsupportedVersion(err)) => {
5691                let msg = err.to_string();
5692                assert!(msg.contains("HTTP endpoint unavailable"), "{msg}");
5693                assert!(msg.contains("band-8 gRPC attempt failed"), "{msg}");
5694                // Band 9 must never be probed blindly: a band-9 connect
5695                // attempt crashes a 3.11 server (see grpc_fallback_driver).
5696                assert!(!msg.contains("band-9 gRPC attempt"), "{msg}");
5697            }
5698            Err(other) => panic!("expected aggregated version-probe failure, got {other}"),
5699            Ok(_) => panic!("expected aggregated version-probe failure, got successful connection"),
5700        }
5701    }
5702
5703    #[cfg(feature = "band8")]
5704    #[test]
5705    fn band8_lazy_validation_failure_is_retryable() {
5706        let result = classify_band8_grpc_version(
5707            "localhost:1729",
5708            Err("protocol handshake failed".to_string()),
5709        )
5710        .expect("transport validation failure should remain retryable");
5711
5712        match result {
5713            Band8GrpcVersion::RetryableFailure(failure) => {
5714                assert!(failure.contains("localhost:1729"), "{failure}");
5715                assert!(failure.contains("protocol handshake failed"), "{failure}");
5716            }
5717            Band8GrpcVersion::Validated(version) => {
5718                panic!("failed lazy connection unexpectedly validated as {version}")
5719            }
5720        }
5721    }
5722
5723    #[cfg(feature = "band8")]
5724    #[test]
5725    fn band8_reported_versions_are_authoritative() {
5726        let unsupported = classify_band8_grpc_version("localhost:1729", Ok("3.7.3".to_string()))
5727            .expect_err("reported below-window version must be terminal");
5728        assert!(matches!(
5729            unsupported,
5730            RuntimeError::UnsupportedVersion(core_version::VersionError::Unsupported {
5731                component: "server",
5732                found: core_version::Version {
5733                    major: 3,
5734                    minor: 7,
5735                    patch: 3,
5736                },
5737            })
5738        ));
5739
5740        let retired = classify_band8_grpc_version("localhost:1729", Ok("3.10.4".to_string()))
5741            .expect_err("reported retired server version must not silently fall through");
5742        assert!(matches!(
5743            retired,
5744            RuntimeError::UnsupportedVersion(core_version::VersionError::Unsupported {
5745                component: "server",
5746                found: core_version::Version {
5747                    major: 3,
5748                    minor: 10,
5749                    patch: 4,
5750                },
5751            })
5752        ));
5753
5754        let malformed =
5755            classify_band8_grpc_version("localhost:1729", Ok("not-a-version".to_string()))
5756                .expect_err("malformed reported version must be terminal");
5757        assert!(matches!(
5758            malformed,
5759            RuntimeError::UnsupportedVersion(core_version::VersionError::Parse(_))
5760        ));
5761
5762        let validated = classify_band8_grpc_version("localhost:1729", Ok("3.11.5".to_string()))
5763            .expect("reported band-8 version should validate");
5764        assert!(matches!(
5765            validated,
5766            Band8GrpcVersion::Validated(core_version::Version {
5767                major: 3,
5768                minor: 11,
5769                patch: 5,
5770            })
5771        ));
5772
5773        let backward_compatible =
5774            classify_band8_grpc_version("localhost:1729", Ok("3.12.0".to_string()))
5775                .expect("reported band-9 server accepts the discovery band");
5776        assert!(matches!(
5777            backward_compatible,
5778            Band8GrpcVersion::Validated(core_version::Version {
5779                major: 3,
5780                minor: 12,
5781                patch: 0,
5782            })
5783        ));
5784    }
5785
5786    /// A caller-supplied exact server version is the gRPC-only escape hatch: it
5787    /// must bypass the HTTP probe but still flow through the normal embedded
5788    /// runtime gate before driver construction.
5789    #[tokio::test]
5790    async fn gated_driver_pinned_version_skips_probe() {
5791        let probe_called = Arc::new(Mutex::new(false));
5792        let captured = Arc::clone(&probe_called);
5793
5794        let result = gated_driver_with_probe(
5795            "localhost:1729",
5796            "admin",
5797            "password",
5798            ConnectOptions {
5799                http_port: 9123,
5800                tls: false,
5801                server_version: Some(core_version::Version::new(3, 11, 5)),
5802            },
5803            move |_addr, _port, _tls| {
5804                *captured.lock().unwrap() = true;
5805                Ok(core_version::Version::new(3, 11, 5))
5806            },
5807        )
5808        .await;
5809
5810        assert!(
5811            !*probe_called.lock().unwrap(),
5812            "pinned server_version must skip the HTTP probe"
5813        );
5814
5815        if let Err(RuntimeError::UnsupportedVersion(_)) = result {
5816            panic!("expected a connection error (no server), not a version gate rejection")
5817        }
5818    }
5819
5820    /// Pinned versions still use the exact semantic version gate, so a gRPC-only
5821    /// path cannot silently accept an unsupported server.
5822    #[tokio::test]
5823    async fn gated_driver_rejects_unsupported_pinned_version_without_probe() {
5824        let probe_called = Arc::new(Mutex::new(false));
5825        let captured = Arc::clone(&probe_called);
5826
5827        let result = gated_driver_with_probe(
5828            "localhost:1729",
5829            "admin",
5830            "password",
5831            ConnectOptions {
5832                http_port: 9123,
5833                tls: false,
5834                server_version: Some(core_version::Version::new(3, 7, 3)),
5835            },
5836            move |_addr, _port, _tls| {
5837                *captured.lock().unwrap() = true;
5838                Ok(core_version::Version::new(3, 11, 5))
5839            },
5840        )
5841        .await;
5842
5843        assert!(
5844            !*probe_called.lock().unwrap(),
5845            "unsupported pinned server_version must skip the HTTP probe"
5846        );
5847
5848        match result {
5849            Err(RuntimeError::UnsupportedVersion(err)) => {
5850                assert!(
5851                    err.to_string().contains("3.7.3"),
5852                    "error should name rejected version: {err}"
5853                );
5854            }
5855            Err(other) => panic!("expected unsupported-version rejection for 3.7.3, got {other}"),
5856            Ok(_) => panic!(
5857                "expected unsupported-version rejection for 3.7.3, got successful connection"
5858            ),
5859        }
5860    }
5861
5862    #[test]
5863    fn cargo_lock_pin() {
5864        let lock_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.lock");
5865        let lock_contents = std::fs::read_to_string(lock_path)
5866            .expect("Cargo.lock not found relative to crate root");
5867
5868        let lock_version = lock_contents
5869            .split("[[package]]")
5870            .find(|block| block.contains("name = \"type-bridge-typedb-driver-b8\""))
5871            .and_then(|block| {
5872                block
5873                    .lines()
5874                    .find(|line| line.trim_start().starts_with("version = "))
5875            })
5876            .and_then(|line| {
5877                let start = line.find('"')? + 1;
5878                let end = line.rfind('"')?;
5879                Some(&line[start..end])
5880            })
5881            .expect("type-bridge-typedb-driver-b8 entry not found in Cargo.lock");
5882
5883        assert_eq!(
5884            lock_version, PINNED_DRIVER_VERSION,
5885            "Cargo.lock resolves type-bridge-typedb-driver-b8 {lock_version} but \
5886             PINNED_DRIVER_VERSION \
5887             is {PINNED_DRIVER_VERSION}; update the runtime constant"
5888        );
5889
5890        let pinned: core_version::Version = PINNED_DRIVER_VERSION.parse().unwrap();
5891        assert_eq!(
5892            core_version::band(&pinned),
5893            Some(8),
5894            "pinned driver version {PINNED_DRIVER_VERSION} left protocol band 8; \
5895             review the gate expectations before accepting the bump"
5896        );
5897    }
5898
5899    /// Portable given-rows lower onto the band-9 driver structures, including
5900    /// temporal string parsing; malformed rows fail before any wire I/O.
5901    #[cfg(feature = "band9")]
5902    #[test]
5903    fn given_rows_lowering() {
5904        let spec = GivenRowsSpec {
5905            variables: vec!["n".into(), "a".into()],
5906            rows: vec![
5907                vec![GivenValue::String("alice".into()), GivenValue::Integer(28)],
5908                vec![GivenValue::String("bob".into()), GivenValue::Integer(26)],
5909            ],
5910        };
5911        let rows = given_rows_b9(spec).expect("valid spec must lower");
5912        let (header, rows) = rows.into_parts();
5913        assert_eq!(header.width(), 2);
5914        assert_eq!(rows.len(), 2);
5915    }
5916
5917    #[cfg(feature = "band9")]
5918    #[test]
5919    fn given_rows_lowering_rejects_width_mismatch() {
5920        let spec = GivenRowsSpec {
5921            variables: vec!["n".into(), "a".into()],
5922            rows: vec![vec![GivenValue::String("alice".into())]],
5923        };
5924        let err = given_rows_b9(spec).expect_err("short row must be rejected");
5925        assert!(matches!(err, RuntimeError::QueryExecution(_)), "{err}");
5926    }
5927
5928    #[cfg(feature = "band9")]
5929    #[test]
5930    fn given_entry_temporal_parsing() {
5931        for value in [
5932            GivenValue::Empty,
5933            GivenValue::Date("2026-07-13".into()),
5934            GivenValue::Datetime("2026-07-13T10:30:00".into()),
5935            GivenValue::DatetimeTz("2026-07-13T10:30:00+09:00".into()),
5936            GivenValue::DatetimeTz("2026-07-13T10:30:00+00:19:32".into()),
5937            GivenValue::DatetimeTzExact {
5938                local: "2024-10-27T01:30:00".into(),
5939                named_zone: Some("Europe/London".into()),
5940                effective_offset_seconds: 3_600,
5941            },
5942            GivenValue::DatetimeTzExact {
5943                local: "2024-07-01T12:00:00".into(),
5944                named_zone: Some("europe/amsterdam".into()),
5945                effective_offset_seconds: 7_200,
5946            },
5947            GivenValue::DatetimeTzExact {
5948                local: "1900-01-01T12:00:00".into(),
5949                named_zone: None,
5950                effective_offset_seconds: 1_172,
5951            },
5952            GivenValue::Decimal("12.30dec".into()),
5953            GivenValue::Decimal("-9223372036854775808".into()),
5954            GivenValue::Duration {
5955                months: u32::MAX,
5956                days: u32::MAX,
5957                nanos: u64::MAX,
5958            },
5959            GivenValue::Boolean(true),
5960            GivenValue::Double(1.5),
5961        ] {
5962            given_entry_b9(value.clone()).unwrap_or_else(|e| panic!("{value:?} must convert: {e}"));
5963        }
5964    }
5965
5966    #[cfg(feature = "band9")]
5967    #[test]
5968    fn exact_given_entries_preserve_named_timezone_and_duration_components() {
5969        use typedb_driver::concept::Value;
5970        use typedb_driver::concept::value::TimeZone;
5971        use typedb_driver::given::GivenRowEntry;
5972
5973        let named = given_entry_b9(GivenValue::DatetimeTzExact {
5974            local: "2024-10-27T01:30:00".into(),
5975            named_zone: Some("Europe/London".into()),
5976            effective_offset_seconds: 3_600,
5977        })
5978        .expect("explicit overlap side");
5979        let GivenRowEntry::Value(Value::DatetimeTZ(named)) = named else {
5980            panic!("datetime-tz given entry")
5981        };
5982        let TimeZone::IANA(timezone) = named.timezone() else {
5983            panic!("authored IANA identity must survive transport")
5984        };
5985        assert_eq!(timezone.name(), "Europe/London");
5986        assert_eq!(named.naive_local().to_string(), "2024-10-27 01:30:00");
5987        assert_eq!(
5988            named
5989                .naive_local()
5990                .signed_duration_since(named.naive_utc())
5991                .num_seconds(),
5992            3_600
5993        );
5994
5995        let lower_case = given_entry_b9(GivenValue::DatetimeTzExact {
5996            local: "2024-07-01T12:00:00".into(),
5997            named_zone: Some("europe/amsterdam".into()),
5998            effective_offset_seconds: 7_200,
5999        })
6000        .expect("case-insensitive names admitted by schema also lower");
6001        let GivenRowEntry::Value(Value::DatetimeTZ(lower_case)) = lower_case else {
6002            panic!("datetime-tz given entry")
6003        };
6004        let TimeZone::IANA(timezone) = lower_case.timezone() else {
6005            panic!("IANA identity must survive transport")
6006        };
6007        assert_eq!(
6008            timezone.name(),
6009            "Europe/Amsterdam",
6010            "the provider canonicalizes accepted authored case"
6011        );
6012
6013        let duration = given_entry_b9(GivenValue::Duration {
6014            months: u32::MAX,
6015            days: u32::MAX,
6016            nanos: u64::MAX,
6017        })
6018        .expect("duration boundary");
6019        let GivenRowEntry::Value(Value::Duration(duration)) = duration else {
6020            panic!("duration given entry")
6021        };
6022        assert_eq!(duration.months, u32::MAX);
6023        assert_eq!(duration.days, u32::MAX);
6024        assert_eq!(duration.nanos, u64::MAX);
6025    }
6026
6027    #[cfg(feature = "band9")]
6028    #[test]
6029    fn given_entry_rejects_malformed_temporal() {
6030        for value in [
6031            GivenValue::Date("not-a-date".into()),
6032            GivenValue::Datetime("2026-13-45T99:00:00".into()),
6033            GivenValue::DatetimeTz("2026-07-13 10:30".into()),
6034            GivenValue::DatetimeTz("2026-07-13T10:30:00+24:00".into()),
6035            GivenValue::DatetimeTzExact {
6036                local: "2024-10-27T01:30:00".into(),
6037                named_zone: Some("Europe/London".into()),
6038                effective_offset_seconds: 7_200,
6039            },
6040            GivenValue::DatetimeTzExact {
6041                local: "2024-03-31T01:30:00".into(),
6042                named_zone: Some("Europe/London".into()),
6043                effective_offset_seconds: 3_600,
6044            },
6045            GivenValue::Decimal("1.00000000000000000000".into()),
6046        ] {
6047            let err = given_entry_b9(value.clone())
6048                .expect_err("malformed temporal string must be rejected");
6049            assert!(
6050                err.to_string().contains("Invalid given"),
6051                "{value:?}: {err}"
6052            );
6053        }
6054    }
6055
6056    #[test]
6057    fn cargo_lock_pin_b9() {
6058        let lock_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.lock");
6059        let lock_contents = std::fs::read_to_string(lock_path)
6060            .expect("Cargo.lock not found relative to crate root");
6061
6062        let lock_version = lock_contents
6063            .split("[[package]]")
6064            .find(|block| block.contains("name = \"typedb-driver\""))
6065            .and_then(|block| {
6066                block
6067                    .lines()
6068                    .find(|line| line.trim_start().starts_with("version = "))
6069            })
6070            .and_then(|line| {
6071                let start = line.find('"')? + 1;
6072                let end = line.rfind('"')?;
6073                Some(&line[start..end])
6074            })
6075            .expect("typedb-driver entry not found in Cargo.lock");
6076
6077        assert_eq!(
6078            lock_version, PINNED_DRIVER_VERSION_B9,
6079            "Cargo.lock resolves typedb-driver {lock_version} but \
6080             PINNED_DRIVER_VERSION_B9 is {PINNED_DRIVER_VERSION_B9}; update the runtime constant"
6081        );
6082
6083        let pinned: core_version::Version = PINNED_DRIVER_VERSION_B9.parse().unwrap();
6084        assert_eq!(
6085            core_version::band(&pinned),
6086            Some(9),
6087            "pinned band-9 driver version {PINNED_DRIVER_VERSION_B9} left protocol band 9; \
6088             review the gate expectations before accepting the bump"
6089        );
6090    }
6091
6092    #[test]
6093    fn materialize_runtime_answer_preserves_empty_kinds() {
6094        let stats = |kind| RuntimeAnswerStats {
6095            kind,
6096            processed_items: 0,
6097            response_bytes: 0,
6098            stopped_early: false,
6099        };
6100        assert!(matches!(
6101            super::materialize_runtime_answer(stats(RuntimeAnswerKind::Ok), Vec::new()),
6102            QueryResult::Ok
6103        ));
6104        assert!(matches!(
6105            super::materialize_runtime_answer(stats(RuntimeAnswerKind::Rows), Vec::new()),
6106            QueryResult::Rows(values) if values.is_empty()
6107        ));
6108        assert!(matches!(
6109            super::materialize_runtime_answer(stats(RuntimeAnswerKind::Documents), Vec::new()),
6110            QueryResult::Documents(values) if values.is_empty()
6111        ));
6112    }
6113
6114    #[test]
6115    fn canonical_decimal_text_normalizes_provider_display() {
6116        assert_eq!(
6117            super::canonical_decimal_text("001234.5600dec".into()).unwrap(),
6118            "1234.56"
6119        );
6120        assert_eq!(
6121            super::canonical_decimal_text("-0.000dec".into()).unwrap(),
6122            "0"
6123        );
6124        assert_eq!(
6125            super::canonical_decimal_text("-9223372036854775808dec".into()).unwrap(),
6126            "-9223372036854775808"
6127        );
6128        assert!(matches!(
6129            super::canonical_decimal_text("not-a-decimal".into()),
6130            Err(RuntimeError::QueryExecution(message)) if message == "provider returned an unparseable decimal"
6131        ));
6132    }
6133}