From 283c1065a49f376240b860e66f2bd849cec23568 Mon Sep 17 00:00:00 2001 From: Handy-caT <37216852+Handy-caT@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:26:19 +0300 Subject: [PATCH] fix persisted optional fields in indexes --- Cargo.toml | 8 +- codegen/Cargo.toml | 2 +- codegen/src/persist_index/generator.rs | 42 +-- tests/persistence/sync/option.rs | 379 +++++++++++++++++++++++++ 4 files changed, 409 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d10b458..a8ba6b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["codegen", "examples", "performance_measurement", "performance_measur [package] name = "worktable" -version = "0.8.18" +version = "0.8.19" edition = "2024" authors = ["Handy-caT"] license = "MIT" @@ -16,7 +16,7 @@ perf_measurements = ["dep:performance_measurement", "dep:performance_measurement # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -worktable_codegen = { path = "codegen", version = "=0.8.18" } +worktable_codegen = { path = "codegen", version = "=0.8.19" } eyre = "0.6.12" derive_more = { version = "2.0.1", features = ["from", "error", "display", "into"] } @@ -27,9 +27,9 @@ lockfree = { version = "0.5.1" } fastrand = "2.3.0" futures = "0.3.30" uuid = { version = "1.10.0", features = ["v4", "v7"] } -data_bucket = "=0.3.9" +data_bucket = "=0.3.10" # data_bucket = { git = "https://github.com/pathscale/DataBucket", branch = "page_cdc_correction", version = "0.2.7" } -# data_bucket = { path = "../DataBucket", version = "0.3.8" } +# data_bucket = { path = "../DataBucket", version = "0.3.9" } performance_measurement_codegen = { path = "performance_measurement/codegen", version = "0.1.0", optional = true } performance_measurement = { path = "performance_measurement", version = "0.1.0", optional = true } indexset = { version = "=0.14.0", features = ["concurrent", "cdc", "multimap"] } diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index f154a80..e143384 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "worktable_codegen" -version = "0.8.18" +version = "0.8.19" edition = "2024" license = "MIT" description = "WorkTable codegeneration crate" diff --git a/codegen/src/persist_index/generator.rs b/codegen/src/persist_index/generator.rs index 940cd43..65239f1 100644 --- a/codegen/src/persist_index/generator.rs +++ b/codegen/src/persist_index/generator.rs @@ -58,23 +58,31 @@ impl Generator { .clone() .expect("index fields should always be named fields"), ); - let index_type = field.ty.to_token_stream().to_string(); - let mut split = index_type.split("<"); - // skip `IndexMap` ident. - split.next(); - let substr = split - .next() - .expect("index type should always contain key generic") - .to_string(); - types.push( - substr - .split(",") - .next() - .expect("index type should always contain key and value generics") - .to_string() - .parse() - .expect("should be valid because parsed from declaration"), - ); + + let syn::Type::Path(type_path) = &field.ty else { + unreachable!() + }; + + let last_segment = type_path + .path + .segments + .last() + .expect("Index type should have at least one segment"); + + let syn::PathArguments::AngleBracketed(arguments) = &last_segment.arguments else { + unreachable!("IndexMap always have angle brackets arguments which are generic") + }; + + let first_arg = arguments + .args + .first() + .expect("Index type should have at least one type argument"); + + let syn::GenericArgument::Type(ty) = first_arg else { + unreachable!("Index type should have at least one type argument") + }; + + types.push(ty.to_token_stream()); } let map = fields.into_iter().zip(types).collect::>(); diff --git a/tests/persistence/sync/option.rs b/tests/persistence/sync/option.rs index efc9e17..5281fda 100644 --- a/tests/persistence/sync/option.rs +++ b/tests/persistence/sync/option.rs @@ -460,3 +460,382 @@ fn test_option_multiple_rows_sync() { } }); } + +worktable! ( + name: TestOptionSyncIndex, + persist: true, + columns: { + id: u64 primary_key autoincrement, + test: u64 optional, + another: u64, + exchange: i32, + }, + indexes: { + another_idx: another unique, + test_idx: test, + exchnage_idx: exchange, + }, + queries: { + update: { + IndexTestById(test) by id, + IndexTestByAnother(test) by another, + IndexTestByExchange(test) by exchange, + } + } +); + +#[test] +fn test_option_indexed_insert_none_sync() { + let config = PersistenceConfig::new( + "tests/data/option_sync/indexed_insert_none", + "tests/data/option_sync/indexed_insert_none", + ); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_io() + .enable_time() + .build() + .unwrap(); + + runtime.block_on(async { + remove_dir_if_exists("tests/data/option_sync/indexed_insert_none".to_string()).await; + + let pk = { + let table = TestOptionSyncIndexWorkTable::load_from_file(config.clone()) + .await + .unwrap(); + let row = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: None, + another: 1, + exchange: 1, + }; + table.insert(row.clone()).unwrap(); + table.wait_for_ops().await; + row.id + }; + + { + let table = TestOptionSyncIndexWorkTable::load_from_file(config) + .await + .unwrap(); + let selected = table.select(pk).unwrap(); + assert_eq!(selected.test, None); + assert_eq!(table.0.pk_gen.get_state(), pk + 1); + } + }); +} + +#[test] +fn test_option_indexed_insert_some_sync() { + let config = PersistenceConfig::new( + "tests/data/option_sync/indexed_insert_some", + "tests/data/option_sync/indexed_insert_some", + ); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_io() + .enable_time() + .build() + .unwrap(); + + runtime.block_on(async { + remove_dir_if_exists("tests/data/option_sync/indexed_insert_some".to_string()).await; + + let pk = { + let table = TestOptionSyncIndexWorkTable::load_from_file(config.clone()) + .await + .unwrap(); + let row = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: Some(42), + another: 1, + exchange: 1, + }; + table.insert(row.clone()).unwrap(); + table.wait_for_ops().await; + row.id + }; + + { + let table = TestOptionSyncIndexWorkTable::load_from_file(config) + .await + .unwrap(); + let selected = table.select(pk).unwrap(); + assert_eq!(selected.test, Some(42)); + assert_eq!(table.0.pk_gen.get_state(), pk + 1); + } + }); +} + +#[test] +fn test_option_indexed_update_none_to_some_by_id_sync() { + let config = PersistenceConfig::new( + "tests/data/option_sync/indexed_none_to_some", + "tests/data/option_sync/indexed_none_to_some", + ); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_io() + .enable_time() + .build() + .unwrap(); + + runtime.block_on(async { + remove_dir_if_exists("tests/data/option_sync/indexed_none_to_some".to_string()).await; + + let pk = { + let table = TestOptionSyncIndexWorkTable::load_from_file(config.clone()) + .await + .unwrap(); + let row = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: None, + another: 1, + exchange: 1, + }; + table.insert(row.clone()).unwrap(); + + table + .update_index_test_by_id(IndexTestByIdQuery { test: Some(55) }, row.id) + .await + .unwrap(); + table.wait_for_ops().await; + row.id + }; + + { + let table = TestOptionSyncIndexWorkTable::load_from_file(config) + .await + .unwrap(); + let selected = table.select(pk).unwrap(); + assert_eq!(selected.test, Some(55)); + assert_eq!(table.0.pk_gen.get_state(), pk + 1); + } + }); +} + +#[test] +fn test_option_indexed_update_some_to_none_by_id_sync() { + let config = PersistenceConfig::new( + "tests/data/option_sync/indexed_some_to_none", + "tests/data/option_sync/indexed_some_to_none", + ); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_io() + .enable_time() + .build() + .unwrap(); + + runtime.block_on(async { + remove_dir_if_exists("tests/data/option_sync/indexed_some_to_none".to_string()).await; + + let pk = { + let table = TestOptionSyncIndexWorkTable::load_from_file(config.clone()) + .await + .unwrap(); + let row = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: Some(100), + another: 1, + exchange: 1, + }; + table.insert(row.clone()).unwrap(); + + table + .update_index_test_by_id(IndexTestByIdQuery { test: None }, row.id) + .await + .unwrap(); + table.wait_for_ops().await; + row.id + }; + + { + let table = TestOptionSyncIndexWorkTable::load_from_file(config) + .await + .unwrap(); + let selected = table.select(pk).unwrap(); + assert_eq!(selected.test, None); + assert_eq!(table.0.pk_gen.get_state(), pk + 1); + } + }); +} + +#[test] +fn test_option_indexed_update_by_another_sync() { + let config = PersistenceConfig::new( + "tests/data/option_sync/indexed_update_by_another", + "tests/data/option_sync/indexed_update_by_another", + ); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_io() + .enable_time() + .build() + .unwrap(); + + runtime.block_on(async { + remove_dir_if_exists("tests/data/option_sync/indexed_update_by_another".to_string()).await; + + let pk = { + let table = TestOptionSyncIndexWorkTable::load_from_file(config.clone()) + .await + .unwrap(); + let row = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: None, + another: 123, + exchange: 1, + }; + table.insert(row.clone()).unwrap(); + + table + .update_index_test_by_another(IndexTestByAnotherQuery { test: Some(77) }, 123) + .await + .unwrap(); + table.wait_for_ops().await; + row.id + }; + + { + let table = TestOptionSyncIndexWorkTable::load_from_file(config) + .await + .unwrap(); + let selected = table.select(pk).unwrap(); + assert_eq!(selected.test, Some(77)); + assert_eq!(table.0.pk_gen.get_state(), pk + 1); + } + }); +} + +#[test] +fn test_option_indexed_multiple_rows_sync() { + let config = PersistenceConfig::new( + "tests/data/option_sync/indexed_multiple_rows", + "tests/data/option_sync/indexed_multiple_rows", + ); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_io() + .enable_time() + .build() + .unwrap(); + + runtime.block_on(async { + remove_dir_if_exists("tests/data/option_sync/indexed_multiple_rows".to_string()).await; + + let (pk1, pk2, pk3) = { + let table = TestOptionSyncIndexWorkTable::load_from_file(config.clone()) + .await + .unwrap(); + + let row1 = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: Some(10), + another: 1, + exchange: 1, + }; + let pk1 = table.insert(row1).unwrap(); + + let row2 = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: None, + another: 2, + exchange: 2, + }; + let pk2 = table.insert(row2).unwrap(); + + let row3 = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: Some(30), + another: 3, + exchange: 3, + }; + let pk3 = table.insert(row3).unwrap(); + + table + .update_index_test_by_id(IndexTestByIdQuery { test: Some(40) }, pk1.clone()) + .await + .unwrap(); + + table + .update_index_test_by_id(IndexTestByIdQuery { test: Some(50) }, pk2.clone()) + .await + .unwrap(); + + table.wait_for_ops().await; + (pk1, pk2, pk3) + }; + + { + let table = TestOptionSyncIndexWorkTable::load_from_file(config) + .await + .unwrap(); + assert_eq!(table.select(pk1).unwrap().test, Some(40)); + assert_eq!(table.select(pk2).unwrap().test, Some(50)); + assert_eq!(table.select(pk3).unwrap().test, Some(30)); + } + }); +} + +#[test] +fn test_option_indexed_full_row_update_sync() { + let config = PersistenceConfig::new( + "tests/data/option_sync/indexed_full_update", + "tests/data/option_sync/indexed_full_update", + ); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_io() + .enable_time() + .build() + .unwrap(); + + runtime.block_on(async { + remove_dir_if_exists("tests/data/option_sync/indexed_full_update".to_string()).await; + + let pk = { + let table = TestOptionSyncIndexWorkTable::load_from_file(config.clone()) + .await + .unwrap(); + let row = TestOptionSyncIndexRow { + id: table.get_next_pk().0, + test: None, + another: 100, + exchange: 200, + }; + table.insert(row.clone()).unwrap(); + + table + .update(TestOptionSyncIndexRow { + id: row.id, + test: Some(99), + another: 100, + exchange: 200, + }) + .await + .unwrap(); + table.wait_for_ops().await; + row.id + }; + + { + let table = TestOptionSyncIndexWorkTable::load_from_file(config) + .await + .unwrap(); + let selected = table.select(pk).unwrap(); + assert_eq!(selected.test, Some(99)); + assert_eq!(selected.another, 100); + assert_eq!(selected.exchange, 200); + assert_eq!(table.0.pk_gen.get_state(), pk + 1); + } + }); +}