From f15b4406e86e884e65fa6dd83766254fbd2d73a2 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Fri, 6 Nov 2026 08:25:02 -0800 Subject: [PATCH 1/2] feat(tables): --layout on tables add, ivf_rebalance_sweep job type, SQL UPDATE/DELETE in the skill - databases tables add --layout analytical|point_lookup; tables show reports the layout - jobs list --job-type ivf_rebalance_sweep - skill: SQL UPDATE and DELETE now run (match by rowid, no key needed); list what is still refused Needs hotdata (sdk-rust) 0.21.0 for TableInfo.layout and JobType::IvfRebalanceSweep. --- plugin/skills/hotdata/SKILL.md | 8 +++---- src/commands/databases.rs | 41 ++++++++++++++++++++++++++++------ src/commands/indexes.rs | 12 +++++----- src/commands/jobs.rs | 3 ++- src/commands/tables.rs | 4 ++++ src/main.rs | 2 ++ 6 files changed, 52 insertions(+), 18 deletions(-) diff --git a/plugin/skills/hotdata/SKILL.md b/plugin/skills/hotdata/SKILL.md index 918fa84..5079b6e 100644 --- a/plugin/skills/hotdata/SKILL.md +++ b/plugin/skills/hotdata/SKILL.md @@ -124,7 +124,7 @@ hotdata databases load (--catalog | --database ) --table [-- # Also available via tables subcommand hotdata databases tables list [--database ] [--schema ] [--workspace-id ] [--output table|json|yaml] -hotdata databases tables add [--database ] [--schema public] [--key ]... [--key-determines ]... [--sorted-by [=asc|desc]]... [--partition-by [=identity|year|month|day|hour]]... [--output table|json|yaml] +hotdata databases tables add [--database ] [--schema public] [--key ]... [--key-determines ]... [--sorted-by [=asc|desc]]... [--partition-by [=identity|year|month|day|hour]]... [--layout analytical|point_lookup] [--output table|json|yaml] hotdata databases tables load
[--database ] [--schema public] (--file | --url | --upload-id | --result-id ) [--mode replace|append|delete|update|upsert] [--append] [--format csv|json|parquet] [--key ]... [--retry-timeout ] [--workspace-id ] hotdata databases tables remove
[--database ] [--schema public] [--workspace-id ] ``` @@ -144,7 +144,7 @@ hotdata databases tables remove
[--database ] [--schema public] [--w - **Concurrent loads** — loads into one table run one at a time. While another load holds the table the server answers 409 `RESOURCE_LOCKED` with a `Retry-After`; the CLI waits and retries for up to `--retry-timeout` seconds (default 60). Pass `--retry-timeout 0` to fail immediately instead. Loads into different tables never block each other. - **Load modes** (`--mode`, default `replace`) — `replace` supersedes the table's contents; `append` adds rows; `delete`, `update`, and `upsert` match existing rows **by key**. `--append` is the old shorthand for `--mode append` and still works, but the two cannot be combined. The keyed modes need a key: declare one with `databases tables add --key`, or name it per-load with `--key` (repeat for a composite key). `delete` uploads only the key columns; `update` replaces matching rows and ignores unmatched ones; `upsert` inserts the unmatched instead. Keyed modes are not available with `--result-id`. - `tables list` — lists tables with `TABLE` (`..
`), `SYNCED`, `LAST_SYNC`. Uses current database when `--database` is omitted. -- `tables add` — declares a table **with its key and storage layout**, which a load cannot infer. `--key` (repeatable) is what enables the `delete`/`update`/`upsert` load modes on that table. `--sorted-by ` or `=desc` sets sort order; `--partition-by ` partitions on the value, `=month` (or `year`/`day`/`hour`) on a calendar part — one partition per calendar month needs **both** `=year` and `=month`, or every March shares a partition. Sort and partition are fixed once the table exists. `--key-determines` (repeatable) asserts a column's value is fixed by the key: it prunes keyed loads harder, and is **correctness-affecting** — declare it only where the invariant really holds, or a keyed load can leave a duplicate key behind. Re-adding an existing table is a conflict (409), and `tables remove` does not clear the declaration — the table leaves the listing but the name stays declared and still conflicts. So **a key cannot be retrofitted onto a table declared without one**: declare it with `--key` up front, or use a new table name. +- `tables add` — declares a table **with its key and storage layout**, which a load cannot infer. `--key` (repeatable) is what enables the `delete`/`update`/`upsert` load modes on that table. `--sorted-by ` or `=desc` sets sort order; `--partition-by ` partitions on the value, `=month` (or `year`/`day`/`hour`) on a calendar part — one partition per calendar month needs **both** `=year` and `=month`, or every March shares a partition. `--layout point_lookup` writes small data pages so rows are cheap to fetch by id — pick it for a table whose main reader is a vector index; scans of it read more pages (default `analytical`). Sort, partition and layout are fixed once the table exists. `--key-determines` (repeatable) asserts a column's value is fixed by the key: it prunes keyed loads harder, and is **correctness-affecting** — declare it only where the invariant really holds, or a keyed load can leave a duplicate key behind. Re-adding an existing table is a conflict (409), and `tables remove` does not clear the declaration — the table leaves the listing but the name stays declared and still conflicts. So **a key cannot be retrofitted onto a table declared without one**: declare it with `--key` up front, or use a new table name. - `tables load` — publishes to an instant-database table from a local file (`--file`), a remote URL (`--url`), a pre-staged upload (`--upload-id`), or a saved query result (`--result-id`, must belong to the target database). Same `--mode`, `--format`, and `--key` flags as the top-level `load` above. - `tables remove` — drops a table from the instant database. - `attach` — attaches **another instant database** to this one, so its **live** tables become visible inside this database's query scope. Name the other database by name, catalog alias, or id. Defaults to the current database; target another with `--database`. `--alias` sets the SQL name it answers to (defaults to the attached database's own catalog alias). **Required when that alias is `default`** — the stock name for a database created without `--catalog` — since `default` cannot be attached under its own name. This is how you **join across databases** — see [Querying across databases](#querying-across-databases-attach). Read-only: loads still target your own database, and attaching is not transitive — you see what you attached, not what it attached. @@ -270,7 +270,7 @@ hotdata databases query status - **A query runs inside one instant database** (current database or `--database`); with none set it fails *"a database is required."* The scope sees the database's own catalog **plus whatever is attached to it only**. To read another database's tables or join across databases, attach it first — see [Querying across databases (attach)](#querying-across-databases-attach). - Use `hotdata databases tables list` and `hotdata databases tables show` for discovery — not `information_schema` via `query`. (Discovery lists every workspace table; queryability still requires the table's catalog to be in the current database's scope.) - **PostgreSQL dialect.** Quote non-lowercase columns with double quotes. To write DuckDB/Postgres/Snowflake SQL instead, pass `--dialect` (server-side transpile; writes follow the same rules as `hotsql`) — details in **`hotdata-analytics`**. -- **SQL writes.** `query` runs `INSERT INTO t [(cols)] VALUES …|SELECT …` (append only), `CREATE TABLE t (col TYPE, …)`, `CREATE TABLE t AS SELECT …`, and `DROP TABLE [IF EXISTS] t` against the **current database's own tables**; attached databases stay read-only. The API key needs the `write` permission. The result is one row, `rows_affected`. Each `INSERT` is a full publish (write lock, new generation, index maintenance), so **batch rows into one statement** and use `hotdata databases load` for anything of size. Refused: `UPDATE`/`DELETE` (*"not supported yet"* — use `hotdata databases load --mode update|delete|upsert` by key), `INSERT OVERWRITE`/`OR REPLACE`/`ON CONFLICT`/`RETURNING`, column constraints on `CREATE TABLE` (`NOT NULL`, `DEFAULT`, `PRIMARY KEY`, `UNIQUE` — every column is nullable), `ALTER`, views, `COPY`, transactions. A table with a vector index must be inserted into with a column list that omits the generated embedding column. Table layout (`--key`, partition, sort) is declared with `databases tables add`, not in SQL. +- **SQL writes.** `query` runs `INSERT INTO t [(cols)] VALUES …|SELECT …`, `UPDATE t SET col = expr, … [WHERE …]`, `DELETE FROM t [WHERE …]`, `CREATE TABLE t (col TYPE, …)`, `CREATE TABLE t AS SELECT …`, and `DROP TABLE [IF EXISTS] t` against the **current database's own tables**; attached databases stay read-only. The API key needs the `write` permission. The result is one row, `rows_affected` (for `UPDATE`, the rows its `WHERE` matched). `UPDATE`/`DELETE` match rows by their internal rowid, so the table **needs no key**, and subqueries in `WHERE` work. Each write is a full publish (write lock, new generation, index maintenance), so **batch rows into one statement** and use `hotdata databases load` for anything of size. For bulk changes by key, `hotdata databases load --mode update|delete|upsert` is still the better tool. Refused: a target alias, `DELETE … LIMIT`/`USING`/`ORDER BY`, `UPDATE … FROM`/joins, `SET t.col` or the same column twice, `SET` of a key or `--key-determines` column on a table that declares `--key-determines`, `INSERT OVERWRITE`/`OR REPLACE`/`ON CONFLICT`/`RETURNING`, column constraints on `CREATE TABLE` (`NOT NULL`, `DEFAULT`, `PRIMARY KEY`, `UNIQUE` — every column is nullable), `ALTER`, views, `COPY`, transactions. A table with a vector index must be inserted into with a column list that omits the generated embedding column. Table layout (`--key`, partition, sort, `--layout`) is declared with `databases tables add`, not in SQL. - Async runs return `query_run_id` → poll with `query status ` (do not re-run the same heavy SQL). `query status` exit codes: `0` succeeded, `1` failed, `2` still running (poll again), `3` succeeded but the result is a truncated/incomplete preview. - **Large results: `-o csv` / `-o json` are complete, `-o table` is capped.** The server returns inline rows only up to a bounded cap and persists the full set under a `result_id`. For `csv` and `json` the CLI **streams** that full result batch by batch, so output size is unbounded and memory stays flat — pipe a big result to a file with `-o csv`/`-o json`, never `table`. For `table` the CLI fetches at most **10,000 rows** and the footer says `N of TOTAL rows — INCOMPLETE PREVIEW` (with `?` when the server sent no total); the process exits **3** so a pipeline cannot mistake it for the whole set. Same rules for `hotdata databases results get`. A result the server is still writing is waited for (up to 5 minutes, honoring `Retry-After`) rather than returned partial. If the full result can't be retrieved, the CLI prints the inline preview, a `warning:` to stderr, and exits 3. `-o json` carries `row_count`, `total_row_count`, and `truncated` — branch on `truncated`, not on the row count. - **Numbers print exactly.** A `DECIMAL(38,2)` or other wide number is printed with every digit the service sent, in every output format; `-o json` emits it as an unquoted JSON number. One consequence: a list or struct cell prints on a single line in `-o json` (re-indenting would mean re-parsing and rounding it); scalars are unaffected. @@ -284,7 +284,7 @@ hotdata jobs list [--workspace-id ] [--job-type ] [--status hotdata jobs [--workspace-id ] [--output table|json|yaml] ``` - `list` shows only active jobs (`pending`, `running`) by default. Use `--all` to see all jobs. -- `--job-type`: `create_index`, `managed_load`, `database_fork`. (`database_fork` is accepted for forward compatibility; today a fork completes synchronously and records no job.) (`data_refresh_table` / `data_refresh_connection` were retired server-side — the engine is push-only; nothing refreshes.) +- `--job-type`: `create_index`, `managed_load`, `database_fork`, `ivf_rebalance_sweep` (background IVF index rebalancing). (`database_fork` is accepted for forward compatibility; today a fork completes synchronously and records no job.) (`data_refresh_table` / `data_refresh_connection` were retired server-side — the engine is push-only; nothing refreshes.) - `--status`: `pending`, `running`, `succeeded`, `partially_succeeded`, `failed`. - Use `hotdata jobs ` to inspect a specific job's status, error, and result. diff --git a/src/commands/databases.rs b/src/commands/databases.rs index fad730e..c1d348b 100644 --- a/src/commands/databases.rs +++ b/src/commands/databases.rs @@ -428,6 +428,12 @@ pub enum DatabaseTablesCommands { #[arg(long = "partition-by")] partition_by: Vec, + /// Storage layout. `point_lookup` writes small data pages so rows are + /// cheap to fetch by id (what vector-index answers do), at the cost of + /// slower scans. Default: `analytical`. Fixed once the table is created. + #[arg(long, value_parser = ["analytical", "point_lookup"])] + layout: Option, + /// Output format #[arg(long = "output", short = 'o', default_value = "table", value_parser = ["table", "json", "yaml"])] output: String, @@ -1085,6 +1091,7 @@ fn table_declaration_body( key_determines: &[String], sorted_by: &[serde_json::Value], partition_by: &[serde_json::Value], + layout: Option<&str>, ) -> serde_json::Value { let mut body = serde_json::json!({ "name": table }); if !key.is_empty() { @@ -1101,6 +1108,9 @@ fn table_declaration_body( if !partition_by.is_empty() { body["partition_by"] = serde_json::json!(partition_by); } + if let Some(layout) = layout { + body["layout"] = serde_json::json!(layout); + } body } @@ -1115,6 +1125,7 @@ pub fn add_table( key_determines: &[String], sorted_by: &[String], partition_by: &[String], + layout: Option<&str>, output: &str, ) { use crossterm::style::Stylize; @@ -1148,7 +1159,14 @@ pub fn add_table( let api = Api::new(Some(workspace_id)); let db = resolve_database(&api, &database); - let body = table_declaration_body(table, key, key_determines, &sorted_by, &partition_by); + let body = table_declaration_body( + table, + key, + key_determines, + &sorted_by, + &partition_by, + layout, + ); let (status, resp) = declare_table(&api, &db.id, schema, &body); @@ -1170,6 +1188,7 @@ pub fn add_table( "key_determines": key_determines, "sorted_by": sorted_by, "partition_by": partition_by, + "layout": layout.unwrap_or("analytical"), }); match output { "json" => println!("{}", serde_json::to_string_pretty(&declared).unwrap()), @@ -1198,6 +1217,9 @@ pub fn add_table( if !partition_by.is_empty() { println!("parts: {}", describe_partition_keys(&partition_by)); } + if let Some(layout) = layout { + println!("layout: {layout}"); + } } } } @@ -4392,7 +4414,7 @@ mod tests { .with_status(200) .with_header("content-type", "application/json") .with_body( - r#"{"count":1,"limit":1000,"tables":[{"connection":"default","schema":"public","table":"b","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"last_sync":null}],"has_more":false,"next_cursor":null}"#, + r#"{"count":1,"limit":1000,"tables":[{"connection":"default","schema":"public","table":"b","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical","last_sync":null}],"has_more":false,"next_cursor":null}"#, ) .create(); let page0 = server @@ -4404,7 +4426,7 @@ mod tests { .with_status(200) .with_header("content-type", "application/json") .with_body( - r#"{"count":1,"limit":1000,"tables":[{"connection":"default","schema":"public","table":"a","synced":false,"partition_by":[],"sorted_by":[],"constant_per_key":[],"last_sync":null}],"has_more":true,"next_cursor":"cur2"}"#, + r#"{"count":1,"limit":1000,"tables":[{"connection":"default","schema":"public","table":"a","synced":false,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical","last_sync":null}],"has_more":true,"next_cursor":"cur2"}"#, ) .create(); @@ -4518,7 +4540,7 @@ mod tests { .with_status(200) .with_header("content-type", "application/json") .with_body( - r#"{"count":1,"limit":5,"tables":[{"connection":"default","schema":"public","table":"a","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"last_sync":null}],"has_more":true,"next_cursor":"page2"}"#, + r#"{"count":1,"limit":5,"tables":[{"connection":"default","schema":"public","table":"a","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical","last_sync":null}],"has_more":true,"next_cursor":"page2"}"#, ) .expect(1) .create(); @@ -4544,7 +4566,7 @@ mod tests { .with_status(200) .with_header("content-type", "application/json") .with_body( - r#"{"count":1,"limit":1000,"tables":[{"connection":"default","schema":"public","table":"orders","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"last_sync":null}],"has_more":false,"next_cursor":null}"#, + r#"{"count":1,"limit":1000,"tables":[{"connection":"default","schema":"public","table":"orders","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical","last_sync":null}],"has_more":false,"next_cursor":null}"#, ) .create(); @@ -4602,7 +4624,8 @@ mod tests { &cols(&["order_id"]), &cols(&["event_date"]), &[], - &[] + &[], + None ), serde_json::json!({ "name": "orders", @@ -4611,9 +4634,13 @@ mod tests { }) ); assert_eq!( - table_declaration_body("orders", &[], &[], &[], &[]), + table_declaration_body("orders", &[], &[], &[], &[], None), serde_json::json!({"name": "orders"}) ); + assert_eq!( + table_declaration_body("orders", &[], &[], &[], &[], Some("point_lookup")), + serde_json::json!({"name": "orders", "layout": "point_lookup"}) + ); } #[test] diff --git a/src/commands/indexes.rs b/src/commands/indexes.rs index cbd7ad1..1b6d81c 100644 --- a/src/commands/indexes.rs +++ b/src/commands/indexes.rs @@ -934,7 +934,7 @@ mod tests { .with_header("content-type", "application/json") .with_body( r#"{"count":1,"limit":100,"tables":[ - {"connection":"__db_abc","schema":"public","table":"vec_mid","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[]} + {"connection":"__db_abc","schema":"public","table":"vec_mid","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical"} ],"has_more":false,"next_cursor":null}"#, ) .create(); @@ -1020,7 +1020,7 @@ mod tests { .with_header("content-type", "application/json") .with_body( r#"{"count":1,"limit":100,"tables":[ - {"connection":"__db_abc","schema":"public","table":"listings","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[]} + {"connection":"__db_abc","schema":"public","table":"listings","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical"} ],"has_more":false,"next_cursor":null}"#, ) .create(); @@ -1075,7 +1075,7 @@ mod tests { .with_header("content-type", "application/json") .with_body( r#"{"count":1,"limit":100,"tables":[ - {"connection":"Warehouse","schema":"public","table":"events","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[]} + {"connection":"Warehouse","schema":"public","table":"events","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical"} ],"has_more":false,"next_cursor":null}"#, ) .create(); @@ -1119,7 +1119,7 @@ mod tests { .with_header("content-type", "application/json") .with_body( r#"{"count":1,"limit":100,"tables":[ - {"connection":"__db_abc","schema":"public","table":"listings","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[]} + {"connection":"__db_abc","schema":"public","table":"listings","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical"} ],"has_more":false,"next_cursor":null}"#, ) .create(); @@ -1165,8 +1165,8 @@ mod tests { .with_header("content-type", "application/json") .with_body( r#"{"count":2,"limit":100,"tables":[ - {"connection":"c1","schema":"public","table":"z","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[]}, - {"connection":"c1","schema":"public","table":"a","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[]} + {"connection":"c1","schema":"public","table":"z","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical"}, + {"connection":"c1","schema":"public","table":"a","synced":true,"partition_by":[],"sorted_by":[],"constant_per_key":[],"layout":"analytical"} ],"has_more":false,"next_cursor":null}"#, ) .create(); diff --git a/src/commands/jobs.rs b/src/commands/jobs.rs index 8ec5406..864d839 100644 --- a/src/commands/jobs.rs +++ b/src/commands/jobs.rs @@ -8,7 +8,7 @@ pub enum JobsCommands { /// List background jobs (shows active jobs by default) List { /// Filter by job type - #[arg(long, value_parser = ["create_index", "managed_load", "database_fork"])] + #[arg(long, value_parser = ["create_index", "managed_load", "database_fork", "ivf_rebalance_sweep"])] job_type: Option, /// Filter by status @@ -74,6 +74,7 @@ fn parse_job_type(s: &str) -> Option { "create_index" => Some(JobType::CreateIndex), "managed_load" => Some(JobType::ManagedLoad), "database_fork" => Some(JobType::DatabaseFork), + "ivf_rebalance_sweep" => Some(JobType::IvfRebalanceSweep), _ => None, } } diff --git a/src/commands/tables.rs b/src/commands/tables.rs index 3285d82..2915a95 100644 --- a/src/commands/tables.rs +++ b/src/commands/tables.rs @@ -23,6 +23,7 @@ struct TableRow { #[derive(Serialize)] struct TableWithColumns { table: String, + layout: String, #[serde(skip_serializing_if = "Vec::is_empty")] columns: Vec, } @@ -181,6 +182,7 @@ pub fn show(workspace_id: &str, table_ref: &str, format: &str) { let out = TableWithColumns { table: format!("{display_catalog}.{}.{}", t.schema, t.table), + layout: t.layout.to_string(), columns: t .columns .flatten() @@ -215,6 +217,8 @@ pub fn show(workspace_id: &str, table_ref: &str, format: &str) { }) .collect(); crate::output::table::print(&["TABLE", "COLUMN", "DATA_TYPE", "NULLABLE"], &rows); + use crossterm::style::Stylize; + eprintln!("{}", format!("layout: {}", out.layout).dark_grey()); } } _ => unreachable!(), diff --git a/src/main.rs b/src/main.rs index d38b0cd..1b2e300 100644 --- a/src/main.rs +++ b/src/main.rs @@ -387,6 +387,7 @@ fn main() { key_determines, sorted_by, partition_by, + layout, output, }) => databases::add_table( &workspace_id, @@ -397,6 +398,7 @@ fn main() { &key_determines, &sorted_by, &partition_by, + layout.as_deref(), &output, ), Some(DatabaseTablesCommands::Show { table, output }) => { From 957aa6729228f30d73c8ebbb722a5a2e69eb699f Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Fri, 6 Nov 2026 08:30:59 -0800 Subject: [PATCH 2/2] chore(deps): bump hotdata SDK to 0.21.0 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 957085b..bfa6e33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1269,9 +1269,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hotdata" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa439609367acf8788081917fef9f85fd8b8cd8b7bee15779f8eef0ca241ecb" +checksum = "3c29a358b8712bca80370acae8dda069752b58dc7599acfcdbc8c447b6f30e4b" dependencies = [ "arrow-array", "arrow-buffer", diff --git a/Cargo.toml b/Cargo.toml index ebecf14..e96cc73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" # behind a shared multi-thread tokio runtime. The `arrow` feature backs result # decode; `upload_file` runs the presigned direct-to-storage upload flow # (see src/sdk.rs::Api::upload). -hotdata = { version = "0.20.0", features = ["arrow"] } +hotdata = { version = "0.21.0", features = ["arrow"] } # Shared multi-thread runtime for the sync wrapper; block_on is called # concurrently from rayon worker threads (see src/indexes.rs). The presigned # upload (src/sdk.rs::Api::upload) drives the SDK's async `upload_file` on this