diff --git a/Cargo.toml b/Cargo.toml index 4a9635a..d724a9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["codegen", "examples", "performance_measurement", "performance_measur [package] name = "worktable" -version = "0.9.2" +version = "0.9.3" edition = "2024" authors = ["Handy-caT"] license = "MIT" @@ -49,7 +49,7 @@ tracing = "0.1" url = { version = "2", optional = true } uuid = { version = "1.10.0", features = ["v4", "v7"] } walkdir = { version = "2", optional = true } -worktable_codegen = { path = "codegen", version = "=0.9.2" } +worktable_codegen = { path = "codegen", version = "=0.9.3" } [dev-dependencies] chrono = "0.4.43" diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index bba25fb..6c6b40e 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "worktable_codegen" -version = "0.9.2" +version = "0.9.3" edition = "2024" license = "MIT" description = "Proc-macro companion crate for worktable: the worktable! macro and its derives." diff --git a/codegen/src/generators/read_only/table/mod.rs b/codegen/src/generators/read_only/table/mod.rs index ab138d3..2c8c31f 100644 --- a/codegen/src/generators/read_only/table/mod.rs +++ b/codegen/src/generators/read_only/table/mod.rs @@ -75,10 +75,14 @@ impl ReadOnlyGenerator { .collect::>(); let pk_types_unsized = is_unsized_vec(pk_types); + // `read_only` and `pk_unsized` are independent: the first selects the read-only + // shape of the table (no persistence engine or task, sync `into_worktable`), the + // second selects the unsized primary index. A read-only table with an unsized key + // needs both, so `read_only` is unconditional here. let derive = if pk_types_unsized { quote! { #[derive(Debug, PersistTable)] - #[table(pk_unsized)] + #[table(read_only, pk_unsized)] } } else { quote! { diff --git a/codegen/src/worktable_version/mod.rs b/codegen/src/worktable_version/mod.rs index 6cce717..ea7c1ed 100644 --- a/codegen/src/worktable_version/mod.rs +++ b/codegen/src/worktable_version/mod.rs @@ -139,6 +139,25 @@ mod tests { ); } + #[test] + fn test_unsized_primary_key_stays_read_only() { + let input = quote! { + name: ThingV1, + columns: { + id: String primary_key, + name: String, + }, + }; + + let res = expand(input).unwrap(); + let output = res.to_string(); + + assert!( + output.contains("table (read_only , pk_unsized)"), + "an unsized primary key must keep read_only, not replace it with pk_unsized" + ); + } + #[test] fn test_rejects_version_after_columns() { let input = quote! { diff --git a/tests/worktable_version/mod.rs b/tests/worktable_version/mod.rs index 1bca5f8..ccaeb9c 100644 --- a/tests/worktable_version/mod.rs +++ b/tests/worktable_version/mod.rs @@ -1 +1,2 @@ mod basic; +mod string_primary_key; diff --git a/tests/worktable_version/string_primary_key.rs b/tests/worktable_version/string_primary_key.rs new file mode 100644 index 0000000..3372a9c --- /dev/null +++ b/tests/worktable_version/string_primary_key.rs @@ -0,0 +1,93 @@ +use crate::remove_dir_if_exists; + +use worktable::prelude::*; +use worktable_codegen::{worktable, worktable_version}; + +// A primary key that is not generated and not fixed-size. The read-only table it produces +// needs both the read-only shape and the unsized primary index, so this exercises the +// `#[table(read_only, pk_unsized)]` pairing that a `u64 primary_key autoincrement` does not. +worktable!( + name: Doc, + persist: true, + columns: { + id: String primary_key, + title: String, + author: String, + }, + indexes: { + author_idx: author, + }, +); + +worktable_version!( + name: DocV1, + columns: { + id: String primary_key, + title: String, + author: String, + }, + indexes: { + author_idx: author, + }, +); + +#[test] +fn test_version_reads_persisted_data_with_string_primary_key() { + let config = DiskConfig::new_with_table_name( + "tests/data/version/string_primary_key", + DocWorkTable::name_snake_case(), + DocWorkTable::version(), + ); + + 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/version/string_primary_key".to_string()).await; + + { + let engine = DocPersistenceEngine::new(config.clone()).await.unwrap(); + let table = DocWorkTable::load(engine).await.unwrap(); + + table + .insert(DocRow { + id: "doc-alpha".to_string(), + title: "Alpha".to_string(), + author: "Alice".to_string(), + }) + .unwrap(); + + table + .insert(DocRow { + id: "doc-beta".to_string(), + title: "Beta".to_string(), + author: "Bob".to_string(), + }) + .unwrap(); + + table.wait_for_ops().await + } + + { + let engine = ReadOnlyPersistenceEngine::create(config.clone()).await.unwrap(); + let table = DocV1WorkTable::load(engine).await.unwrap(); + + assert_eq!(table.count(), 2); + + let rows = table.select_all().execute().unwrap(); + assert_eq!(rows.len(), 2); + + let titles: Vec<_> = rows.iter().map(|r| r.title.clone()).collect(); + assert!(titles.contains(&"Alpha".to_string())); + assert!(titles.contains(&"Beta".to_string())); + + // Look the row up by its string key, not just by scanning every row. + let alpha = table.select("doc-alpha".to_string()).unwrap(); + assert_eq!(alpha.author, "Alice".to_string()); + } + }); +}