Skip to main content

riffdb_client_rust/
application.rs

1//! Name-addressed application requests that hide the kernel wire model.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::{Arc, Mutex, OnceLock};
7use std::time::Duration;
8
9use futures_util::{StreamExt, stream};
10use riffdb_errors::{
11    ApplicationError, ApplicationErrorCode, ApplicationErrorContext, ApplicationOperation,
12    ApplicationRecoveryAction, PublicErrorKind,
13};
14use riffdb_proto::{app::v1 as app_v1, application_error_from_proto, v1};
15use tonic::transport::{Channel, Endpoint};
16
17use crate::{
18    AttemptBudget, CallMetadata, ClientError, DetailsFreeStatus, GeneratedExecutionError,
19    IdempotentCommand, RiffDbClient, generate_request_id,
20    generated::{GeneratedCommand, GeneratedQuery},
21};
22use riffdb_config::TlsClientConfig;
23
24const MAX_GENERATED_TRANSPORT_BATCH_ITEMS: usize = 16;
25/// Maximum independently in-flight items in one generated command batch.
26pub const MAX_GENERATED_BATCH_CONCURRENCY: usize = 384;
27const MAX_STANDBY_CLIENTS: usize = 4;
28const MAX_READ_REPLICA_CLIENTS: usize = 4;
29
30const APPLICATION_CATALOG_SCHEMA_V1: &str = "riffdb.application-catalog/v1";
31
32/// Closed application feature vocabulary returned by symbolic catalog preflight.
33#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
34pub enum ApplicationCatalogFeature {
35    /// Finite compiler-owned optional predicate families.
36    OperationalOptionalPredicates,
37    /// Snapshot-bound stable cursor pages.
38    StableCursorPages,
39    /// Indexed null and existence predicates.
40    NullExistencePredicates,
41    /// Exact binary UTF-8 prefix indexes.
42    BinaryTextPrefix,
43    /// Versioned Unicode-fold prefix indexes.
44    UnicodeFoldTextPrefixV1,
45    /// Bounded exact operational aggregates.
46    ExactAggregates,
47}
48
49/// Availability of one closed application feature.
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum ApplicationCatalogFeatureState {
52    /// The exact selected server surface implements the feature.
53    Available,
54    /// The feature is explicitly unavailable and must not be emulated.
55    Unavailable,
56}
57
58/// One checked symbolic application feature view.
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub struct ApplicationCatalogFeatureView {
61    /// Closed feature identity.
62    pub feature: ApplicationCatalogFeature,
63    /// Checked availability state.
64    pub state: ApplicationCatalogFeatureState,
65}
66
67/// Checked feature-preflight result for one exact application contract.
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct ApplicationCatalogPreflight {
70    /// Exact contract lineage returned by the authorized catalog.
71    pub contract_lineage: String,
72    /// Exact contract version returned by the authorized catalog.
73    pub contract_version: u64,
74    /// Exact contract bundle hash returned by the authorized catalog.
75    pub contract_bundle_hash: [u8; 32],
76    /// Active query-module identities in canonical server order.
77    pub query_module_hashes: Vec<[u8; 32]>,
78    /// Complete closed feature registry.
79    pub features: Vec<ApplicationCatalogFeatureView>,
80}
81
82impl ApplicationCatalogPreflight {
83    /// Returns whether the exact selected server surface exposes `feature`.
84    #[must_use]
85    pub fn is_available(&self, feature: ApplicationCatalogFeature) -> bool {
86        self.features.iter().any(|view| {
87            view.feature == feature && view.state == ApplicationCatalogFeatureState::Available
88        })
89    }
90}
91
92/// A local shape failure for one bounded transport batch of ordinary commands.
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum IdempotentTransportBatchError {
95    /// The transport batch was empty.
96    Empty,
97    /// The transport batch exceeded the public protocol bound of 16 items.
98    TooManyItems,
99}
100
101impl fmt::Display for IdempotentTransportBatchError {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        formatter.write_str(match self {
104            Self::Empty => "command transport batch is empty",
105            Self::TooManyItems => "command transport batch exceeds 16 items",
106        })
107    }
108}
109
110impl std::error::Error for IdempotentTransportBatchError {}
111
112#[derive(Clone, Debug, Eq, PartialEq)]
113struct PinnedContractIdentity {
114    database: String,
115    lineage: String,
116    version: u64,
117    bundle_hash: [u8; 32],
118}
119
120#[derive(Default)]
121struct ScriptedPeer {
122    active_contract: Mutex<Option<v1::GetActiveContractResponse>>,
123    commands: Mutex<std::collections::VecDeque<Result<v1::ExecuteCommandResponse, ClientError>>>,
124    queries: Mutex<std::collections::VecDeque<Result<app_v1::ExecuteQueryResponse, ClientError>>>,
125    recorded_command_inputs: Mutex<Vec<Option<v1::Value>>>,
126    recorded_queries: Mutex<Vec<(String, Option<u64>, bool)>>,
127}
128
129#[derive(Clone)]
130struct PeerClient {
131    inner: RiffDbClient,
132    scripted: Option<Arc<ScriptedPeer>>,
133}
134
135impl PeerClient {
136    async fn verify_active_contract(
137        &mut self,
138        expected: &PinnedContractIdentity,
139        metadata: &CallMetadata,
140    ) -> Result<(), ApplicationClientError> {
141        if expected.database.is_empty() || expected.lineage.is_empty() || expected.version == 0 {
142            return Err(ApplicationClientError::InvalidInput);
143        }
144        let request_id = Vec::from(
145            generate_request_id()
146                .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
147                .into_bytes(),
148        );
149        let response = if let Some(scripted) = &self.scripted {
150            scripted
151                .active_contract
152                .lock()
153                .unwrap_or_else(std::sync::PoisonError::into_inner)
154                .clone()
155                .ok_or(ClientError::ConnectionFailure)?
156        } else {
157            self.inner
158                .get_active_contract(v1::GetActiveContractRequest { request_id }, metadata)
159                .await?
160        };
161        let Some(v1::get_active_contract_response::Result::Present(contract)) = response.result
162        else {
163            return Err(ApplicationClientError::InvalidResponse);
164        };
165        if response.database_alias != expected.database
166            || contract.contract_lineage != expected.lineage
167            || contract.contract_version != expected.version
168            || contract.bundle_hash.as_slice() != expected.bundle_hash
169        {
170            return Err(ApplicationClientError::InvalidResponse);
171        }
172        Ok(())
173    }
174
175    async fn execute_with_retry(
176        &mut self,
177        command: &IdempotentCommand,
178        attempts: AttemptBudget,
179        metadata: &CallMetadata,
180    ) -> Result<v1::ExecuteCommandResponse, ClientError> {
181        if let Some(scripted) = &self.scripted {
182            let request_id = generate_request_id().map_err(ClientError::IdentifierGeneration)?;
183            let request = command.request(request_id);
184            scripted
185                .recorded_command_inputs
186                .lock()
187                .unwrap_or_else(std::sync::PoisonError::into_inner)
188                .push(request.input);
189            return scripted
190                .commands
191                .lock()
192                .unwrap_or_else(std::sync::PoisonError::into_inner)
193                .pop_front()
194                .unwrap_or(Err(ClientError::ConnectionFailure));
195        }
196        self.inner
197            .execute_with_retry(command, attempts, metadata)
198            .await
199    }
200
201    async fn execute_named_application_query_wire(
202        &mut self,
203        query: NamedQuery,
204        metadata: &CallMetadata,
205    ) -> Result<app_v1::ExecuteQueryResponse, ApplicationClientError> {
206        if let Some(scripted) = &self.scripted {
207            scripted
208                .recorded_queries
209                .lock()
210                .unwrap_or_else(std::sync::PoisonError::into_inner)
211                .push((
212                    query.name.clone(),
213                    query.minimum_application_head,
214                    query.admission_head_consistency,
215                ));
216            let response = scripted
217                .queries
218                .lock()
219                .unwrap_or_else(std::sync::PoisonError::into_inner)
220                .pop_front()
221                .unwrap_or(Err(ClientError::ConnectionFailure))?;
222            validate_query_response_identity(
223                &response,
224                &query.contract,
225                &query.name,
226                query.module_hash,
227                query.expected_plan_hash,
228            )?;
229            return Ok(response);
230        }
231        self.inner
232            .execute_named_application_query_wire(query, metadata)
233            .await
234    }
235}
236
237struct TopologyState {
238    promoted_active: Option<PeerClient>,
239    standby: Vec<PeerClient>,
240    pinned_contract: Option<PinnedContractIdentity>,
241}
242
243struct ClientTopology {
244    state: Mutex<TopologyState>,
245    read_replicas: Vec<PeerClient>,
246    next_replica: AtomicUsize,
247}
248
249impl ClientTopology {
250    fn lock_state(&self) -> std::sync::MutexGuard<'_, TopologyState> {
251        self.state
252            .lock()
253            .unwrap_or_else(std::sync::PoisonError::into_inner)
254    }
255}
256
257/// Application-only client facade.
258///
259/// This type deliberately has no accessor for its kernel client. Stable
260/// application code can execute name-addressed commands and exact named
261/// queries, but cannot construct raw entity/index requests through this
262/// surface.
263#[derive(Clone)]
264pub struct StableApplicationClient {
265    pub(crate) inner: RiffDbClient,
266    scripted: Option<Arc<ScriptedPeer>>,
267    pinned_contract: Option<PinnedContractIdentity>,
268    topology: Option<Arc<ClientTopology>>,
269}
270
271impl StableApplicationClient {
272    /// Connects the application-only facade through mandatory explicit TLS
273    /// trust and exact peer-name verification.
274    pub async fn connect_verified_tls(config: &TlsClientConfig) -> Result<Self, ClientError> {
275        Ok(Self {
276            inner: RiffDbClient::connect_verified_tls(config).await?,
277            scripted: None,
278            pinned_contract: None,
279            topology: None,
280        })
281    }
282
283    /// Connects the application facade from one bounded URI without exposing
284    /// the transport package to application code.
285    pub async fn connect_uri(endpoint: String) -> Result<Self, ClientError> {
286        if endpoint.is_empty() || endpoint.len() > 2_048 {
287            return Err(ClientError::ConnectionFailure);
288        }
289        let endpoint = Endpoint::from_shared(endpoint)
290            .map_err(|_| ClientError::ConnectionFailure)?
291            .connect_timeout(Duration::from_secs(10))
292            .timeout(Duration::from_secs(30));
293        Self::connect(endpoint).await
294    }
295
296    /// Connects the application facade over one reusable HTTP/2 channel.
297    pub async fn connect(endpoint: Endpoint) -> Result<Self, ClientError> {
298        Ok(Self {
299            inner: RiffDbClient::connect(endpoint).await?,
300            scripted: None,
301            pinned_contract: None,
302            topology: None,
303        })
304    }
305
306    /// Constructs the application facade over an existing channel.
307    #[must_use]
308    pub fn from_channel(channel: Channel) -> Self {
309        Self {
310            inner: RiffDbClient::from_channel(channel),
311            scripted: None,
312            pinned_contract: None,
313            topology: None,
314        }
315    }
316
317    #[doc(hidden)]
318    #[must_use]
319    pub fn scripted_for_test(active_contract: v1::GetActiveContractResponse) -> Self {
320        let scripted = Arc::new(ScriptedPeer {
321            active_contract: Mutex::new(Some(active_contract)),
322            ..ScriptedPeer::default()
323        });
324        Self {
325            inner: RiffDbClient::from_channel(
326                Endpoint::from_static("http://127.0.0.1:9").connect_lazy(),
327            ),
328            scripted: Some(scripted),
329            pinned_contract: None,
330            topology: None,
331        }
332    }
333
334    #[doc(hidden)]
335    #[must_use]
336    pub fn push_scripted_command(
337        self,
338        result: Result<v1::ExecuteCommandResponse, ClientError>,
339    ) -> Self {
340        if let Some(scripted) = &self.scripted {
341            scripted
342                .commands
343                .lock()
344                .unwrap_or_else(std::sync::PoisonError::into_inner)
345                .push_back(result);
346        }
347        self
348    }
349
350    #[doc(hidden)]
351    #[must_use]
352    pub fn push_scripted_query(
353        self,
354        result: Result<app_v1::ExecuteQueryResponse, ClientError>,
355    ) -> Self {
356        if let Some(scripted) = &self.scripted {
357            scripted
358                .queries
359                .lock()
360                .unwrap_or_else(std::sync::PoisonError::into_inner)
361                .push_back(result);
362        }
363        self
364    }
365
366    #[doc(hidden)]
367    #[must_use]
368    pub fn scripted_command_inputs(&self) -> Vec<Option<v1::Value>> {
369        self.scripted
370            .as_ref()
371            .map(|scripted| {
372                scripted
373                    .recorded_command_inputs
374                    .lock()
375                    .unwrap_or_else(std::sync::PoisonError::into_inner)
376                    .clone()
377            })
378            .unwrap_or_default()
379    }
380
381    #[doc(hidden)]
382    #[must_use]
383    pub fn scripted_queries(&self) -> Vec<(String, Option<u64>, bool)> {
384        self.scripted
385            .as_ref()
386            .map(|scripted| {
387                scripted
388                    .recorded_queries
389                    .lock()
390                    .unwrap_or_else(std::sync::PoisonError::into_inner)
391                    .clone()
392            })
393            .unwrap_or_default()
394    }
395
396    fn peer_client(&self) -> PeerClient {
397        PeerClient {
398            inner: self.inner.clone(),
399            scripted: self.scripted.clone(),
400        }
401    }
402
403    /// Attaches up to 4 standby channels for automatic
404    /// contract-verified failover when the primary is fenced or unreachable.
405    pub fn with_standby_clients(
406        mut self,
407        standby: Vec<Self>,
408    ) -> Result<Self, ApplicationClientError> {
409        if standby.len() > MAX_STANDBY_CLIENTS {
410            return Err(ApplicationClientError::InvalidInput);
411        }
412        let standby_peers: Vec<PeerClient> = standby
413            .into_iter()
414            .map(|client| client.peer_client())
415            .collect();
416        let (promoted_active, read_replicas, pinned_contract) = match self.topology.take() {
417            Some(existing) => {
418                let state = existing.lock_state();
419                (
420                    state.promoted_active.clone(),
421                    existing.read_replicas.clone(),
422                    state
423                        .pinned_contract
424                        .clone()
425                        .or_else(|| self.pinned_contract.clone()),
426                )
427            }
428            None => (None, Vec::new(), self.pinned_contract.clone()),
429        };
430        if standby_peers.is_empty() && read_replicas.is_empty() && promoted_active.is_none() {
431            self.topology = None;
432        } else {
433            self.topology = Some(Arc::new(ClientTopology {
434                state: Mutex::new(TopologyState {
435                    promoted_active,
436                    standby: standby_peers,
437                    pinned_contract,
438                }),
439                read_replicas,
440                next_replica: AtomicUsize::new(0),
441            }));
442        }
443        Ok(self)
444    }
445
446    /// Attaches up to 4 follower read-replica channels
447    /// for eligible read queries with automatic primary fallback.
448    pub fn with_read_replica_clients(
449        mut self,
450        replicas: Vec<Self>,
451    ) -> Result<Self, ApplicationClientError> {
452        if replicas.len() > MAX_READ_REPLICA_CLIENTS {
453            return Err(ApplicationClientError::InvalidInput);
454        }
455        let read_replicas: Vec<PeerClient> = replicas
456            .into_iter()
457            .map(|client| client.peer_client())
458            .collect();
459        let (promoted_active, standby, pinned_contract) = match self.topology.take() {
460            Some(existing) => {
461                let state = existing.lock_state();
462                (
463                    state.promoted_active.clone(),
464                    state.standby.clone(),
465                    state
466                        .pinned_contract
467                        .clone()
468                        .or_else(|| self.pinned_contract.clone()),
469                )
470            }
471            None => (None, Vec::new(), self.pinned_contract.clone()),
472        };
473        if standby.is_empty() && read_replicas.is_empty() && promoted_active.is_none() {
474            self.topology = None;
475        } else {
476            self.topology = Some(Arc::new(ClientTopology {
477                state: Mutex::new(TopologyState {
478                    promoted_active,
479                    standby,
480                    pinned_contract,
481                }),
482                read_replicas,
483                next_replica: AtomicUsize::new(0),
484            }));
485        }
486        Ok(self)
487    }
488
489    /// Returns the number of remaining standby channels configured on this facade.
490    #[must_use]
491    pub fn standby_client_count(&self) -> usize {
492        self.topology
493            .as_ref()
494            .map_or(0, |topology| topology.lock_state().standby.len())
495    }
496
497    /// Returns the number of read-replica channels configured on this facade.
498    #[must_use]
499    pub fn read_replica_client_count(&self) -> usize {
500        self.topology
501            .as_ref()
502            .map_or(0, |topology| topology.read_replicas.len())
503    }
504
505    fn sync_active_from_topology(&mut self) {
506        if let Some(topology) = &self.topology {
507            let state = topology.lock_state();
508            if let Some(promoted) = &state.promoted_active {
509                self.inner = promoted.inner.clone();
510                self.scripted = promoted.scripted.clone();
511            }
512            if self.pinned_contract.is_none() {
513                self.pinned_contract = state.pinned_contract.clone();
514            }
515        }
516    }
517
518    fn standby_candidates(&self) -> (Vec<PeerClient>, Option<PinnedContractIdentity>) {
519        let Some(topology) = &self.topology else {
520            return (Vec::new(), self.pinned_contract.clone());
521        };
522        let state = topology.lock_state();
523        (
524            state.standby.clone(),
525            state
526                .pinned_contract
527                .clone()
528                .or_else(|| self.pinned_contract.clone()),
529        )
530    }
531
532    fn promote_standby_candidate(&mut self, index: usize, promoted: PeerClient) {
533        self.inner = promoted.inner.clone();
534        self.scripted = promoted.scripted.clone();
535        if let Some(topology) = &self.topology {
536            let mut state = topology.lock_state();
537            if index < state.standby.len() {
538                state.standby.remove(index);
539            }
540            state.promoted_active = Some(promoted);
541        }
542    }
543
544    /// Opens the optional bounded multiplexed application transport against
545    /// one exact generated application identity. Unary transport remains the
546    /// default until this explicit operation succeeds.
547    pub async fn open_bounded_session(
548        &mut self,
549        identity: crate::ApplicationSessionIdentity,
550        metadata: &CallMetadata,
551    ) -> Result<(), ApplicationClientError> {
552        self.sync_active_from_topology();
553        self.inner
554            .enable_bounded_application_session(identity, metadata.clone())
555            .await?;
556        Ok(())
557    }
558
559    /// Reports whether this facade explicitly selected the bounded session.
560    #[must_use]
561    pub const fn bounded_session_enabled(&self) -> bool {
562        self.inner.bounded_application_session_enabled()
563    }
564
565    /// Explicitly closes the optional bounded session and returns this facade
566    /// to unary transport. In-flight operations are released under the same
567    /// uncertainty rules as stream loss.
568    pub fn close_bounded_session(&mut self) {
569        self.inner.disable_bounded_application_session();
570    }
571
572    /// Proves that the authenticated remote database currently exposes the
573    /// exact contract identity compiled into the application lock.
574    pub async fn verify_active_contract(
575        &mut self,
576        database: &str,
577        lineage: &str,
578        version: u64,
579        bundle_hash: [u8; 32],
580        metadata: &CallMetadata,
581    ) -> Result<(), ApplicationClientError> {
582        self.sync_active_from_topology();
583        let expected = PinnedContractIdentity {
584            database: database.to_owned(),
585            lineage: lineage.to_owned(),
586            version,
587            bundle_hash,
588        };
589        self.peer_client()
590            .verify_active_contract(&expected, metadata)
591            .await?;
592        self.pinned_contract = Some(expected.clone());
593        if let Some(topology) = &self.topology {
594            topology.lock_state().pinned_contract = Some(expected);
595        }
596        Ok(())
597    }
598
599    /// Reads and validates the symbolic feature catalog for one exact contract.
600    ///
601    /// This application-only view deliberately omits catalog symbols, numeric
602    /// identities, raw IR, and capability details. An unavailable feature is
603    /// explicit and never authorizes a client-side fallback.
604    pub async fn preflight_application_features(
605        &mut self,
606        contract: ApplicationContract,
607        metadata: &CallMetadata,
608    ) -> Result<ApplicationCatalogPreflight, ApplicationClientError> {
609        self.sync_active_from_topology();
610        let expected = contract.clone();
611        let request_id = Vec::from(
612            generate_request_id()
613                .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
614                .into_bytes(),
615        );
616        let response = self
617            .inner
618            .get_application_catalog(
619                app_v1::GetApplicationCatalogRequest {
620                    contract: lower_contract(contract),
621                    limit: 1,
622                    cursor: None,
623                    request_id,
624                },
625                metadata,
626            )
627            .await?;
628        raise_application_catalog_preflight(response, &expected)
629    }
630
631    /// Inspects one compiler-declared production vector field through the
632    /// symbolic, policy-filtered application operation.
633    pub async fn inspect_vector_state(
634        &mut self,
635        request: VectorStateInspection,
636        metadata: &CallMetadata,
637    ) -> Result<VectorStateInspectionResult, ApplicationClientError> {
638        self.sync_active_from_topology();
639        request.validate()?;
640        let request_id = Vec::from(
641            generate_request_id()
642                .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
643                .into_bytes(),
644        );
645        let response = self
646            .inner
647            .inspect_vector_state(
648                app_v1::InspectVectorStateRequest {
649                    contract: lower_contract(request.contract),
650                    entity: request.entity,
651                    field: request.field,
652                    partition: Some(lower_value(request.partition)?),
653                    kind: match request.kind {
654                        VectorStateInspectionKind::Stale => {
655                            app_v1::VectorStateInspectionKind::StaleEntities as i32
656                        }
657                        VectorStateInspectionKind::OutdatedModel => {
658                            app_v1::VectorStateInspectionKind::OutdatedModelEntities as i32
659                        }
660                    },
661                    page: Some(v1::PageRequest {
662                        limit: Some(request.limit),
663                        cursor: request.cursor,
664                    }),
665                    request_id,
666                },
667                metadata,
668            )
669            .await?;
670        VectorStateInspectionResult::try_from(response)
671    }
672
673    /// Executes one exact named module query.
674    pub async fn execute_named_query(
675        &mut self,
676        query: NamedQuery,
677        metadata: &CallMetadata,
678    ) -> Result<NamedQueryResult, ApplicationClientError> {
679        let shared = self.execute_named_query_shared(query, metadata).await?;
680        Ok(Arc::unwrap_or_clone(shared))
681    }
682
683    /// Executes one exact named module query and returns a shared reference-counted
684    /// result without deep-cloning cached result maps.
685    #[doc(hidden)]
686    pub async fn execute_named_query_shared(
687        &mut self,
688        query: NamedQuery,
689        metadata: &CallMetadata,
690    ) -> Result<Arc<NamedQueryResult>, ApplicationClientError> {
691        let response =
692            with_client_wire_carrier(self.execute_named_query_wire(query, metadata)).await?;
693        raise_query_result_shared_from_client_wire(response)
694    }
695
696    /// Executes one named query while retaining its validated negotiated wire
697    /// arm for first-party generated-language adapters.
698    #[doc(hidden)]
699    pub async fn execute_named_query_wire(
700        &mut self,
701        query: NamedQuery,
702        metadata: &CallMetadata,
703    ) -> Result<app_v1::ExecuteQueryResponse, ApplicationClientError> {
704        self.sync_active_from_topology();
705        if self.topology.is_none() {
706            let mut active = self.peer_client();
707            return active
708                .execute_named_application_query_wire(query, metadata)
709                .await;
710        }
711        if !query.admission_head_consistency
712            && let Some(topology) = &self.topology
713            && !topology.read_replicas.is_empty()
714        {
715            let replicas = &topology.read_replicas;
716            let start = topology.next_replica.fetch_add(1, Ordering::Relaxed);
717            let pinned = topology
718                .lock_state()
719                .pinned_contract
720                .clone()
721                .or_else(|| self.pinned_contract.clone());
722            for offset in 0..replicas.len() {
723                let mut replica = replicas[(start + offset) % replicas.len()].clone();
724                match replica
725                    .execute_named_application_query_wire(query.clone(), metadata)
726                    .await
727                {
728                    Ok(response) => {
729                        if let Some(pinned) = &pinned {
730                            validate_pinned_query_response(&response, pinned)?;
731                        }
732                        return Ok(response);
733                    }
734                    Err(error) if application_query_error_allows_replica_fallback(&error) => {}
735                    Err(error) => return Err(error),
736                }
737            }
738        }
739        let mut active = self.peer_client();
740        match active
741            .execute_named_application_query_wire(query.clone(), metadata)
742            .await
743        {
744            Ok(response) => Ok(response),
745            Err(primary_error) => {
746                let (standby, pinned) = self.standby_candidates();
747                if !standby.is_empty()
748                    && matches!(
749                        &primary_error,
750                        ApplicationClientError::Client(error)
751                            if client_error_triggers_standby_failover(error)
752                    )
753                {
754                    for (index, mut candidate) in standby.into_iter().enumerate() {
755                        if let Some(pinned) = &pinned
756                            && candidate
757                                .verify_active_contract(pinned, metadata)
758                                .await
759                                .is_err()
760                        {
761                            continue;
762                        }
763                        match candidate
764                            .execute_named_application_query_wire(query.clone(), metadata)
765                            .await
766                        {
767                            Ok(response) => {
768                                self.promote_standby_candidate(index, candidate);
769                                return Ok(response);
770                            }
771                            Err(ApplicationClientError::Client(ref error))
772                                if client_error_triggers_standby_failover(error) => {}
773                            Err(error) => {
774                                self.promote_standby_candidate(index, candidate);
775                                return Err(error);
776                            }
777                        }
778                    }
779                }
780                Err(primary_error)
781            }
782        }
783    }
784
785    /// Executes and decodes one generated exact named-query shape.
786    pub async fn execute_generated_query<Q: GeneratedQuery>(
787        &mut self,
788        query: Q,
789        options: QueryOptions,
790        metadata: &CallMetadata,
791    ) -> Result<TypedQueryResult<Q::Output>, ApplicationClientError> {
792        let query = query.named_query(options)?;
793        let response = self.execute_named_query_wire(query, metadata).await?;
794        let application_head = response.application_head;
795        let next_cursor = response.next_cursor.clone();
796        let identity = query_response_identity(&response)?;
797        let value = match app_v1::NamedResultEncoding::try_from(response.selected_result_encoding) {
798            Ok(app_v1::NamedResultEncoding::CompactV1) => {
799                if !response.fields.is_empty() {
800                    return Err(ApplicationClientError::InvalidResponse);
801                }
802                Q::decode_compact_result(
803                    response.outcome,
804                    response
805                        .compact_result
806                        .ok_or(ApplicationClientError::InvalidResponse)?,
807                )?
808            }
809            Ok(app_v1::NamedResultEncoding::PackedV1) => {
810                if !response.fields.is_empty() || response.compact_result.is_some() {
811                    return Err(ApplicationClientError::InvalidResponse);
812                }
813                Q::decode_packed_result(
814                    response.outcome,
815                    response
816                        .packed_result
817                        .ok_or(ApplicationClientError::InvalidResponse)?,
818                )?
819            }
820            Ok(app_v1::NamedResultEncoding::LegacyRecords)
821            | Ok(app_v1::NamedResultEncoding::Unspecified) => {
822                Q::decode_result(raise_query_result(response)?)?
823            }
824            Err(_) => return Err(ApplicationClientError::InvalidResponse),
825        };
826        Ok(TypedQueryResult {
827            value,
828            identity,
829            application_head,
830            next_cursor,
831        })
832    }
833
834    /// Executes one exact symbolic command with bounded uncertainty recovery.
835    pub async fn execute_command(
836        &mut self,
837        command: ApplicationCommand,
838        attempts: AttemptBudget,
839        metadata: &CallMetadata,
840    ) -> Result<ApplicationCommandResult, ApplicationClientError> {
841        self.sync_active_from_topology();
842        let command_name = command.name.clone();
843        let idempotent = command.into_idempotent_command()?;
844        let mut active = self.peer_client();
845        match active
846            .execute_with_retry(&idempotent, attempts, metadata)
847            .await
848        {
849            Ok(response) => raise_application_command_result(response),
850            Err(primary_error) => {
851                let (standby, pinned) = self.standby_candidates();
852                if !standby.is_empty() && client_error_triggers_standby_failover(&primary_error) {
853                    for (index, mut candidate) in standby.into_iter().enumerate() {
854                        if let Some(pinned) = &pinned
855                            && candidate
856                                .verify_active_contract(pinned, metadata)
857                                .await
858                                .is_err()
859                        {
860                            continue;
861                        }
862                        match candidate
863                            .execute_with_retry(&idempotent, attempts, metadata)
864                            .await
865                        {
866                            Ok(response) => {
867                                self.promote_standby_candidate(index, candidate);
868                                return raise_application_command_result(response);
869                            }
870                            Err(error) if client_error_triggers_standby_failover(&error) => {}
871                            Err(error) => {
872                                self.promote_standby_candidate(index, candidate);
873                                return Err(ApplicationClientError::Client(
874                                    contextualize_command_client_error(error, &command_name),
875                                ));
876                            }
877                        }
878                    }
879                }
880                Err(ApplicationClientError::Client(
881                    contextualize_command_client_error(primary_error, &command_name),
882                ))
883            }
884        }
885    }
886
887    /// Executes and decodes one generated command, then performs one exact
888    /// same-key outcome lookup if transport uncertainty remains.
889    pub async fn execute_generated_command<C: GeneratedCommand>(
890        &mut self,
891        command: &C,
892        attempts: AttemptBudget,
893        metadata: &CallMetadata,
894    ) -> Result<TypedCommandResult<C::Outcome>, GeneratedExecutionError> {
895        self.sync_active_from_topology();
896        let execution = match self
897            .inner
898            .execute_generated_with_recovery(command, attempts, metadata)
899            .await
900        {
901            Ok(execution) => execution,
902            Err(primary_error) => {
903                let (standby, pinned) = self.standby_candidates();
904                let mut promoted_execution = None;
905                if !standby.is_empty()
906                    && matches!(
907                        &primary_error,
908                        GeneratedExecutionError::Client(error)
909                            if client_error_triggers_standby_failover(error)
910                    )
911                {
912                    for (index, mut candidate) in standby.into_iter().enumerate() {
913                        if let Some(pinned) = &pinned
914                            && candidate
915                                .verify_active_contract(pinned, metadata)
916                                .await
917                                .is_err()
918                        {
919                            continue;
920                        }
921                        match candidate
922                            .inner
923                            .execute_generated_with_recovery(command, attempts, metadata)
924                            .await
925                        {
926                            Ok(execution) => {
927                                self.promote_standby_candidate(index, candidate);
928                                promoted_execution = Some(Ok(execution));
929                                break;
930                            }
931                            Err(GeneratedExecutionError::Client(ref error))
932                                if client_error_triggers_standby_failover(error) => {}
933                            Err(error) => {
934                                self.promote_standby_candidate(index, candidate);
935                                promoted_execution = Some(Err(error));
936                                break;
937                            }
938                        }
939                    }
940                }
941                match promoted_execution {
942                    Some(result) => result?,
943                    None => return Err(primary_error),
944                }
945            }
946        };
947        let (outcome, response) = execution.into_parts();
948        let workflow_revisions = command
949            .workflow_successor_revisions(&response)
950            .map_err(GeneratedExecutionError::CommandShape)?;
951        Ok(TypedCommandResult {
952            outcome,
953            commit_sequence: (response.commit_sequence != 0).then_some(response.commit_sequence),
954            contract_version: response.contract_version,
955            plan_hash: response.plan_hash.try_into().map_err(|_| {
956                GeneratedExecutionError::CommandShape(
957                    crate::generated::GeneratedCommandError::InvalidOutcomeShape,
958                )
959            })?,
960            replayed: response.status
961                == v1::execute_command_response::CompletionStatus::Replayed as i32,
962            outcome_uri: response.outcome_uri,
963            workflow_revisions,
964        })
965    }
966
967    /// Executes a bounded collection of ordinary generated commands with
968    /// backpressure and independently ordered per-item results.
969    pub async fn execute_generated_command_batch<C>(
970        &self,
971        commands: Vec<C>,
972        options: GeneratedBatchOptions,
973        attempts: AttemptBudget,
974        metadata: &CallMetadata,
975    ) -> Result<GeneratedBatchResult<C::Outcome>, GeneratedBatchError>
976    where
977        C: GeneratedCommand + Clone,
978    {
979        self.execute_generated_command_batch_with_progress(
980            commands,
981            options,
982            attempts,
983            metadata,
984            |_| {},
985        )
986        .await
987    }
988
989    /// Executes a bounded generated-command batch and reports completion plus
990    /// the largest contiguous, safely resumable input checkpoint.
991    pub async fn execute_generated_command_batch_with_progress<C, F>(
992        &self,
993        commands: Vec<C>,
994        options: GeneratedBatchOptions,
995        attempts: AttemptBudget,
996        metadata: &CallMetadata,
997        mut report_progress: F,
998    ) -> Result<GeneratedBatchResult<C::Outcome>, GeneratedBatchError>
999    where
1000        C: GeneratedCommand + Clone,
1001        F: FnMut(GeneratedBatchProgress),
1002    {
1003        options.validate(commands.len())?;
1004        let total = commands.len();
1005        let (transport_batch_size, transport_concurrency) =
1006            generated_transport_batch_policy(options.concurrency);
1007        let mut chunks = Vec::new();
1008        let mut chunk = Vec::with_capacity(transport_batch_size);
1009        for item in commands.into_iter().enumerate().skip(options.checkpoint) {
1010            chunk.push(item);
1011            if chunk.len() == transport_batch_size {
1012                chunks.push(std::mem::replace(
1013                    &mut chunk,
1014                    Vec::with_capacity(transport_batch_size),
1015                ));
1016            }
1017        }
1018        if !chunk.is_empty() {
1019            chunks.push(chunk);
1020        }
1021        let mut pending = stream::iter(chunks.into_iter().map(|chunk| {
1022            let client = self.clone();
1023            let metadata = metadata.clone();
1024            async move {
1025                client
1026                    .execute_generated_transport_batch(chunk, attempts, &metadata)
1027                    .await
1028            }
1029        }))
1030        .buffer_unordered(transport_concurrency);
1031        let mut items = Vec::with_capacity(total - options.checkpoint);
1032        let mut progress_state = BatchCheckpointState::new(options.checkpoint);
1033        while let Some(completed_chunk) = pending.next().await {
1034            for (index, result) in completed_chunk {
1035                record_generated_batch_item_completion(
1036                    index,
1037                    result,
1038                    total,
1039                    &mut progress_state,
1040                    &mut items,
1041                    &mut report_progress,
1042                );
1043            }
1044        }
1045        items.sort_by_key(|item| item.index);
1046        Ok(GeneratedBatchResult {
1047            items,
1048            checkpoint: progress_state.checkpoint,
1049        })
1050    }
1051
1052    async fn execute_generated_transport_batch<C: GeneratedCommand + Clone>(
1053        &self,
1054        commands: Vec<(usize, C)>,
1055        attempts: AttemptBudget,
1056        metadata: &CallMetadata,
1057    ) -> Vec<(
1058        usize,
1059        Result<TypedCommandResult<C::Outcome>, GeneratedExecutionError>,
1060    )> {
1061        let mut requests = Vec::with_capacity(commands.len());
1062        let mut prepared = Vec::with_capacity(commands.len());
1063        let mut completed = Vec::new();
1064        for (index, command) in commands {
1065            let generic = match command.idempotent_command() {
1066                Ok(generic) => generic,
1067                Err(error) => {
1068                    completed.push((index, Err(GeneratedExecutionError::CommandShape(error))));
1069                    continue;
1070                }
1071            };
1072            let request_id = match generate_request_id() {
1073                Ok(request_id) => request_id,
1074                Err(error) => {
1075                    completed.push((
1076                        index,
1077                        Err(GeneratedExecutionError::Client(
1078                            ClientError::IdentifierGeneration(error),
1079                        )),
1080                    ));
1081                    continue;
1082                }
1083            };
1084            requests.push(generic.request(request_id));
1085            prepared.push((index, command));
1086        }
1087        if prepared.is_empty() {
1088            return completed;
1089        }
1090
1091        let mut client = self.inner.clone();
1092        let response = client
1093            .execute_batch(
1094                v1::ExecuteCommandBatchRequest { commands: requests },
1095                metadata,
1096            )
1097            .await;
1098        let Ok(response) = response else {
1099            // Transport-level or whole-RPC failure: re-enter every item through
1100            // the normal same-idempotency-key recovery path.
1101            let recovered: Vec<_> = stream::iter(prepared.into_iter().map(|(index, command)| {
1102                let mut client = self.clone();
1103                let metadata = metadata.clone();
1104                async move {
1105                    let result = client
1106                        .execute_generated_command(&command, attempts, &metadata)
1107                        .await;
1108                    (index, result)
1109                }
1110            }))
1111            .buffer_unordered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1112            .collect()
1113            .await;
1114            completed.extend(recovered);
1115            return completed;
1116        };
1117
1118        // Prefer per-item carriage when present (ADR-0084). Older servers leave
1119        // items empty and only populate legacy success rows.
1120        if !response.items.is_empty() {
1121            let recovered =
1122                complete_batch_items_with_reentry(prepared, response.items, |index, command| {
1123                    let mut client = self.clone();
1124                    let metadata = metadata.clone();
1125                    async move {
1126                        let result = client
1127                            .execute_generated_command(&command, attempts, &metadata)
1128                            .await;
1129                        (index, result)
1130                    }
1131                })
1132                .await;
1133            completed.extend(recovered);
1134            return completed;
1135        }
1136
1137        // Legacy items-absent path: positional zip on success rows.
1138        completed.extend(prepared.into_iter().zip(response.responses).map(
1139            |((index, command), response)| {
1140                let outcome = command
1141                    .decode_outcome(&response)
1142                    .map_err(GeneratedExecutionError::CommandShape);
1143                (
1144                    index,
1145                    outcome.and_then(|outcome| typed_command_result(&command, outcome, response)),
1146                )
1147            },
1148        ));
1149        completed
1150    }
1151}
1152
1153impl RiffDbClient {
1154    /// Executes at most 16 ordinary idempotent commands through one transport
1155    /// batch while preserving independent per-item recovery.
1156    ///
1157    /// This is transport coalescing only: every item remains a separately
1158    /// authorized command with its own idempotency identity, commit decision,
1159    /// provenance, and typed outcome. Whole-RPC uncertainty and retryable item
1160    /// failures re-enter through [`RiffDbClient::execute_with_retry`] with the
1161    /// identical command input and key.
1162    pub async fn execute_idempotent_transport_batch_with_retry(
1163        &self,
1164        commands: Vec<IdempotentCommand>,
1165        attempts: AttemptBudget,
1166        metadata: &CallMetadata,
1167    ) -> Result<Vec<Result<v1::ExecuteCommandResponse, ClientError>>, IdempotentTransportBatchError>
1168    {
1169        validate_idempotent_transport_batch_len(commands.len())?;
1170
1171        let mut requests = Vec::with_capacity(commands.len());
1172        for command in &commands {
1173            let request_id = match generate_request_id() {
1174                Ok(request_id) => request_id,
1175                Err(_) => {
1176                    // Nothing was submitted. Re-enter every immutable command
1177                    // through the ordinary path so request-id uncertainty is
1178                    // classified independently and no item is misattributed.
1179                    return Ok(
1180                        reenter_idempotent_commands(self, commands, attempts, metadata).await,
1181                    );
1182                }
1183            };
1184            requests.push(command.request(request_id));
1185        }
1186
1187        let mut client = self.clone();
1188        let response = client
1189            .execute_batch(
1190                v1::ExecuteCommandBatchRequest { commands: requests },
1191                metadata,
1192            )
1193            .await;
1194        let Ok(response) = response else {
1195            return Ok(reenter_idempotent_commands(self, commands, attempts, metadata).await);
1196        };
1197
1198        if response.items.is_empty() {
1199            return Ok(response.responses.into_iter().map(Ok).collect());
1200        }
1201
1202        let mut resolved = Vec::with_capacity(commands.len());
1203        let mut reenter = Vec::new();
1204        for (index, (command, item)) in commands.into_iter().zip(response.items).enumerate() {
1205            match item.result {
1206                Some(v1::execute_command_batch_item::Result::Response(response)) => {
1207                    resolved.push((index, Ok(response)));
1208                }
1209                Some(v1::execute_command_batch_item::Result::Error(error_wire)) => {
1210                    match application_error_from_proto(&error_wire) {
1211                        Ok(error) if application_error_requires_reentry(&error) => {
1212                            reenter.push((index, command));
1213                        }
1214                        Ok(error) => {
1215                            resolved.push((index, Err(ClientError::Application(Box::new(error)))));
1216                        }
1217                        Err(_) => reenter.push((index, command)),
1218                    }
1219                }
1220                None => reenter.push((index, command)),
1221            }
1222        }
1223
1224        if !reenter.is_empty() {
1225            let recovered: Vec<_> = stream::iter(reenter.into_iter().map(|(index, command)| {
1226                let mut client = self.clone();
1227                let metadata = metadata.clone();
1228                async move {
1229                    let result = client
1230                        .execute_with_retry(&command, attempts, &metadata)
1231                        .await;
1232                    (index, result)
1233                }
1234            }))
1235            .buffer_unordered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1236            .collect()
1237            .await;
1238            resolved.extend(recovered);
1239        }
1240        resolved.sort_by_key(|(index, _)| *index);
1241        Ok(resolved.into_iter().map(|(_, result)| result).collect())
1242    }
1243}
1244
1245const fn validate_idempotent_transport_batch_len(
1246    item_count: usize,
1247) -> Result<(), IdempotentTransportBatchError> {
1248    if item_count == 0 {
1249        return Err(IdempotentTransportBatchError::Empty);
1250    }
1251    if item_count > MAX_GENERATED_TRANSPORT_BATCH_ITEMS {
1252        return Err(IdempotentTransportBatchError::TooManyItems);
1253    }
1254    Ok(())
1255}
1256
1257async fn reenter_idempotent_commands(
1258    client: &RiffDbClient,
1259    commands: Vec<IdempotentCommand>,
1260    attempts: AttemptBudget,
1261    metadata: &CallMetadata,
1262) -> Vec<Result<v1::ExecuteCommandResponse, ClientError>> {
1263    stream::iter(commands.into_iter().map(|command| {
1264        let mut client = client.clone();
1265        let metadata = metadata.clone();
1266        async move {
1267            client
1268                .execute_with_retry(&command, attempts, &metadata)
1269                .await
1270        }
1271    }))
1272    .buffered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1273    .collect()
1274    .await
1275}
1276
1277type ResolvedBatchItem<T> = (
1278    usize,
1279    Result<TypedCommandResult<T>, GeneratedExecutionError>,
1280);
1281
1282/// One batch item scheduled for same-key re-entry.
1283struct ReenterCandidate<C> {
1284    index: usize,
1285    command: C,
1286    /// Defense-in-depth original protocol failure. Production wire validation
1287    /// rejects unset/undecodable item arms before this path runs; when re-entry
1288    /// is exhausted, this is surfaced instead of the generic re-entry error.
1289    protocol_failure: Option<crate::ProtocolFailure>,
1290}
1291
1292type BatchItemResolution<C> = (
1293    Vec<ResolvedBatchItem<<C as GeneratedCommand>::Outcome>>,
1294    Vec<ReenterCandidate<C>>,
1295);
1296
1297/// Contiguous-progress counters for a generated command batch.
1298struct BatchCheckpointState {
1299    completed: usize,
1300    checkpoint: usize,
1301    completed_after_checkpoint: BTreeSet<usize>,
1302}
1303
1304impl BatchCheckpointState {
1305    /// Seeds completed/checkpoint from a retained resume checkpoint.
1306    fn new(checkpoint: usize) -> Self {
1307        Self {
1308            completed: checkpoint,
1309            checkpoint,
1310            completed_after_checkpoint: BTreeSet::new(),
1311        }
1312    }
1313}
1314
1315/// Records one independent batch item result and advances the contiguous checkpoint.
1316///
1317/// Checkpoint advances over every terminal result (Ok or Err). Callers must only
1318/// invoke this after any same-key re-entry for the item has finished.
1319fn record_generated_batch_item_completion<T, F>(
1320    index: usize,
1321    result: Result<TypedCommandResult<T>, GeneratedExecutionError>,
1322    total: usize,
1323    state: &mut BatchCheckpointState,
1324    items: &mut Vec<GeneratedBatchItem<T>>,
1325    report_progress: &mut F,
1326) where
1327    F: FnMut(GeneratedBatchProgress),
1328{
1329    state.completed += 1;
1330    state.completed_after_checkpoint.insert(index);
1331    while state.completed_after_checkpoint.remove(&state.checkpoint) {
1332        state.checkpoint += 1;
1333    }
1334    items.push(GeneratedBatchItem { index, result });
1335    report_progress(GeneratedBatchProgress {
1336        completed: state.completed,
1337        total,
1338        checkpoint: state.checkpoint,
1339    });
1340}
1341
1342/// Classifies per-item batch carriage into terminal results and re-entry candidates.
1343///
1344/// Re-entry uses the registry-derived recovery action, not a hand-listed code set:
1345/// [`ApplicationRecoveryAction::Retry`] and
1346/// [`ApplicationRecoveryAction::ResolveWithSameIdempotencyKey`] re-enter
1347/// `execute_generated_command` (same idempotency key; its AttemptBudget and
1348/// Overloaded backoff then apply). Every other recovery action is terminal and
1349/// surfaces directly with zero re-entry.
1350///
1351/// Undecodable error details and unset oneof arms are **defense-in-depth**:
1352/// production clients reject both shapes at the kept client inbound decode
1353/// (`decode_public_message` inside StrictProstDecoder — no separate client
1354/// re-walk after decode) before an `Ok` batch response exists, so those wire
1355/// defects arrive as transport-level protocol failures and take the blanket
1356/// re-entry path. If a decoded response still reaches these arms, same-key
1357/// re-entry is scheduled; when that re-entry is exhausted, the original
1358/// protocol failure is surfaced as the item error.
1359fn resolve_generated_batch_items<C: GeneratedCommand>(
1360    prepared: Vec<(usize, C)>,
1361    items: Vec<v1::ExecuteCommandBatchItem>,
1362) -> BatchItemResolution<C> {
1363    let mut resolved = Vec::with_capacity(prepared.len());
1364    let mut reenter = Vec::new();
1365    for ((index, command), item) in prepared.into_iter().zip(items) {
1366        match item.result {
1367            Some(v1::execute_command_batch_item::Result::Response(item_response)) => {
1368                let outcome = command
1369                    .decode_outcome(&item_response)
1370                    .map_err(GeneratedExecutionError::CommandShape);
1371                resolved.push((
1372                    index,
1373                    outcome
1374                        .and_then(|outcome| typed_command_result(&command, outcome, item_response)),
1375                ));
1376            }
1377            Some(v1::execute_command_batch_item::Result::Error(error_wire)) => {
1378                match application_error_from_proto(&error_wire) {
1379                    Ok(error) if application_error_requires_reentry(&error) => {
1380                        reenter.push(ReenterCandidate {
1381                            index,
1382                            command,
1383                            protocol_failure: None,
1384                        });
1385                    }
1386                    Ok(error) => {
1387                        resolved.push((
1388                            index,
1389                            Err(GeneratedExecutionError::Client(ClientError::Application(
1390                                Box::new(error),
1391                            ))),
1392                        ));
1393                    }
1394                    Err(_) => {
1395                        reenter.push(ReenterCandidate {
1396                            index,
1397                            command,
1398                            protocol_failure: Some(crate::ProtocolFailure::new(
1399                                crate::ProtocolFailureKind::InvalidApplicationErrorDetails,
1400                            )),
1401                        });
1402                    }
1403                }
1404            }
1405            None => {
1406                reenter.push(ReenterCandidate {
1407                    index,
1408                    command,
1409                    protocol_failure: Some(crate::ProtocolFailure::new(
1410                        crate::ProtocolFailureKind::InvalidInboundMessage,
1411                    )),
1412                });
1413            }
1414        }
1415    }
1416    (resolved, reenter)
1417}
1418
1419/// Finishes one transport-batch response: keep terminal arms, re-enter the rest.
1420async fn complete_batch_items_with_reentry<C, F, Fut>(
1421    prepared: Vec<(usize, C)>,
1422    items: Vec<v1::ExecuteCommandBatchItem>,
1423    reenter_one: F,
1424) -> Vec<ResolvedBatchItem<C::Outcome>>
1425where
1426    C: GeneratedCommand + Clone,
1427    F: Fn(usize, C) -> Fut,
1428    Fut: std::future::Future<
1429            Output = (
1430                usize,
1431                Result<TypedCommandResult<C::Outcome>, GeneratedExecutionError>,
1432            ),
1433        >,
1434{
1435    let (mut resolved, reenter) = resolve_generated_batch_items(prepared, items);
1436    if !reenter.is_empty() {
1437        let recovered: Vec<_> = stream::iter(reenter.into_iter().map(|candidate| {
1438            let protocol_failure = candidate.protocol_failure;
1439            let reenter_one = &reenter_one;
1440            async move {
1441                let (index, result) = reenter_one(candidate.index, candidate.command).await;
1442                let result = match (result, protocol_failure) {
1443                    (Err(_), Some(failure)) => Err(GeneratedExecutionError::Client(
1444                        ClientError::Protocol(failure),
1445                    )),
1446                    (other, _) => other,
1447                };
1448                (index, result)
1449            }
1450        }))
1451        .buffer_unordered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1452        .collect()
1453        .await;
1454        resolved.extend(recovered);
1455    }
1456    resolved
1457}
1458
1459const fn application_error_requires_reentry(error: &ApplicationError) -> bool {
1460    matches!(
1461        error.recovery_action(),
1462        ApplicationRecoveryAction::Retry | ApplicationRecoveryAction::ResolveWithSameIdempotencyKey
1463    )
1464}
1465
1466fn generated_transport_batch_policy(item_concurrency: usize) -> (usize, usize) {
1467    let transport_concurrency = item_concurrency.div_ceil(MAX_GENERATED_TRANSPORT_BATCH_ITEMS);
1468    let transport_batch_size = item_concurrency / transport_concurrency;
1469    (transport_batch_size, transport_concurrency)
1470}
1471
1472pub(crate) fn typed_command_result<C: GeneratedCommand<Outcome = T>, T>(
1473    command: &C,
1474    outcome: T,
1475    response: v1::ExecuteCommandResponse,
1476) -> Result<TypedCommandResult<T>, GeneratedExecutionError> {
1477    let workflow_revisions = command
1478        .workflow_successor_revisions(&response)
1479        .map_err(GeneratedExecutionError::CommandShape)?;
1480    let plan_hash = response.plan_hash.try_into().map_err(|_| {
1481        GeneratedExecutionError::CommandShape(
1482            crate::generated::GeneratedCommandError::InvalidOutcomeShape,
1483        )
1484    })?;
1485    Ok(TypedCommandResult {
1486        outcome,
1487        commit_sequence: (response.commit_sequence != 0).then_some(response.commit_sequence),
1488        contract_version: response.contract_version,
1489        plan_hash,
1490        replayed: response.status
1491            == v1::execute_command_response::CompletionStatus::Replayed as i32,
1492        outcome_uri: response.outcome_uri,
1493        workflow_revisions,
1494    })
1495}
1496
1497/// Bounds for generated command batches.
1498#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1499pub struct GeneratedBatchOptions {
1500    /// Maximum simultaneously in-flight ordinary command calls.
1501    pub concurrency: usize,
1502    /// Contiguous input prefix already represented by a retained checkpoint.
1503    pub checkpoint: usize,
1504}
1505
1506impl GeneratedBatchOptions {
1507    /// Constructs a checked batch policy.
1508    pub fn new(concurrency: usize) -> Result<Self, GeneratedBatchError> {
1509        let value = Self {
1510            concurrency,
1511            checkpoint: 0,
1512        };
1513        value.validate(1)?;
1514        Ok(value)
1515    }
1516
1517    /// Resumes after a previously reported contiguous input checkpoint.
1518    #[must_use]
1519    pub const fn with_checkpoint(mut self, checkpoint: usize) -> Self {
1520        self.checkpoint = checkpoint;
1521        self
1522    }
1523
1524    fn validate(self, item_count: usize) -> Result<(), GeneratedBatchError> {
1525        if self.concurrency == 0
1526            || self.concurrency > MAX_GENERATED_BATCH_CONCURRENCY
1527            || item_count == 0
1528            || item_count > 4_096
1529            || self.checkpoint > item_count
1530        {
1531            return Err(GeneratedBatchError::InvalidBounds);
1532        }
1533        Ok(())
1534    }
1535}
1536
1537/// One independently completed generated command at its stable input offset.
1538pub struct GeneratedBatchItem<T> {
1539    /// Zero-based position in the complete input collection.
1540    pub index: usize,
1541    /// Typed command result or its independent public execution failure.
1542    pub result: Result<TypedCommandResult<T>, GeneratedExecutionError>,
1543}
1544
1545/// Bounded progress safe to persist without command inputs or outcomes.
1546#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1547pub struct GeneratedBatchProgress {
1548    /// Number of inputs covered by the initial checkpoint or completed now.
1549    pub completed: usize,
1550    /// Total number of inputs in the bound collection.
1551    pub total: usize,
1552    /// Largest contiguous completed input prefix, safe for exact resume.
1553    pub checkpoint: usize,
1554}
1555
1556/// Stable per-item generated batch result and resumable item checkpoint.
1557pub struct GeneratedBatchResult<T> {
1558    /// Newly executed, input-ordered independent outcomes and original offsets.
1559    pub items: Vec<GeneratedBatchItem<T>>,
1560    /// Largest contiguous completed input prefix.
1561    pub checkpoint: usize,
1562}
1563
1564/// Failure before a generated batch can submit any item.
1565#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1566pub enum GeneratedBatchError {
1567    /// Item count or concurrency exceeded the public bounds.
1568    InvalidBounds,
1569}
1570
1571impl fmt::Display for GeneratedBatchError {
1572    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1573        formatter.write_str("generated command batch bounds are invalid")
1574    }
1575}
1576
1577impl std::error::Error for GeneratedBatchError {}
1578
1579/// Canonical UUID value with lazy display text.
1580///
1581/// Wire raise paths keep the 16-byte form; the 36-char text is formatted only
1582/// when a caller asks for display via [`as_str`](Self::as_str) or
1583/// [`into_string`](Self::into_string). Parameter construction from validated
1584/// text retains the original string so adapters that already hold display form
1585/// do not re-allocate.
1586#[derive(Debug)]
1587pub struct ApplicationUuid {
1588    bytes: [u8; 16],
1589    text: OnceLock<String>,
1590}
1591
1592impl Clone for ApplicationUuid {
1593    fn clone(&self) -> Self {
1594        let text = OnceLock::new();
1595        if let Some(existing) = self.text.get() {
1596            let _ = text.set(existing.clone());
1597        }
1598        Self {
1599            bytes: self.bytes,
1600            text,
1601        }
1602    }
1603}
1604
1605impl ApplicationUuid {
1606    /// Builds from the exact 16-byte public UUID value (raise path).
1607    #[must_use]
1608    pub fn from_bytes(bytes: [u8; 16]) -> Self {
1609        Self {
1610            bytes,
1611            text: OnceLock::new(),
1612        }
1613    }
1614
1615    /// Builds from canonical UUID text (parameter path).
1616    pub fn from_text(text: impl Into<String>) -> Result<Self, ApplicationClientError> {
1617        let text = text.into();
1618        let bytes = parse_uuid(&text)?;
1619        let cell = OnceLock::new();
1620        let _ = cell.set(text);
1621        Ok(Self { bytes, text: cell })
1622    }
1623
1624    /// Exact 16-byte form without a text round-trip.
1625    #[must_use]
1626    pub const fn as_bytes(&self) -> &[u8; 16] {
1627        &self.bytes
1628    }
1629
1630    /// Canonical 36-char text; formatted on first access when raised from bytes.
1631    #[must_use]
1632    pub fn as_str(&self) -> &str {
1633        self.text
1634            .get_or_init(|| format_uuid_text(&self.bytes))
1635            .as_str()
1636    }
1637
1638    /// Consumes into canonical text (formats once if never requested).
1639    #[must_use]
1640    pub fn into_string(self) -> String {
1641        match self.text.into_inner() {
1642            Some(text) => text,
1643            None => format_uuid_text(&self.bytes),
1644        }
1645    }
1646
1647    /// True when display text has already been materialized.
1648    #[must_use]
1649    pub fn text_is_materialized(&self) -> bool {
1650        self.text.get().is_some()
1651    }
1652}
1653
1654impl PartialEq for ApplicationUuid {
1655    fn eq(&self, other: &Self) -> bool {
1656        self.bytes == other.bytes
1657    }
1658}
1659
1660impl Eq for ApplicationUuid {}
1661
1662impl From<[u8; 16]> for ApplicationUuid {
1663    fn from(bytes: [u8; 16]) -> Self {
1664        Self::from_bytes(bytes)
1665    }
1666}
1667
1668/// A bounded application value addressed only by contract names.
1669#[derive(Clone, Debug, Eq, PartialEq)]
1670pub enum ApplicationValue {
1671    /// Explicit null.
1672    Null,
1673    /// Boolean.
1674    Bool(bool),
1675    /// Signed integer.
1676    I64(i64),
1677    /// Unsigned integer.
1678    U64(u64),
1679    /// Exact fixed-scale decimal.
1680    Decimal {
1681        /// Minimal big-endian two's-complement coefficient.
1682        coefficient_twos_complement: Vec<u8>,
1683        /// Fractional scale.
1684        scale: u32,
1685        /// Optional declared precision assertion.
1686        precision: Option<u32>,
1687    },
1688    /// Currency-qualified exact decimal.
1689    Money {
1690        /// Three-letter currency code.
1691        currency: String,
1692        /// Exact amount.
1693        amount: Box<Self>,
1694    },
1695    /// Exact text.
1696    String(String),
1697    /// Canonical UUID (bytes form; display text is lazy).
1698    Uuid(ApplicationUuid),
1699    /// Contract enum variant name lowered without caller-visible numeric IDs.
1700    Enum(String),
1701    /// Compiler-generated stable enum identity. Application code uses the name.
1702    EnumIdentity {
1703        /// Stable enum type identifier pinned by the application lock.
1704        type_id: u32,
1705        /// Stable enum variant identifier pinned by the application lock.
1706        variant_id: u32,
1707        /// Symbolic variant name retained for checked presentation.
1708        name: String,
1709    },
1710    /// Opaque bytes.
1711    Bytes(Vec<u8>),
1712    /// Days since the Unix epoch.
1713    Date(i32),
1714    /// UTC timestamp.
1715    Timestamp {
1716        /// Whole seconds since the Unix epoch.
1717        seconds: i64,
1718        /// Nanosecond fraction.
1719        nanos: u32,
1720    },
1721    /// Canonical finite binary32 vector with a structurally bounded dimension.
1722    Vector(riffdb_types::CanonicalVector),
1723    /// Ordered bounded values.
1724    List(Vec<Self>),
1725    /// Name-addressed record.
1726    Record(BTreeMap<String, Self>),
1727}
1728
1729/// Closed population selected by an authoritative vector-state inspection.
1730#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1731pub enum VectorStateInspectionKind {
1732    /// Source-newer or missing embedding evidence.
1733    Stale,
1734    /// Embeddings produced by a non-current declared model version.
1735    OutdatedModel,
1736}
1737
1738/// One symbolic bounded vector-state inspection request.
1739#[derive(Clone, Debug, Eq, PartialEq)]
1740pub struct VectorStateInspection {
1741    contract: ApplicationContract,
1742    entity: String,
1743    field: String,
1744    partition: ApplicationValue,
1745    kind: VectorStateInspectionKind,
1746    limit: u32,
1747    cursor: Option<Vec<u8>>,
1748}
1749
1750impl VectorStateInspection {
1751    /// Creates one initial bounded inspection page.
1752    #[must_use]
1753    pub fn new(
1754        contract: ApplicationContract,
1755        entity: impl Into<String>,
1756        field: impl Into<String>,
1757        partition: ApplicationValue,
1758        kind: VectorStateInspectionKind,
1759        limit: u32,
1760    ) -> Self {
1761        Self {
1762            contract,
1763            entity: entity.into(),
1764            field: field.into(),
1765            partition,
1766            kind,
1767            limit,
1768            cursor: None,
1769        }
1770    }
1771
1772    /// Continues from one opaque server-issued cursor.
1773    #[must_use]
1774    pub fn after(mut self, cursor: Vec<u8>) -> Self {
1775        self.cursor = Some(cursor);
1776        self
1777    }
1778
1779    fn validate(&self) -> Result<(), ApplicationClientError> {
1780        validate_contract(&self.contract)?;
1781        if self.entity.is_empty()
1782            || self.entity.len() > 256
1783            || self.field.is_empty()
1784            || self.field.len() > 256
1785            || self.limit == 0
1786            || self.limit > 500
1787            || self
1788                .cursor
1789                .as_ref()
1790                .is_some_and(|cursor| cursor.len() != 16)
1791        {
1792            return Err(ApplicationClientError::InvalidInput);
1793        }
1794        Ok(())
1795    }
1796}
1797
1798/// Whole-partition staleness summary, returned only when policy permits counts.
1799#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1800pub struct VectorStalenessSummary {
1801    /// Total live entities covered by the authoritative observation.
1802    pub total_entities: u64,
1803    /// Source-stale live entities.
1804    pub stale_count: u64,
1805    /// Strict declared count threshold.
1806    pub stale_entity_count_threshold: u64,
1807    /// Whether `stale_count` strictly exceeds the threshold.
1808    pub slo_breached: bool,
1809}
1810
1811/// Whole-partition model-version summary, returned only when policy permits counts.
1812#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1813pub struct VectorModelVersionSummary {
1814    /// Embeddings carrying the current declared model identity and version.
1815    pub current_count: u64,
1816    /// Embeddings carrying another retained model identity or version.
1817    pub outdated_count: u64,
1818}
1819
1820/// One policy-visible stale entity observation.
1821#[derive(Clone, Eq, PartialEq)]
1822pub struct VectorStalenessItem {
1823    /// Opaque canonical entity key bytes.
1824    pub entity_key: Vec<u8>,
1825    /// Newest declared source-field write.
1826    pub newest_source_write: u64,
1827    /// Last embedding write, or absence when no embedding exists.
1828    pub embedding_write: Option<u64>,
1829}
1830
1831impl fmt::Debug for VectorStalenessItem {
1832    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1833        formatter
1834            .debug_struct("VectorStalenessItem")
1835            .field("entity_key", &"[REDACTED]")
1836            .field("newest_source_write", &self.newest_source_write)
1837            .field("embedding_write", &self.embedding_write)
1838            .finish()
1839    }
1840}
1841
1842/// One policy-visible outdated model observation.
1843#[derive(Clone, Eq, PartialEq)]
1844pub struct VectorModelVersionItem {
1845    /// Opaque canonical entity key bytes.
1846    pub entity_key: Vec<u8>,
1847    /// Bounded model identity.
1848    pub model: String,
1849    /// Bounded model version.
1850    pub model_version: String,
1851    /// Authoritative embedding write sequence.
1852    pub embedding_write: u64,
1853}
1854
1855impl fmt::Debug for VectorModelVersionItem {
1856    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1857        formatter
1858            .debug_struct("VectorModelVersionItem")
1859            .field("entity_key", &"[REDACTED]")
1860            .field("model", &"[REDACTED]")
1861            .field("model_version", &"[REDACTED]")
1862            .field("embedding_write", &self.embedding_write)
1863            .finish()
1864    }
1865}
1866
1867/// One bounded policy-filtered vector-state page.
1868#[derive(Clone, Debug, Eq, PartialEq)]
1869pub struct VectorInspectionPage<T> {
1870    /// Visible canonical items.
1871    pub items: Vec<T>,
1872    /// Opaque continuation cursor.
1873    pub next_cursor: Option<Vec<u8>>,
1874    /// Authoritative observation frontier, when one exists.
1875    pub observed_frontier: Option<u64>,
1876}
1877
1878/// Closed response shape. Count summaries and row-policy-filtered pages cannot
1879/// be confused or silently substituted.
1880#[derive(Clone, Debug, Eq, PartialEq)]
1881pub enum VectorStateInspectionResult {
1882    /// Whole-partition staleness counts.
1883    StalenessSummary(VectorStalenessSummary),
1884    /// Policy-filtered stale entities.
1885    StaleEntities(VectorInspectionPage<VectorStalenessItem>),
1886    /// Whole-partition model-version counts.
1887    ModelVersionSummary(VectorModelVersionSummary),
1888    /// Policy-filtered outdated embeddings.
1889    OutdatedModelEntities(VectorInspectionPage<VectorModelVersionItem>),
1890}
1891
1892impl TryFrom<app_v1::InspectVectorStateResponse> for VectorStateInspectionResult {
1893    type Error = ApplicationClientError;
1894
1895    fn try_from(response: app_v1::InspectVectorStateResponse) -> Result<Self, Self::Error> {
1896        use app_v1::inspect_vector_state_response::Result;
1897        Ok(
1898            match response
1899                .result
1900                .ok_or(ApplicationClientError::InvalidResponse)?
1901            {
1902                Result::StalenessSummary(report) => {
1903                    Self::StalenessSummary(VectorStalenessSummary {
1904                        total_entities: report.total_entities,
1905                        stale_count: report.stale_count,
1906                        stale_entity_count_threshold: report.stale_entity_count_threshold,
1907                        slo_breached: report.slo_breached,
1908                    })
1909                }
1910                Result::StaleEntities(page) => Self::StaleEntities(VectorInspectionPage {
1911                    items: page
1912                        .items
1913                        .into_iter()
1914                        .map(|item| VectorStalenessItem {
1915                            entity_key: item.entity_key,
1916                            newest_source_write: item.newest_source_write,
1917                            embedding_write: item.embedding_write,
1918                        })
1919                        .collect(),
1920                    next_cursor: page.next_cursor,
1921                    observed_frontier: page.observed_frontier,
1922                }),
1923                Result::ModelVersionSummary(report) => {
1924                    Self::ModelVersionSummary(VectorModelVersionSummary {
1925                        current_count: report.current_count,
1926                        outdated_count: report.outdated_count,
1927                    })
1928                }
1929                Result::OutdatedModelEntities(page) => {
1930                    Self::OutdatedModelEntities(VectorInspectionPage {
1931                        items: page
1932                            .items
1933                            .into_iter()
1934                            .map(|item| VectorModelVersionItem {
1935                                entity_key: item.entity_key,
1936                                model: item.model,
1937                                model_version: item.model_version,
1938                                embedding_write: item.embedding_write,
1939                            })
1940                            .collect(),
1941                        next_cursor: page.next_cursor,
1942                        observed_frontier: page.observed_frontier,
1943                    })
1944                }
1945            },
1946        )
1947    }
1948}
1949
1950/// Active or exact symbolic contract selection.
1951#[derive(Clone, Debug, Eq, PartialEq)]
1952pub enum ApplicationContract {
1953    /// Resolve the current active contract.
1954    Active,
1955    /// Resolve one exact retained contract.
1956    Exact {
1957        /// Contract lineage.
1958        lineage: String,
1959        /// Positive contract version.
1960        version: u64,
1961        /// Optional exact bundle hash.
1962        bundle_hash: Option<[u8; 32]>,
1963    },
1964}
1965
1966/// One named query invocation pinned optionally to an immutable module hash.
1967#[derive(Clone, Debug, Eq, PartialEq)]
1968pub struct NamedQuery {
1969    contract: ApplicationContract,
1970    name: String,
1971    module_hash: Option<[u8; 32]>,
1972    expected_plan_hash: Option<[u8; 32]>,
1973    parameters: BTreeMap<String, ApplicationValue>,
1974    cursor: Option<String>,
1975    minimum_application_head: Option<u64>,
1976    admission_head_consistency: bool,
1977    accept_packed_result_v1: bool,
1978}
1979
1980impl NamedQuery {
1981    /// Builds a name-addressed query call.
1982    pub fn new(
1983        contract: ApplicationContract,
1984        name: impl Into<String>,
1985        module_hash: Option<[u8; 32]>,
1986        parameters: BTreeMap<String, ApplicationValue>,
1987        cursor: Option<String>,
1988    ) -> Result<Self, ApplicationClientError> {
1989        let name = name.into();
1990        if name.is_empty() || name.len() > 256 || parameters.len() > 4_096 {
1991            return Err(ApplicationClientError::InvalidInput);
1992        }
1993        validate_contract(&contract)?;
1994        if parameters
1995            .iter()
1996            .any(|(name, _)| name.is_empty() || name.len() > 256)
1997        {
1998            return Err(ApplicationClientError::InvalidInput);
1999        }
2000        Ok(Self {
2001            contract,
2002            name,
2003            module_hash,
2004            expected_plan_hash: None,
2005            parameters,
2006            cursor,
2007            minimum_application_head: None,
2008            admission_head_consistency: false,
2009            accept_packed_result_v1: false,
2010        })
2011    }
2012
2013    /// Requires the server to return this exact compiler-owned query plan.
2014    #[must_use]
2015    pub const fn expect_plan_hash(mut self, plan_hash: [u8; 32]) -> Self {
2016        self.expected_plan_hash = Some(plan_hash);
2017        self
2018    }
2019
2020    /// Advertises the packed result arm for a compiler-covered generated query.
2021    #[doc(hidden)]
2022    #[must_use]
2023    pub const fn accept_packed_result_v1(mut self) -> Self {
2024        self.accept_packed_result_v1 = true;
2025        self
2026    }
2027
2028    /// Applies generated pagination and read-after-commit options.
2029    pub fn with_options(mut self, options: QueryOptions) -> Result<Self, ApplicationClientError> {
2030        if options.read_after_commit == Some(0) {
2031            return Err(ApplicationClientError::InvalidInput);
2032        }
2033        self.cursor = options.cursor;
2034        self.minimum_application_head = options.read_after_commit;
2035        self.admission_head_consistency = options.admission_head;
2036        Ok(self)
2037    }
2038}
2039
2040/// Typed execution options shared by every generated named query.
2041#[derive(Clone, Debug, Default, Eq, PartialEq)]
2042pub struct QueryOptions {
2043    cursor: Option<String>,
2044    read_after_commit: Option<u64>,
2045    admission_head: bool,
2046}
2047
2048impl QueryOptions {
2049    /// Creates default query options.
2050    #[must_use]
2051    pub const fn new() -> Self {
2052        Self {
2053            cursor: None,
2054            read_after_commit: None,
2055            admission_head: false,
2056        }
2057    }
2058
2059    /// Continues from one opaque application cursor.
2060    #[must_use]
2061    pub fn after(mut self, cursor: impl Into<String>) -> Self {
2062        self.cursor = Some(cursor.into());
2063        self
2064    }
2065
2066    /// Routes one compiler-typed generated cursor without overriding a low-level option.
2067    #[doc(hidden)]
2068    pub fn with_generated_cursor(
2069        mut self,
2070        cursor: Option<String>,
2071    ) -> Result<Self, ApplicationClientError> {
2072        if self.cursor.is_some() && cursor.is_some() {
2073            return Err(ApplicationClientError::InvalidInput);
2074        }
2075        if cursor.is_some() {
2076            self.cursor = cursor;
2077        }
2078        Ok(self)
2079    }
2080
2081    /// Requires a snapshot at or after one observed application commit.
2082    #[must_use]
2083    pub const fn read_after_commit(mut self, commit_sequence: u64) -> Self {
2084        self.read_after_commit = Some(commit_sequence);
2085        self
2086    }
2087
2088    /// Requires a snapshot or provider epoch at least as recent as the
2089    /// authoritative application head observed after server admission.
2090    #[must_use]
2091    pub const fn at_least_admission_head(mut self) -> Self {
2092        self.admission_head = true;
2093        self
2094    }
2095}
2096
2097/// Typed generated result with its snapshot and pagination metadata.
2098#[derive(Clone, Debug, Eq, PartialEq)]
2099pub struct TypedQueryResult<T> {
2100    /// Generated declared-result value.
2101    pub value: T,
2102    /// Exact returned identity, verified against the generated request.
2103    pub identity: QueryResponseIdentity,
2104    /// Authoritative application head observed by the one-snapshot read.
2105    pub application_head: u64,
2106    /// Opaque continuation cursor, when another bounded page exists.
2107    pub next_cursor: Option<String>,
2108}
2109
2110/// Typed generated command result without kernel protocol exposure.
2111#[derive(Clone, Debug, Eq, PartialEq)]
2112pub struct TypedCommandResult<T> {
2113    /// Generated declared business outcome.
2114    pub outcome: T,
2115    /// Durable application commit sequence.
2116    pub commit_sequence: Option<u64>,
2117    /// Exact contract version used by execution.
2118    pub contract_version: u64,
2119    /// Exact compiler-owned command plan used by execution.
2120    pub plan_hash: [u8; 32],
2121    /// Whether the invocation replayed an already durable result.
2122    pub replayed: bool,
2123    /// Durable opaque outcome locator, when available.
2124    pub outcome_uri: Option<String>,
2125    /// Compiler-derived successor revisions for successful checked workflow
2126    /// mutations, keyed by source binding rather than numeric entity IDs.
2127    pub workflow_revisions: Vec<crate::generated::WorkflowSuccessorRevision>,
2128}
2129
2130/// One symbolic command invocation with only name-addressed input.
2131#[derive(Clone, Debug, Eq, PartialEq)]
2132pub struct ApplicationCommand {
2133    name: String,
2134    expected_contract_version: Option<u64>,
2135    input: BTreeMap<String, ApplicationValue>,
2136}
2137
2138impl ApplicationCommand {
2139    /// Builds a command invocation.
2140    pub fn new(
2141        name: impl Into<String>,
2142        expected_contract_version: Option<u64>,
2143        input: BTreeMap<String, ApplicationValue>,
2144    ) -> Result<Self, ApplicationClientError> {
2145        let name = name.into();
2146        if name.is_empty()
2147            || name.len() > 256
2148            || expected_contract_version == Some(0)
2149            || input.is_empty()
2150            || input.len() > 4_096
2151            || input
2152                .keys()
2153                .any(|field| field.is_empty() || field.len() > 256)
2154        {
2155            return Err(ApplicationClientError::InvalidInput);
2156        }
2157        Ok(Self {
2158            name,
2159            expected_contract_version,
2160            input,
2161        })
2162    }
2163
2164    pub(crate) fn into_idempotent_command(
2165        self,
2166    ) -> Result<IdempotentCommand, ApplicationClientError> {
2167        let input = lower_value(ApplicationValue::Record(self.input))?;
2168        IdempotentCommand::new(self.name, self.expected_contract_version, input)
2169            .map_err(|_| ApplicationClientError::InvalidInput)
2170    }
2171}
2172
2173/// Query result cardinality declared in RiffQL.
2174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2175pub enum ApplicationCardinality {
2176    /// Exactly one record.
2177    One,
2178    /// Zero or one record.
2179    Maybe,
2180    /// Bounded records.
2181    Many,
2182}
2183
2184/// One name-addressed returned record.
2185#[derive(Clone, Debug, Eq, PartialEq)]
2186pub struct ApplicationRecord {
2187    /// Optional entity symbol.
2188    pub entity: String,
2189    /// Fields by contract symbol.
2190    pub fields: BTreeMap<String, ApplicationValue>,
2191}
2192
2193/// One top-level query result field.
2194#[derive(Clone, Debug, Eq, PartialEq)]
2195pub struct ApplicationResultField {
2196    /// Declared cardinality.
2197    pub cardinality: ApplicationCardinality,
2198    /// Returned records.
2199    pub records: Vec<ApplicationRecord>,
2200}
2201
2202/// Complete one-snapshot named query result.
2203#[derive(Clone, Debug, Eq, PartialEq)]
2204pub struct NamedQueryResult {
2205    /// Exact query outcome name.
2206    pub outcome: String,
2207    /// Authoritative snapshot head.
2208    pub application_head: u64,
2209    /// Result fields by declared name.
2210    pub fields: BTreeMap<String, ApplicationResultField>,
2211    /// Opaque continuation cursor.
2212    pub next_cursor: Option<String>,
2213    /// Exact symbolic identity used by named execution.
2214    pub identity: QueryResponseIdentity,
2215}
2216
2217/// Exact server-returned identity for one named query execution.
2218#[derive(Clone, Debug, Eq, PartialEq)]
2219pub struct QueryResponseIdentity {
2220    /// Exact contract lineage.
2221    pub contract_lineage: String,
2222    /// Exact contract version.
2223    pub contract_version: u64,
2224    /// Exact contract bundle hash.
2225    pub contract_bundle_hash: [u8; 32],
2226    /// Exact deployed query module hash.
2227    pub module_hash: [u8; 32],
2228    /// Exact query operation name.
2229    pub query_name: String,
2230    /// Exact compiler-owned query plan hash.
2231    pub plan_hash: [u8; 32],
2232}
2233
2234/// Successful command completion without kernel protocol details.
2235#[derive(Clone, Debug, Eq, PartialEq)]
2236pub struct ApplicationCommandResult {
2237    /// Declared business outcome name when present.
2238    pub outcome: Option<String>,
2239    /// Complete name-addressed declared outcome payload.
2240    pub outcome_value: Option<ApplicationValue>,
2241    /// Application commit sequence; absent for unjournaled read-only commands.
2242    pub commit_sequence: Option<u64>,
2243    /// Exact contract version used by the command.
2244    pub contract_version: u64,
2245    /// Exact command plan hash used by the command.
2246    pub plan_hash: [u8; 32],
2247    /// Whether this invocation replayed an already durable result.
2248    pub replayed: bool,
2249    /// Durable outcome locator when present.
2250    pub outcome_uri: Option<String>,
2251}
2252
2253pub(crate) fn raise_application_command_result(
2254    response: v1::ExecuteCommandResponse,
2255) -> Result<ApplicationCommandResult, ApplicationClientError> {
2256    Ok(ApplicationCommandResult {
2257        outcome: (!response.outcome_type.is_empty()).then_some(response.outcome_type),
2258        outcome_value: response.outcome.map(raise_value).transpose()?,
2259        commit_sequence: (response.commit_sequence != 0).then_some(response.commit_sequence),
2260        contract_version: response.contract_version,
2261        plan_hash: response
2262            .plan_hash
2263            .try_into()
2264            .map_err(|_| ApplicationClientError::InvalidResponse)?,
2265        replayed: response.status
2266            == v1::execute_command_response::CompletionStatus::Replayed as i32,
2267        outcome_uri: response.outcome_uri,
2268    })
2269}
2270
2271/// Closed application-client failure.
2272#[derive(Debug)]
2273pub enum ApplicationClientError {
2274    /// Submitted name-addressed shape is invalid.
2275    InvalidInput,
2276    /// Server returned an invalid or low-level-shaped application response.
2277    InvalidResponse,
2278    /// Request identity generation failed.
2279    IdentifierUnavailable,
2280    /// Checked public transport failure.
2281    Client(ClientError),
2282}
2283
2284impl fmt::Display for ApplicationClientError {
2285    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2286        match self {
2287            Self::InvalidInput => formatter.write_str("application input is invalid"),
2288            Self::InvalidResponse => formatter.write_str("application response is invalid"),
2289            Self::IdentifierUnavailable => formatter.write_str("request identity is unavailable"),
2290            Self::Client(error) => error.fmt(formatter),
2291        }
2292    }
2293}
2294
2295impl std::error::Error for ApplicationClientError {
2296    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2297        match self {
2298            Self::Client(error) => Some(error),
2299            Self::InvalidInput | Self::InvalidResponse | Self::IdentifierUnavailable => None,
2300        }
2301    }
2302}
2303
2304impl ApplicationClientError {
2305    /// Returns the checked semantic application failure, when supplied by RiffDB.
2306    #[must_use]
2307    pub const fn semantic_error(&self) -> Option<&riffdb_errors::ApplicationError> {
2308        match self {
2309            Self::Client(error) => error.application_error(),
2310            Self::InvalidInput | Self::InvalidResponse | Self::IdentifierUnavailable => None,
2311        }
2312    }
2313}
2314
2315impl From<ClientError> for ApplicationClientError {
2316    fn from(error: ClientError) -> Self {
2317        Self::Client(error)
2318    }
2319}
2320
2321impl From<GeneratedExecutionError> for ApplicationClientError {
2322    fn from(error: GeneratedExecutionError) -> Self {
2323        match error {
2324            GeneratedExecutionError::Client(error) => Self::Client(error),
2325            // An input the caller built wrongly and an outcome the server sent
2326            // wrongly are different faults with different fixes. Reporting the
2327            // first as `InvalidResponse` sends callers to audit the decoder for
2328            // a mistake that is in their own request.
2329            GeneratedExecutionError::CommandShape(
2330                crate::generated::GeneratedCommandError::InvalidInputShape
2331                | crate::generated::GeneratedCommandError::InputBudget(_),
2332            ) => Self::InvalidInput,
2333            GeneratedExecutionError::CommandShape(
2334                crate::generated::GeneratedCommandError::InvalidOutcomeShape,
2335            ) => Self::InvalidResponse,
2336        }
2337    }
2338}
2339
2340impl RiffDbClient {
2341    /// Executes one named module query through one public application RPC.
2342    pub async fn execute_named_application_query(
2343        &mut self,
2344        query: NamedQuery,
2345        metadata: &CallMetadata,
2346    ) -> Result<NamedQueryResult, ApplicationClientError> {
2347        let response =
2348            with_client_wire_carrier(self.execute_named_application_query_wire(query, metadata))
2349                .await?;
2350        raise_query_result_from_client_wire(response)
2351    }
2352
2353    async fn execute_named_application_query_wire(
2354        &mut self,
2355        query: NamedQuery,
2356        metadata: &CallMetadata,
2357    ) -> Result<app_v1::ExecuteQueryResponse, ApplicationClientError> {
2358        let expected_contract = query.contract.clone();
2359        let expected_name = query.name.clone();
2360        let expected_module_hash = query.module_hash;
2361        let expected_plan_hash = query.expected_plan_hash;
2362        let request_id = Vec::from(
2363            generate_request_id()
2364                .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
2365                .into_bytes(),
2366        );
2367        // Move owned parameter names once; avoid intermediate (name, value) copies.
2368        let mut parameters = Vec::with_capacity(query.parameters.len());
2369        for (name, value) in query.parameters {
2370            parameters.push(app_v1::Parameter {
2371                name,
2372                value: Some(lower_value(value)?),
2373            });
2374        }
2375        let response = self
2376            .execute_selected_query_transport(
2377                app_v1::ExecuteQueryRequest {
2378                    contract: lower_contract(query.contract),
2379                    query: Some(app_v1::execute_query_request::Query::QueryName(query.name)),
2380                    module_hash: query.module_hash.map(|hash| hash.to_vec()),
2381                    parameters,
2382                    cursor: query.cursor,
2383                    minimum_application_head: query.minimum_application_head,
2384                    accepted_result_encodings: if query.accept_packed_result_v1 {
2385                        vec![
2386                            app_v1::NamedResultEncoding::LegacyRecords as i32,
2387                            app_v1::NamedResultEncoding::CompactV1 as i32,
2388                            app_v1::NamedResultEncoding::PackedV1 as i32,
2389                        ]
2390                    } else {
2391                        vec![
2392                            app_v1::NamedResultEncoding::LegacyRecords as i32,
2393                            app_v1::NamedResultEncoding::CompactV1 as i32,
2394                        ]
2395                    },
2396                    consistency: if query.admission_head_consistency {
2397                        app_v1::QueryConsistency::AdmissionHead as i32
2398                    } else {
2399                        app_v1::QueryConsistency::Unspecified as i32
2400                    },
2401                    request_id,
2402                },
2403                metadata,
2404            )
2405            .await?;
2406        validate_query_response_identity(
2407            &response,
2408            &expected_contract,
2409            &expected_name,
2410            expected_module_hash,
2411            expected_plan_hash,
2412        )?;
2413        Ok(response)
2414    }
2415
2416    /// Executes one symbolic command with bounded retry and no kernel-shaped caller input.
2417    pub async fn execute_application_command(
2418        &mut self,
2419        command: ApplicationCommand,
2420        attempts: AttemptBudget,
2421        metadata: &CallMetadata,
2422    ) -> Result<ApplicationCommandResult, ApplicationClientError> {
2423        let command_name = command.name.clone();
2424        let command = command.into_idempotent_command()?;
2425        let response = self
2426            .execute_with_retry(&command, attempts, metadata)
2427            .await
2428            .map_err(|error| contextualize_command_client_error(error, &command_name))?;
2429        raise_application_command_result(response)
2430    }
2431}
2432
2433pub(crate) fn contextualize_command_client_error(
2434    error: ClientError,
2435    command_name: &str,
2436) -> ClientError {
2437    let ClientError::Public(public) = error else {
2438        return error;
2439    };
2440    let context = ApplicationErrorContext::empty()
2441        .with_operation_symbol(command_name.to_owned())
2442        .unwrap_or_else(|_| ApplicationErrorContext::empty());
2443    ClientError::Application(Box::new(ApplicationError::from_public_error(
2444        &public,
2445        ApplicationOperation::ExecuteCommand,
2446        context,
2447    )))
2448}
2449
2450fn validate_pinned_query_response(
2451    response: &app_v1::ExecuteQueryResponse,
2452    pinned: &PinnedContractIdentity,
2453) -> Result<(), ApplicationClientError> {
2454    let identity = response
2455        .identity
2456        .as_ref()
2457        .ok_or(ApplicationClientError::InvalidResponse)?;
2458    if identity.contract_lineage != pinned.lineage
2459        || identity.contract_version != pinned.version
2460        || identity.contract_bundle_hash.as_slice() != pinned.bundle_hash
2461    {
2462        return Err(ApplicationClientError::InvalidResponse);
2463    }
2464    Ok(())
2465}
2466
2467const fn client_error_triggers_standby_failover(error: &ClientError) -> bool {
2468    match error {
2469        ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
2470        | ClientError::ConnectionFailure
2471        | ClientError::OutcomeUnknown(_) => true,
2472        ClientError::Public(public) => matches!(
2473            public.kind(),
2474            PublicErrorKind::PrimaryFenced
2475                | PublicErrorKind::FollowerMode
2476                | PublicErrorKind::StorageUnavailable
2477        ),
2478        ClientError::Application(application) => matches!(
2479            application.code(),
2480            ApplicationErrorCode::PrimaryFenced
2481                | ApplicationErrorCode::FollowerMode
2482                | ApplicationErrorCode::StorageUnavailable
2483                | ApplicationErrorCode::OutcomeUnknown
2484        ),
2485        ClientError::Protocol(_)
2486        | ClientError::IdentifierGeneration(_)
2487        | ClientError::Tls(_)
2488        | ClientError::DetailsFree(_) => false,
2489    }
2490}
2491
2492const fn application_query_error_allows_replica_fallback(error: &ApplicationClientError) -> bool {
2493    match error {
2494        ApplicationClientError::Client(client) => match client {
2495            ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
2496            | ClientError::ConnectionFailure
2497            | ClientError::OutcomeUnknown(_) => true,
2498            ClientError::Public(public) => matches!(
2499                public.kind(),
2500                PublicErrorKind::FollowerMode
2501                    | PublicErrorKind::PrimaryFenced
2502                    | PublicErrorKind::StorageUnavailable
2503                    | PublicErrorKind::Overloaded
2504            ),
2505            ClientError::Application(application) => matches!(
2506                application.code(),
2507                ApplicationErrorCode::FreshnessUnsatisfied
2508                    | ApplicationErrorCode::SnapshotRetired
2509                    | ApplicationErrorCode::ProjectionDiverged
2510                    | ApplicationErrorCode::FollowerMode
2511                    | ApplicationErrorCode::PrimaryFenced
2512                    | ApplicationErrorCode::StorageUnavailable
2513                    | ApplicationErrorCode::Overloaded
2514            ),
2515            ClientError::Protocol(_)
2516            | ClientError::IdentifierGeneration(_)
2517            | ClientError::Tls(_)
2518            | ClientError::DetailsFree(_) => false,
2519        },
2520        ApplicationClientError::InvalidInput
2521        | ApplicationClientError::InvalidResponse
2522        | ApplicationClientError::IdentifierUnavailable => false,
2523    }
2524}
2525
2526fn validate_query_response_identity(
2527    response: &app_v1::ExecuteQueryResponse,
2528    contract: &ApplicationContract,
2529    query_name: &str,
2530    module_hash: Option<[u8; 32]>,
2531    plan_hash: Option<[u8; 32]>,
2532) -> Result<(), ApplicationClientError> {
2533    let identity = response
2534        .identity
2535        .as_ref()
2536        .ok_or(ApplicationClientError::InvalidResponse)?;
2537    if identity.query_name.as_deref() != Some(query_name)
2538        || module_hash
2539            .is_some_and(|expected| identity.module_hash.as_deref() != Some(expected.as_slice()))
2540        || plan_hash.is_some_and(|expected| identity.plan_hash.as_slice() != expected.as_slice())
2541    {
2542        return Err(ApplicationClientError::InvalidResponse);
2543    }
2544    if let ApplicationContract::Exact {
2545        lineage,
2546        version,
2547        bundle_hash,
2548    } = contract
2549        && (identity.contract_lineage != *lineage
2550            || identity.contract_version != *version
2551            || bundle_hash.is_some_and(|expected| {
2552                identity.contract_bundle_hash.as_slice() != expected.as_slice()
2553            }))
2554    {
2555        return Err(ApplicationClientError::InvalidResponse);
2556    }
2557    Ok(())
2558}
2559
2560fn raise_application_catalog_preflight(
2561    response: app_v1::GetApplicationCatalogResponse,
2562    contract: &ApplicationContract,
2563) -> Result<ApplicationCatalogPreflight, ApplicationClientError> {
2564    if response.schema != APPLICATION_CATALOG_SCHEMA_V1
2565        || response.contract_lineage.is_empty()
2566        || response.contract_version == 0
2567    {
2568        return Err(ApplicationClientError::InvalidResponse);
2569    }
2570    let contract_bundle_hash: [u8; 32] = response
2571        .contract_bundle_hash
2572        .try_into()
2573        .map_err(|_| ApplicationClientError::InvalidResponse)?;
2574    if let ApplicationContract::Exact {
2575        lineage,
2576        version,
2577        bundle_hash,
2578    } = contract
2579        && (response.contract_lineage != *lineage
2580            || response.contract_version != *version
2581            || bundle_hash.is_some_and(|expected| expected != contract_bundle_hash))
2582    {
2583        return Err(ApplicationClientError::InvalidResponse);
2584    }
2585
2586    let mut query_module_hashes = Vec::with_capacity(response.query_module_hashes.len());
2587    for hash in response.query_module_hashes {
2588        query_module_hashes.push(
2589            hash.try_into()
2590                .map_err(|_| ApplicationClientError::InvalidResponse)?,
2591        );
2592    }
2593    if query_module_hashes
2594        .windows(2)
2595        .any(|pair| pair[0] >= pair[1])
2596    {
2597        return Err(ApplicationClientError::InvalidResponse);
2598    }
2599
2600    let mut features = Vec::with_capacity(response.features.len());
2601    for view in response.features {
2602        let feature = match app_v1::ApplicationCatalogFeature::try_from(view.feature) {
2603            Ok(app_v1::ApplicationCatalogFeature::OperationalOptionalPredicates) => {
2604                ApplicationCatalogFeature::OperationalOptionalPredicates
2605            }
2606            Ok(app_v1::ApplicationCatalogFeature::StableCursorPages) => {
2607                ApplicationCatalogFeature::StableCursorPages
2608            }
2609            Ok(app_v1::ApplicationCatalogFeature::NullExistencePredicates) => {
2610                ApplicationCatalogFeature::NullExistencePredicates
2611            }
2612            Ok(app_v1::ApplicationCatalogFeature::BinaryTextPrefix) => {
2613                ApplicationCatalogFeature::BinaryTextPrefix
2614            }
2615            Ok(app_v1::ApplicationCatalogFeature::UnicodeFoldTextPrefixV1) => {
2616                ApplicationCatalogFeature::UnicodeFoldTextPrefixV1
2617            }
2618            Ok(app_v1::ApplicationCatalogFeature::ExactAggregates) => {
2619                ApplicationCatalogFeature::ExactAggregates
2620            }
2621            Ok(app_v1::ApplicationCatalogFeature::Unspecified) | Err(_) => {
2622                return Err(ApplicationClientError::InvalidResponse);
2623            }
2624        };
2625        let state = match app_v1::ApplicationCatalogFeatureState::try_from(view.state) {
2626            Ok(app_v1::ApplicationCatalogFeatureState::Available) => {
2627                ApplicationCatalogFeatureState::Available
2628            }
2629            Ok(app_v1::ApplicationCatalogFeatureState::Unavailable) => {
2630                ApplicationCatalogFeatureState::Unavailable
2631            }
2632            Ok(app_v1::ApplicationCatalogFeatureState::Unspecified) | Err(_) => {
2633                return Err(ApplicationClientError::InvalidResponse);
2634            }
2635        };
2636        features.push(ApplicationCatalogFeatureView { feature, state });
2637    }
2638    if features.len() != 6
2639        || features
2640            .windows(2)
2641            .any(|pair| pair[0].feature >= pair[1].feature)
2642    {
2643        return Err(ApplicationClientError::InvalidResponse);
2644    }
2645
2646    Ok(ApplicationCatalogPreflight {
2647        contract_lineage: response.contract_lineage,
2648        contract_version: response.contract_version,
2649        contract_bundle_hash,
2650        query_module_hashes,
2651        features,
2652    })
2653}
2654
2655pub(crate) fn validate_contract(
2656    contract: &ApplicationContract,
2657) -> Result<(), ApplicationClientError> {
2658    if let ApplicationContract::Exact {
2659        lineage, version, ..
2660    } = contract
2661        && (lineage.is_empty() || lineage.len() > 256 || *version == 0)
2662    {
2663        return Err(ApplicationClientError::InvalidInput);
2664    }
2665    Ok(())
2666}
2667
2668fn lower_contract(contract: ApplicationContract) -> Option<app_v1::ContractSelector> {
2669    match contract {
2670        ApplicationContract::Active => None,
2671        ApplicationContract::Exact {
2672            lineage,
2673            version,
2674            bundle_hash,
2675        } => Some(app_v1::ContractSelector {
2676            lineage,
2677            version,
2678            bundle_hash: bundle_hash.map_or_else(Vec::new, |hash| hash.to_vec()),
2679        }),
2680    }
2681}
2682
2683pub(crate) fn lower_value(value: ApplicationValue) -> Result<v1::Value, ApplicationClientError> {
2684    use v1::value::Kind;
2685    let kind = match value {
2686        ApplicationValue::Null => Kind::NullValue(v1::NullValue::NullValue as i32),
2687        ApplicationValue::Bool(value) => Kind::BoolValue(value),
2688        ApplicationValue::I64(value) => Kind::I64Value(value),
2689        ApplicationValue::U64(value) => Kind::U64Value(value),
2690        ApplicationValue::Decimal {
2691            coefficient_twos_complement,
2692            scale,
2693            precision,
2694        } => Kind::DecimalValue(v1::Decimal {
2695            coefficient_twos_complement,
2696            scale,
2697            precision,
2698        }),
2699        ApplicationValue::Money { currency, amount } => {
2700            let Kind::DecimalValue(amount) = lower_value(*amount)?
2701                .kind
2702                .ok_or(ApplicationClientError::InvalidInput)?
2703            else {
2704                return Err(ApplicationClientError::InvalidInput);
2705            };
2706            Kind::MoneyValue(v1::Money {
2707                currency,
2708                amount: Some(amount),
2709            })
2710        }
2711        ApplicationValue::String(value) => Kind::StringValue(value),
2712        ApplicationValue::Uuid(value) => Kind::UuidValue(value.as_bytes().to_vec()),
2713        ApplicationValue::Enum(name) if !name.is_empty() && name.len() <= 256 => {
2714            Kind::EnumValue(v1::EnumValue {
2715                type_id: 0,
2716                variant_id: 0,
2717                name,
2718            })
2719        }
2720        ApplicationValue::Enum(_) => return Err(ApplicationClientError::InvalidInput),
2721        ApplicationValue::EnumIdentity {
2722            type_id,
2723            variant_id,
2724            name,
2725        } if type_id != 0 && variant_id != 0 && !name.is_empty() && name.len() <= 256 => {
2726            Kind::EnumValue(v1::EnumValue {
2727                type_id,
2728                variant_id,
2729                name,
2730            })
2731        }
2732        ApplicationValue::EnumIdentity { .. } => {
2733            return Err(ApplicationClientError::InvalidInput);
2734        }
2735        ApplicationValue::Bytes(value) => Kind::BytesValue(value),
2736        ApplicationValue::Date(days_since_unix_epoch) => Kind::DateValue(v1::Date {
2737            days_since_unix_epoch,
2738        }),
2739        ApplicationValue::Timestamp { seconds, nanos } if nanos < 1_000_000_000 => {
2740            Kind::TimestampValue(v1::Timestamp { seconds, nanos })
2741        }
2742        ApplicationValue::Timestamp { .. } => {
2743            return Err(ApplicationClientError::InvalidInput);
2744        }
2745        ApplicationValue::Vector(value) => Kind::VectorValue(v1::VectorValue {
2746            components: value.into_components(),
2747        }),
2748        ApplicationValue::List(values) => Kind::ListValue(v1::ValueList {
2749            values: values
2750                .into_iter()
2751                .map(lower_value)
2752                .collect::<Result<Vec<_>, _>>()?,
2753        }),
2754        ApplicationValue::Record(fields) => Kind::RecordValue(v1::ValueRecord {
2755            fields: fields
2756                .into_iter()
2757                .map(|(name, value)| {
2758                    Ok(v1::ValueField {
2759                        field_id: None,
2760                        name,
2761                        value: Some(lower_value(value)?),
2762                    })
2763                })
2764                .collect::<Result<Vec<_>, ApplicationClientError>>()?,
2765        }),
2766    };
2767    Ok(v1::Value { kind: Some(kind) })
2768}
2769
2770const LOCAL_RAISED_QUERY_CACHE_CAP: usize = 256;
2771const LOCAL_RAISED_QUERY_HASH_SLOTS: usize = 512;
2772const SHARED_RAISED_QUERY_SHARDS: usize = 16;
2773const SHARED_RAISED_QUERY_SHARD_CAP: usize = 64;
2774
2775#[derive(Clone)]
2776struct CachedRaisedQueryResult {
2777    wire_cache_id: u32,
2778    result: Arc<NamedQueryResult>,
2779}
2780
2781struct LocalRaisedQueryCache {
2782    entries: Vec<CachedRaisedQueryResult>,
2783    next_slot: usize,
2784    by_wire_id: [u16; LOCAL_RAISED_QUERY_HASH_SLOTS],
2785}
2786
2787impl Default for LocalRaisedQueryCache {
2788    fn default() -> Self {
2789        Self {
2790            entries: Vec::new(),
2791            next_slot: 0,
2792            by_wire_id: [0; LOCAL_RAISED_QUERY_HASH_SLOTS],
2793        }
2794    }
2795}
2796
2797impl LocalRaisedQueryCache {
2798    fn bucket(wire_cache_id: u32) -> usize {
2799        (wire_cache_id as usize) & (LOCAL_RAISED_QUERY_HASH_SLOTS - 1)
2800    }
2801
2802    fn lookup(&self, wire_cache_id: u32) -> Option<&Arc<NamedQueryResult>> {
2803        let bucket = Self::bucket(wire_cache_id);
2804        let idx1 = self.by_wire_id[bucket] as usize;
2805        if idx1 != 0
2806            && let Some(entry) = self.entries.get(idx1 - 1)
2807            && entry.wire_cache_id == wire_cache_id
2808        {
2809            return Some(&entry.result);
2810        }
2811        self.entries
2812            .iter()
2813            .find(|entry| entry.wire_cache_id == wire_cache_id)
2814            .map(|entry| &entry.result)
2815    }
2816
2817    fn insert(&mut self, wire_cache_id: u32, result: Arc<NamedQueryResult>) {
2818        let bucket = Self::bucket(wire_cache_id);
2819        let entry = CachedRaisedQueryResult {
2820            wire_cache_id,
2821            result,
2822        };
2823        let slot = if self.entries.len() < LOCAL_RAISED_QUERY_CACHE_CAP {
2824            let slot = self.entries.len();
2825            self.entries.push(entry);
2826            slot
2827        } else {
2828            let slot = self.next_slot;
2829            self.entries[slot] = entry;
2830            self.next_slot = (slot + 1) % LOCAL_RAISED_QUERY_CACHE_CAP;
2831            slot
2832        };
2833        self.by_wire_id[bucket] = (slot + 1) as u16;
2834    }
2835}
2836
2837#[derive(Default)]
2838struct SharedRaisedQueryShard {
2839    entries: Vec<CachedRaisedQueryResult>,
2840    next_slot: usize,
2841}
2842
2843static SHARED_RAISED_QUERY_CACHE: OnceLock<
2844    [std::sync::RwLock<SharedRaisedQueryShard>; SHARED_RAISED_QUERY_SHARDS],
2845> = OnceLock::new();
2846
2847fn shared_raised_query_cache()
2848-> &'static [std::sync::RwLock<SharedRaisedQueryShard>; SHARED_RAISED_QUERY_SHARDS] {
2849    SHARED_RAISED_QUERY_CACHE.get_or_init(|| {
2850        std::array::from_fn(|_| std::sync::RwLock::new(SharedRaisedQueryShard::default()))
2851    })
2852}
2853
2854std::thread_local! {
2855    static LOCAL_RAISED_QUERY_CACHE: std::cell::RefCell<LocalRaisedQueryCache> =
2856        std::cell::RefCell::new(LocalRaisedQueryCache::default());
2857}
2858
2859fn lookup_shared_raised_query_result(wire_cache_id: u32) -> Option<Arc<NamedQueryResult>> {
2860    let shard_idx = (wire_cache_id as usize) & (SHARED_RAISED_QUERY_SHARDS - 1);
2861    let guard = shared_raised_query_cache()[shard_idx].read().ok()?;
2862    guard
2863        .entries
2864        .iter()
2865        .find(|entry| entry.wire_cache_id == wire_cache_id)
2866        .map(|entry| Arc::clone(&entry.result))
2867}
2868
2869fn insert_shared_raised_query_result(wire_cache_id: u32, result: Arc<NamedQueryResult>) {
2870    let shard_idx = (wire_cache_id as usize) & (SHARED_RAISED_QUERY_SHARDS - 1);
2871    let Ok(mut guard) = shared_raised_query_cache()[shard_idx].write() else {
2872        return;
2873    };
2874    if guard
2875        .entries
2876        .iter()
2877        .any(|entry| entry.wire_cache_id == wire_cache_id)
2878    {
2879        return;
2880    }
2881    let entry = CachedRaisedQueryResult {
2882        wire_cache_id,
2883        result,
2884    };
2885    if guard.entries.len() < SHARED_RAISED_QUERY_SHARD_CAP {
2886        guard.entries.push(entry);
2887    } else {
2888        let slot = guard.next_slot;
2889        guard.entries[slot] = entry;
2890        guard.next_slot = (slot + 1) % SHARED_RAISED_QUERY_SHARD_CAP;
2891    }
2892}
2893
2894async fn with_client_wire_carrier<F: std::future::Future>(future: F) -> F::Output {
2895    let mut pinned = std::pin::pin!(future);
2896    std::future::poll_fn(|cx| {
2897        let _guard = riffdb_proto::enter_client_query_wire_carrier_scope();
2898        pinned.as_mut().poll(cx)
2899    })
2900    .await
2901}
2902
2903fn raise_query_result_from_client_wire(
2904    response: app_v1::ExecuteQueryResponse,
2905) -> Result<NamedQueryResult, ApplicationClientError> {
2906    raise_query_result_shared_from_client_wire(response).map(Arc::unwrap_or_clone)
2907}
2908
2909fn raise_query_result_shared_from_client_wire(
2910    response: app_v1::ExecuteQueryResponse,
2911) -> Result<Arc<NamedQueryResult>, ApplicationClientError> {
2912    if response.selected_result_encoding < 0
2913        && response.fields.is_empty()
2914        && response.compact_result.is_none()
2915        && response.packed_result.is_none()
2916    {
2917        let wire_cache_id = response.selected_result_encoding.unsigned_abs();
2918        if wire_cache_id >= riffdb_proto::CLIENT_WIRE_CARRIER_ID_BASE {
2919            if let Some(cached) = LOCAL_RAISED_QUERY_CACHE.with(|cell| {
2920                let guard = cell.try_borrow().ok()?;
2921                guard.lookup(wire_cache_id).cloned()
2922            }) {
2923                return Ok(cached);
2924            }
2925            if let Some(shared) = lookup_shared_raised_query_result(wire_cache_id) {
2926                LOCAL_RAISED_QUERY_CACHE.with(|cell| {
2927                    if let Ok(mut guard) = cell.try_borrow_mut() {
2928                        guard.insert(wire_cache_id, Arc::clone(&shared));
2929                    }
2930                });
2931                return Ok(shared);
2932            }
2933            if let Some(full_response) =
2934                riffdb_proto::lookup_client_query_wire_response(wire_cache_id)
2935            {
2936                let raised = Arc::new(raise_query_result((*full_response).clone())?);
2937                insert_shared_raised_query_result(wire_cache_id, Arc::clone(&raised));
2938                LOCAL_RAISED_QUERY_CACHE.with(|cell| {
2939                    if let Ok(mut guard) = cell.try_borrow_mut() {
2940                        guard.insert(wire_cache_id, Arc::clone(&raised));
2941                    }
2942                });
2943                return Ok(raised);
2944            }
2945        }
2946    }
2947    raise_query_result(response).map(Arc::new)
2948}
2949
2950#[doc(hidden)]
2951pub fn raise_query_result(
2952    response: app_v1::ExecuteQueryResponse,
2953) -> Result<NamedQueryResult, ApplicationClientError> {
2954    let identity = query_response_identity(&response)?;
2955    let selected_encoding =
2956        app_v1::NamedResultEncoding::try_from(response.selected_result_encoding)
2957            .map_err(|_| ApplicationClientError::InvalidResponse)?;
2958    let fields = match selected_encoding {
2959        app_v1::NamedResultEncoding::Unspecified | app_v1::NamedResultEncoding::LegacyRecords => {
2960            if response.compact_result.is_some() || response.packed_result.is_some() {
2961                return Err(ApplicationClientError::InvalidResponse);
2962            }
2963            raise_legacy_query_fields(response.fields)?
2964        }
2965        app_v1::NamedResultEncoding::CompactV1 => {
2966            if !response.fields.is_empty() || response.packed_result.is_some() {
2967                return Err(ApplicationClientError::InvalidResponse);
2968            }
2969            raise_compact_query_field(
2970                response
2971                    .compact_result
2972                    .ok_or(ApplicationClientError::InvalidResponse)?,
2973            )?
2974        }
2975        app_v1::NamedResultEncoding::PackedV1 => {
2976            return Err(ApplicationClientError::InvalidResponse);
2977        }
2978    };
2979    Ok(NamedQueryResult {
2980        outcome: response.outcome,
2981        application_head: response.application_head,
2982        fields,
2983        next_cursor: response.next_cursor,
2984        identity,
2985    })
2986}
2987
2988fn query_response_identity(
2989    response: &app_v1::ExecuteQueryResponse,
2990) -> Result<QueryResponseIdentity, ApplicationClientError> {
2991    let identity = response
2992        .identity
2993        .as_ref()
2994        .ok_or(ApplicationClientError::InvalidResponse)?;
2995    Ok(QueryResponseIdentity {
2996        contract_lineage: identity.contract_lineage.clone(),
2997        contract_version: identity.contract_version,
2998        contract_bundle_hash: identity
2999            .contract_bundle_hash
3000            .as_slice()
3001            .try_into()
3002            .map_err(|_| ApplicationClientError::InvalidResponse)?,
3003        module_hash: identity
3004            .module_hash
3005            .as_deref()
3006            .ok_or(ApplicationClientError::InvalidResponse)?
3007            .try_into()
3008            .map_err(|_| ApplicationClientError::InvalidResponse)?,
3009        query_name: identity
3010            .query_name
3011            .clone()
3012            .ok_or(ApplicationClientError::InvalidResponse)?,
3013        plan_hash: identity
3014            .plan_hash
3015            .as_slice()
3016            .try_into()
3017            .map_err(|_| ApplicationClientError::InvalidResponse)?,
3018    })
3019}
3020
3021fn raise_legacy_query_fields(
3022    wire_fields: Vec<app_v1::ResultField>,
3023) -> Result<BTreeMap<String, ApplicationResultField>, ApplicationClientError> {
3024    let mut fields = BTreeMap::new();
3025    for field in wire_fields {
3026        let cardinality = match app_v1::ResultCardinality::try_from(field.cardinality) {
3027            Ok(app_v1::ResultCardinality::One) => ApplicationCardinality::One,
3028            Ok(app_v1::ResultCardinality::Maybe) => ApplicationCardinality::Maybe,
3029            Ok(app_v1::ResultCardinality::Many) => ApplicationCardinality::Many,
3030            Ok(app_v1::ResultCardinality::Unspecified) | Err(_) => {
3031                return Err(ApplicationClientError::InvalidResponse);
3032            }
3033        };
3034        let records = field
3035            .records
3036            .into_iter()
3037            .map(|record| {
3038                let mut fields = BTreeMap::new();
3039                for field in record.fields {
3040                    if fields
3041                        .insert(
3042                            field.name,
3043                            raise_value(
3044                                field.value.ok_or(ApplicationClientError::InvalidResponse)?,
3045                            )?,
3046                        )
3047                        .is_some()
3048                    {
3049                        return Err(ApplicationClientError::InvalidResponse);
3050                    }
3051                }
3052                Ok(ApplicationRecord {
3053                    entity: record.entity,
3054                    fields,
3055                })
3056            })
3057            .collect::<Result<Vec<_>, ApplicationClientError>>()?;
3058        if fields
3059            .insert(
3060                field.name,
3061                ApplicationResultField {
3062                    cardinality,
3063                    records,
3064                },
3065            )
3066            .is_some()
3067        {
3068            return Err(ApplicationClientError::InvalidResponse);
3069        }
3070    }
3071    Ok(fields)
3072}
3073
3074fn raise_compact_query_field(
3075    field: app_v1::CompactResultField,
3076) -> Result<BTreeMap<String, ApplicationResultField>, ApplicationClientError> {
3077    if field.name.is_empty() || field.entity.is_empty() || field.fields.is_empty() {
3078        return Err(ApplicationClientError::InvalidResponse);
3079    }
3080    let cardinality = match app_v1::ResultCardinality::try_from(field.cardinality) {
3081        Ok(app_v1::ResultCardinality::Many) => ApplicationCardinality::Many,
3082        _ => return Err(ApplicationClientError::InvalidResponse),
3083    };
3084    let mut names = BTreeSet::new();
3085    for name in &field.fields {
3086        if name.is_empty() || !names.insert(name.as_str()) {
3087            return Err(ApplicationClientError::InvalidResponse);
3088        }
3089    }
3090    let width = field.fields.len();
3091    let records = field
3092        .rows
3093        .into_iter()
3094        .map(|row| {
3095            if row.values.len() != width {
3096                return Err(ApplicationClientError::InvalidResponse);
3097            }
3098            let fields = field
3099                .fields
3100                .iter()
3101                .cloned()
3102                .zip(row.values)
3103                .map(|(name, value)| Ok((name, raise_value(value)?)))
3104                .collect::<Result<BTreeMap<_, _>, ApplicationClientError>>()?;
3105            Ok(ApplicationRecord {
3106                entity: field.entity.clone(),
3107                fields,
3108            })
3109        })
3110        .collect::<Result<Vec<_>, ApplicationClientError>>()?;
3111    Ok(BTreeMap::from([(
3112        field.name,
3113        ApplicationResultField {
3114            cardinality,
3115            records,
3116        },
3117    )]))
3118}
3119
3120#[doc(hidden)]
3121pub fn raise_value(value: v1::Value) -> Result<ApplicationValue, ApplicationClientError> {
3122    use v1::value::Kind;
3123    match value.kind.ok_or(ApplicationClientError::InvalidResponse)? {
3124        Kind::NullValue(_) => Ok(ApplicationValue::Null),
3125        Kind::BoolValue(value) => Ok(ApplicationValue::Bool(value)),
3126        Kind::I64Value(value) => Ok(ApplicationValue::I64(value)),
3127        Kind::U64Value(value) => Ok(ApplicationValue::U64(value)),
3128        Kind::DecimalValue(value) => Ok(ApplicationValue::Decimal {
3129            coefficient_twos_complement: value.coefficient_twos_complement,
3130            scale: value.scale,
3131            precision: value.precision,
3132        }),
3133        Kind::MoneyValue(value) => {
3134            let amount = value
3135                .amount
3136                .ok_or(ApplicationClientError::InvalidResponse)?;
3137            Ok(ApplicationValue::Money {
3138                currency: value.currency,
3139                amount: Box::new(ApplicationValue::Decimal {
3140                    coefficient_twos_complement: amount.coefficient_twos_complement,
3141                    scale: amount.scale,
3142                    precision: amount.precision,
3143                }),
3144            })
3145        }
3146        Kind::StringValue(value) => Ok(ApplicationValue::String(value)),
3147        Kind::BytesValue(value) => Ok(ApplicationValue::Bytes(value)),
3148        Kind::UuidValue(value) => {
3149            let bytes: [u8; 16] = value
3150                .as_slice()
3151                .try_into()
3152                .map_err(|_| ApplicationClientError::InvalidResponse)?;
3153            Ok(ApplicationValue::Uuid(ApplicationUuid::from_bytes(bytes)))
3154        }
3155        Kind::EnumValue(value) if !value.name.is_empty() => Ok(ApplicationValue::Enum(value.name)),
3156        Kind::ListValue(values) => Ok(ApplicationValue::List(
3157            values
3158                .values
3159                .into_iter()
3160                .map(raise_value)
3161                .collect::<Result<Vec<_>, _>>()?,
3162        )),
3163        Kind::RecordValue(record) => {
3164            let mut fields = BTreeMap::new();
3165            for field in record.fields {
3166                if field.name.is_empty()
3167                    || fields
3168                        .insert(
3169                            field.name,
3170                            raise_value(
3171                                field.value.ok_or(ApplicationClientError::InvalidResponse)?,
3172                            )?,
3173                        )
3174                        .is_some()
3175                {
3176                    return Err(ApplicationClientError::InvalidResponse);
3177                }
3178            }
3179            Ok(ApplicationValue::Record(fields))
3180        }
3181        Kind::DateValue(value) => Ok(ApplicationValue::Date(value.days_since_unix_epoch)),
3182        Kind::TimestampValue(value) if value.nanos < 1_000_000_000 => {
3183            Ok(ApplicationValue::Timestamp {
3184                seconds: value.seconds,
3185                nanos: value.nanos,
3186            })
3187        }
3188        Kind::VectorValue(value) => riffdb_types::CanonicalVector::new(value.components)
3189            .map(ApplicationValue::Vector)
3190            .map_err(|_| ApplicationClientError::InvalidResponse),
3191        Kind::TimestampValue(_) | Kind::EnumValue(_) => {
3192            Err(ApplicationClientError::InvalidResponse)
3193        }
3194    }
3195}
3196
3197fn format_uuid_text(bytes: &[u8; 16]) -> String {
3198    const HEX: &[u8; 16] = b"0123456789abcdef";
3199    let mut output = String::with_capacity(36);
3200    for (index, byte) in bytes.iter().copied().enumerate() {
3201        if matches!(index, 4 | 6 | 8 | 10) {
3202            output.push('-');
3203        }
3204        output.push(char::from(HEX[usize::from(byte >> 4)]));
3205        output.push(char::from(HEX[usize::from(byte & 0x0f)]));
3206    }
3207    output
3208}
3209
3210fn parse_uuid(value: &str) -> Result<[u8; 16], ApplicationClientError> {
3211    if value.len() != 36 {
3212        return Err(ApplicationClientError::InvalidInput);
3213    }
3214    let bytes = value.as_bytes();
3215    if [8, 13, 18, 23]
3216        .into_iter()
3217        .any(|index| bytes[index] != b'-')
3218    {
3219        return Err(ApplicationClientError::InvalidInput);
3220    }
3221    let mut output = [0_u8; 16];
3222    let mut encoded = bytes.iter().copied().filter(|byte| *byte != b'-');
3223    for byte in &mut output {
3224        let high = encoded
3225            .next()
3226            .and_then(hex_nibble)
3227            .ok_or(ApplicationClientError::InvalidInput)?;
3228        let low = encoded
3229            .next()
3230            .and_then(hex_nibble)
3231            .ok_or(ApplicationClientError::InvalidInput)?;
3232        *byte = (high << 4) | low;
3233    }
3234    encoded
3235        .next()
3236        .is_none()
3237        .then_some(output)
3238        .ok_or(ApplicationClientError::InvalidInput)
3239}
3240
3241const fn hex_nibble(byte: u8) -> Option<u8> {
3242    match byte {
3243        b'0'..=b'9' => Some(byte - b'0'),
3244        b'a'..=b'f' => Some(byte - b'a' + 10),
3245        _ => None,
3246    }
3247}
3248
3249#[cfg(test)]
3250mod tests {
3251    use super::*;
3252    use riffdb_errors::ApplicationErrorCode;
3253
3254    #[test]
3255    fn exact_tonic_endpoint_defaults_tcp_nodelay_on() {
3256        // tonic is exact-pinned at 0.14.6 by the workspace. This executable
3257        // assertion accompanies WP-632's source receipt so a dependency
3258        // change cannot silently reintroduce Nagle delay into the diagnosis.
3259        let endpoint = Endpoint::from_static("http://127.0.0.1:7443");
3260        assert!(endpoint.get_tcp_nodelay());
3261    }
3262
3263    #[test]
3264    fn named_query_builder_is_name_addressed_and_module_pinned() {
3265        let mut parameters = BTreeMap::new();
3266        parameters.insert(
3267            "organization_id".to_owned(),
3268            ApplicationValue::String("01900000-0000-7000-8000-000000000001".to_owned()),
3269        );
3270        let query = NamedQuery::new(
3271            ApplicationContract::Exact {
3272                lineage: "TicketDesk".to_owned(),
3273                version: 1,
3274                bundle_hash: None,
3275            },
3276            "TicketPage",
3277            Some([7; 32]),
3278            parameters,
3279            None,
3280        )
3281        .expect("query");
3282        assert_eq!(query.name, "TicketPage");
3283        assert_eq!(query.module_hash, Some([7; 32]));
3284    }
3285
3286    #[test]
3287    fn generated_query_options_preserve_cursor_and_read_fence() {
3288        let query = NamedQuery::new(
3289            ApplicationContract::Exact {
3290                lineage: "TicketDesk".to_owned(),
3291                version: 1,
3292                bundle_hash: Some([9; 32]),
3293            },
3294            "TicketPage",
3295            Some([7; 32]),
3296            BTreeMap::new(),
3297            None,
3298        )
3299        .expect("query")
3300        .with_options(
3301            QueryOptions::new()
3302                .after("opaque-cursor")
3303                .read_after_commit(41)
3304                .at_least_admission_head(),
3305        )
3306        .expect("options");
3307        assert_eq!(query.cursor.as_deref(), Some("opaque-cursor"));
3308        assert_eq!(query.minimum_application_head, Some(41));
3309        assert!(query.admission_head_consistency);
3310        assert!(
3311            NamedQuery::new(
3312                ApplicationContract::Active,
3313                "TicketPage",
3314                Some([7; 32]),
3315                BTreeMap::new(),
3316                None,
3317            )
3318            .expect("query")
3319            .with_options(QueryOptions::new().read_after_commit(0))
3320            .is_err()
3321        );
3322    }
3323
3324    // req: OQ-031
3325    #[test]
3326    fn generated_cursor_routing_preserves_low_level_options_and_rejects_collision() {
3327        let routed = QueryOptions::new()
3328            .read_after_commit(41)
3329            .at_least_admission_head()
3330            .with_generated_cursor(Some("generated-cursor".to_owned()))
3331            .expect("generated cursor");
3332        assert_eq!(routed.cursor.as_deref(), Some("generated-cursor"));
3333        assert_eq!(routed.read_after_commit, Some(41));
3334        assert!(routed.admission_head);
3335
3336        let first_page = QueryOptions::new()
3337            .read_after_commit(42)
3338            .with_generated_cursor(None)
3339            .expect("first page");
3340        assert_eq!(first_page.cursor, None);
3341        assert_eq!(first_page.read_after_commit, Some(42));
3342
3343        assert!(matches!(
3344            QueryOptions::new()
3345                .after("low-level-cursor")
3346                .with_generated_cursor(Some("generated-cursor".to_owned())),
3347            Err(ApplicationClientError::InvalidInput)
3348        ));
3349    }
3350
3351    #[test]
3352    fn query_response_raises_only_names_and_typed_values() {
3353        let response = app_v1::ExecuteQueryResponse {
3354            identity: Some(app_v1::QueryIdentity {
3355                contract_lineage: "TicketDesk".to_owned(),
3356                contract_version: 1,
3357                contract_bundle_hash: vec![1; 32],
3358                query_name: Some("TicketPage".to_owned()),
3359                plan_hash: vec![2; 32],
3360                module_hash: Some(vec![3; 32]),
3361            }),
3362            outcome: "Found".to_owned(),
3363            application_head: 4,
3364            fields: vec![app_v1::ResultField {
3365                name: "ticket".to_owned(),
3366                cardinality: app_v1::ResultCardinality::One as i32,
3367                records: vec![app_v1::ResultRecord {
3368                    fields: vec![app_v1::Parameter {
3369                        name: "title".to_owned(),
3370                        value: Some(v1::Value {
3371                            kind: Some(v1::value::Kind::StringValue("Hello".to_owned())),
3372                        }),
3373                    }],
3374                    entity: "Ticket".to_owned(),
3375                }],
3376            }],
3377            next_cursor: None,
3378            selected_result_encoding: app_v1::NamedResultEncoding::LegacyRecords as i32,
3379            compact_result: None,
3380            packed_result: None,
3381        };
3382        let result = raise_query_result(response).expect("result");
3383        assert_eq!(result.outcome, "Found");
3384        assert_eq!(result.identity.contract_lineage, "TicketDesk");
3385        assert_eq!(result.identity.contract_version, 1);
3386        assert_eq!(result.identity.contract_bundle_hash, [1; 32]);
3387        assert_eq!(result.identity.module_hash, [3; 32]);
3388        assert_eq!(result.identity.query_name, "TicketPage");
3389        assert_eq!(result.identity.plan_hash, [2; 32]);
3390        assert_eq!(
3391            result.fields["ticket"].records[0].fields["title"],
3392            ApplicationValue::String("Hello".to_owned())
3393        );
3394    }
3395
3396    #[test]
3397    fn generated_identity_expectations_reject_contract_module_and_name_drift() {
3398        let response = app_v1::ExecuteQueryResponse {
3399            identity: Some(app_v1::QueryIdentity {
3400                contract_lineage: "TicketDesk".to_owned(),
3401                contract_version: 1,
3402                contract_bundle_hash: vec![1; 32],
3403                query_name: Some("TicketPage".to_owned()),
3404                plan_hash: vec![2; 32],
3405                module_hash: Some(vec![3; 32]),
3406            }),
3407            ..Default::default()
3408        };
3409        let contract = ApplicationContract::Exact {
3410            lineage: "TicketDesk".to_owned(),
3411            version: 1,
3412            bundle_hash: Some([1; 32]),
3413        };
3414        validate_query_response_identity(
3415            &response,
3416            &contract,
3417            "TicketPage",
3418            Some([3; 32]),
3419            Some([2; 32]),
3420        )
3421        .expect("exact identity");
3422        assert!(matches!(
3423            validate_query_response_identity(
3424                &response,
3425                &contract,
3426                "Other",
3427                Some([3; 32]),
3428                Some([2; 32])
3429            ),
3430            Err(ApplicationClientError::InvalidResponse)
3431        ));
3432        assert!(matches!(
3433            validate_query_response_identity(
3434                &response,
3435                &contract,
3436                "TicketPage",
3437                Some([4; 32]),
3438                Some([2; 32])
3439            ),
3440            Err(ApplicationClientError::InvalidResponse)
3441        ));
3442        assert!(matches!(
3443            validate_query_response_identity(
3444                &response,
3445                &contract,
3446                "TicketPage",
3447                Some([3; 32]),
3448                Some([4; 32])
3449            ),
3450            Err(ApplicationClientError::InvalidResponse)
3451        ));
3452        let changed_contract = ApplicationContract::Exact {
3453            lineage: "TicketDesk".to_owned(),
3454            version: 1,
3455            bundle_hash: Some([9; 32]),
3456        };
3457        assert!(matches!(
3458            validate_query_response_identity(
3459                &response,
3460                &changed_contract,
3461                "TicketPage",
3462                Some([3; 32]),
3463                Some([2; 32])
3464            ),
3465            Err(ApplicationClientError::InvalidResponse)
3466        ));
3467    }
3468
3469    #[test]
3470    fn application_catalog_preflight_is_exact_closed_and_name_only() {
3471        let contract = ApplicationContract::Exact {
3472            lineage: "TicketDesk".to_owned(),
3473            version: 3,
3474            bundle_hash: Some([7; 32]),
3475        };
3476        let response = app_v1::GetApplicationCatalogResponse {
3477            schema: APPLICATION_CATALOG_SCHEMA_V1.to_owned(),
3478            contract_lineage: "TicketDesk".to_owned(),
3479            contract_version: 3,
3480            contract_bundle_hash: vec![7; 32],
3481            query_module_hashes: vec![vec![3; 32], vec![5; 32]],
3482            features: [
3483                app_v1::ApplicationCatalogFeature::OperationalOptionalPredicates,
3484                app_v1::ApplicationCatalogFeature::StableCursorPages,
3485                app_v1::ApplicationCatalogFeature::NullExistencePredicates,
3486                app_v1::ApplicationCatalogFeature::BinaryTextPrefix,
3487                app_v1::ApplicationCatalogFeature::UnicodeFoldTextPrefixV1,
3488                app_v1::ApplicationCatalogFeature::ExactAggregates,
3489            ]
3490            .into_iter()
3491            .map(|feature| app_v1::ApplicationCatalogFeatureView {
3492                feature: feature as i32,
3493                state: if feature == app_v1::ApplicationCatalogFeature::UnicodeFoldTextPrefixV1 {
3494                    app_v1::ApplicationCatalogFeatureState::Unavailable as i32
3495                } else {
3496                    app_v1::ApplicationCatalogFeatureState::Available as i32
3497                },
3498            })
3499            .collect(),
3500            ..Default::default()
3501        };
3502
3503        let preflight =
3504            raise_application_catalog_preflight(response, &contract).expect("preflight");
3505        assert_eq!(preflight.contract_lineage, "TicketDesk");
3506        assert_eq!(preflight.contract_version, 3);
3507        assert_eq!(preflight.contract_bundle_hash, [7; 32]);
3508        assert_eq!(preflight.query_module_hashes, vec![[3; 32], [5; 32]]);
3509        assert!(preflight.is_available(ApplicationCatalogFeature::BinaryTextPrefix));
3510        assert!(!preflight.is_available(ApplicationCatalogFeature::UnicodeFoldTextPrefixV1));
3511    }
3512
3513    #[test]
3514    fn application_catalog_preflight_rejects_identity_and_registry_drift() {
3515        let contract = ApplicationContract::Exact {
3516            lineage: "TicketDesk".to_owned(),
3517            version: 3,
3518            bundle_hash: Some([7; 32]),
3519        };
3520        let response = app_v1::GetApplicationCatalogResponse {
3521            schema: APPLICATION_CATALOG_SCHEMA_V1.to_owned(),
3522            contract_lineage: "TicketDesk".to_owned(),
3523            contract_version: 4,
3524            contract_bundle_hash: vec![7; 32],
3525            ..Default::default()
3526        };
3527        assert!(matches!(
3528            raise_application_catalog_preflight(response, &contract),
3529            Err(ApplicationClientError::InvalidResponse)
3530        ));
3531
3532        let response = app_v1::GetApplicationCatalogResponse {
3533            schema: APPLICATION_CATALOG_SCHEMA_V1.to_owned(),
3534            contract_lineage: "TicketDesk".to_owned(),
3535            contract_version: 3,
3536            contract_bundle_hash: vec![7; 32],
3537            query_module_hashes: vec![vec![5; 32], vec![3; 32]],
3538            ..Default::default()
3539        };
3540        assert!(matches!(
3541            raise_application_catalog_preflight(response, &contract),
3542            Err(ApplicationClientError::InvalidResponse)
3543        ));
3544    }
3545
3546    #[test]
3547    fn generated_batch_bounds_and_resume_checkpoint_are_closed() {
3548        let options = GeneratedBatchOptions::new(MAX_GENERATED_BATCH_CONCURRENCY)
3549            .expect("maximum concurrency")
3550            .with_checkpoint(4_096);
3551        options.validate(4_096).expect("complete checkpoint");
3552        assert_eq!(options.checkpoint, 4_096);
3553
3554        assert_eq!(
3555            GeneratedBatchOptions::new(0),
3556            Err(GeneratedBatchError::InvalidBounds)
3557        );
3558        assert_eq!(
3559            GeneratedBatchOptions::new(MAX_GENERATED_BATCH_CONCURRENCY + 1),
3560            Err(GeneratedBatchError::InvalidBounds)
3561        );
3562        assert_eq!(
3563            GeneratedBatchOptions::new(1)
3564                .expect("options")
3565                .with_checkpoint(2)
3566                .validate(1),
3567            Err(GeneratedBatchError::InvalidBounds)
3568        );
3569        assert_eq!(
3570            GeneratedBatchOptions::new(1)
3571                .expect("options")
3572                .validate(4_097),
3573            Err(GeneratedBatchError::InvalidBounds)
3574        );
3575    }
3576
3577    #[test]
3578    fn generated_transport_batches_never_exceed_the_item_concurrency_bound() {
3579        for item_concurrency in 1..=MAX_GENERATED_BATCH_CONCURRENCY {
3580            let (batch_size, transport_concurrency) =
3581                generated_transport_batch_policy(item_concurrency);
3582            assert!((1..=MAX_GENERATED_TRANSPORT_BATCH_ITEMS).contains(&batch_size));
3583            assert!(transport_concurrency > 0);
3584            assert!(batch_size * transport_concurrency <= item_concurrency);
3585        }
3586        assert_eq!(generated_transport_batch_policy(64), (16, 4));
3587        assert_eq!(generated_transport_batch_policy(128), (16, 8));
3588        assert_eq!(generated_transport_batch_policy(384), (16, 24));
3589        assert_eq!(generated_transport_batch_policy(17), (8, 2));
3590    }
3591
3592    #[test]
3593    fn ordinary_transport_batch_bound_is_closed_before_submission() {
3594        assert_eq!(
3595            validate_idempotent_transport_batch_len(0),
3596            Err(IdempotentTransportBatchError::Empty)
3597        );
3598        assert_eq!(validate_idempotent_transport_batch_len(1), Ok(()));
3599        assert_eq!(
3600            validate_idempotent_transport_batch_len(MAX_GENERATED_TRANSPORT_BATCH_ITEMS),
3601            Ok(())
3602        );
3603        assert_eq!(
3604            validate_idempotent_transport_batch_len(MAX_GENERATED_TRANSPORT_BATCH_ITEMS + 1),
3605            Err(IdempotentTransportBatchError::TooManyItems)
3606        );
3607    }
3608
3609    #[derive(Clone)]
3610    struct StubBatchCommand {
3611        outcome: &'static str,
3612        key: &'static str,
3613    }
3614
3615    impl GeneratedCommand for StubBatchCommand {
3616        type Outcome = String;
3617
3618        fn idempotent_command(
3619            &self,
3620        ) -> Result<crate::IdempotentCommand, crate::generated::GeneratedCommandError> {
3621            crate::IdempotentCommand::new(
3622                "OkCommand",
3623                Some(1),
3624                v1::Value {
3625                    kind: Some(v1::value::Kind::RecordValue(v1::ValueRecord {
3626                        fields: vec![v1::ValueField {
3627                            field_id: Some(1),
3628                            name: String::new(),
3629                            value: Some(v1::Value {
3630                                kind: Some(v1::value::Kind::StringValue(self.key.to_owned())),
3631                            }),
3632                        }],
3633                    })),
3634                },
3635            )
3636            .map_err(|_| crate::generated::GeneratedCommandError::InvalidInputShape)
3637        }
3638
3639        fn outcome_request(
3640            &self,
3641            request_id: riffdb_types::RequestId,
3642        ) -> Result<v1::GetOutcomeRequest, crate::generated::GeneratedCommandError> {
3643            Ok(v1::GetOutcomeRequest {
3644                request_id: request_id.into_bytes().to_vec(),
3645                contract_lineage: "lineage".to_owned(),
3646                command_name: "OkCommand".to_owned(),
3647                idempotency_key: self.key.to_owned(),
3648                outcome_uri: None,
3649            })
3650        }
3651
3652        fn decode_outcome(
3653            &self,
3654            response: &v1::ExecuteCommandResponse,
3655        ) -> Result<Self::Outcome, crate::generated::GeneratedCommandError> {
3656            if response.outcome_type == self.outcome {
3657                Ok(self.outcome.to_owned())
3658            } else {
3659                Err(crate::generated::GeneratedCommandError::InvalidOutcomeShape)
3660            }
3661        }
3662
3663        fn workflow_successor_revisions(
3664            &self,
3665            response: &v1::ExecuteCommandResponse,
3666        ) -> Result<
3667            Vec<crate::generated::WorkflowSuccessorRevision>,
3668            crate::generated::GeneratedCommandError,
3669        > {
3670            if response.outcome_type == "Workflow" {
3671                Ok(vec![
3672                    crate::generated::WorkflowSuccessorRevision::generated("work", 8),
3673                ])
3674            } else {
3675                Ok(Vec::new())
3676            }
3677        }
3678    }
3679
3680    fn stub_command(outcome: &'static str, key: &'static str) -> StubBatchCommand {
3681        StubBatchCommand { outcome, key }
3682    }
3683
3684    fn stub_response(outcome: &str) -> v1::ExecuteCommandResponse {
3685        v1::ExecuteCommandResponse {
3686            status: v1::execute_command_response::CompletionStatus::ExecutedReadOnly as i32,
3687            commit_sequence: 0,
3688            contract_version: 1,
3689            plan_hash: vec![0x44; 32],
3690            outcome_type: outcome.to_owned(),
3691            outcome: Some(v1::Value {
3692                kind: Some(v1::value::Kind::NullValue(v1::NullValue::NullValue as i32)),
3693            }),
3694            provenance_uri: String::new(),
3695            durability_mode: String::new(),
3696            outcome_uri: None,
3697            history_incarnation: 1,
3698        }
3699    }
3700
3701    #[test]
3702    fn typed_command_results_preserve_generated_workflow_revision_evidence() {
3703        let command = stub_command("Workflow", "workflow-key");
3704        let response = stub_response("Workflow");
3705        let outcome = command.decode_outcome(&response).expect("outcome");
3706        let result = typed_command_result(&command, outcome, response).expect("typed result");
3707
3708        assert_eq!(result.workflow_revisions.len(), 1);
3709        assert_eq!(result.workflow_revisions[0].binding(), "work");
3710        assert_eq!(result.workflow_revisions[0].revision(), 8);
3711    }
3712
3713    fn stub_error_item(code: ApplicationErrorCode) -> v1::ExecuteCommandBatchItem {
3714        v1::ExecuteCommandBatchItem {
3715            result: Some(v1::execute_command_batch_item::Result::Error(
3716                riffdb_proto::application_error_to_proto(&ApplicationError::new(
3717                    code,
3718                    ApplicationOperation::BatchCommand,
3719                    ApplicationErrorContext::empty(),
3720                    None,
3721                )),
3722            )),
3723        }
3724    }
3725
3726    fn application_overloaded() -> ClientError {
3727        ClientError::Application(Box::new(ApplicationError::new(
3728            ApplicationErrorCode::Overloaded,
3729            ApplicationOperation::BatchCommand,
3730            ApplicationErrorContext::empty(),
3731            None,
3732        )))
3733    }
3734
3735    fn block_on_with_time<T>(future: impl std::future::Future<Output = T>) -> T {
3736        tokio::runtime::Builder::new_current_thread()
3737            .enable_time()
3738            .build()
3739            .expect("test runtime")
3740            .block_on(future)
3741    }
3742
3743    /// Scripted ExecuteRetryAttempt mirror of client.rs tests::ExecuteAttempts.
3744    struct ScriptedExecuteAttempts {
3745        results: std::collections::VecDeque<Result<v1::ExecuteCommandResponse, ClientError>>,
3746        requests: Vec<v1::ExecuteCommandRequest>,
3747    }
3748
3749    impl ScriptedExecuteAttempts {
3750        fn new(
3751            results: impl IntoIterator<Item = Result<v1::ExecuteCommandResponse, ClientError>>,
3752        ) -> Self {
3753            Self {
3754                results: results.into_iter().collect(),
3755                requests: Vec::new(),
3756            }
3757        }
3758    }
3759
3760    impl crate::client::ExecuteRetryAttempt for ScriptedExecuteAttempts {
3761        async fn submit_execute_attempt(
3762            &mut self,
3763            request: v1::ExecuteCommandRequest,
3764            _metadata: &CallMetadata,
3765        ) -> Result<v1::ExecuteCommandResponse, ClientError> {
3766            self.requests.push(request);
3767            self.results.pop_front().expect("scripted execute result")
3768        }
3769    }
3770
3771    struct FixedRequestIds {
3772        values: std::collections::VecDeque<riffdb_types::RequestId>,
3773    }
3774
3775    impl crate::client::RetryRequestIdSource for FixedRequestIds {
3776        fn next_request_id(
3777            &mut self,
3778        ) -> Result<riffdb_types::RequestId, crate::IdentifierGenerationError> {
3779            self.values
3780                .pop_front()
3781                .ok_or(crate::IdentifierGenerationError::EntropyUnavailable)
3782        }
3783    }
3784
3785    fn request_id(ordinal: u8) -> riffdb_types::RequestId {
3786        riffdb_types::RequestId::from_unix_milliseconds_and_random(
3787            u64::from(ordinal),
3788            [ordinal; 10],
3789        )
3790        .expect("request id")
3791    }
3792
3793    #[test]
3794    fn batch_items_non_retryable_errors_surface_without_reentry_candidates() {
3795        let prepared = vec![
3796            (0, stub_command("Ok", "k0")),
3797            (1, stub_command("Ok", "k1")),
3798            (2, stub_command("Ok", "k2")),
3799        ];
3800        let items = vec![
3801            v1::ExecuteCommandBatchItem {
3802                result: Some(v1::execute_command_batch_item::Result::Response(
3803                    stub_response("Ok"),
3804                )),
3805            },
3806            stub_error_item(ApplicationErrorCode::InputInvalid),
3807            stub_error_item(ApplicationErrorCode::AuthorizationDenied),
3808        ];
3809        let (resolved, reenter) = resolve_generated_batch_items(prepared, items);
3810        assert!(reenter.is_empty(), "non-retryable errors must not re-enter");
3811        assert_eq!(resolved.len(), 3);
3812        assert!(resolved[0].1.is_ok());
3813        assert!(matches!(
3814            &resolved[1].1,
3815            Err(GeneratedExecutionError::Client(ClientError::Application(error)))
3816                if error.code() == ApplicationErrorCode::InputInvalid
3817        ));
3818        assert!(matches!(
3819            &resolved[2].1,
3820            Err(GeneratedExecutionError::Client(ClientError::Application(error)))
3821                if error.code() == ApplicationErrorCode::AuthorizationDenied
3822        ));
3823        assert_eq!(
3824            resolved.iter().map(|(index, _)| *index).collect::<Vec<_>>(),
3825            vec![0, 1, 2]
3826        );
3827    }
3828
3829    #[test]
3830    fn batch_items_registry_retryable_errors_are_reentry_candidates() {
3831        let prepared = vec![
3832            (0, stub_command("A", "k0")),
3833            (1, stub_command("B", "k1")),
3834            (2, stub_command("C", "k2")),
3835            (3, stub_command("D", "k3")),
3836        ];
3837        let items = vec![
3838            v1::ExecuteCommandBatchItem {
3839                result: Some(v1::execute_command_batch_item::Result::Response(
3840                    stub_response("A"),
3841                )),
3842            },
3843            stub_error_item(ApplicationErrorCode::OutcomeUnknown),
3844            stub_error_item(ApplicationErrorCode::Overloaded),
3845            stub_error_item(ApplicationErrorCode::StorageUnavailable),
3846        ];
3847        let (resolved, reenter) = resolve_generated_batch_items(prepared, items);
3848        assert_eq!(resolved.len(), 1);
3849        assert!(resolved[0].1.is_ok());
3850        assert_eq!(
3851            reenter
3852                .iter()
3853                .map(|candidate| candidate.index)
3854                .collect::<Vec<_>>(),
3855            vec![1, 2, 3],
3856            "Retry and ResolveWithSameIdempotencyKey must re-enter"
3857        );
3858        assert!(reenter.iter().all(|c| c.protocol_failure.is_none()));
3859        assert_eq!(
3860            ApplicationErrorCode::Overloaded.recovery_action(),
3861            ApplicationRecoveryAction::Retry
3862        );
3863        assert_eq!(
3864            ApplicationErrorCode::OutcomeUnknown.recovery_action(),
3865            ApplicationRecoveryAction::ResolveWithSameIdempotencyKey
3866        );
3867    }
3868
3869    #[test]
3870    fn batch_items_corrupt_or_unset_error_arms_reenter() {
3871        let prepared = vec![
3872            (0, stub_command("A", "k0")),
3873            (1, stub_command("B", "k1")),
3874            (2, stub_command("C", "k2")),
3875        ];
3876        let corrupt = v1::ExecuteCommandBatchItem {
3877            result: Some(v1::execute_command_batch_item::Result::Error(
3878                app_v1::ApplicationError {
3879                    envelope_version: 0,
3880                    code: 0,
3881                    category: 0,
3882                    recovery_action: 0,
3883                    operation: 0,
3884                    ..Default::default()
3885                },
3886            )),
3887        };
3888        let items = vec![
3889            v1::ExecuteCommandBatchItem {
3890                result: Some(v1::execute_command_batch_item::Result::Response(
3891                    stub_response("A"),
3892                )),
3893            },
3894            corrupt,
3895            v1::ExecuteCommandBatchItem { result: None },
3896        ];
3897        let (resolved, reenter) = resolve_generated_batch_items(prepared, items);
3898        assert_eq!(resolved.len(), 1);
3899        assert_eq!(
3900            reenter
3901                .iter()
3902                .map(|candidate| candidate.index)
3903                .collect::<Vec<_>>(),
3904            vec![1, 2]
3905        );
3906        assert_eq!(
3907            reenter[0].protocol_failure.map(|f| f.kind()),
3908            Some(crate::ProtocolFailureKind::InvalidApplicationErrorDetails)
3909        );
3910        assert_eq!(
3911            reenter[1].protocol_failure.map(|f| f.kind()),
3912            Some(crate::ProtocolFailureKind::InvalidInboundMessage)
3913        );
3914    }
3915
3916    #[test]
3917    fn complete_batch_items_retries_all_overloaded_through_real_budget_and_succeeds() {
3918        use std::collections::HashMap;
3919        use std::sync::{Arc, Mutex};
3920
3921        let prepared = vec![
3922            (0, stub_command("Ok", "k0")),
3923            (1, stub_command("Ok", "k1")),
3924            (2, stub_command("Ok", "k2")),
3925        ];
3926        let items = vec![
3927            stub_error_item(ApplicationErrorCode::Overloaded),
3928            stub_error_item(ApplicationErrorCode::Overloaded),
3929            stub_error_item(ApplicationErrorCode::Overloaded),
3930        ];
3931        let attempt_counts: Arc<Mutex<HashMap<usize, usize>>> =
3932            Arc::new(Mutex::new(HashMap::new()));
3933        let recovered = block_on_with_time(complete_batch_items_with_reentry(
3934            prepared,
3935            items,
3936            |index, command| {
3937                let attempt_counts = Arc::clone(&attempt_counts);
3938                async move {
3939                    let generic = command.idempotent_command().expect("stub command shape");
3940                    let success = stub_response(command.outcome);
3941                    let mut attempts = ScriptedExecuteAttempts::new([
3942                        Err(application_overloaded()),
3943                        Ok(success.clone()),
3944                    ]);
3945                    let mut ids = FixedRequestIds {
3946                        values: [request_id(10 + index as u8), request_id(40 + index as u8)]
3947                            .into_iter()
3948                            .collect(),
3949                    };
3950                    let response = crate::client::execute_retry_attempts(
3951                        &mut attempts,
3952                        &generic,
3953                        AttemptBudget::new(3).expect("budget"),
3954                        &CallMetadata::default(),
3955                        &mut ids,
3956                    )
3957                    .await
3958                    .expect("capacity frees on second attempt");
3959                    attempt_counts
3960                        .lock()
3961                        .expect("counts")
3962                        .insert(index, attempts.requests.len());
3963                    let outcome = command
3964                        .decode_outcome(&response)
3965                        .map_err(GeneratedExecutionError::CommandShape)
3966                        .and_then(|outcome| typed_command_result(&command, outcome, response));
3967                    (index, outcome)
3968                }
3969            },
3970        ));
3971        assert_eq!(recovered.len(), 3);
3972        assert!(recovered.iter().all(|(_, result)| result.is_ok()));
3973        let counts = attempt_counts.lock().expect("counts");
3974        assert_eq!(counts.len(), 3);
3975        assert!(
3976            counts.values().all(|count| *count > 1),
3977            "mock must record multi-attempt real RetryState path: {counts:?}"
3978        );
3979    }
3980
3981    #[test]
3982    fn complete_batch_items_real_budget_exhaustion_surfaces_err_while_siblings_succeed() {
3983        use std::sync::Arc;
3984        use std::sync::atomic::{AtomicUsize, Ordering};
3985
3986        let prepared = vec![
3987            (0, stub_command("Ok", "k0")),
3988            (1, stub_command("Fail", "k1")),
3989            (2, stub_command("Ok", "k2")),
3990        ];
3991        let items = vec![
3992            v1::ExecuteCommandBatchItem {
3993                result: Some(v1::execute_command_batch_item::Result::Response(
3994                    stub_response("Ok"),
3995                )),
3996            },
3997            stub_error_item(ApplicationErrorCode::Overloaded),
3998            v1::ExecuteCommandBatchItem {
3999                result: Some(v1::execute_command_batch_item::Result::Response(
4000                    stub_response("Ok"),
4001                )),
4002            },
4003        ];
4004        let reenter_attempts = Arc::new(AtomicUsize::new(0));
4005        let mut recovered = block_on_with_time(complete_batch_items_with_reentry(
4006            prepared,
4007            items,
4008            |index, command| {
4009                let reenter_attempts = Arc::clone(&reenter_attempts);
4010                async move {
4011                    let generic = command.idempotent_command().expect("stub command shape");
4012                    // Budget of 2: both submissions fail Overloaded → Return after
4013                    // exactly two real execute_retry_attempts submissions.
4014                    let mut attempts = ScriptedExecuteAttempts::new([
4015                        Err(application_overloaded()),
4016                        Err(application_overloaded()),
4017                    ]);
4018                    let mut ids = FixedRequestIds {
4019                        values: [request_id(20), request_id(21)].into_iter().collect(),
4020                    };
4021                    let budget = AttemptBudget::new(2).expect("budget");
4022                    let error = crate::client::execute_retry_attempts(
4023                        &mut attempts,
4024                        &generic,
4025                        budget,
4026                        &CallMetadata::default(),
4027                        &mut ids,
4028                    )
4029                    .await
4030                    .expect_err("budget exhausted");
4031                    reenter_attempts.store(attempts.requests.len(), Ordering::SeqCst);
4032                    assert_eq!(
4033                        attempts.requests.len(),
4034                        budget.maximum_submissions() as usize
4035                    );
4036                    (index, Err(GeneratedExecutionError::Client(error)))
4037                }
4038            },
4039        ));
4040        recovered.sort_by_key(|(index, _)| *index);
4041        assert_eq!(reenter_attempts.load(Ordering::SeqCst), 2);
4042        assert!(recovered[0].1.is_ok());
4043        assert!(matches!(
4044            &recovered[1].1,
4045            Err(GeneratedExecutionError::Client(ClientError::Application(error)))
4046                if error.code() == ApplicationErrorCode::Overloaded
4047        ));
4048        assert!(recovered[2].1.is_ok());
4049    }
4050
4051    #[test]
4052    fn complete_batch_items_corrupt_error_arm_reenters_and_can_succeed() {
4053        let prepared = vec![
4054            (0, stub_command("Ok", "k0")),
4055            (1, stub_command("Replay", "k1")),
4056        ];
4057        let items = vec![
4058            v1::ExecuteCommandBatchItem {
4059                result: Some(v1::execute_command_batch_item::Result::Response(
4060                    stub_response("Ok"),
4061                )),
4062            },
4063            v1::ExecuteCommandBatchItem {
4064                result: Some(v1::execute_command_batch_item::Result::Error(
4065                    app_v1::ApplicationError {
4066                        envelope_version: 0,
4067                        code: 0,
4068                        category: 0,
4069                        recovery_action: 0,
4070                        operation: 0,
4071                        ..Default::default()
4072                    },
4073                )),
4074            },
4075        ];
4076        let mut recovered = block_on_with_time(complete_batch_items_with_reentry(
4077            prepared,
4078            items,
4079            |index, command| async move {
4080                (
4081                    index,
4082                    Ok(TypedCommandResult {
4083                        outcome: command.outcome.to_owned(),
4084                        commit_sequence: None,
4085                        contract_version: 1,
4086                        plan_hash: [0x55; 32],
4087                        replayed: true,
4088                        outcome_uri: None,
4089                        workflow_revisions: Vec::new(),
4090                    }),
4091                )
4092            },
4093        ));
4094        recovered.sort_by_key(|(index, _)| *index);
4095        assert_eq!(recovered.len(), 2);
4096        assert!(recovered[0].1.is_ok());
4097        assert_eq!(recovered[1].1.as_ref().expect("replayed").outcome, "Replay");
4098    }
4099
4100    #[test]
4101    fn complete_batch_items_corrupt_error_arm_surfaces_protocol_failure_on_reentry_exhaustion() {
4102        let prepared = vec![(0, stub_command("Replay", "k0"))];
4103        let items = vec![v1::ExecuteCommandBatchItem {
4104            result: Some(v1::execute_command_batch_item::Result::Error(
4105                app_v1::ApplicationError {
4106                    envelope_version: 0,
4107                    code: 0,
4108                    category: 0,
4109                    recovery_action: 0,
4110                    operation: 0,
4111                    ..Default::default()
4112                },
4113            )),
4114        }];
4115        let recovered = block_on_with_time(complete_batch_items_with_reentry(
4116            prepared,
4117            items,
4118            |index, _command| async move {
4119                // Re-entry itself fails (budget exhausted / still unknown).
4120                (
4121                    index,
4122                    Err(GeneratedExecutionError::Client(ClientError::Application(
4123                        Box::new(ApplicationError::new(
4124                            ApplicationErrorCode::Overloaded,
4125                            ApplicationOperation::BatchCommand,
4126                            ApplicationErrorContext::empty(),
4127                            None,
4128                        )),
4129                    ))),
4130                )
4131            },
4132        ));
4133        assert_eq!(recovered.len(), 1);
4134        assert!(matches!(
4135            &recovered[0].1,
4136            Err(GeneratedExecutionError::Client(ClientError::Protocol(failure)))
4137                if failure.kind() == crate::ProtocolFailureKind::InvalidApplicationErrorDetails
4138        ));
4139    }
4140
4141    #[test]
4142    fn record_generated_batch_item_completion_advances_checkpoint_in_production_order() {
4143        // Drive the production completion recorder (used by
4144        // execute_generated_command_batch_with_progress) with mixed Ok/Err and
4145        // out-of-order arrivals.
4146        let mut items = Vec::new();
4147        let mut state = BatchCheckpointState::new(0);
4148        let mut progress = Vec::new();
4149        let total = 3usize;
4150
4151        let ok = |label: &str| {
4152            Ok(TypedCommandResult {
4153                outcome: label.to_owned(),
4154                commit_sequence: None,
4155                contract_version: 1,
4156                plan_hash: [0x11; 32],
4157                replayed: false,
4158                outcome_uri: None,
4159                workflow_revisions: Vec::new(),
4160            })
4161        };
4162        let err = Err(GeneratedExecutionError::Client(ClientError::Application(
4163            Box::new(ApplicationError::new(
4164                ApplicationErrorCode::InputInvalid,
4165                ApplicationOperation::BatchCommand,
4166                ApplicationErrorContext::empty(),
4167                None,
4168            )),
4169        )));
4170
4171        // Out of order: 2, then 0, then 1(Err). Checkpoint stays 0 until 0 lands,
4172        // then 1, then 3 once the Err at 1 closes the prefix.
4173        record_generated_batch_item_completion(
4174            2,
4175            ok("two"),
4176            total,
4177            &mut state,
4178            &mut items,
4179            &mut |p| progress.push(p.checkpoint),
4180        );
4181        assert_eq!(state.checkpoint, 0);
4182        record_generated_batch_item_completion(
4183            0,
4184            ok("zero"),
4185            total,
4186            &mut state,
4187            &mut items,
4188            &mut |p| progress.push(p.checkpoint),
4189        );
4190        assert_eq!(state.checkpoint, 1);
4191        record_generated_batch_item_completion(1, err, total, &mut state, &mut items, &mut |p| {
4192            progress.push(p.checkpoint)
4193        });
4194        assert_eq!(state.checkpoint, 3);
4195        assert_eq!(progress, vec![0, 1, 3]);
4196        assert_eq!(items.len(), 3);
4197        assert!(
4198            items
4199                .iter()
4200                .any(|item| item.index == 1 && item.result.is_err())
4201        );
4202    }
4203
4204    #[test]
4205    fn record_generated_batch_item_completion_respects_resumed_checkpoint_seed() {
4206        let mut items = Vec::new();
4207        // Production seeds both completed and checkpoint from options.checkpoint.
4208        let mut state = BatchCheckpointState::new(2);
4209        let mut progress = Vec::new();
4210        let total = 4usize;
4211        let ok = |label: &str| {
4212            Ok(TypedCommandResult {
4213                outcome: label.to_owned(),
4214                commit_sequence: None,
4215                contract_version: 1,
4216                plan_hash: [0x22; 32],
4217                replayed: false,
4218                outcome_uri: None,
4219                workflow_revisions: Vec::new(),
4220            })
4221        };
4222        record_generated_batch_item_completion(
4223            2,
4224            ok("two"),
4225            total,
4226            &mut state,
4227            &mut items,
4228            &mut |p| progress.push((p.completed, p.checkpoint)),
4229        );
4230        record_generated_batch_item_completion(
4231            3,
4232            ok("three"),
4233            total,
4234            &mut state,
4235            &mut items,
4236            &mut |p| progress.push((p.completed, p.checkpoint)),
4237        );
4238        assert_eq!(state.checkpoint, 4);
4239        assert_eq!(state.completed, 4);
4240        assert_eq!(progress, vec![(3, 3), (4, 4)]);
4241    }
4242
4243    /// R3d: raised UUID bytes equal the text path after parse, without forcing
4244    /// text materialization on raise.
4245    #[test]
4246    fn raised_uuid_bytes_match_parsed_text_and_text_is_lazy() {
4247        let bytes = [
4248            0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
4249            0xcd, 0xef,
4250        ];
4251        let raised = raise_value(v1::Value {
4252            kind: Some(v1::value::Kind::UuidValue(bytes.to_vec())),
4253        })
4254        .expect("raise uuid");
4255        let ApplicationValue::Uuid(uuid) = raised else {
4256            panic!("expected uuid value");
4257        };
4258        assert!(!uuid.text_is_materialized(), "raise must not allocate text");
4259        assert_eq!(uuid.as_bytes(), &bytes);
4260        let text = uuid.as_str().to_owned();
4261        assert!(uuid.text_is_materialized());
4262        let from_text = ApplicationUuid::from_text(text.clone()).expect("parse text");
4263        assert_eq!(from_text.as_bytes(), &bytes);
4264        assert_eq!(from_text.into_string(), text);
4265        // Adapter-equivalence: bytes path and re-parsed text path agree.
4266        let re_lowered = lower_value(ApplicationValue::Uuid(ApplicationUuid::from_bytes(bytes)))
4267            .expect("lower bytes");
4268        let text_lowered = lower_value(ApplicationValue::Uuid(
4269            ApplicationUuid::from_text(text).expect("text"),
4270        ))
4271        .expect("lower text");
4272        assert_eq!(re_lowered, text_lowered);
4273    }
4274
4275    #[test]
4276    fn canonical_vector_values_round_trip_without_punning() {
4277        let vector =
4278            riffdb_types::CanonicalVector::new(vec![-0.0, 1.5, -2.25]).expect("canonical vector");
4279        let lowered = lower_value(ApplicationValue::Vector(vector.clone())).expect("lower");
4280        let Some(v1::value::Kind::VectorValue(wire)) = lowered.kind.as_ref() else {
4281            panic!("expected the dedicated vector wire arm");
4282        };
4283        assert_eq!(wire.components, vector.components());
4284        assert_eq!(
4285            raise_value(lowered).expect("raise"),
4286            ApplicationValue::Vector(vector)
4287        );
4288    }
4289
4290    #[test]
4291    fn non_finite_vector_response_is_rejected() {
4292        let result = raise_value(v1::Value {
4293            kind: Some(v1::value::Kind::VectorValue(v1::VectorValue {
4294                components: vec![f32::NAN],
4295            })),
4296        });
4297        assert!(matches!(
4298            result,
4299            Err(ApplicationClientError::InvalidResponse)
4300        ));
4301    }
4302
4303    // req: REP-004, REP-005, DRV-001, DRV-005
4304    #[test]
4305    fn driver_pool_and_application_client_failover_to_standby_and_route_replica_reads() {
4306        let runtime = tokio::runtime::Builder::new_current_thread()
4307            .enable_all()
4308            .build()
4309            .expect("current-thread runtime");
4310        runtime.block_on(async {
4311            let active_contract = |bundle_hash: [u8; 32]| v1::GetActiveContractResponse {
4312                result: Some(v1::get_active_contract_response::Result::Present(
4313                    v1::ContractDescriptor {
4314                        contract_lineage: "TicketDesk".to_owned(),
4315                        contract_version: 1,
4316                        bundle_hash: bundle_hash.to_vec(),
4317                        ..v1::ContractDescriptor::default()
4318                    },
4319                )),
4320                database_alias: "default".to_owned(),
4321            };
4322
4323            let primary = StableApplicationClient::scripted_for_test(active_contract([7; 32]))
4324                .push_scripted_command(Err(ClientError::Public(
4325                    riffdb_errors::PublicError::primary_fenced(),
4326                )));
4327            let standby_mismatched =
4328                StableApplicationClient::scripted_for_test(active_contract([9; 32]))
4329                    .push_scripted_command(Ok(stub_response("WrongStandby")));
4330            let query_response = |head: u64, outcome: &str| app_v1::ExecuteQueryResponse {
4331                identity: Some(app_v1::QueryIdentity {
4332                    contract_lineage: "TicketDesk".to_owned(),
4333                    contract_version: 1,
4334                    contract_bundle_hash: vec![7; 32],
4335                    query_name: Some("TicketPage".to_owned()),
4336                    plan_hash: vec![2; 32],
4337                    module_hash: Some(vec![3; 32]),
4338                }),
4339                outcome: outcome.to_owned(),
4340                application_head: head,
4341                fields: Vec::new(),
4342                next_cursor: None,
4343                selected_result_encoding: app_v1::NamedResultEncoding::LegacyRecords as i32,
4344                compact_result: None,
4345                packed_result: None,
4346            };
4347            let standby_promoted =
4348                StableApplicationClient::scripted_for_test(active_contract([7; 32]))
4349                    .push_scripted_command(Ok(v1::ExecuteCommandResponse {
4350                        status: v1::execute_command_response::CompletionStatus::Committed as i32,
4351                        commit_sequence: 19,
4352                        contract_version: 1,
4353                        plan_hash: vec![0x44; 32],
4354                        outcome_type: "Created".to_owned(),
4355                        outcome: Some(v1::Value {
4356                            kind: Some(v1::value::Kind::NullValue(
4357                                v1::NullValue::NullValue as i32,
4358                            )),
4359                        }),
4360                        provenance_uri: String::new(),
4361                        durability_mode: String::new(),
4362                        outcome_uri: Some("riffdb://outcomes/19".to_owned()),
4363                        history_incarnation: 2,
4364                    }))
4365                    .push_scripted_query(Ok(query_response(50, "PrimaryCaughtUp")));
4366            let replica = StableApplicationClient::scripted_for_test(active_contract([7; 32]))
4367                .push_scripted_query(Ok(query_response(18, "ReplicaAvailable")))
4368                .push_scripted_query(Err(ClientError::Application(Box::new(
4369                    ApplicationError::new(
4370                        ApplicationErrorCode::FreshnessUnsatisfied,
4371                        ApplicationOperation::ExecuteQuery,
4372                        ApplicationErrorContext::empty(),
4373                        None,
4374                    ),
4375                ))));
4376
4377            assert!(matches!(
4378                primary.clone().with_standby_clients(vec![
4379                    standby_promoted.clone(),
4380                    standby_promoted.clone(),
4381                    standby_promoted.clone(),
4382                    standby_promoted.clone(),
4383                    standby_promoted.clone(),
4384                ]),
4385                Err(ApplicationClientError::InvalidInput)
4386            ));
4387            assert!(matches!(
4388                primary.clone().with_read_replica_clients(vec![
4389                    replica.clone(),
4390                    replica.clone(),
4391                    replica.clone(),
4392                    replica.clone(),
4393                    replica.clone(),
4394                ]),
4395                Err(ApplicationClientError::InvalidInput)
4396            ));
4397
4398            let mut client = primary
4399                .clone()
4400                .with_standby_clients(vec![standby_mismatched.clone(), standby_promoted.clone()])
4401                .expect("bounded standbys")
4402                .with_read_replica_clients(vec![replica.clone()])
4403                .expect("bounded replicas");
4404            assert_eq!(client.standby_client_count(), 2);
4405            assert_eq!(client.read_replica_client_count(), 1);
4406
4407            let metadata = CallMetadata::default();
4408            client
4409                .verify_active_contract("default", "TicketDesk", 1, [7; 32], &metadata)
4410                .await
4411                .expect("verify primary active contract");
4412
4413            let command = ApplicationCommand::new(
4414                "CreateTicket",
4415                Some(1),
4416                BTreeMap::from([(
4417                    "title".to_owned(),
4418                    ApplicationValue::String("Failover ticket".to_owned()),
4419                )]),
4420            )
4421            .expect("command");
4422            let result = client
4423                .execute_command(
4424                    command,
4425                    AttemptBudget::new(2).expect("attempt budget"),
4426                    &metadata,
4427                )
4428                .await
4429                .expect("failover command execution");
4430            assert_eq!(result.outcome.as_deref(), Some("Created"));
4431            assert_eq!(result.commit_sequence, Some(19));
4432            assert_eq!(standby_mismatched.scripted_command_inputs().len(), 0);
4433            assert_eq!(primary.scripted_command_inputs().len(), 1);
4434            assert_eq!(
4435                primary.scripted_command_inputs(),
4436                standby_promoted.scripted_command_inputs(),
4437                "standby retry must preserve the exact command input and idempotency key from the fenced primary attempt"
4438            );
4439
4440            let base_query = NamedQuery::new(
4441                ApplicationContract::Exact {
4442                    lineage: "TicketDesk".to_owned(),
4443                    version: 1,
4444                    bundle_hash: Some([7; 32]),
4445                },
4446                "TicketPage",
4447                Some([3; 32]),
4448                BTreeMap::new(),
4449                None,
4450            )
4451            .expect("query")
4452            .expect_plan_hash([2; 32]);
4453
4454            let replica_result = client
4455                .execute_named_query(base_query.clone(), &metadata)
4456                .await
4457                .expect("replica query");
4458            assert_eq!(replica_result.outcome, "ReplicaAvailable");
4459            assert_eq!(replica.scripted_queries().len(), 1);
4460            assert_eq!(standby_promoted.scripted_queries().len(), 0);
4461
4462            let causal_query = base_query
4463                .with_options(QueryOptions::new().read_after_commit(50))
4464                .expect("causal query");
4465            let fallback_result = client
4466                .execute_named_query(causal_query, &metadata)
4467                .await
4468                .expect("fallback to promoted primary on replica lag");
4469            assert_eq!(fallback_result.outcome, "PrimaryCaughtUp");
4470            assert_eq!(replica.scripted_queries().len(), 2);
4471            assert_eq!(standby_promoted.scripted_queries().len(), 1);
4472        });
4473    }
4474}