From d5bce6378a556e668daf7b9f928a8f12e1131265 Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 17 Jul 2026 22:46:57 +0200 Subject: [PATCH] optimization on the client steel scripts + fixed proto comments --- client | 2 +- common/proto/adresar.proto | 1 - common/proto/analytics.proto | 2 +- common/proto/search2.proto | 2 +- common/proto/table_definition.proto | 2 +- common/proto/table_script.proto | 110 ++++++++++-- common/src/proto/descriptor.bin | Bin 100599 -> 105788 bytes common/src/proto/komp_ac.analytics.rs | 2 +- common/src/proto/komp_ac.search2.rs | 2 +- common/src/proto/komp_ac.table_definition.rs | 2 +- common/src/proto/komp_ac.table_script.rs | 177 ++++++++++++++++--- server | 2 +- 12 files changed, 253 insertions(+), 51 deletions(-) diff --git a/client b/client index 2549893..a0695c0 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 254989329acf6b31909e633fb1f1b0def457b5c5 +Subproject commit a0695c0d27c97c4f6215c6431239002ffda8e35b diff --git a/common/proto/adresar.proto b/common/proto/adresar.proto index 00094be..d918ad5 100644 --- a/common/proto/adresar.proto +++ b/common/proto/adresar.proto @@ -3,7 +3,6 @@ syntax = "proto3"; package komp_ac.adresar; import "common.proto"; -// import "table_structure.proto"; service Adresar { rpc PostAdresar(PostAdresarRequest) returns (AdresarResponse); diff --git a/common/proto/analytics.proto b/common/proto/analytics.proto index 5733c2d..898e7f9 100644 --- a/common/proto/analytics.proto +++ b/common/proto/analytics.proto @@ -55,7 +55,7 @@ message ExecuteAnalyticsQueryRequest { // Exactly one read-only SELECT query. Table and column names are public aliases. string sql = 2; - // Optional result cap. Zero uses the server default; the server maximum still applies. + // Result cap. Zero uses the server default; the server maximum still applies. uint32 max_rows = 3; } diff --git a/common/proto/search2.proto b/common/proto/search2.proto index 0b2b4bf..5becf89 100644 --- a/common/proto/search2.proto +++ b/common/proto/search2.proto @@ -10,7 +10,7 @@ message Search2Request { string profile_name = 1; string table_name = 2; repeated ColumnFilter column_filters = 3; - optional string text_query = 4; // Optional fallback text search + optional string text_query = 4; // Fallback text search optional int32 limit = 5; optional string order_by = 6; optional bool order_desc = 7; diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index 4306755..75f802b 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -210,7 +210,7 @@ message CopyProfileResponse { message GetColumnAliasRenameHistoryRequest { string profile_name = 1; - // Optional filter. When omitted, returns all tables in the profile. + // Filter. When omitted, returns all tables in the profile. optional int64 table_definition_id = 2; } diff --git a/common/proto/table_script.proto b/common/proto/table_script.proto index 5713890..65d7d2c 100644 --- a/common/proto/table_script.proto +++ b/common/proto/table_script.proto @@ -2,7 +2,8 @@ syntax = "proto3"; package komp_ac.table_script; -// Manages column-computation scripts for user-defined tables. +// Manages column-computation scripts for user-defined tables and supplies the +// dependency data used by the client-side Steel runtime. // Each script belongs to a single table (table_definition_id) and populates // exactly one target column in that table. The server: // - Validates script syntax (non-empty, balanced parentheses, starts with '(') @@ -13,7 +14,16 @@ package komp_ac.table_script; // - Analyzes dependencies and prevents cycles across the schema // - Transforms the script to decimal-safe math (steel_decimal) // - Upserts into table_scripts and records dependencies in script_dependencies -// The whole operation is transactional. +// - Hydrates external column and aggregate inputs requested by the client +// +// The client fetches stored scripts and their declared dependencies, builds a +// restricted Steel context, and executes scripts for immediate form feedback. +// Current-table values come from the client's active row snapshot. External +// values must come from HydrateScriptDependencies; the client must not query +// arbitrary database data from inside the Steel VM. +// +// Server-side persistence remains authoritative and recalculates affected rows. +// Script creation and update are transactional. service TableScript { // Create or update a script for a specific table and target column. // @@ -41,10 +51,28 @@ service TableScript { // - Returns the stored, transformed script from table_scripts // - Includes normalized dependency metadata from script_dependencies // - Returns an empty scripts list when the table has no scripts + // + // Client use: + // - Registers each client-evaluable script with the computed-field runtime + // - Uses dependencies as the allowlist for values exposed to the Steel VM + // - Uses target_column_type to validate and convert the script result rpc GetTableScripts(GetTableScriptsRequest) returns (GetTableScriptsResponse); - rpc GetScriptDependencyValues(GetScriptDependencyValuesRequest) - returns (GetScriptDependencyValuesResponse); + // Build the external data snapshot needed to execute a table's scripts in + // the client-side Steel runtime. + // + // 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. + // + // Current-table column values are intentionally not returned. The client + // supplies those directly from row_data so unsaved edits participate in + // immediate calculations. The response replaces the client's previous + // script dependency cache as one complete hydration snapshot. + rpc HydrateScriptDependencies(HydrateScriptDependenciesRequest) + returns (HydrateScriptDependenciesResponse); } // Request to create or update a script bound to a specific table and column. @@ -77,8 +105,10 @@ message PostTableScriptRequest { // @sum(table.column), @min(table.column), @max(table.column), // @count(table.column), @count_distinct(table.column), // @any(table.boolean), @all(table.boolean), - // @count_rows(table), @exists(table) - // The related table must be a direct child or share exactly one parent. + // @count_rows(table via anchor), @exists(table via anchor) + // Every related aggregate requires the `via anchor` clause. The related + // table must be reachable through that anchor by an unambiguous FK path; + // the path may span multiple FK hops. // - Raw SQL access is not supported; steel_query_sql is rejected // // Math operations: @@ -120,45 +150,97 @@ message GetTableScriptsRequest { } message GetTableScriptsResponse { + // Scripts and dependency allowlists used to configure the client Steel runtime. repeated StoredTableScript scripts = 1; } message StoredTableScript { + // Persistent script identifier. int64 id = 1; + // Display-name key of the current-table field populated by this script. string target_column = 2; + // Logical type used by the client to validate and convert the result. string target_column_type = 3; + // Validated and transformed Steel expression executed by the client for + // immediate feedback and by the server for authoritative persistence. string script = 4; string description = 5; + // Complete allowlist of data inputs that may be exposed to this script. repeated ScriptDependency dependencies = 6; + // Whether changes to dependency rows trigger authoritative server propagation. bool recompute_on_dependency_change = 7; } message ScriptDependency { + // Logical table name referenced by the script. string target_table = 1; + // Normalized dependency kind, such as column_access or related_aggregate. string dependency_type = 2; + // Logical column name. Empty for aggregates that operate on rows only. string column = 3; // Deprecated legacy field. Raw SQL dependencies are no longer produced. string query_fragment = 4; + // Aggregate operation name, such as sum, count_rows, or exists; empty for + // column_access dependencies. string operation = 5; // Relationship table used to match the owner row to the related collection. string via_table = 6; } -message GetScriptDependencyValuesRequest { +// Identifies the active form row whose external Steel inputs must be hydrated. +message HydrateScriptDependenciesRequest { + // Required profile/database schema containing the scripted table. string profile_name = 1; + // Required logical name of the table owning the scripts. string table_name = 2; + // Persisted owner-row ID, or zero for a new unsaved row. int64 row_id = 3; + // Complete current client form snapshot keyed by logical column name. + // It contains unsaved current-table values and related foreign keys such as + // "customer_id". The server uses it to resolve related rows and new-row + // aggregate semantics; it is not an arbitrary dependency request. map row_data = 4; } -message ScriptDependencyValue { - string operation = 1; - string target_table = 2; +// One declared cross-table column input for the client Steel context. +message HydratedColumnValue { + // Logical related-table name used by steel_get_column. + string target_table = 1; + // Related row from which the value was loaded. + int64 row_id = 2; + // Logical column name used by steel_get_column. string column = 3; - string via_table = 4; - string value = 5; + // String-encoded database value. An empty string represents NULL/empty input. + string value = 4; + // Logical database type used to create a typed client ScriptValue. + string field_type = 5; + // Related table's base currency for MONEY values; otherwise empty. + string base_currency = 6; } -message GetScriptDependencyValuesResponse { - repeated ScriptDependencyValue values = 1; +// One declared related-collection aggregate input for the client Steel context. +message HydratedAggregateValue { + // Normalized aggregate operation: sum, min, max, count, count_distinct, + // any, all, count_rows, or exists. + string operation = 1; + // Logical table whose related rows were aggregated. + string target_table = 2; + // Logical aggregated column; empty for count_rows and exists. + string column = 3; + // Relationship anchor used to distinguish aggregate dependency paths. + string via_table = 4; + // String-encoded aggregate result. + string value = 5; + // Logical source-column type; empty for row-only aggregates. + string field_type = 6; + // Aggregate table's base currency for MONEY values; otherwise empty. + string base_currency = 7; +} + +// Complete external dependency snapshot for client-side Steel execution. +message HydrateScriptDependenciesResponse { + // Exact related-column inputs declared by stored scripts. + repeated HydratedColumnValue columns = 1; + // Exact related aggregate inputs declared by stored scripts. + repeated HydratedAggregateValue aggregates = 2; } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 7c46e25c6e8ea61e338586fc757bfc0f6a957f46..2f815c0ee5b419601b9a35390566d4cae9873b6c 100644 GIT binary patch delta 11398 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 O5Nivy%m4cN_5T7sOIz;% diff --git a/common/src/proto/komp_ac.analytics.rs b/common/src/proto/komp_ac.analytics.rs index 4e1f78d..8d6a273 100644 --- a/common/src/proto/komp_ac.analytics.rs +++ b/common/src/proto/komp_ac.analytics.rs @@ -55,7 +55,7 @@ pub struct ExecuteAnalyticsQueryRequest { /// Exactly one read-only SELECT query. Table and column names are public aliases. #[prost(string, tag = "2")] pub sql: ::prost::alloc::string::String, - /// Optional result cap. Zero uses the server default; the server maximum still applies. + /// Result cap. Zero uses the server default; the server maximum still applies. #[prost(uint32, tag = "3")] pub max_rows: u32, } diff --git a/common/src/proto/komp_ac.search2.rs b/common/src/proto/komp_ac.search2.rs index c532813..c006812 100644 --- a/common/src/proto/komp_ac.search2.rs +++ b/common/src/proto/komp_ac.search2.rs @@ -7,7 +7,7 @@ pub struct Search2Request { pub table_name: ::prost::alloc::string::String, #[prost(message, repeated, tag = "3")] pub column_filters: ::prost::alloc::vec::Vec, - /// Optional fallback text search + /// Fallback text search #[prost(string, optional, tag = "4")] pub text_query: ::core::option::Option<::prost::alloc::string::String>, #[prost(int32, optional, tag = "5")] diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index 0835ab8..2f3565b 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -193,7 +193,7 @@ pub struct CopyProfileResponse { pub struct GetColumnAliasRenameHistoryRequest { #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, - /// Optional filter. When omitted, returns all tables in the profile. + /// Filter. When omitted, returns all tables in the profile. #[prost(int64, optional, tag = "2")] pub table_definition_id: ::core::option::Option, } diff --git a/common/src/proto/komp_ac.table_script.rs b/common/src/proto/komp_ac.table_script.rs index 2f06491..de0b87b 100644 --- a/common/src/proto/komp_ac.table_script.rs +++ b/common/src/proto/komp_ac.table_script.rs @@ -33,8 +33,10 @@ pub struct PostTableScriptRequest { /// @sum(table.column), @min(table.column), @max(table.column), /// @count(table.column), @count_distinct(table.column), /// @any(table.boolean), @all(table.boolean), - /// @count_rows(table), @exists(table) - /// The related table must be a direct child or share exactly one parent. + /// @count_rows(table via anchor), @exists(table via anchor) + /// Every related aggregate requires the `via anchor` clause. The related + /// table must be reachable through that anchor by an unambiguous FK path; + /// the path may span multiple FK hops. /// * Raw SQL access is not supported; steel_query_sql is rejected /// /// Math operations: @@ -82,74 +84,135 @@ pub struct GetTableScriptsRequest { } #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableScriptsResponse { + /// Scripts and dependency allowlists used to configure the client Steel runtime. #[prost(message, repeated, tag = "1")] pub scripts: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct StoredTableScript { + /// Persistent script identifier. #[prost(int64, tag = "1")] pub id: i64, + /// Display-name key of the current-table field populated by this script. #[prost(string, tag = "2")] pub target_column: ::prost::alloc::string::String, + /// Logical type used by the client to validate and convert the result. #[prost(string, tag = "3")] pub target_column_type: ::prost::alloc::string::String, + /// Validated and transformed Steel expression executed by the client for + /// immediate feedback and by the server for authoritative persistence. #[prost(string, tag = "4")] pub script: ::prost::alloc::string::String, #[prost(string, tag = "5")] pub description: ::prost::alloc::string::String, + /// Complete allowlist of data inputs that may be exposed to this script. #[prost(message, repeated, tag = "6")] pub dependencies: ::prost::alloc::vec::Vec, + /// Whether changes to dependency rows trigger authoritative server propagation. #[prost(bool, tag = "7")] pub recompute_on_dependency_change: bool, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ScriptDependency { + /// Logical table name referenced by the script. #[prost(string, tag = "1")] pub target_table: ::prost::alloc::string::String, + /// Normalized dependency kind, such as column_access or related_aggregate. #[prost(string, tag = "2")] pub dependency_type: ::prost::alloc::string::String, + /// Logical column name. Empty for aggregates that operate on rows only. #[prost(string, tag = "3")] pub column: ::prost::alloc::string::String, /// Deprecated legacy field. Raw SQL dependencies are no longer produced. #[prost(string, tag = "4")] pub query_fragment: ::prost::alloc::string::String, + /// Aggregate operation name, such as sum, count_rows, or exists; empty for + /// column_access dependencies. #[prost(string, tag = "5")] pub operation: ::prost::alloc::string::String, /// Relationship table used to match the owner row to the related collection. #[prost(string, tag = "6")] pub via_table: ::prost::alloc::string::String, } +/// Identifies the active form row whose external Steel inputs must be hydrated. #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetScriptDependencyValuesRequest { +pub struct HydrateScriptDependenciesRequest { + /// Required profile/database schema containing the scripted table. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Required logical name of the table owning the scripts. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, + /// Persisted owner-row ID, or zero for a new unsaved row. #[prost(int64, tag = "3")] pub row_id: i64, + /// Complete current client form snapshot keyed by logical column name. + /// It contains unsaved current-table values and related foreign keys such as + /// "customer_id". The server uses it to resolve related rows and new-row + /// aggregate semantics; it is not an arbitrary dependency request. #[prost(map = "string, string", tag = "4")] pub row_data: ::std::collections::HashMap< ::prost::alloc::string::String, ::prost::alloc::string::String, >, } +/// One declared cross-table column input for the client Steel context. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ScriptDependencyValue { +pub struct HydratedColumnValue { + /// Logical related-table name used by steel_get_column. #[prost(string, tag = "1")] - pub operation: ::prost::alloc::string::String, - #[prost(string, tag = "2")] pub target_table: ::prost::alloc::string::String, + /// Related row from which the value was loaded. + #[prost(int64, tag = "2")] + pub row_id: i64, + /// Logical column name used by steel_get_column. #[prost(string, tag = "3")] pub column: ::prost::alloc::string::String, + /// String-encoded database value. An empty string represents NULL/empty input. + #[prost(string, tag = "4")] + pub value: ::prost::alloc::string::String, + /// Logical database type used to create a typed client ScriptValue. + #[prost(string, tag = "5")] + pub field_type: ::prost::alloc::string::String, + /// Related table's base currency for MONEY values; otherwise empty. + #[prost(string, tag = "6")] + pub base_currency: ::prost::alloc::string::String, +} +/// One declared related-collection aggregate input for the client Steel context. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct HydratedAggregateValue { + /// Normalized aggregate operation: sum, min, max, count, count_distinct, + /// any, all, count_rows, or exists. + #[prost(string, tag = "1")] + pub operation: ::prost::alloc::string::String, + /// Logical table whose related rows were aggregated. + #[prost(string, tag = "2")] + pub target_table: ::prost::alloc::string::String, + /// Logical aggregated column; empty for count_rows and exists. + #[prost(string, tag = "3")] + pub column: ::prost::alloc::string::String, + /// Relationship anchor used to distinguish aggregate dependency paths. #[prost(string, tag = "4")] pub via_table: ::prost::alloc::string::String, + /// String-encoded aggregate result. #[prost(string, tag = "5")] pub value: ::prost::alloc::string::String, + /// Logical source-column type; empty for row-only aggregates. + #[prost(string, tag = "6")] + pub field_type: ::prost::alloc::string::String, + /// Aggregate table's base currency for MONEY values; otherwise empty. + #[prost(string, tag = "7")] + pub base_currency: ::prost::alloc::string::String, } +/// Complete external dependency snapshot for client-side Steel execution. #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetScriptDependencyValuesResponse { +pub struct HydrateScriptDependenciesResponse { + /// Exact related-column inputs declared by stored scripts. #[prost(message, repeated, tag = "1")] - pub values: ::prost::alloc::vec::Vec, + pub columns: ::prost::alloc::vec::Vec, + /// Exact related aggregate inputs declared by stored scripts. + #[prost(message, repeated, tag = "2")] + pub aggregates: ::prost::alloc::vec::Vec, } /// Generated client implementations. pub mod table_script_client { @@ -162,7 +225,8 @@ pub mod table_script_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; - /// Manages column-computation scripts for user-defined tables. + /// Manages column-computation scripts for user-defined tables and supplies the + /// dependency data used by the client-side Steel runtime. /// Each script belongs to a single table (table_definition_id) and populates /// exactly one target column in that table. The server: /// @@ -174,7 +238,16 @@ pub mod table_script_client { /// * Analyzes dependencies and prevents cycles across the schema /// * Transforms the script to decimal-safe math (steel_decimal) /// * Upserts into table_scripts and records dependencies in script_dependencies - /// The whole operation is transactional. + /// * Hydrates external column and aggregate inputs requested by the client + /// + /// The client fetches stored scripts and their declared dependencies, builds a + /// restricted Steel context, and executes scripts for immediate form feedback. + /// Current-table values come from the client's active row snapshot. External + /// values must come from HydrateScriptDependencies; the client must not query + /// arbitrary database data from inside the Steel VM. + /// + /// Server-side persistence remains authoritative and recalculates affected rows. + /// Script creation and update are transactional. #[derive(Debug, Clone)] pub struct TableScriptClient { inner: tonic::client::Grpc, @@ -310,6 +383,12 @@ pub mod table_script_client { /// * Returns the stored, transformed script from table_scripts /// * Includes normalized dependency metadata from script_dependencies /// * Returns an empty scripts list when the table has no scripts + /// + /// Client use: + /// + /// * Registers each client-evaluable script with the computed-field runtime + /// * Uses dependencies as the allowlist for values exposed to the Steel VM + /// * Uses target_column_type to validate and convert the script result pub async fn get_table_scripts( &mut self, request: impl tonic::IntoRequest, @@ -339,11 +418,24 @@ pub mod table_script_client { ); self.inner.unary(req, path, codec).await } - pub async fn get_script_dependency_values( + /// Build the external data snapshot needed to execute a table's scripts in + /// the client-side Steel runtime. + /// + /// 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. + /// + /// Current-table column values are intentionally not returned. The client + /// supplies those directly from row_data so unsaved edits participate in + /// immediate calculations. The response replaces the client's previous + /// script dependency cache as one complete hydration snapshot. + pub async fn hydrate_script_dependencies( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, > { self.inner @@ -356,14 +448,14 @@ pub mod table_script_client { })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( - "/komp_ac.table_script.TableScript/GetScriptDependencyValues", + "/komp_ac.table_script.TableScript/HydrateScriptDependencies", ); let mut req = request.into_request(); req.extensions_mut() .insert( GrpcMethod::new( "komp_ac.table_script.TableScript", - "GetScriptDependencyValues", + "HydrateScriptDependencies", ), ); self.inner.unary(req, path, codec).await @@ -416,6 +508,12 @@ pub mod table_script_server { /// * Returns the stored, transformed script from table_scripts /// * Includes normalized dependency metadata from script_dependencies /// * Returns an empty scripts list when the table has no scripts + /// + /// Client use: + /// + /// * Registers each client-evaluable script with the computed-field runtime + /// * Uses dependencies as the allowlist for values exposed to the Steel VM + /// * Uses target_column_type to validate and convert the script result async fn get_table_scripts( &self, request: tonic::Request, @@ -423,15 +521,29 @@ pub mod table_script_server { tonic::Response, tonic::Status, >; - async fn get_script_dependency_values( + /// Build the external data snapshot needed to execute a table's scripts in + /// the client-side Steel runtime. + /// + /// 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. + /// + /// Current-table column values are intentionally not returned. The client + /// supplies those directly from row_data so unsaved edits participate in + /// immediate calculations. The response replaces the client's previous + /// script dependency cache as one complete hydration snapshot. + async fn hydrate_script_dependencies( &self, - request: tonic::Request, + request: tonic::Request, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, >; } - /// Manages column-computation scripts for user-defined tables. + /// Manages column-computation scripts for user-defined tables and supplies the + /// dependency data used by the client-side Steel runtime. /// Each script belongs to a single table (table_definition_id) and populates /// exactly one target column in that table. The server: /// @@ -443,7 +555,16 @@ pub mod table_script_server { /// * Analyzes dependencies and prevents cycles across the schema /// * Transforms the script to decimal-safe math (steel_decimal) /// * Upserts into table_scripts and records dependencies in script_dependencies - /// The whole operation is transactional. + /// * Hydrates external column and aggregate inputs requested by the client + /// + /// The client fetches stored scripts and their declared dependencies, builds a + /// restricted Steel context, and executes scripts for immediate form feedback. + /// Current-table values come from the client's active row snapshot. External + /// values must come from HydrateScriptDependencies; the client must not query + /// arbitrary database data from inside the Steel VM. + /// + /// Server-side persistence remains authoritative and recalculates affected rows. + /// Script creation and update are transactional. #[derive(Debug)] pub struct TableScriptServer { inner: Arc, @@ -610,15 +731,15 @@ pub mod table_script_server { }; Box::pin(fut) } - "/komp_ac.table_script.TableScript/GetScriptDependencyValues" => { + "/komp_ac.table_script.TableScript/HydrateScriptDependencies" => { #[allow(non_camel_case_types)] - struct GetScriptDependencyValuesSvc(pub Arc); + struct HydrateScriptDependenciesSvc(pub Arc); impl< T: TableScript, > tonic::server::UnaryService< - super::GetScriptDependencyValuesRequest, - > for GetScriptDependencyValuesSvc { - type Response = super::GetScriptDependencyValuesResponse; + super::HydrateScriptDependenciesRequest, + > for HydrateScriptDependenciesSvc { + type Response = super::HydrateScriptDependenciesResponse; type Future = BoxFuture< tonic::Response, tonic::Status, @@ -626,12 +747,12 @@ pub mod table_script_server { fn call( &mut self, request: tonic::Request< - super::GetScriptDependencyValuesRequest, + super::HydrateScriptDependenciesRequest, >, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_script_dependency_values( + ::hydrate_script_dependencies( &inner, request, ) @@ -646,7 +767,7 @@ pub mod table_script_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetScriptDependencyValuesSvc(inner); + let method = HydrateScriptDependenciesSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( diff --git a/server b/server index 379e379..8a9306c 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 379e37986a8d64a9a7b999dca318d95ad5e19451 +Subproject commit 8a9306cd000990ba3dda5daaf63d84ad776ecf4a