diff --git a/Cargo.toml b/Cargo.toml index 1024dd6..cb49e32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "3" [workspace.package] # TODO: idk how to do the name, fix later # name = "komp_ac" -version = "0.8.26" +version = "0.8.27" edition = "2024" license = "GPL-3.0-or-later" authors = ["Filip Priečinský "] diff --git a/client b/client index a0695c0..f045b8f 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit a0695c0d27c97c4f6215c6431239002ffda8e35b +Subproject commit f045b8fa8eb65797b929f951bb255464bc553666 diff --git a/common/build.rs b/common/build.rs index a58bd0d..5c2f890 100644 --- a/common/build.rs +++ b/common/build.rs @@ -216,6 +216,7 @@ fn main() -> Result<(), Box> { .compile_protos( &[ "proto/common.proto", + "proto/currency_conversion.proto", "proto/document_data.proto", "proto/adresar.proto", "proto/analytics.proto", @@ -242,6 +243,7 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=build.rs"); for proto in [ "proto/common.proto", + "proto/currency_conversion.proto", "proto/document_data.proto", "proto/adresar.proto", "proto/analytics.proto", diff --git a/common/proto/currency_conversion.proto b/common/proto/currency_conversion.proto new file mode 100644 index 0000000..59222ff --- /dev/null +++ b/common/proto/currency_conversion.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; +package komp_ac.currency_conversion; + +service CurrencyConversion { + rpc ConvertCurrency(ConvertCurrencyRequest) returns (ConvertCurrencyResponse); + rpc ResolveRate(ResolveRateRequest) returns (ResolvedFxRate); + rpc ListRequiredCurrencies(ListRequiredCurrenciesRequest) + returns (ListRequiredCurrenciesResponse); +} + +message ConvertCurrencyRequest { + string amount = 1; + string source_currency = 2; + string target_currency = 3; + string source = 4; + string as_of_date = 5; +} + +message ResolveRateRequest { + string source_currency = 1; + string target_currency = 2; + string source = 3; + string as_of_date = 4; +} + +message FxRateComponent { + string currency = 1; + string units_per_base = 2; +} + +message ResolvedFxRate { + string source_currency = 1; + string target_currency = 2; + string rate = 3; + string effective_date = 4; + string source = 5; + int64 rate_set_id = 6; + int32 revision = 7; + string base_currency = 8; + repeated FxRateComponent components = 9; +} + +message ConvertCurrencyResponse { + string amount = 1; + string currency = 2; + ResolvedFxRate resolved_rate = 3; +} + +message ListRequiredCurrenciesRequest {} + +message ListRequiredCurrenciesResponse { + repeated string currencies = 1; +} diff --git a/common/proto/table_script.proto b/common/proto/table_script.proto index 65d7d2c..fd795f0 100644 --- a/common/proto/table_script.proto +++ b/common/proto/table_script.proto @@ -63,9 +63,9 @@ service TableScript { // // The server derives the required inputs from stored script_dependencies; // callers do not choose arbitrary tables or columns. Direct related-column - // reads are grouped by related row, while related aggregates are evaluated - // through the analytics runtime. Returned values include logical type and - // currency metadata so the client can create correctly typed ScriptValues. + // reads and related aggregates are evaluated through SQLx in one read-only + // PostgreSQL snapshot. Returned values include logical type and currency + // metadata so the client can create correctly typed ScriptValues. // // Current-table column values are intentionally not returned. The client // supplies those directly from row_data so unsaved edits participate in @@ -185,6 +185,14 @@ message ScriptDependency { string operation = 5; // Relationship table used to match the owner row to the related collection. string via_table = 6; + // Source currency for an fx_conversion dependency. + string from_currency = 7; + // Target currency for an fx_conversion dependency. + string to_currency = 8; + // Persisted exchange-rate provider/source identifier. + string source = 9; + // Requested YYYY-MM-DD date, or empty for the latest stored snapshot. + string as_of_date = 10; } // Identifies the active form row whose external Steel inputs must be hydrated. @@ -243,4 +251,18 @@ message HydrateScriptDependenciesResponse { repeated HydratedColumnValue columns = 1; // Exact related aggregate inputs declared by stored scripts. repeated HydratedAggregateValue aggregates = 2; + // Exact resolved rates declared by the table's stored scripts. + repeated HydratedFxRate fx_rates = 3; +} + +message HydratedFxRate { + string from_currency = 1; + string to_currency = 2; + string source = 3; + string requested_as_of_date = 4; + string rate = 5; + string effective_date = 6; + int64 rate_set_id = 7; + int32 revision = 8; + string base_currency = 9; } diff --git a/common/src/lib.rs b/common/src/lib.rs index edba9b1..b975297 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -20,6 +20,9 @@ pub mod proto { pub mod common { include!("proto/komp_ac.common.rs"); } + pub mod currency_conversion { + include!("proto/komp_ac.currency_conversion.rs"); + } pub mod document_data { include!("proto/komp_ac.document_data.rs"); } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 2f815c0..a78b801 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.currency_conversion.rs b/common/src/proto/komp_ac.currency_conversion.rs new file mode 100644 index 0000000..08896f2 --- /dev/null +++ b/common/src/proto/komp_ac.currency_conversion.rs @@ -0,0 +1,535 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ConvertCurrencyRequest { + #[prost(string, tag = "1")] + pub amount: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub source_currency: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub target_currency: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub source: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub as_of_date: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ResolveRateRequest { + #[prost(string, tag = "1")] + pub source_currency: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub target_currency: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub source: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub as_of_date: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct FxRateComponent { + #[prost(string, tag = "1")] + pub currency: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub units_per_base: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResolvedFxRate { + #[prost(string, tag = "1")] + pub source_currency: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub target_currency: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub rate: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub effective_date: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub source: ::prost::alloc::string::String, + #[prost(int64, tag = "6")] + pub rate_set_id: i64, + #[prost(int32, tag = "7")] + pub revision: i32, + #[prost(string, tag = "8")] + pub base_currency: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "9")] + pub components: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConvertCurrencyResponse { + #[prost(string, tag = "1")] + pub amount: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub currency: ::prost::alloc::string::String, + #[prost(message, optional, tag = "3")] + pub resolved_rate: ::core::option::Option, +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListRequiredCurrenciesRequest {} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListRequiredCurrenciesResponse { + #[prost(string, repeated, tag = "1")] + pub currencies: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +/// Generated client implementations. +pub mod currency_conversion_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct CurrencyConversionClient { + inner: tonic::client::Grpc, + } + impl CurrencyConversionClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl CurrencyConversionClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> CurrencyConversionClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + CurrencyConversionClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn convert_currency( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.currency_conversion.CurrencyConversion/ConvertCurrency", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.currency_conversion.CurrencyConversion", + "ConvertCurrency", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn resolve_rate( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.currency_conversion.CurrencyConversion/ResolveRate", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.currency_conversion.CurrencyConversion", + "ResolveRate", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn list_required_currencies( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.currency_conversion.CurrencyConversion/ListRequiredCurrencies", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.currency_conversion.CurrencyConversion", + "ListRequiredCurrencies", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod currency_conversion_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with CurrencyConversionServer. + #[async_trait] + pub trait CurrencyConversion: std::marker::Send + std::marker::Sync + 'static { + async fn convert_currency( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn resolve_rate( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn list_required_currencies( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + #[derive(Debug)] + pub struct CurrencyConversionServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl CurrencyConversionServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for CurrencyConversionServer + where + T: CurrencyConversion, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/komp_ac.currency_conversion.CurrencyConversion/ConvertCurrency" => { + #[allow(non_camel_case_types)] + struct ConvertCurrencySvc(pub Arc); + impl< + T: CurrencyConversion, + > tonic::server::UnaryService + for ConvertCurrencySvc { + type Response = super::ConvertCurrencyResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::convert_currency(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ConvertCurrencySvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.currency_conversion.CurrencyConversion/ResolveRate" => { + #[allow(non_camel_case_types)] + struct ResolveRateSvc(pub Arc); + impl< + T: CurrencyConversion, + > tonic::server::UnaryService + for ResolveRateSvc { + type Response = super::ResolvedFxRate; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::resolve_rate(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ResolveRateSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.currency_conversion.CurrencyConversion/ListRequiredCurrencies" => { + #[allow(non_camel_case_types)] + struct ListRequiredCurrenciesSvc(pub Arc); + impl< + T: CurrencyConversion, + > tonic::server::UnaryService + for ListRequiredCurrenciesSvc { + type Response = super::ListRequiredCurrenciesResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_required_currencies( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListRequiredCurrenciesSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for CurrencyConversionServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "komp_ac.currency_conversion.CurrencyConversion"; + impl tonic::server::NamedService for CurrencyConversionServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/common/src/proto/komp_ac.table_script.rs b/common/src/proto/komp_ac.table_script.rs index de0b87b..6e30b98 100644 --- a/common/src/proto/komp_ac.table_script.rs +++ b/common/src/proto/komp_ac.table_script.rs @@ -133,6 +133,18 @@ pub struct ScriptDependency { /// Relationship table used to match the owner row to the related collection. #[prost(string, tag = "6")] pub via_table: ::prost::alloc::string::String, + /// Source currency for an fx_conversion dependency. + #[prost(string, tag = "7")] + pub from_currency: ::prost::alloc::string::String, + /// Target currency for an fx_conversion dependency. + #[prost(string, tag = "8")] + pub to_currency: ::prost::alloc::string::String, + /// Persisted exchange-rate provider/source identifier. + #[prost(string, tag = "9")] + pub source: ::prost::alloc::string::String, + /// Requested YYYY-MM-DD date, or empty for the latest stored snapshot. + #[prost(string, tag = "10")] + pub as_of_date: ::prost::alloc::string::String, } /// Identifies the active form row whose external Steel inputs must be hydrated. #[derive(Clone, PartialEq, ::prost::Message)] @@ -213,6 +225,30 @@ pub struct HydrateScriptDependenciesResponse { /// Exact related aggregate inputs declared by stored scripts. #[prost(message, repeated, tag = "2")] pub aggregates: ::prost::alloc::vec::Vec, + /// Exact resolved rates declared by the table's stored scripts. + #[prost(message, repeated, tag = "3")] + pub fx_rates: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct HydratedFxRate { + #[prost(string, tag = "1")] + pub from_currency: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub to_currency: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub source: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub requested_as_of_date: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub rate: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub effective_date: ::prost::alloc::string::String, + #[prost(int64, tag = "7")] + pub rate_set_id: i64, + #[prost(int32, tag = "8")] + pub revision: i32, + #[prost(string, tag = "9")] + pub base_currency: ::prost::alloc::string::String, } /// Generated client implementations. pub mod table_script_client { @@ -423,9 +459,9 @@ pub mod table_script_client { /// /// The server derives the required inputs from stored script_dependencies; /// callers do not choose arbitrary tables or columns. Direct related-column - /// reads are grouped by related row, while related aggregates are evaluated - /// through the analytics runtime. Returned values include logical type and - /// currency metadata so the client can create correctly typed ScriptValues. + /// reads and related aggregates are evaluated through SQLx in one read-only + /// PostgreSQL snapshot. Returned values include logical type and currency + /// metadata so the client can create correctly typed ScriptValues. /// /// Current-table column values are intentionally not returned. The client /// supplies those directly from row_data so unsaved edits participate in @@ -526,9 +562,9 @@ pub mod table_script_server { /// /// The server derives the required inputs from stored script_dependencies; /// callers do not choose arbitrary tables or columns. Direct related-column - /// reads are grouped by related row, while related aggregates are evaluated - /// through the analytics runtime. Returned values include logical type and - /// currency metadata so the client can create correctly typed ScriptValues. + /// reads and related aggregates are evaluated through SQLx in one read-only + /// PostgreSQL snapshot. Returned values include logical type and currency + /// metadata so the client can create correctly typed ScriptValues. /// /// Current-table column values are intentionally not returned. The client /// supplies those directly from row_data so unsaved edits participate in diff --git a/server b/server index 8a9306c..fb2cecc 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 8a9306cd000990ba3dda5daaf63d84ad776ecf4a +Subproject commit fb2cecc1397e04477582310fd5b5afdaf6b249da