Skip to main content

type_bridge/
remote.rs

1#![deny(missing_docs)]
2//! Authenticated one-exchange remote execution for generated queries.
3
4use std::future::Future;
5use std::marker::PhantomData;
6use std::pin::Pin;
7use std::sync::Arc;
8
9use type_bridge_contract::query_remote::RemoteCapabilities;
10use type_bridge_contract::query_remote_v2::RemoteLimitsV2;
11use type_bridge_orm::_registry::DescriptorRegistry;
12use type_bridge_orm::query_v2_prepared::QueryAuthority;
13use type_bridge_orm::{
14    InstalledRuntimeProjection, RemoteModelQueryV2Error, ValidatedMatchRequest,
15    ValidatedMatchResult, prepare_remote_model_query_v2,
16};
17
18use crate::Result;
19use crate::error::Error;
20use crate::query::QuerySession;
21use crate::schema::{Schema, SchemaPackage, Unbound};
22
23/// One caller-owned asynchronous transport for the authenticated V2 routes.
24///
25/// Implementations fetch the exact `/v2/capabilities` bytes once at connect
26/// time and perform exactly one `/v2/query` exchange per terminal.
27pub trait RemoteQueryTransport: Send + Sync + 'static {
28    /// Fetch the executor's exact signed capability advertisement.
29    fn capabilities(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + '_>>;
30
31    /// Exchange one exact canonical request for one exact signed reply.
32    fn exchange<'a>(
33        &'a self,
34        request: &'a [u8],
35    ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + 'a>>;
36}
37
38/// Explicit immutable budgets for one remote generated-query terminal.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct RemoteQueryLimits {
41    limits: RemoteLimitsV2,
42}
43
44impl RemoteQueryLimits {
45    /// Construct one explicit remote response and hydration budget.
46    #[must_use]
47    pub const fn new(
48        max_items: u64,
49        max_bytes: u64,
50        max_collection_members: u64,
51        max_graph_nodes: u64,
52        max_attribute_values: u64,
53        max_role_players: u64,
54    ) -> Self {
55        Self {
56            limits: RemoteLimitsV2 {
57                deadline_ms: None,
58                max_bytes,
59                max_items,
60                max_collection_members,
61                max_graph_nodes,
62                max_attribute_values,
63                max_role_players,
64            },
65        }
66    }
67
68    /// Attach an optional executor deadline in milliseconds.
69    #[must_use]
70    pub const fn deadline_ms(mut self, deadline_ms: u64) -> Self {
71        self.limits.deadline_ms = Some(deadline_ms);
72        self
73    }
74}
75
76/// Connection-time authority, transport, and limit configuration.
77pub struct RemoteConnectionOptions {
78    scope: Option<String>,
79    semantic_profile: Option<String>,
80    limits: RemoteQueryLimits,
81    transport: Arc<dyn RemoteQueryTransport>,
82    advertisement: Option<Vec<u8>>,
83}
84
85impl RemoteConnectionOptions {
86    /// Construct remote options for one managed schema scope and semantic
87    /// profile.
88    #[must_use]
89    pub fn new(
90        scope: impl Into<String>,
91        semantic_profile: impl Into<String>,
92        limits: RemoteQueryLimits,
93        transport: impl RemoteQueryTransport,
94    ) -> Self {
95        Self {
96            scope: Some(scope.into()),
97            semantic_profile: Some(semantic_profile.into()),
98            limits,
99            transport: Arc::new(transport),
100            advertisement: None,
101        }
102    }
103
104    /// Construct normal generated-package options; schema scope and semantic
105    /// profile are derived from [`SchemaPackage`] during binding.
106    #[must_use]
107    pub fn generated(limits: RemoteQueryLimits, transport: impl RemoteQueryTransport) -> Self {
108        Self {
109            scope: None,
110            semantic_profile: None,
111            limits,
112            transport: Arc::new(transport),
113            advertisement: None,
114        }
115    }
116}
117
118struct RemoteRuntime {
119    advertisement: Vec<u8>,
120    authority: Arc<QueryAuthority>,
121    limits: RemoteLimitsV2,
122    transport: Arc<dyn RemoteQueryTransport>,
123}
124
125/// A client-owned remote generated-query database branded by schema `S`.
126pub struct RemoteDatabase<S: Schema = Unbound> {
127    options: Option<RemoteConnectionOptions>,
128    runtime: Option<Arc<RemoteRuntime>>,
129    installed: Option<Arc<InstalledRuntimeProjection>>,
130    registry: Option<Arc<DescriptorRegistry>>,
131    marker: PhantomData<fn() -> S>,
132}
133
134impl<S: Schema> std::fmt::Debug for RemoteDatabase<S> {
135    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        formatter
137            .debug_struct("RemoteDatabase")
138            .field("schema_bound", &self.installed.is_some())
139            .finish_non_exhaustive()
140    }
141}
142
143impl RemoteDatabase<Unbound> {
144    /// Fetch and validate one immutable executor advertisement.
145    pub async fn connect(mut options: RemoteConnectionOptions) -> Result<Self> {
146        let advertisement = options.transport.capabilities().await?;
147        RemoteCapabilities::decode(&advertisement).map_err(remote_diagnostic)?;
148        options.advertisement = Some(advertisement);
149        Ok(Self {
150            options: Some(options),
151            runtime: None,
152            installed: None,
153            registry: None,
154            marker: PhantomData,
155        })
156    }
157
158    /// Verify and bind one generated schema package and its remote authority.
159    pub fn with_schema<S: Schema>(mut self, schema: SchemaPackage<S>) -> Result<RemoteDatabase<S>> {
160        let (installed, embedded_authority) = schema.verify_and_install_with_authority()?;
161        let registry = Arc::new(installed.match_registry().map_err(Error::from_orm)?);
162        let options = self.options.take().ok_or_else(|| Error::Other {
163            message: "remote connection options are unavailable".into(),
164            source: None,
165        })?;
166        let authority = if let Some(embedded) = embedded_authority {
167            let embedded_scope = embedded.managed_scope().id().as_str();
168            let embedded_profile = embedded.semantic_profile().id().as_str();
169            if options
170                .scope
171                .as_deref()
172                .is_some_and(|scope| scope != embedded_scope)
173                || options
174                    .semantic_profile
175                    .as_deref()
176                    .is_some_and(|profile| profile != embedded_profile)
177            {
178                return Err(Error::SchemaVerification {
179                    message: "remote options disagree with generated schema authority".into(),
180                    source: None,
181                });
182            }
183            let declared =
184                type_bridge_contract::schema::encode_declared_schema(embedded.declared_schema())
185                    .map_err(|error| Error::SchemaVerification {
186                        message: "verified generated authority cannot reconstruct its declaration"
187                            .into(),
188                        source: Some(Box::new(error)),
189                    })?;
190            QueryAuthority::from_declared_bytes(&declared, embedded_scope, embedded_profile)
191                .map_err(remote_diagnostic)?
192        } else {
193            let declared =
194                schema
195                    .declared_schema_json()
196                    .ok_or_else(|| Error::SchemaVerification {
197                        message: "generated schema package omits remote declared-schema authority"
198                            .into(),
199                        source: None,
200                    })?;
201            let scope = options
202                .scope
203                .as_deref()
204                .ok_or_else(|| Error::SchemaVerification {
205                    message: "schema package has no embedded managed scope".into(),
206                    source: None,
207                })?;
208            let semantic_profile =
209                options
210                    .semantic_profile
211                    .as_deref()
212                    .ok_or_else(|| Error::SchemaVerification {
213                        message: "schema package has no embedded semantic profile".into(),
214                        source: None,
215                    })?;
216            QueryAuthority::from_declared_bytes(declared.as_bytes(), scope, semantic_profile)
217                .map_err(remote_diagnostic)?
218        };
219        if !authority.matches_semantic_fingerprint(installed.projection().semantic_fingerprint()) {
220            return Err(Error::SchemaVerification {
221                message: "remote declared-schema authority does not match the generated projection"
222                    .into(),
223                source: None,
224            });
225        }
226        let runtime = Arc::new(RemoteRuntime {
227            advertisement: options.advertisement.ok_or_else(|| Error::Other {
228                message: "remote capability advertisement is unavailable".into(),
229                source: None,
230            })?,
231            authority: Arc::new(authority),
232            limits: options.limits.limits,
233            transport: options.transport,
234        });
235        Ok(RemoteDatabase {
236            options: None,
237            runtime: Some(runtime),
238            installed: Some(installed),
239            registry: Some(registry),
240            marker: PhantomData,
241        })
242    }
243}
244
245impl<S: Schema> RemoteDatabase<S> {
246    /// Start one owner-branded query session over this remote executor.
247    pub fn query(&self) -> Result<QuerySession<'_, S>> {
248        let installed = self.installed.as_deref().ok_or_else(remote_not_bound)?;
249        let registry = self.registry.as_ref().ok_or_else(remote_not_bound)?;
250        Ok(QuerySession::remote(installed, Arc::clone(registry), self))
251    }
252
253    pub(crate) async fn execute_match(
254        &self,
255        registry: &DescriptorRegistry,
256        validated: ValidatedMatchRequest,
257    ) -> Result<(ValidatedMatchRequest, ValidatedMatchResult)> {
258        let runtime = self.runtime.as_ref().ok_or_else(remote_not_bound)?;
259        let pending = prepare_remote_model_query_v2(
260            &runtime.authority,
261            registry,
262            validated,
263            &runtime.advertisement,
264            runtime.limits,
265        )
266        .map_err(remote_model_input_error)?;
267        let request = pending.request_bytes().to_vec();
268        let response = runtime.transport.exchange(&request).await?;
269        let claimed = pending
270            .claim_reply()
271            .map_err(remote_model_hydration_error)?;
272        if response.len() > claimed.response_snapshot_limit() {
273            return Err(Error::classified(
274                crate::ErrorCategory::ResourceLimit,
275                None,
276                "remote_response_limit",
277                Vec::new(),
278                "remote query reply exceeds the authenticated response ceiling",
279                None,
280            ));
281        }
282        let (request, result, _registry) = claimed
283            .decode(&response)
284            .map_err(remote_model_hydration_error)?;
285        Ok((request, result))
286    }
287}
288
289fn remote_not_bound() -> Error {
290    Error::ModelValidation {
291        phase: crate::ModelValidationPhase::Input,
292        code: "schema_not_bound".into(),
293        path: vec![],
294        message: "remote database is not schema-bound".into(),
295        source: None,
296    }
297}
298
299fn remote_diagnostic(error: type_bridge_contract::diagnostic::Diagnostic) -> Error {
300    Error::from_remote_diagnostic(error)
301}
302
303fn remote_model_input_error(error: RemoteModelQueryV2Error) -> Error {
304    match error {
305        RemoteModelQueryV2Error::Diagnostic(error) => Error::from_remote_diagnostic(error),
306        RemoteModelQueryV2Error::Match(error) => {
307            Error::from_match(error, crate::ModelValidationPhase::Input)
308        }
309    }
310}
311
312fn remote_model_hydration_error(error: RemoteModelQueryV2Error) -> Error {
313    match error {
314        RemoteModelQueryV2Error::Diagnostic(error) => Error::from_remote_diagnostic(error),
315        RemoteModelQueryV2Error::Match(error) => {
316            Error::from_match(error, crate::ModelValidationPhase::Hydration)
317        }
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use std::sync::Mutex;
324
325    use type_bridge_contract::capability::CapabilitySet;
326    use type_bridge_contract::codec::to_canonical_json;
327    use type_bridge_contract::diagnostic::{
328        Diagnostic, DiagnosticCategory, DiagnosticCode, DiagnosticPath, DiagnosticPathSegment,
329    };
330    use type_bridge_contract::fingerprint::SemanticProfileId;
331    use type_bridge_contract::managed_scope::ManagedScopeId;
332    use type_bridge_contract::migration_assertion::BindingId;
333    use type_bridge_contract::projection::{BindingTarget, ProjectionConfig};
334    use type_bridge_contract::query_plan::query_plan_v2_capability_vocabulary;
335    use type_bridge_contract::query_remote::RemoteExecutorBinding;
336    use type_bridge_contract::query_remote_v2::{
337        HydrationGraphV2, RemoteOutcomeV2, RemoteQueryFailureV2, RemoteQueryRequestV2,
338        RemoteQueryResponseV2, RemoteResultKindV2, query_remote_v2_required_capabilities,
339    };
340    use type_bridge_contract::schema::{DocumentId, encode_declared_schema};
341    use type_bridge_orm::OrmError;
342    use type_bridge_orm::match_request::SessionHandle;
343    use type_bridge_orm::query_v2_remote::RemoteReplySigningKey;
344    use type_bridge_schema::{
345        ManagedDeltaContext, SchemaDocumentSet, build_schema_authority, encode_schema_authority,
346        normalize_documents, project, resolve,
347    };
348    use type_bridge_schema_codegen::RustEmitter;
349
350    use super::*;
351    use crate::__codegen::{
352        self, CompleteModel, EncodedCreate, EntityModel, HydratedRow, HydrationCapability,
353        IntoEncodedCreate, MaterializeModel, Model, ThingModel, ValidationError,
354    };
355    use crate::schema::sealed;
356
357    struct TestSchema;
358    impl sealed::Sealed for TestSchema {}
359    impl Schema for TestSchema {}
360
361    #[derive(Debug)]
362    struct Person;
363    impl sealed::Sealed for Person {}
364    impl Model for Person {
365        type Schema = TestSchema;
366        const TYPE_ID_JSON: &'static str = r#"{"kind":"entity","label":"person"}"#;
367    }
368    impl ThingModel for Person {
369        fn thing_kind() -> __codegen::ThingKind {
370            __codegen::ThingKind::Entity
371        }
372    }
373    impl EntityModel for Person {}
374    impl CompleteModel for Person {
375        type Create = PersonCreate;
376
377        fn iid(&self) -> &str {
378            unreachable!()
379        }
380    }
381    impl MaterializeModel for Person {
382        fn materialize(
383            _: &HydratedRow,
384            _: &HydrationCapability,
385        ) -> std::result::Result<Self, ValidationError> {
386            Ok(Self)
387        }
388    }
389
390    #[derive(Clone)]
391    struct PersonCreate;
392    impl sealed::Sealed for PersonCreate {}
393    impl IntoEncodedCreate for PersonCreate {
394        fn into_encoded_create(self) -> std::result::Result<EncodedCreate, ValidationError> {
395            Ok(EncodedCreate::new(Person::TYPE_ID_JSON, vec![], vec![]))
396        }
397    }
398
399    #[test]
400    fn local_and_remote_failures_preserve_classification_codes_and_paths() {
401        let session = SessionHandle::new(Arc::new(DescriptorRegistry::new()));
402        let match_error = match session.exact("missing") {
403            Err(OrmError::Match(error)) => error,
404            Err(other) => panic!("unexpected ORM error: {other:?}"),
405            Ok(_) => panic!("missing descriptor unexpectedly resolved"),
406        };
407        let local = Error::from_orm(OrmError::Match(match_error.clone()));
408        let remote = remote_model_input_error(RemoteModelQueryV2Error::Match(match_error));
409
410        assert_eq!(local.category(), crate::ErrorCategory::QueryAuthoring);
411        assert_eq!(remote.category(), local.category());
412        assert_eq!(remote.code(), Some("unknown_descriptor"));
413        assert_eq!(remote.code(), local.code());
414        assert_eq!(remote.path(), local.path());
415        assert_eq!(
416            remote.model_validation_phase(),
417            local.model_validation_phase()
418        );
419
420        let diagnostic = Diagnostic::new(
421            DiagnosticCategory::UnsupportedCapability,
422            DiagnosticCode::new("missing_remote_capability").unwrap(),
423            "the remote executor does not advertise one required capability",
424        )
425        .at(DiagnosticPathSegment::Field("capabilities".into()))
426        .at(DiagnosticPathSegment::Index(2));
427        let classified = remote_diagnostic(diagnostic);
428
429        assert_eq!(classified.category(), crate::ErrorCategory::Capability);
430        assert_eq!(classified.code(), Some("missing_remote_capability"));
431        assert_eq!(
432            classified.path(),
433            Some(&["capabilities".to_owned(), "[2]".to_owned()][..])
434        );
435        assert_eq!(classified.model_validation_phase(), None);
436    }
437
438    fn package() -> SchemaPackage<TestSchema> {
439        let documents = SchemaDocumentSet::parse([(
440            DocumentId::new("remote.yaml").unwrap(),
441            "format: typebridge.schema/v2\nattributes:\n  name: { value: string }\nentities:\n  person:\n    owns: { name: { key: true } }\n",
442        )])
443        .unwrap();
444        let declared = normalize_documents(&documents).unwrap();
445        let profile = SemanticProfileId::new("typedb-3.12.1/v1").unwrap();
446        let resolved = resolve(&declared, &profile).unwrap();
447        let authority = build_schema_authority(
448            &declared,
449            declared.required_capabilities(),
450            &ManagedDeltaContext::new(
451                ManagedScopeId::new("rust-client-test").unwrap(),
452                profile,
453                CapabilitySet::new(),
454            ),
455        )
456        .unwrap();
457        let emitter = RustEmitter::new();
458        let projection = project(
459            &resolved,
460            BindingTarget::Rust,
461            &ProjectionConfig::rust(),
462            &emitter.generator_handlers(),
463            &emitter.code_resources().unwrap(),
464        )
465        .unwrap();
466        let leak = |bytes: Vec<u8>| {
467            Box::leak(String::from_utf8(bytes).unwrap().into_boxed_str()) as &'static str
468        };
469        SchemaPackage::new_with_authority(
470            leak(to_canonical_json(projection.semantic_fingerprint()).unwrap()),
471            leak(to_canonical_json(projection.projection_fingerprint()).unwrap()),
472            leak(to_canonical_json(&projection).unwrap()),
473            leak(encode_schema_authority(&authority)),
474            leak(encode_declared_schema(&declared).unwrap()),
475            "rust-client-test",
476            "typedb-3.12.1/v1",
477        )
478    }
479
480    fn released_declared_package() -> SchemaPackage<TestSchema> {
481        let generated = package();
482        SchemaPackage::new_with_declared(
483            generated.semantic_fingerprint_json(),
484            generated.projection_fingerprint_json(),
485            generated.runtime_projection_json(),
486            generated
487                .declared_schema_json()
488                .expect("test package carries a declaration"),
489        )
490    }
491
492    struct UnusedTransport;
493
494    impl RemoteQueryTransport for UnusedTransport {
495        fn capabilities(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + '_>> {
496            Box::pin(async { panic!("compatibility test performs no transport I/O") })
497        }
498
499        fn exchange<'a>(
500            &'a self,
501            _request: &'a [u8],
502        ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + 'a>> {
503            Box::pin(async { panic!("compatibility test performs no transport I/O") })
504        }
505    }
506
507    fn unconnected_remote(mut options: RemoteConnectionOptions) -> RemoteDatabase<Unbound> {
508        options.advertisement = Some(Vec::new());
509        RemoteDatabase {
510            options: Some(options),
511            runtime: None,
512            installed: None,
513            registry: None,
514            marker: PhantomData,
515        }
516    }
517
518    #[test]
519    fn generated_authority_accepts_matching_legacy_options_and_rejects_overrides() {
520        let limits = || RemoteQueryLimits::new(10, 1 << 20, 10, 100, 100, 100);
521        let matching = RemoteConnectionOptions::new(
522            "rust-client-test",
523            "typedb-3.12.1/v1",
524            limits(),
525            UnusedTransport,
526        );
527        unconnected_remote(matching)
528            .with_schema(package())
529            .expect("matching 2.0.1-style options remain compatible");
530
531        for mismatched in [
532            RemoteConnectionOptions::new(
533                "other-scope",
534                "typedb-3.12.1/v1",
535                limits(),
536                UnusedTransport,
537            ),
538            RemoteConnectionOptions::new(
539                "rust-client-test",
540                "typedb-3.11.5/v1",
541                limits(),
542                UnusedTransport,
543            ),
544        ] {
545            let error = unconnected_remote(mismatched)
546                .with_schema(package())
547                .expect_err("caller strings cannot override generated authority");
548            assert!(
549                error
550                    .to_string()
551                    .contains("disagree with generated schema authority"),
552                "{error}"
553            );
554        }
555    }
556
557    #[test]
558    fn released_declared_package_retains_explicit_remote_options_compatibility() {
559        let limits = || RemoteQueryLimits::new(10, 1 << 20, 10, 100, 100, 100);
560        let options = RemoteConnectionOptions::new(
561            "rust-client-test",
562            "typedb-3.12.1/v1",
563            limits(),
564            UnusedTransport,
565        );
566        unconnected_remote(options)
567            .with_schema(released_declared_package())
568            .expect("2.0.1 generated package and explicit options remain compatible");
569
570        let generated_options = RemoteConnectionOptions::generated(limits(), UnusedTransport);
571        let error = unconnected_remote(generated_options)
572            .with_schema(released_declared_package())
573            .expect_err("detached 2.0.1 package cannot invent embedded deployment authority");
574        assert!(error.to_string().contains("no embedded managed scope"));
575    }
576
577    struct Transport {
578        advertisement_contract: RemoteCapabilities,
579        advertisement: Vec<u8>,
580        capabilities: Arc<Mutex<usize>>,
581        exchanges: Arc<Mutex<Vec<Vec<u8>>>>,
582        failure: Option<Diagnostic>,
583        signer: RemoteReplySigningKey,
584    }
585
586    impl RemoteQueryTransport for Transport {
587        fn capabilities(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + '_>> {
588            *self.capabilities.lock().unwrap() += 1;
589            let bytes = self.advertisement.clone();
590            Box::pin(async move { Ok(bytes) })
591        }
592
593        fn exchange<'a>(
594            &'a self,
595            request: &'a [u8],
596        ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + 'a>> {
597            self.exchanges.lock().unwrap().push(request.to_vec());
598            let response = (|| {
599                let request = RemoteQueryRequestV2::decode(request).map_err(remote_diagnostic)?;
600                request
601                    .validate_advertisement(&self.advertisement_contract)
602                    .map_err(remote_diagnostic)?;
603                if let Some(diagnostic) = &self.failure {
604                    return RemoteQueryFailureV2::bound(
605                        request.nonce(),
606                        &request.fingerprint().map_err(remote_diagnostic)?,
607                        diagnostic,
608                    )
609                    .and_then(|failure| {
610                        failure.encode_signed(
611                            &self.advertisement_contract.fingerprint()?,
612                            &self.signer,
613                        )
614                    })
615                    .map_err(remote_diagnostic);
616                }
617                let plan = request.plan().map_err(remote_diagnostic)?;
618                let root = BindingId::new(0).map_err(remote_diagnostic)?;
619                let outcome = match request.result_kind() {
620                    RemoteResultKindV2::DistinctCount => {
621                        RemoteOutcomeV2::DistinctCount { root, value: 7 }
622                    }
623                    RemoteResultKindV2::DistinctExists => {
624                        RemoteOutcomeV2::DistinctExists { root, value: true }
625                    }
626                    RemoteResultKindV2::HydratedRows => RemoteOutcomeV2::HydratedRows {
627                        graph: HydrationGraphV2::new(vec![]).map_err(remote_diagnostic)?,
628                        rows: vec![],
629                    },
630                    RemoteResultKindV2::HydratedPage => RemoteOutcomeV2::HydratedPage {
631                        entries: vec![],
632                        graph: HydrationGraphV2::new(vec![]).map_err(remote_diagnostic)?,
633                        limit: 2,
634                        offset: 0,
635                        root,
636                        total: Some(0),
637                    },
638                    _ => {
639                        return Err(Error::Other {
640                            message: "test transport received an unexpected terminal".into(),
641                            source: None,
642                        });
643                    }
644                };
645                RemoteQueryResponseV2::new(
646                    request.nonce(),
647                    &plan,
648                    &request.fingerprint().map_err(remote_diagnostic)?,
649                    request.result_kind(),
650                    outcome,
651                )
652                .and_then(|response| {
653                    response
654                        .encode_signed(&self.advertisement_contract.fingerprint()?, &self.signer)
655                })
656                .map_err(remote_diagnostic)
657            })();
658            Box::pin(async move { response })
659        }
660    }
661
662    #[tokio::test]
663    async fn remote_database_fetches_capabilities_once_and_exchanges_once_per_terminal() {
664        let signer = RemoteReplySigningKey::from_secret_bytes([0x31; 32]);
665        let mut capabilities = query_plan_v2_capability_vocabulary();
666        for capability in query_remote_v2_required_capabilities(true) {
667            capabilities.insert(capability);
668        }
669        let advertisement_contract = RemoteCapabilities::new(
670            capabilities,
671            RemoteExecutorBinding::new("rust-client-test", "epoch-00000000001").unwrap(),
672            signer.public_key(),
673        );
674        let advertisement = advertisement_contract.encode().unwrap();
675        let capability_calls = Arc::new(Mutex::new(0));
676        let exchanges = Arc::new(Mutex::new(Vec::new()));
677        let transport = Transport {
678            advertisement_contract,
679            advertisement,
680            capabilities: Arc::clone(&capability_calls),
681            exchanges: Arc::clone(&exchanges),
682            failure: None,
683            signer,
684        };
685        let options = RemoteConnectionOptions::generated(
686            RemoteQueryLimits::new(10, 1 << 20, 10, 100, 100, 100),
687            transport,
688        );
689        let remote = RemoteDatabase::connect(options)
690            .await
691            .unwrap()
692            .with_schema(package())
693            .unwrap();
694        let mut session = remote.query().unwrap();
695        let person = session.exact::<Person>().unwrap();
696        let query = session.query(person).unwrap();
697
698        assert_eq!(query.count().await.unwrap(), 7);
699        assert!(query.exists().await.unwrap());
700        assert!(
701            query
702                .rows(crate::RowsOptions::new(2))
703                .await
704                .unwrap()
705                .is_empty()
706        );
707        let page = query
708            .page_by(person, crate::PageOptions::new(2).include_total(true))
709            .await
710            .unwrap();
711        assert!(page.items().is_empty());
712        assert_eq!(page.total(), Some(0));
713        let error = query
714            .aggregate((crate::aggregate::count(),))
715            .await
716            .expect_err("native-only reductions fail before transport exchange");
717        assert!(
718            error
719                .to_string()
720                .contains("query_remote_v2_native_only_operation")
721        );
722        assert_eq!(*capability_calls.lock().unwrap(), 1);
723        let requests = exchanges.lock().unwrap();
724        assert_eq!(requests.len(), 4);
725        assert!(
726            std::str::from_utf8(&requests[0])
727                .unwrap()
728                .contains("\"format\":\"typebridge.query-remote-request/v2\"")
729        );
730    }
731
732    #[tokio::test]
733    async fn generated_remote_query_preserves_complete_authenticated_structured_diagnostic() {
734        let signer = RemoteReplySigningKey::from_secret_bytes([0x42; 32]);
735        let mut capabilities = query_plan_v2_capability_vocabulary();
736        for capability in query_remote_v2_required_capabilities(true) {
737            capabilities.insert(capability);
738        }
739        let advertisement_contract = RemoteCapabilities::new(
740            capabilities,
741            RemoteExecutorBinding::new("rust-generated-acceptance", "epoch-00000000002").unwrap(),
742            signer.public_key(),
743        );
744        let advertisement = advertisement_contract.encode().unwrap();
745        let capability_calls = Arc::new(Mutex::new(0));
746        let exchanges = Arc::new(Mutex::new(Vec::new()));
747        let diagnostic = Diagnostic::new(
748            DiagnosticCategory::InvalidContract,
749            DiagnosticCode::new("remote_application_failure").unwrap(),
750            "the remote application rejected this query",
751        )
752        .with_path(DiagnosticPath::from_segments([
753            DiagnosticPathSegment::Field("plan".into()),
754            DiagnosticPathSegment::Index(0),
755            DiagnosticPathSegment::Identifier("person".into()),
756        ]))
757        .with_detail("attempt", 7_i64)
758        .with_detail("expected", vec!["person".to_owned(), "employee".to_owned()])
759        .with_detail("retryable", false)
760        .with_detail("subject", "person");
761        let transport = Transport {
762            advertisement_contract,
763            advertisement,
764            capabilities: Arc::clone(&capability_calls),
765            exchanges: Arc::clone(&exchanges),
766            failure: Some(diagnostic),
767            signer,
768        };
769        let options = RemoteConnectionOptions::generated(
770            RemoteQueryLimits::new(10, 1 << 20, 10, 100, 100, 100),
771            transport,
772        );
773        let remote = RemoteDatabase::connect(options)
774            .await
775            .unwrap()
776            .with_schema(package())
777            .unwrap();
778        let mut session = remote.query().unwrap();
779        let person = session.exact::<Person>().unwrap();
780        let error = session
781            .query(person)
782            .unwrap()
783            .one()
784            .await
785            .expect_err("generated query must return the authenticated application failure");
786
787        assert_eq!(error.category(), crate::ErrorCategory::Remote);
788        assert_eq!(error.code(), Some("remote_application_failure"));
789        assert_eq!(
790            error.message(),
791            "the remote application rejected this query"
792        );
793        assert_eq!(
794            error.path(),
795            Some(&["plan".to_owned(), "[0]".to_owned(), "person".to_owned()][..])
796        );
797        assert_eq!(
798            error.diagnostic_path(),
799            Some(
800                &[
801                    crate::ErrorPathSegment::Field("plan".into()),
802                    crate::ErrorPathSegment::Index(0),
803                    crate::ErrorPathSegment::Identifier("person".into()),
804                ][..]
805            )
806        );
807        let details = error.details().expect("authenticated diagnostic details");
808        assert_eq!(details.get("attempt"), Some(&crate::ErrorDetail::Long(7)));
809        assert_eq!(
810            details.get("expected"),
811            Some(&crate::ErrorDetail::TextList(vec![
812                "person".to_owned(),
813                "employee".to_owned(),
814            ]))
815        );
816        assert_eq!(
817            details.get("retryable"),
818            Some(&crate::ErrorDetail::Boolean(false))
819        );
820        assert_eq!(
821            details.get("subject"),
822            Some(&crate::ErrorDetail::Text("person".to_owned()))
823        );
824        assert_eq!(*capability_calls.lock().unwrap(), 1);
825        assert_eq!(exchanges.lock().unwrap().len(), 1);
826    }
827}