diff --git a/Cargo.lock b/Cargo.lock index 8ae516c..b5957a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4148,6 +4148,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-money" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ac0a3274868e5d2615e749ad3b944d93cf846deefd46ea918a225e5d53856d" +dependencies = [ + "rust_decimal", +] + [[package]] name = "ryu" version = "1.0.23" @@ -4294,6 +4303,7 @@ dependencies = [ "rust-stemmers", "rust_decimal", "rust_decimal_macros", + "rusty-money", "search", "serde", "serde_json", diff --git a/client b/client index 8954ca8..4fb2df8 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 8954ca848390ece033c1ad316fa39c1dce93abd4 +Subproject commit 4fb2df8b0251667fba0976e465266f9b9856d703 diff --git a/common/build.rs b/common/build.rs index 1d71c08..02cc87c 100644 --- a/common/build.rs +++ b/common/build.rs @@ -169,6 +169,18 @@ fn main() -> Result<(), Box> { ".komp_ac.table_definition.AddTableColumnsRequest", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + ".komp_ac.table_definition.MoneyColumnOptions", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_definition.PostMoneyTableDefinitionRequest", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_definition.AddMoneyTableColumnsRequest", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) .type_attribute( ".komp_ac.table_definition.TableDefinitionResponse", "#[derive(serde::Serialize, serde::Deserialize)]" diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index bcf966b..5995529 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -14,10 +14,16 @@ service TableDefinition { // Also inserts metadata and default validation rules. Entirely transactional. rpc PostTableDefinition(PostTableDefinitionRequest) returns (TableDefinitionResponse); + // Creates a table using the money-aware definition contract. + rpc PostMoneyTableDefinition(PostMoneyTableDefinitionRequest) returns (TableDefinitionResponse); + // Appends new user-defined columns to an existing table. // Existing columns, links, and table logic are never changed by this call. rpc AddTableColumns(AddTableColumnsRequest) returns (TableDefinitionResponse); + // Adds columns using the money-aware definition contract. + rpc AddMoneyTableColumns(AddMoneyTableColumnsRequest) returns (TableDefinitionResponse); + // Lists all profiles (schemas) and their tables with declared dependencies. // This provides a tree-like overview of table relationships. rpc GetProfileTree(komp_ac.common.Empty) returns (ProfileTreeResponse); @@ -77,6 +83,13 @@ message PostTableDefinitionRequest { // Same naming rules as table_name; cannot collide with reserved schemas // like "public", "information_schema", or ones starting with "pg_". string profile_name = 5; + +} + +message PostMoneyTableDefinitionRequest { + PostTableDefinitionRequest definition = 1; + string base_currency = 2; + repeated MoneyColumnOptions money_columns = 3; } // Defines append-only column additions for an existing table. @@ -92,6 +105,24 @@ message AddTableColumnsRequest { // Optional indexes for the new columns only. repeated string indexes = 4; + +} + +message AddMoneyTableColumnsRequest { + AddTableColumnsRequest definition = 1; + repeated MoneyColumnOptions money_columns = 2; + string base_currency = 3; +} + +enum MoneyRounding { + MONEY_ROUNDING_NONE = 0; + MONEY_ROUNDING_HALF_UP = 1; +} + +message MoneyColumnOptions { + string column_name = 1; + bool immutable = 2; + MoneyRounding rounding = 3; } // Describes one user-defined column for a table. @@ -102,12 +133,12 @@ message ColumnDefinition { string name = 1; // Logical column type. Supported values (case-insensitive): - // TEXT / STRING + // TEXT // BOOLEAN - // TIMESTAMP / TIMESTAMPTZ / TIME - // MONEY (= NUMERIC(14,4)) - // INTEGER / INT - // BIGINTEGER / BIGINT + // TIMESTAMPTZ + // MONEY (= unconstrained NUMERIC; currency comes from the table) + // INT + // BIGINT // DATE // DECIMAL(p,s) → NUMERIC(p,s) // DECIMAL args must be integers (no sign, no dot, no leading zeros); @@ -209,6 +240,8 @@ message TableDetail { int64 id = 2; repeated ColumnDefinition columns = 3; repeated ScriptInfo scripts = 4; + string base_currency = 5; + repeated MoneyColumnOptions money_columns = 6; } // A script that targets a specific column in a table. diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index b99ee32..79df9f5 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.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index 482afe5..3649e2b 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -43,6 +43,16 @@ pub struct PostTableDefinitionRequest { #[prost(string, tag = "5")] pub profile_name: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostMoneyTableDefinitionRequest { + #[prost(message, optional, tag = "1")] + pub definition: ::core::option::Option, + #[prost(string, tag = "2")] + pub base_currency: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub money_columns: ::prost::alloc::vec::Vec, +} /// Defines append-only column additions for an existing table. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -60,6 +70,26 @@ pub struct AddTableColumnsRequest { #[prost(string, repeated, tag = "4")] pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddMoneyTableColumnsRequest { + #[prost(message, optional, tag = "1")] + pub definition: ::core::option::Option, + #[prost(message, repeated, tag = "2")] + pub money_columns: ::prost::alloc::vec::Vec, + #[prost(string, tag = "3")] + pub base_currency: ::prost::alloc::string::String, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct MoneyColumnOptions { + #[prost(string, tag = "1")] + pub column_name: ::prost::alloc::string::String, + #[prost(bool, tag = "2")] + pub immutable: bool, + #[prost(enumeration = "MoneyRounding", tag = "3")] + pub rounding: i32, +} /// Describes one user-defined column for a table. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -70,12 +100,12 @@ pub struct ColumnDefinition { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, /// Logical column type. Supported values (case-insensitive): - /// TEXT / STRING + /// TEXT /// BOOLEAN - /// TIMESTAMP / TIMESTAMPTZ / TIME - /// MONEY (= NUMERIC(14,4)) - /// INTEGER / INT - /// BIGINTEGER / BIGINT + /// TIMESTAMPTZ + /// MONEY (= unconstrained NUMERIC; currency comes from the table) + /// INT + /// BIGINT /// DATE /// DECIMAL(p,s) → NUMERIC(p,s) /// DECIMAL args must be integers (no sign, no dot, no leading zeros); @@ -215,6 +245,10 @@ pub struct TableDetail { pub columns: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag = "4")] pub scripts: ::prost::alloc::vec::Vec, + #[prost(string, tag = "5")] + pub base_currency: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "6")] + pub money_columns: ::prost::alloc::vec::Vec, } /// A script that targets a specific column in a table. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -273,6 +307,32 @@ pub struct DeleteTableResponse { #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum MoneyRounding { + None = 0, + HalfUp = 1, +} +impl MoneyRounding { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::None => "MONEY_ROUNDING_NONE", + Self::HalfUp => "MONEY_ROUNDING_HALF_UP", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "MONEY_ROUNDING_NONE" => Some(Self::None), + "MONEY_ROUNDING_HALF_UP" => Some(Self::HalfUp), + _ => None, + } + } +} /// Generated client implementations. pub mod table_definition_client { #![allow( @@ -400,6 +460,36 @@ pub mod table_definition_client { ); self.inner.unary(req, path, codec).await } + /// Creates a table using the money-aware definition contract. + pub async fn post_money_table_definition( + &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.table_definition.TableDefinition/PostMoneyTableDefinition", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_definition.TableDefinition", + "PostMoneyTableDefinition", + ), + ); + self.inner.unary(req, path, codec).await + } /// Appends new user-defined columns to an existing table. /// Existing columns, links, and table logic are never changed by this call. pub async fn add_table_columns( @@ -431,6 +521,36 @@ pub mod table_definition_client { ); self.inner.unary(req, path, codec).await } + /// Adds columns using the money-aware definition contract. + pub async fn add_money_table_columns( + &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.table_definition.TableDefinition/AddMoneyTableColumns", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_definition.TableDefinition", + "AddMoneyTableColumns", + ), + ); + self.inner.unary(req, path, codec).await + } /// Lists all profiles (schemas) and their tables with declared dependencies. /// This provides a tree-like overview of table relationships. pub async fn get_profile_tree( @@ -638,6 +758,14 @@ pub mod table_definition_server { tonic::Response, tonic::Status, >; + /// Creates a table using the money-aware definition contract. + async fn post_money_table_definition( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Appends new user-defined columns to an existing table. /// Existing columns, links, and table logic are never changed by this call. async fn add_table_columns( @@ -647,6 +775,14 @@ pub mod table_definition_server { tonic::Response, tonic::Status, >; + /// Adds columns using the money-aware definition contract. + async fn add_money_table_columns( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Lists all profiles (schemas) and their tables with declared dependencies. /// This provides a tree-like overview of table relationships. async fn get_profile_tree( @@ -827,6 +963,57 @@ pub mod table_definition_server { }; Box::pin(fut) } + "/komp_ac.table_definition.TableDefinition/PostMoneyTableDefinition" => { + #[allow(non_camel_case_types)] + struct PostMoneyTableDefinitionSvc(pub Arc); + impl< + T: TableDefinition, + > tonic::server::UnaryService + for PostMoneyTableDefinitionSvc { + type Response = super::TableDefinitionResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::PostMoneyTableDefinitionRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::post_money_table_definition( + &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 = PostMoneyTableDefinitionSvc(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.table_definition.TableDefinition/AddTableColumns" => { #[allow(non_camel_case_types)] struct AddTableColumnsSvc(pub Arc); @@ -873,6 +1060,55 @@ pub mod table_definition_server { }; Box::pin(fut) } + "/komp_ac.table_definition.TableDefinition/AddMoneyTableColumns" => { + #[allow(non_camel_case_types)] + struct AddMoneyTableColumnsSvc(pub Arc); + impl< + T: TableDefinition, + > tonic::server::UnaryService + for AddMoneyTableColumnsSvc { + type Response = super::TableDefinitionResponse; + 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 { + ::add_money_table_columns( + &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 = AddMoneyTableColumnsSvc(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.table_definition.TableDefinition/GetProfileTree" => { #[allow(non_camel_case_types)] struct GetProfileTreeSvc(pub Arc); diff --git a/server b/server index d97b28f..de3235a 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit d97b28ff62579b9547d020f1e9a1da245c59db62 +Subproject commit de3235a13c2966c2c4ffab356d7d9a3aaa5ce05a