From 71c80eea7b296553040ff45c333443ab680b4fcd Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 1 Aug 2026 06:01:18 +0700 Subject: [PATCH 1/2] fix: keep worktable_version! read-only when the primary key is unsized The read-only table generator emitted the PersistTable derive attribute as either #[table(pk_unsized)] or #[table(read_only)], never both. The two flags are independent: read_only selects the read-only shape (no persistence engine or task, sync into_worktable, 1-tuple table struct), pk_unsized selects the unsized primary index. A read-only table with an unsized key needs both, so picking pk_unsized silently dropped read_only and generated the full persist shape against a read-only table struct. The result was that worktable_version! did not compile at all for any table whose primary key is not fixed-size, a String key being the common case: load() called a sync into_worktable() that had been generated as async and returning Self(table, PersistenceTask), against a struct with one field. Fixed by emitting #[table(read_only, pk_unsized)]; the attribute parser already accepts both, and every downstream read_only branch already composes with pk_unsized. The sized path is unchanged. This was never caught because tests/worktable_version/basic.rs, the only user of the macro, uses u64 primary_key autoincrement. --- codegen/src/generators/read_only/table/mod.rs | 6 +- codegen/src/worktable_version/mod.rs | 19 ++++ tests/worktable_version/mod.rs | 1 + tests/worktable_version/string_primary_key.rs | 93 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/worktable_version/string_primary_key.rs 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()); + } + }); +} From 0eb83d4f0293d64b5e80ad6bb699cdde80b23e9b Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 1 Aug 2026 06:01:54 +0700 Subject: [PATCH 2/2] release: 0.9.3 Ships the worktable_version! fix for unsized primary keys. The macro did not compile for any table whose key is not fixed-size, which is every table that keys on a String, so no schema migration could be written against one. Bumps worktable and worktable_codegen to 0.9.3 with the exact pin. --- Cargo.toml | 4 ++-- codegen/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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."