1#![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
49pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum CommitFailureCertainty {
55 DefinitelyAborted,
57 Unknown,
59}
60
61#[derive(Debug, thiserror::Error)]
63pub enum RuntimeError {
64 #[error("Unsupported version: {0}")]
67 UnsupportedVersion(#[from] core_version::VersionError),
68
69 #[error("Connection error: {0}")]
71 Connection(String),
72
73 #[error("Query execution error: {0}")]
75 QueryExecution(String),
76
77 #[error("Transaction error: {0}")]
79 Transaction(String),
80
81 #[error("Resource limit [{code}]: {message}")]
83 ResourceLimit {
84 code: &'static str,
86 message: &'static str,
88 },
89
90 #[error("Answer consumer rejected a streamed provider item")]
92 AnswerConsumer,
93}
94
95#[derive(Debug, thiserror::Error)]
102pub enum RuntimeCommitError {
103 #[error(transparent)]
105 Runtime(#[from] RuntimeError),
106 #[error("Transaction error: Commit failed: {message}")]
108 Driver {
109 certainty: CommitFailureCertainty,
111 message: String,
113 },
114}
115
116impl RuntimeCommitError {
117 #[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
216pub type Result<T> = std::result::Result<T, RuntimeError>;
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub enum QueryResult {
222 Ok,
224 Documents(Vec<serde_json::Value>),
226 Rows(Vec<serde_json::Value>),
228}
229
230#[derive(Debug, Clone, PartialEq)]
232pub enum RuntimeAnswerItem {
233 Row(serde_json::Value),
235 Document(serde_json::Value),
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum RuntimeAnswerKind {
242 Ok,
244 Rows,
246 Documents,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum RuntimeAnswerControl {
253 Continue,
255 Stop,
257}
258
259#[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 pub fn from_shared(cancelled: watch::Sender<bool>) -> Self {
275 Self { cancelled }
276 }
277
278 pub fn cancel(&self) {
280 self.cancelled.send_replace(true);
281 }
282
283 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#[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 #[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
338const BOUNDED_QUERY_PREFETCH_SIZE: u64 = 2;
351const _: () = assert!(BOUNDED_QUERY_PREFETCH_SIZE >= 2);
352
353#[derive(Debug, Clone)]
355pub struct RuntimeAnswerLimits {
356 pub max_items: u64,
358 pub max_bytes: u64,
360 pub deadline: Option<Instant>,
362 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#[derive(Debug, Clone)]
386pub struct QueryV2RuntimeAnswerLimits {
387 pub answer: RuntimeAnswerLimits,
389 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub struct RuntimeAnswerStats {
410 pub kind: RuntimeAnswerKind,
412 pub processed_items: u64,
414 pub response_bytes: u64,
416 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
433pub enum TxType {
434 Read,
436 Write,
438 Schema,
440}
441
442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448pub enum GivenValue {
449 Empty,
451 Boolean(bool),
453 Integer(i64),
455 Double(f64),
457 String(String),
459 Date(String),
461 Datetime(String),
463 DatetimeTz(String),
465 DatetimeTzExact {
468 local: String,
470 named_zone: Option<String>,
472 effective_offset_seconds: i32,
474 },
475 Decimal(String),
477 Duration {
479 months: u32,
481 days: u32,
483 nanos: u64,
485 },
486}
487
488#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493pub struct GivenRowsSpec {
494 pub variables: Vec<String>,
496 pub rows: Vec<Vec<GivenValue>>,
498}
499
500pub const PINNED_DRIVER_VERSION: &str = "3.11.5";
511
512pub const PINNED_DRIVER_VERSION_B9: &str = "3.12.3";
520
521const EMBEDDED_BANDS: &[u8] = &[
530 #[cfg(feature = "band8")]
531 8,
532 #[cfg(feature = "band9")]
533 9,
534];
535
536pub 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#[derive(Debug, Clone, Copy)]
559pub struct ConnectOptions {
560 pub http_port: u16,
562 pub tls: bool,
564 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#[derive(Debug, Clone, PartialEq, Eq, Default)]
588pub enum TlsMode {
589 #[default]
591 Disabled,
592 NativeRoots,
594 CustomRootCa(PathBuf),
596}
597
598impl TlsMode {
599 #[must_use]
601 pub const fn is_enabled(&self) -> bool {
602 !matches!(self, Self::Disabled)
603 }
604}
605
606#[derive(Debug, Clone, PartialEq, Eq)]
608pub struct SecureConnectOptions {
609 pub http_port: u16,
611 pub tls_mode: TlsMode,
613 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 pub fn validate_transport(&self) -> SecureResult<()> {
639 self.prepare_transport().map(|_| ())
640 }
641
642 #[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 #[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 #[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#[derive(Debug, thiserror::Error)]
718#[non_exhaustive]
719pub enum SecureConnectError {
720 #[error(transparent)]
722 TlsConfiguration(#[from] core_version::TlsConfigurationError),
723 #[error(
725 "TLS configuration error [tls_driver_lowering_failed]: TypeDB driver band {band} rejected the TLS policy"
726 )]
727 DriverTlsConfiguration {
728 band: u8,
730 },
731 #[error(transparent)]
733 Runtime(#[from] RuntimeError),
734}
735
736impl SecureConnectError {
737 #[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 #[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 #[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
899pub type SecureResult<T> = std::result::Result<T, SecureConnectError>;
901
902#[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#[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 #[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 #[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 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
1261enum 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 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 state.store(DRIVER_CLOSING, AtomicOrdering::Release);
1308 }
1309 DRIVER_CLOSING => {
1310 }
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
1342pub(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 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 if let Err(error) = self.force_close() {
1447 trace_driver_drop_close_failure(&error);
1448 }
1449 }
1450}
1451
1452pub struct TypeDBRuntime {
1454 driver: Arc<DriverHandle>,
1455 server_version: Option<core_version::Version>,
1461}
1462
1463impl TypeDBRuntime {
1464 pub fn force_close(&self) -> Result<()> {
1477 self.driver.force_close()
1478 }
1479}
1480
1481#[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
1513async 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 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 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 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 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 #[cfg(feature = "band8")]
1794 {
1795 match connect_band8_driver(address, username, password, tls, meter).await {
1796 Ok(driver) => {
1797 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 #[cfg(feature = "band9")]
1840 if core_version::negotiate_server_band(&server_version, EMBEDDED_BANDS)
1841 == Some(9)
1842 {
1843 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 return Ok(Band8GrpcVersion::RetryableFailure(format!(
1913 "band-8 gRPC version validation failed after connect to {address}: {error}"
1914 )));
1915 }
1916 };
1917
1918 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
1955async 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
1967async 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 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 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 #[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 pub fn server_version(&self) -> Option<core_version::Version> {
2066 self.server_version
2067 }
2068
2069 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
2106pub 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
2128pub 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#[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
2175pub 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
2191pub 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#[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
2229pub 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#[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 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 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 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 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 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 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 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
2484enum 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
2496pub struct RuntimeTransaction {
2498 inner: RuntimeTransactionInner,
2499 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 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 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 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 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 #[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 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 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 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 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 pub fn commit_classified(
3046 &mut self,
3047 ) -> BoxFuture<'_, std::result::Result<(), RuntimeCommitError>> {
3048 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 pub fn rollback(&mut self) -> BoxFuture<'_, Result<()>> {
3087 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 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#[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#[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#[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 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#[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#[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#[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 (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(); direct.charge_version_evidence(version).unwrap();
4934 direct.charge_statement().unwrap(); 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(); fallback.charge_statement().unwrap(); fallback.charge_statement().unwrap(); fallback.charge_version_evidence(version).unwrap();
4950 assert!(matches!(
4951 fallback.charge_statement(), 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 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 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 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 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 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 #[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 #[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 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 #[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 #[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 #[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}