diff --git a/README.md b/README.md index 07b21c8..af5ec8c 100644 --- a/README.md +++ b/README.md @@ -222,30 +222,32 @@ bt eval foo.eval.ts -- --description "Prod" --shard=1/4 - Detail view: `t` span/thread, `Left/Right` switch panes, `Backspace`/`Esc` back - Global: `q` quit -## `bt util xact` +## `bt util version` -Local transaction-id conversion helpers: +Local version and pagination-key conversion helpers: - Convert transaction id to pretty version id: - - `bt util xact to-pretty 1000192656880881099` + - `bt util version to-pretty 1000192656880881099` - Convert pretty version id to transaction id: - - `bt util xact from-pretty 81cd05ee665fdfb3` + - `bt util version from-pretty 81cd05ee665fdfb3` - Convert transaction id, pretty version id, or pagination key to timestamp (local timezone by default): - - `bt util xact to-time 1000192656880881099` - - `bt util xact to-time 81cd05ee665fdfb3` - - `bt util xact to-time p07639577379371417602` - - `bt util xact to-time p07639577379371417602 --utc` - - `bt util xact to-time 1000192656880881099 --format unix` + - `bt util version to-time 1000192656880881099` + - `bt util version to-time 81cd05ee665fdfb3` + - `bt util version to-time p07639577379371417602` + - `bt util version to-time p07639577379371417602 --utc` + - `bt util version to-time 1000192656880881099 --format unix` - Convert timestamp to transaction id: - - `bt util xact from-time` (defaults to current time) - - `bt util xact from-time 2025-01-01` (date-only ISO at UTC midnight) - - `bt util xact from-time 2024-03-14T18:00:00Z` - - `bt util xact from-time 1710439200 --input unix --counter 42` -- Inspect any xact value: - - `bt util xact inspect 1000192656880881099` - - `bt util xact inspect 81cd05ee665fdfb3` - - `bt util xact inspect p07639577379371417602` - - `bt util xact inspect p07639577379371417602 --utc` + - `bt util version from-time` (defaults to current time) + - `bt util version from-time 2025-01-01` (date-only ISO at UTC midnight) + - `bt util version from-time 2024-03-14T18:00:00Z` + - `bt util version from-time 1710439200 --input unix --counter 42` +- Convert timestamp to pagination key: + - `bt util version from-time 2026-05-14T08:00:09-07:00 --pagination-key` +- Inspect any version-like value: + - `bt util version inspect 1000192656880881099` + - `bt util version inspect 81cd05ee665fdfb3` + - `bt util version inspect p07639577379371417602` + - `bt util version inspect p07639577379371417602 --utc` ## `bt auth` diff --git a/src/util_cmd.rs b/src/util_cmd.rs index c78c680..7ff723e 100644 --- a/src/util_cmd.rs +++ b/src/util_cmd.rs @@ -18,25 +18,26 @@ pub struct UtilArgs { #[derive(Debug, Clone, Subcommand)] enum UtilCommands { - /// Transaction-id conversion utilities - Xact(XactArgs), + /// Version and pagination-key conversion utilities + #[command(name = "version")] + Version(VersionArgs), } #[derive(Debug, Clone, Args)] -struct XactArgs { +struct VersionArgs { #[command(subcommand)] - command: XactCommands, + command: VersionCommands, } #[derive(Debug, Clone, Subcommand)] -enum XactCommands { +enum VersionCommands { /// Convert a transaction id to a pretty version id ToPretty(ToPrettyArgs), /// Convert a pretty version id to a transaction id FromPretty(FromPrettyArgs), /// Convert a transaction id to a timestamp ToTime(ToTimeArgs), - /// Convert a timestamp to a transaction id + /// Convert a timestamp to a transaction id or pagination key FromTime(FromTimeArgs), /// Decode and display transaction id details Inspect(InspectArgs), @@ -91,6 +92,10 @@ struct FromTimeArgs { /// Low 16-bit transaction counter value #[arg(long, default_value_t = 0)] counter: u16, + + /// Output a pagination key instead of a transaction id + #[arg(long)] + pagination_key: bool, } #[derive(Debug, Clone, Copy, ValueEnum, Eq, PartialEq)] @@ -135,17 +140,17 @@ struct XactInfo { pub async fn run(base: BaseArgs, args: UtilArgs) -> Result<()> { match args.command { - UtilCommands::Xact(xact) => run_xact(base, xact), + UtilCommands::Version(version) => run_version(base, version), } } -fn run_xact(base: BaseArgs, args: XactArgs) -> Result<()> { +fn run_version(base: BaseArgs, args: VersionArgs) -> Result<()> { match args.command { - XactCommands::ToPretty(args) => run_to_pretty(base.json, args), - XactCommands::FromPretty(args) => run_from_pretty(base.json, args), - XactCommands::ToTime(args) => run_to_time(base.json, args), - XactCommands::FromTime(args) => run_from_time(base.json, args), - XactCommands::Inspect(args) => run_inspect(base.json, args), + VersionCommands::ToPretty(args) => run_to_pretty(base.json, args), + VersionCommands::FromPretty(args) => run_from_pretty(base.json, args), + VersionCommands::ToTime(args) => run_to_time(base.json, args), + VersionCommands::FromTime(args) => run_from_time(base.json, args), + VersionCommands::Inspect(args) => run_inspect(base.json, args), } } @@ -212,21 +217,32 @@ fn run_from_time(json: bool, args: FromTimeArgs) -> Result<()> { let unix_seconds = parse_timestamp_or_now(args.timestamp.as_deref(), args.input)?; let xact = build_xact_id(unix_seconds, args.counter); let pretty = prettify_xact(xact); + let pagination_key = build_pagination_key(unix_seconds, args.counter, 0); + let output_kind = if args.pagination_key { + "pagination_key" + } else { + "xact_id" + }; if json { - println!( - "{}", - serde_json::to_string(&serde_json::json!({ - "input_timestamp": args.timestamp, - "input_format": match args.input { - TimeInputFormat::Iso => "iso", - TimeInputFormat::Unix => "unix", - }, - "unix_seconds": unix_seconds, - "counter": args.counter, - "xact_id": xact.to_string(), - "pretty_version": pretty, - }))? - ); + let mut payload = serde_json::json!({ + "output_kind": output_kind, + "input_timestamp": args.timestamp, + "input_format": match args.input { + TimeInputFormat::Iso => "iso", + TimeInputFormat::Unix => "unix", + }, + "unix_seconds": unix_seconds, + "counter": args.counter, + "xact_id": xact.to_string(), + "pretty_version": pretty, + }); + if args.pagination_key { + payload["pagination_key"] = serde_json::json!(format_pagination_key(pagination_key)); + payload["pagination_row_num"] = serde_json::json!(0); + } + println!("{}", serde_json::to_string(&payload)?); + } else if args.pagination_key { + println!("{}", format_pagination_key(pagination_key)); } else { println!("{xact}"); } @@ -401,6 +417,10 @@ fn build_xact_id(unix_seconds: u64, counter: u16) -> u64 { TOP_BITS | ((unix_seconds & 0xffff_ffff_ffff) << 16) | u64::from(counter) } +fn build_pagination_key(unix_seconds: u64, counter: u16, row_num: u16) -> u64 { + ((unix_seconds & 0xffff_ffff) << 32) | (u64::from(counter) << 16) | u64::from(row_num) +} + fn format_pagination_key(pagination_key: u64) -> String { format!("p{pagination_key:020}") } @@ -507,6 +527,20 @@ mod tests { assert_eq!(xact_counter(xact), counter); } + #[test] + fn from_time_to_pagination_key_uses_xact_counter() { + let unix_seconds = 1_778_727_718u64; + let counter = 31_627u16; + let pagination_key = build_pagination_key(unix_seconds, counter, 0); + assert_eq!( + format_pagination_key(pagination_key), + "p07639577379371417600" + ); + assert_eq!(pagination_key_to_unix_seconds(pagination_key), unix_seconds); + assert_eq!(pagination_key_xact_counter(pagination_key), counter); + assert_eq!(pagination_key_row_num(pagination_key), 0); + } + #[test] fn load_pretty_passthrough_for_non_pretty_input() { assert_eq!(load_pretty_xact("123").unwrap(), "123"); diff --git a/tests/cli.rs b/tests/cli.rs index 1348eca..425767a 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -122,14 +122,35 @@ fn setup_instrument_accepts_deprecated_agents_alias() { } #[test] -fn util_xact_to_time_accepts_pagination_key_with_utc() { +fn util_version_to_time_accepts_pagination_key_with_utc() { bt_command() - .args(["util", "xact", "to-time", "p07639577379371417602", "--utc"]) + .args([ + "util", + "version", + "to-time", + "p07639577379371417602", + "--utc", + ]) .assert() .success() .stdout(predicate::str::contains("2026-05-14T03:01:58Z")); } +#[test] +fn util_version_from_time_can_output_pagination_key() { + bt_command() + .args([ + "util", + "version", + "from-time", + "2026-05-14T08:00:09-07:00", + "--pagination-key", + ]) + .assert() + .success() + .stdout(predicate::str::contains("p07639762451734462464")); +} + #[test] fn setup_uses_codex_detected_on_path_without_explicit_agent() { let repo = make_git_repo();