1#![deny(missing_docs)]
2use 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
23pub trait RemoteQueryTransport: Send + Sync + 'static {
28 fn capabilities(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + '_>>;
30
31 fn exchange<'a>(
33 &'a self,
34 request: &'a [u8],
35 ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + 'a>>;
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct RemoteQueryLimits {
41 limits: RemoteLimitsV2,
42}
43
44impl RemoteQueryLimits {
45 #[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 #[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
76pub 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 #[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 #[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
125pub 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 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 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 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}