diff --git a/client b/client index bcd298b3..1d3e75b1 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit bcd298b3f1a9930e71f3c65caa3fa4a1361737f5 +Subproject commit 1d3e75b1502956959ae5ea43c1514ecd23904a0b diff --git a/common/proto/table_structure.proto b/common/proto/table_structure.proto index 8d44b06e..7d66a6b1 100644 --- a/common/proto/table_structure.proto +++ b/common/proto/table_structure.proto @@ -143,6 +143,8 @@ message GetTableImportDescriptorResponse { string storage_profile_name = 2; string table_name = 3; int64 table_id = 4; + // Version of this import contract. Send it as expected_table_revision on + // every staged chunk so schema/rule changes refuse stale prepared data. int64 table_revision = 5; repeated ImportFieldDescriptor fields = 6; } diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index a71f3881..e38f047b 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -199,6 +199,9 @@ message StageTableDataImportRequest { string import_id = 1; string table_name = 2; repeated TableDataImportRow rows = 3; + // Import-contract revision returned by GetTableImportDescriptor. A zero + // value is accepted for older clients and snapshots the current revision. + int64 expected_table_revision = 4; } message TableDataImportRow { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 8a984bf5..29713fe9 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_structure.rs b/common/src/proto/komp_ac.table_structure.rs index 4a02457a..0f0e52aa 100644 --- a/common/src/proto/komp_ac.table_structure.rs +++ b/common/src/proto/komp_ac.table_structure.rs @@ -135,6 +135,8 @@ pub struct GetTableImportDescriptorResponse { pub table_name: ::prost::alloc::string::String, #[prost(int64, tag = "4")] pub table_id: i64, + /// Version of this import contract. Send it as expected_table_revision on + /// every staged chunk so schema/rule changes refuse stale prepared data. #[prost(int64, tag = "5")] pub table_revision: i64, #[prost(message, repeated, tag = "6")] diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index cc3e2b70..f9c47dc1 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -95,6 +95,10 @@ pub struct StageTableDataImportRequest { pub table_name: ::prost::alloc::string::String, #[prost(message, repeated, tag = "3")] pub rows: ::prost::alloc::vec::Vec, + /// Import-contract revision returned by GetTableImportDescriptor. A zero + /// value is accepted for older clients and snapshots the current revision. + #[prost(int64, tag = "4")] + pub expected_table_revision: i64, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableDataImportRow { diff --git a/server b/server index e4392276..3b4f7ee7 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit e4392276bcdf31be965f638e84eb8cde746d6cdb +Subproject commit 3b4f7ee71ec316ead162c698f934b15959fcce27 diff --git a/web/src/pages/import_export/import/logic.rs b/web/src/pages/import_export/import/logic.rs index 6c721553..e4cebcdc 100644 --- a/web/src/pages/import_export/import/logic.rs +++ b/web/src/pages/import_export/import/logic.rs @@ -12,7 +12,7 @@ use axum_extra::extract::Form; use crate::{ AppState, definitions::{ - table_structure::GetTableStructureRequest, + table_structure::{GetTableImportDescriptorRequest, GetTableStructureRequest}, tables_data::{ AbortTableDataImportRequest, BeginTableDataImportRequest, CommitTableDataImportRequest, StageTableDataImportRequest, TableDataImportRow, @@ -52,6 +52,7 @@ const CHUNK_ROWS: usize = 1_000; /// a refusal on the next click instead of a value written somewhere else. struct Destination { table_name: String, + table_revision: i64, columns: Vec, types: HashMap, } @@ -258,6 +259,7 @@ pub(crate) async fn import_csv( id: id.clone(), profile_name, table_name: destination.table_name.clone(), + table_revision: destination.table_revision, rows: converted, })); @@ -363,6 +365,7 @@ struct Running { id: String, profile_name: String, table_name: String, + table_revision: i64, rows: Vec, } @@ -390,6 +393,7 @@ async fn run_import(job: Running) { import_id: import_id.clone(), table_name: job.table_name.clone(), rows: chunk.to_vec(), + expected_table_revision: job.table_revision, }; let Ok(request) = authenticated_request(&job.headers, request) else { abort_import(&job, &import_id).await; @@ -522,11 +526,24 @@ async fn destination( )); } + let descriptor_request = GetTableImportDescriptorRequest { + profile_name: profile_name.clone(), + table_name: table_name.clone(), + }; + let mut structures = state.structures.clone(); + let descriptor = structures + .get_table_import_descriptor( + authenticated_request(headers, descriptor_request) + .map_err(|_| Redirect::to("/login").into_response())?, + ) + .await + .map_err(|error| grpc_error(headers, &error))? + .into_inner(); + let request = GetTableStructureRequest { profile_name, table_names: vec![table_name.clone()], }; - let mut structures = state.structures.clone(); let structure = structures .get_table_structure( authenticated_request(headers, request) @@ -552,6 +569,7 @@ async fn destination( } Ok(Destination { table_name, + table_revision: descriptor.table_revision, types: column_types(&structure), columns, })