#[cfg(not(any(feature = "band7", feature = "band8")))]
compile_error!(
"type-bridge-typedb-runtime requires at least one band feature; enable `band7` and/or `band8` (both are default)"
);
use std::future::Future;
use std::pin::Pin;
use futures::TryStreamExt;
use serde::{Deserialize, Serialize};
use type_bridge_core_lib::version as core_version;
use type_bridge_core_lib::version::DEFAULT_HTTP_PORT;
#[cfg(feature = "band8")]
use typedb_driver::answer::QueryAnswer as B8QueryAnswer;
#[cfg(feature = "band8")]
use typedb_driver::{
Addresses, Credentials as B8Credentials, DriverOptions, DriverTlsConfig,
TransactionType as B8TransactionType, TypeDBDriver as B8Driver,
};
#[cfg(feature = "band7")]
use type_bridge_typedb_driver_b7::answer::QueryAnswer as B7QueryAnswer;
#[cfg(feature = "band7")]
use type_bridge_typedb_driver_b7::{
Credentials as B7Credentials, DriverOptions as B7DriverOptions,
TransactionType as B7TransactionType, TypeDBDriver as B7Driver,
};
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("Unsupported version: {0}")]
UnsupportedVersion(#[from] core_version::VersionError),
#[error("Connection error: {0}")]
Connection(String),
#[error("Query execution error: {0}")]
QueryExecution(String),
#[error("Transaction error: {0}")]
Transaction(String),
}
pub type Result<T> = std::result::Result<T, RuntimeError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum QueryResult {
Ok,
Documents(Vec<serde_json::Value>),
Rows(Vec<serde_json::Value>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TxType {
Read,
Write,
Schema,
}
pub const PINNED_DRIVER_VERSION: &str = "3.11.5";
pub const PINNED_DRIVER_VERSION_B7: &str = "3.8.1";
const EMBEDDED_BANDS: &[u8] = &[
#[cfg(feature = "band7")]
7,
#[cfg(feature = "band8")]
8,
];
pub fn embedded_driver_versions() -> &'static [(u8, &'static str)] {
&[
#[cfg(feature = "band7")]
(7, PINNED_DRIVER_VERSION_B7),
#[cfg(feature = "band8")]
(8, PINNED_DRIVER_VERSION),
]
}
#[derive(Debug, Clone, Copy)]
pub struct ConnectOptions {
pub http_port: u16,
pub tls: bool,
pub server_version: Option<core_version::Version>,
}
impl Default for ConnectOptions {
fn default() -> Self {
Self {
http_port: DEFAULT_HTTP_PORT,
tls: false,
server_version: None,
}
}
}
pub(crate) enum DriverHandle {
#[cfg(feature = "band7")]
B7(B7Driver),
#[cfg(feature = "band8")]
B8(B8Driver),
}
pub struct TypeDBRuntime {
driver: DriverHandle,
}
pub(crate) async fn gated_driver_with_probe<F>(
address: &str,
username: &str,
password: &str,
options: ConnectOptions,
probe: F,
) -> Result<DriverHandle>
where
F: FnOnce(
&str,
u16,
bool,
) -> std::result::Result<core_version::Version, core_version::VersionError>
+ Send
+ 'static,
{
if let Some(server_version) = options.server_version {
return driver_for_server_version(address, username, password, server_version).await;
}
let address_owned = address.to_string();
let http_port = options.http_port;
let tls = options.tls;
match tokio::task::spawn_blocking(move || probe(&address_owned, http_port, tls))
.await
.map_err(|e| RuntimeError::Connection(format!("Version probe task panicked: {e}")))?
{
Ok(server_version) => {
driver_for_server_version(address, username, password, server_version).await
}
Err(http_error) => grpc_fallback_driver(address, username, password, http_error).await,
}
}
fn validate_server_band(server_version: &core_version::Version) -> Result<u8> {
core_version::check_server_supported(server_version, EMBEDDED_BANDS)
.map_err(RuntimeError::UnsupportedVersion)?;
Ok(core_version::band(server_version)
.expect("check_server_supported accepted a server without a mapped band"))
}
async fn driver_for_server_version(
address: &str,
username: &str,
password: &str,
server_version: core_version::Version,
) -> Result<DriverHandle> {
let band = validate_server_band(&server_version)?;
tracing::debug!(
address,
band = ?band,
server_version = %server_version,
"Embedded version gate passed"
);
#[cfg(feature = "band7")]
if band == 7 {
return connect_band7_driver(address, username, password)
.await
.map(DriverHandle::B7);
}
#[cfg(feature = "band8")]
{
connect_band8_driver(address, username, password)
.await
.map(DriverHandle::B8)
}
#[cfg(not(feature = "band8"))]
Err(RuntimeError::Connection(format!(
"No compiled driver band supports the detected server band ({band:?})"
)))
}
#[cfg(feature = "band7")]
async fn connect_band7_driver(address: &str, username: &str, password: &str) -> Result<B7Driver> {
let opts = B7DriverOptions::new(false, None)
.map_err(|e| RuntimeError::Connection(format!("Band-7 driver options error: {e}")))?;
B7Driver::new(address, B7Credentials::new(username, password), opts)
.await
.map_err(|e| RuntimeError::Connection(format!("Failed to connect to {address}: {e}")))
}
#[cfg(feature = "band8")]
async fn connect_band8_driver(address: &str, username: &str, password: &str) -> Result<B8Driver> {
let addresses = Addresses::try_from_address_str(address)
.map_err(|e| RuntimeError::Connection(format!("Invalid TypeDB address {address}: {e}")))?;
B8Driver::new(
addresses,
B8Credentials::new(username, password),
DriverOptions::new(DriverTlsConfig::disabled()),
)
.await
.map_err(|e| RuntimeError::Connection(format!("Failed to connect to {address}: {e}")))
}
async fn grpc_fallback_driver(
address: &str,
username: &str,
password: &str,
http_error: core_version::VersionError,
) -> Result<DriverHandle> {
let mut failures = vec![format!("HTTP version probe failed: {http_error}")];
#[cfg(feature = "band8")]
{
match connect_band8_driver(address, username, password).await {
Ok(driver) => {
let reported = driver.server_version().await.map_err(|e| {
RuntimeError::Connection(format!(
"Band-8 gRPC version validation failed after connect to {address}: {e}"
))
})?;
let server_version = reported
.version()
.parse::<core_version::Version>()
.map_err(RuntimeError::UnsupportedVersion)?;
let band = validate_server_band(&server_version)?;
if band != 8 {
return Err(RuntimeError::UnsupportedVersion(
core_version::VersionError::Probe(format!(
"band-8 gRPC connection reported non-band-8 server version {server_version}"
)),
));
}
tracing::debug!(
address,
server_version = %server_version,
"Connected through gRPC band-8 fallback after HTTP version probe failed"
);
return Ok(DriverHandle::B8(driver));
}
Err(error) => failures.push(format!("band-8 gRPC attempt failed: {error}")),
}
}
#[cfg(not(feature = "band8"))]
failures.push("band-8 gRPC attempt skipped: band8 feature is not compiled in".to_string());
#[cfg(feature = "band7")]
{
match connect_band7_driver(address, username, password).await {
Ok(driver) => {
tracing::warn!(
address,
"Connected through gRPC band-7 fallback after HTTP version probe failed; \
exact server version is unavailable on band 7, so use server_version=... \
for strict gRPC-only version validation"
);
return Ok(DriverHandle::B7(driver));
}
Err(error) => failures.push(format!("band-7 gRPC attempt failed: {error}")),
}
}
#[cfg(not(feature = "band7"))]
failures.push("band-7 gRPC attempt skipped: band7 feature is not compiled in".to_string());
Err(RuntimeError::UnsupportedVersion(
core_version::VersionError::Probe(format!(
"HTTP version probe and gRPC fallback both failed: {}",
failures.join("; ")
)),
))
}
async fn gated_driver(
address: &str,
username: &str,
password: &str,
options: ConnectOptions,
) -> Result<DriverHandle> {
gated_driver_with_probe(
address,
username,
password,
options,
core_version::server_version,
)
.await
}
impl TypeDBRuntime {
pub async fn connect(
address: &str,
username: &str,
password: &str,
options: ConnectOptions,
) -> Result<Self> {
let driver = gated_driver(address, username, password, options).await?;
tracing::info!(address, "Connected to TypeDB");
Ok(Self { driver })
}
}
pub async fn ensure_database_exists(
address: &str,
database: &str,
username: &str,
password: &str,
options: ConnectOptions,
) -> Result<()> {
let driver = gated_driver(address, username, password, options).await?;
match driver {
#[cfg(feature = "band7")]
DriverHandle::B7(d) => {
let databases = d.databases();
let exists = databases
.contains(database)
.await
.map_err(|e| RuntimeError::Connection(format!("Database lookup failed: {e}")))?;
if !exists {
databases.create(database).await.map_err(|e| {
RuntimeError::Connection(format!("Database create failed: {e}"))
})?;
}
}
#[cfg(feature = "band8")]
DriverHandle::B8(d) => {
let databases = d.databases();
let exists = databases
.contains(database)
.await
.map_err(|e| RuntimeError::Connection(format!("Database lookup failed: {e}")))?;
if !exists {
databases.create(database).await.map_err(|e| {
RuntimeError::Connection(format!("Database create failed: {e}"))
})?;
}
}
}
Ok(())
}
impl TypeDBRuntime {
pub fn open_transaction(
&self,
database: &str,
tx_type: TxType,
) -> BoxFuture<'_, Result<RuntimeTransaction>> {
let db = database.to_string();
Box::pin(async move {
match &self.driver {
#[cfg(feature = "band7")]
DriverHandle::B7(d) => {
let typedb_tx_type = match tx_type {
TxType::Read => B7TransactionType::Read,
TxType::Write => B7TransactionType::Write,
TxType::Schema => B7TransactionType::Schema,
};
let transaction = d.transaction(&db, typedb_tx_type).await.map_err(|e| {
RuntimeError::Transaction(format!("Failed to open transaction: {e}"))
})?;
Ok(RuntimeTransaction {
inner: RuntimeTransactionInner::B7(Some(transaction)),
})
}
#[cfg(feature = "band8")]
DriverHandle::B8(d) => {
let typedb_tx_type = match tx_type {
TxType::Read => B8TransactionType::Read,
TxType::Write => B8TransactionType::Write,
TxType::Schema => B8TransactionType::Schema,
};
let transaction = d.transaction(&db, typedb_tx_type).await.map_err(|e| {
RuntimeError::Transaction(format!("Failed to open transaction: {e}"))
})?;
Ok(RuntimeTransaction {
inner: RuntimeTransactionInner::B8(Some(transaction)),
})
}
}
})
}
pub fn is_open(&self) -> bool {
match &self.driver {
#[cfg(feature = "band7")]
DriverHandle::B7(d) => d.is_open(),
#[cfg(feature = "band8")]
DriverHandle::B8(d) => d.is_open(),
}
}
pub fn database_exists(&self, database: &str) -> BoxFuture<'_, Result<bool>> {
let database = database.to_string();
Box::pin(async move {
match &self.driver {
#[cfg(feature = "band7")]
DriverHandle::B7(d) => {
d.databases().contains(database).await.map_err(|e| {
RuntimeError::Connection(format!("Database lookup failed: {e}"))
})
}
#[cfg(feature = "band8")]
DriverHandle::B8(d) => {
d.databases().contains(database).await.map_err(|e| {
RuntimeError::Connection(format!("Database lookup failed: {e}"))
})
}
}
})
}
pub fn create_database(&self, database: &str) -> BoxFuture<'_, Result<()>> {
let database = database.to_string();
Box::pin(async move {
match &self.driver {
#[cfg(feature = "band7")]
DriverHandle::B7(d) => {
d.databases().create(database).await.map_err(|e| {
RuntimeError::Connection(format!("Database create failed: {e}"))
})
}
#[cfg(feature = "band8")]
DriverHandle::B8(d) => {
d.databases().create(database).await.map_err(|e| {
RuntimeError::Connection(format!("Database create failed: {e}"))
})
}
}
})
}
pub fn delete_database(&self, database: &str) -> BoxFuture<'_, Result<()>> {
let database = database.to_string();
Box::pin(async move {
match &self.driver {
#[cfg(feature = "band7")]
DriverHandle::B7(d) => {
let db = d.databases().get(&database).await.map_err(|e| {
RuntimeError::Connection(format!("Database lookup failed: {e}"))
})?;
db.delete().await.map_err(|e| {
RuntimeError::Connection(format!("Database delete failed: {e}"))
})
}
#[cfg(feature = "band8")]
DriverHandle::B8(d) => {
let db = d.databases().get(database).await.map_err(|e| {
RuntimeError::Connection(format!("Database lookup failed: {e}"))
})?;
db.delete().await.map_err(|e| {
RuntimeError::Connection(format!("Database delete failed: {e}"))
})
}
}
})
}
pub fn schema_text(&self, database: &str) -> BoxFuture<'_, Result<String>> {
let database = database.to_string();
Box::pin(async move {
match &self.driver {
#[cfg(feature = "band7")]
DriverHandle::B7(d) => {
let db = d.databases().get(&database).await.map_err(|e| {
RuntimeError::Connection(format!("Database lookup failed: {e}"))
})?;
db.schema()
.await
.map_err(|e| RuntimeError::Connection(format!("Schema export failed: {e}")))
}
#[cfg(feature = "band8")]
DriverHandle::B8(d) => {
let db = d.databases().get(&database).await.map_err(|e| {
RuntimeError::Connection(format!("Database lookup failed: {e}"))
})?;
db.schema()
.await
.map_err(|e| RuntimeError::Connection(format!("Schema export failed: {e}")))
}
}
})
}
}
enum RuntimeTransactionInner {
#[cfg(feature = "band7")]
B7(Option<type_bridge_typedb_driver_b7::Transaction>),
#[cfg(feature = "band8")]
B8(Option<typedb_driver::Transaction>),
}
pub struct RuntimeTransaction {
inner: RuntimeTransactionInner,
}
impl RuntimeTransaction {
pub fn query(&mut self, typeql: &str) -> BoxFuture<'_, Result<QueryResult>> {
let tql = typeql.to_string();
Box::pin(async move {
match &self.inner {
#[cfg(feature = "band7")]
RuntimeTransactionInner::B7(opt) => {
let tx = opt.as_ref().ok_or_else(|| {
RuntimeError::Transaction("Transaction already consumed".into())
})?;
let answer = tx
.query(&tql)
.await
.map_err(|e| RuntimeError::QueryExecution(format!("{e}")))?;
match answer {
B7QueryAnswer::Ok(_) => Ok(QueryResult::Ok),
B7QueryAnswer::ConceptRowStream(_, stream) => {
let rows: Vec<_> = stream.try_collect().await.map_err(|e| {
RuntimeError::QueryExecution(format!("Row collect: {e}"))
})?;
let json_rows = rows
.iter()
.map(|row| {
let mut obj = serde_json::Map::new();
for (i, col) in row.get_column_names().iter().enumerate() {
let value = row
.row
.get(i)
.and_then(|c| c.as_ref())
.map(concept_to_json_b7)
.unwrap_or(serde_json::Value::Null);
obj.insert(col.clone(), value);
}
serde_json::Value::Object(obj)
})
.collect();
Ok(QueryResult::Rows(json_rows))
}
B7QueryAnswer::ConceptDocumentStream(_, stream) => {
let docs: Vec<_> = stream.try_collect().await.map_err(|e| {
RuntimeError::QueryExecution(format!("Doc collect: {e}"))
})?;
let json_docs = docs
.into_iter()
.map(|doc| {
serde_json::to_value(doc.into_json())
.unwrap_or(serde_json::Value::Null)
})
.collect();
Ok(QueryResult::Documents(json_docs))
}
}
}
#[cfg(feature = "band8")]
RuntimeTransactionInner::B8(opt) => {
let tx = opt.as_ref().ok_or_else(|| {
RuntimeError::Transaction("Transaction already consumed".into())
})?;
let answer = tx
.query(&tql)
.await
.map_err(|e| RuntimeError::QueryExecution(format!("{e}")))?;
match answer {
B8QueryAnswer::Ok(_) => Ok(QueryResult::Ok),
B8QueryAnswer::ConceptRowStream(_, stream) => {
let rows: Vec<_> = stream.try_collect().await.map_err(|e| {
RuntimeError::QueryExecution(format!("Row collect: {e}"))
})?;
let json_rows = rows
.iter()
.map(|row| {
let mut obj = serde_json::Map::new();
for (i, col) in row.get_column_names().iter().enumerate() {
let value = row
.row
.get(i)
.and_then(|c| c.as_ref())
.map(concept_to_json_b8)
.unwrap_or(serde_json::Value::Null);
obj.insert(col.clone(), value);
}
serde_json::Value::Object(obj)
})
.collect();
Ok(QueryResult::Rows(json_rows))
}
B8QueryAnswer::ConceptDocumentStream(_, stream) => {
let docs: Vec<_> = stream.try_collect().await.map_err(|e| {
RuntimeError::QueryExecution(format!("Doc collect: {e}"))
})?;
let json_docs = docs
.into_iter()
.map(|doc| {
serde_json::to_value(doc.into_json())
.unwrap_or(serde_json::Value::Null)
})
.collect();
Ok(QueryResult::Documents(json_docs))
}
}
}
}
})
}
pub fn commit(&mut self) -> BoxFuture<'_, Result<()>> {
match &mut self.inner {
#[cfg(feature = "band7")]
RuntimeTransactionInner::B7(opt) => {
let tx = opt.take();
Box::pin(async move {
let t = tx.ok_or_else(|| {
RuntimeError::Transaction("Transaction already consumed".into())
})?;
t.commit()
.await
.map_err(|e| RuntimeError::Transaction(format!("Commit failed: {e}")))
})
}
#[cfg(feature = "band8")]
RuntimeTransactionInner::B8(opt) => {
let tx = opt.take();
Box::pin(async move {
let t = tx.ok_or_else(|| {
RuntimeError::Transaction("Transaction already consumed".into())
})?;
t.commit()
.await
.map_err(|e| RuntimeError::Transaction(format!("Commit failed: {e}")))
})
}
}
}
pub fn rollback(&mut self) -> BoxFuture<'_, Result<()>> {
match &mut self.inner {
#[cfg(feature = "band7")]
RuntimeTransactionInner::B7(opt) => {
let tx = opt.take();
Box::pin(async move {
let t = tx.ok_or_else(|| {
RuntimeError::Transaction("Transaction already consumed".into())
})?;
t.rollback()
.await
.map_err(|e| RuntimeError::Transaction(format!("Rollback failed: {e}")))
})
}
#[cfg(feature = "band8")]
RuntimeTransactionInner::B8(opt) => {
let tx = opt.take();
Box::pin(async move {
let t = tx.ok_or_else(|| {
RuntimeError::Transaction("Transaction already consumed".into())
})?;
t.rollback()
.await
.map_err(|e| RuntimeError::Transaction(format!("Rollback failed: {e}")))
})
}
}
}
pub fn close(&mut self) -> BoxFuture<'_, Result<()>> {
match &mut self.inner {
#[cfg(feature = "band7")]
RuntimeTransactionInner::B7(opt) => {
let tx = opt.take();
Box::pin(async move {
let Some(t) = tx else {
return Ok(());
};
t.close()
.await
.map_err(|e| RuntimeError::Transaction(format!("Close failed: {e}")))
})
}
#[cfg(feature = "band8")]
RuntimeTransactionInner::B8(opt) => {
let tx = opt.take();
Box::pin(async move {
let Some(t) = tx else {
return Ok(());
};
t.close()
.await
.map_err(|e| RuntimeError::Transaction(format!("Close failed: {e}")))
})
}
}
}
}
#[cfg(feature = "band7")]
fn concept_to_json_b7(
concept: &type_bridge_typedb_driver_b7::concept::Concept,
) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert(
"category".into(),
serde_json::Value::String(concept.get_category().name().into()),
);
obj.insert(
"label".into(),
serde_json::Value::String(concept.get_label().into()),
);
if let Some(iid) = concept.try_get_iid() {
obj.insert("iid".into(), serde_json::Value::String(iid.to_string()));
}
if let Some(value) = concept.try_get_value() {
obj.insert("value".into(), value_to_json_b7(value));
}
if let Some(vt) = concept.try_get_value_type() {
obj.insert(
"value_type".into(),
serde_json::Value::String(vt.name().into()),
);
}
serde_json::Value::Object(obj)
}
#[cfg(feature = "band7")]
fn value_to_json_b7(value: &type_bridge_typedb_driver_b7::concept::Value) -> serde_json::Value {
if let Some(b) = value.get_boolean() {
return serde_json::Value::Bool(b);
}
if let Some(i) = value.get_integer() {
return serde_json::json!(i);
}
if let Some(d) = value.get_double() {
return serde_json::json!(d);
}
if let Some(s) = value.get_string() {
return serde_json::Value::String(s.to_string());
}
if let Some(date) = value.get_date() {
return serde_json::Value::String(date.to_string());
}
if let Some(dt) = value.get_datetime() {
return serde_json::Value::String(dt.to_string());
}
if let Some(dt_tz) = value.get_datetime_tz() {
return serde_json::Value::String(dt_tz.to_string());
}
if let Some(dec) = value.get_decimal() {
return serde_json::Value::String(dec.to_string());
}
if let Some(dur) = value.get_duration() {
return serde_json::Value::String(dur.to_string());
}
serde_json::Value::String(value.to_string())
}
#[cfg(feature = "band8")]
fn concept_to_json_b8(concept: &typedb_driver::concept::Concept) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert(
"category".into(),
serde_json::Value::String(concept.get_category().name().into()),
);
obj.insert(
"label".into(),
serde_json::Value::String(concept.get_label().into()),
);
if let Some(iid) = concept.try_get_iid() {
obj.insert("iid".into(), serde_json::Value::String(iid.to_string()));
}
if let Some(value) = concept.try_get_value() {
obj.insert("value".into(), value_to_json_b8(value));
}
if let Some(vt) = concept.try_get_value_type() {
obj.insert(
"value_type".into(),
serde_json::Value::String(vt.name().into()),
);
}
serde_json::Value::Object(obj)
}
#[cfg(feature = "band8")]
fn value_to_json_b8(value: &typedb_driver::concept::Value) -> serde_json::Value {
if let Some(b) = value.get_boolean() {
return serde_json::Value::Bool(b);
}
if let Some(i) = value.get_integer() {
return serde_json::json!(i);
}
if let Some(d) = value.get_double() {
return serde_json::json!(d);
}
if let Some(s) = value.get_string() {
return serde_json::Value::String(s.to_string());
}
if let Some(date) = value.get_date() {
return serde_json::Value::String(date.to_string());
}
if let Some(dt) = value.get_datetime() {
return serde_json::Value::String(dt.to_string());
}
if let Some(dt_tz) = value.get_datetime_tz() {
return serde_json::Value::String(dt_tz.to_string());
}
if let Some(dec) = value.get_decimal() {
return serde_json::Value::String(dec.to_string());
}
if let Some(dur) = value.get_duration() {
return serde_json::Value::String(dur.to_string());
}
serde_json::Value::String(value.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn connect_options_default_matches_ssot() {
let options = ConnectOptions::default();
assert_eq!(options.http_port, DEFAULT_HTTP_PORT);
assert!(!options.tls);
assert_eq!(options.server_version, None);
}
#[tokio::test]
async fn gated_driver_probe_receives_configured_port() {
let recorded_port: Arc<Mutex<Option<u16>>> = Arc::new(Mutex::new(None));
let captured = Arc::clone(&recorded_port);
let result = gated_driver_with_probe(
"localhost:1729",
"admin",
"password",
ConnectOptions {
http_port: 9123,
tls: false,
server_version: None,
},
move |_addr, port, _tls| {
*captured.lock().unwrap() = Some(port);
Ok(core_version::Version::new(3, 8, 3))
},
)
.await;
let observed = recorded_port.lock().unwrap().expect("probe was not called");
assert_eq!(
observed, 9123,
"probe must receive the configured http_port (9123), got {observed}"
);
if let Err(RuntimeError::UnsupportedVersion(_)) = result {
panic!("expected a connection error (no server), not a version gate rejection")
}
}
#[tokio::test]
async fn gated_driver_http_failure_reports_grpc_fallback_failures() {
let result = gated_driver_with_probe(
"127.0.0.1:1",
"admin",
"password",
ConnectOptions {
http_port: 9123,
tls: false,
server_version: None,
},
move |_addr, _port, _tls| {
Err(core_version::VersionError::Probe(
"HTTP endpoint unavailable".to_string(),
))
},
)
.await;
match result {
Err(RuntimeError::UnsupportedVersion(err)) => {
let msg = err.to_string();
assert!(msg.contains("HTTP endpoint unavailable"), "{msg}");
assert!(msg.contains("band-8 gRPC attempt failed"), "{msg}");
assert!(msg.contains("band-7 gRPC attempt failed"), "{msg}");
}
Err(other) => panic!("expected aggregated version-probe failure, got {other}"),
Ok(_) => panic!("expected aggregated version-probe failure, got successful connection"),
}
}
#[tokio::test]
async fn gated_driver_pinned_version_skips_probe() {
let probe_called = Arc::new(Mutex::new(false));
let captured = Arc::clone(&probe_called);
let result = gated_driver_with_probe(
"localhost:1729",
"admin",
"password",
ConnectOptions {
http_port: 9123,
tls: false,
server_version: Some(core_version::Version::new(3, 8, 3)),
},
move |_addr, _port, _tls| {
*captured.lock().unwrap() = true;
Ok(core_version::Version::new(3, 11, 5))
},
)
.await;
assert!(
!*probe_called.lock().unwrap(),
"pinned server_version must skip the HTTP probe"
);
if let Err(RuntimeError::UnsupportedVersion(_)) = result {
panic!("expected a connection error (no server), not a version gate rejection")
}
}
#[tokio::test]
async fn gated_driver_rejects_unsupported_pinned_version_without_probe() {
let probe_called = Arc::new(Mutex::new(false));
let captured = Arc::clone(&probe_called);
let result = gated_driver_with_probe(
"localhost:1729",
"admin",
"password",
ConnectOptions {
http_port: 9123,
tls: false,
server_version: Some(core_version::Version::new(3, 7, 3)),
},
move |_addr, _port, _tls| {
*captured.lock().unwrap() = true;
Ok(core_version::Version::new(3, 8, 3))
},
)
.await;
assert!(
!*probe_called.lock().unwrap(),
"unsupported pinned server_version must skip the HTTP probe"
);
match result {
Err(RuntimeError::UnsupportedVersion(err)) => {
assert!(
err.to_string().contains("3.7.3"),
"error should name rejected version: {err}"
);
}
Err(other) => panic!("expected unsupported-version rejection for 3.7.3, got {other}"),
Ok(_) => panic!(
"expected unsupported-version rejection for 3.7.3, got successful connection"
),
}
}
#[test]
fn cargo_lock_pin() {
let lock_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.lock");
let lock_contents = std::fs::read_to_string(lock_path)
.expect("Cargo.lock not found relative to crate root");
let lock_version = lock_contents
.split("[[package]]")
.find(|block| block.contains("name = \"typedb-driver\""))
.and_then(|block| {
block
.lines()
.find(|line| line.trim_start().starts_with("version = "))
})
.and_then(|line| {
let start = line.find('"')? + 1;
let end = line.rfind('"')?;
Some(&line[start..end])
})
.expect("typedb-driver entry not found in Cargo.lock");
assert_eq!(
lock_version, PINNED_DRIVER_VERSION,
"Cargo.lock resolves typedb-driver {lock_version} but PINNED_DRIVER_VERSION \
is {PINNED_DRIVER_VERSION}; update the runtime constant"
);
let pinned: core_version::Version = PINNED_DRIVER_VERSION.parse().unwrap();
assert_eq!(
core_version::band(&pinned),
Some(8),
"pinned driver version {PINNED_DRIVER_VERSION} left protocol band 8; \
review the gate expectations before accepting the bump"
);
}
#[test]
fn cargo_lock_pin_b7() {
let lock_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.lock");
let lock_contents = std::fs::read_to_string(lock_path)
.expect("Cargo.lock not found relative to crate root");
let lock_version = lock_contents
.split("[[package]]")
.find(|block| block.contains("name = \"type-bridge-typedb-driver-b7\""))
.and_then(|block| {
block
.lines()
.find(|line| line.trim_start().starts_with("version = "))
})
.and_then(|line| {
let start = line.find('"')? + 1;
let end = line.rfind('"')?;
Some(&line[start..end])
})
.expect("type-bridge-typedb-driver-b7 entry not found in Cargo.lock");
assert_eq!(
lock_version, PINNED_DRIVER_VERSION_B7,
"Cargo.lock resolves type-bridge-typedb-driver-b7 {lock_version} but \
PINNED_DRIVER_VERSION_B7 is {PINNED_DRIVER_VERSION_B7}; update the runtime constant"
);
let pinned: core_version::Version = PINNED_DRIVER_VERSION_B7.parse().unwrap();
assert_eq!(
core_version::band(&pinned),
Some(7),
"pinned band-7 fork version {PINNED_DRIVER_VERSION_B7} left protocol band 7; \
review the gate expectations before accepting the bump"
);
}
}