Skip to content

Commit db7c05d

Browse files
authored
feat(databases): Add databases run command for and isolated database CLI (#118)
1 parent 15605d8 commit db7c05d

7 files changed

Lines changed: 534 additions & 6 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ hotdata databases list [-w <id>] [-o table|json|yaml]
137137
hotdata databases create --name <name> [--table <table> ...] [--schema public] [-o table|json|yaml]
138138
hotdata databases <name_or_id> [-o table|json|yaml]
139139
hotdata databases delete <name_or_id>
140+
hotdata databases run [--database <id>] [--description <label>] [--schema public] [--table <table> ...] [--expires-at <duration|timestamp>] <cmd> [args...]
141+
hotdata databases <id> run <cmd> [args...]
140142

141143
hotdata databases tables list <database> [--schema <name>] [-o table|json|yaml]
142144
hotdata databases tables load <database> <table> --file ./data.parquet [--schema public]
@@ -146,6 +148,7 @@ hotdata databases tables delete <database> <table> [--schema public]
146148

147149
- `create` registers a managed connection (`source_type: managed`) with no external credentials. Use `--table` to declare tables up front (required before `tables load` on the current API).
148150
- `tables load` uploads a **parquet** file (or uses a staged `upload_id` from `POST /v1/files`) and publishes it as the table generation (`replace` mode).
151+
- `run` mints a database-scoped JWT and execs `<cmd>` with `HOTDATA_DATABASE_TOKEN`, `HOTDATA_DATABASE_REFRESH_TOKEN`, `HOTDATA_DATABASE`, `HOTDATA_WORKSPACE`, and `HOTDATA_API_URL` injected into its environment. Pass a database id (group-positional `<id>` like `sandbox run`, or `--database <id>`) to scope an existing database; omit both to auto-create a scratch one using `--description` / `--schema` / `--table` / `--expires-at`. Useful for launching an agent or child process whose API access is restricted to a single database.
149152
- For CSV/JSON uploads without a managed database, use `hotdata datasets create` instead (`datasets.main.*`).
150153

151154
Example:

skills/hotdata/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ hotdata databases create [--description <label>] [--table <table> ...] [--schema
191191
hotdata databases set <id_or_description>
192192
hotdata databases <id_or_description> [--workspace-id <workspace_id>] [--output table|json|yaml]
193193
hotdata databases delete <id_or_description> [--workspace-id <workspace_id>]
194+
hotdata databases run [--database <id>] [--description <label>] [--schema public] [--table <table> ...] [--expires-at <duration|timestamp>] [--workspace-id <workspace_id>] <cmd> [args...]
195+
hotdata databases <id> run <cmd> [args...]
194196
195197
# Dot-notation shorthand for load: database.table or database.schema.table
196198
hotdata databases load <database.table> [--file ./data.parquet] [--url <url>] [--upload-id <id>] [--workspace-id <workspace_id>]
@@ -209,6 +211,7 @@ hotdata databases tables delete <table> [--database <id_or_desc>] [--schema publ
209211
- `tables list` — lists tables with `TABLE` (`<database_id>.<schema>.<table>`), `SYNCED`, `LAST_SYNC`. Uses active database when `--database` is omitted.
210212
- `tables load` — uploads a local parquet file (`--file`), a remote parquet URL (`--url`), or a pre-staged upload (`--upload-id`) and publishes with **replace** mode.
211213
- `tables delete` — drops a table from the managed database.
214+
- `run` — mints a database-scoped JWT (via `POST /v1/auth/database`) and execs `<cmd>` with `HOTDATA_DATABASE_TOKEN`, `HOTDATA_DATABASE_REFRESH_TOKEN`, `HOTDATA_DATABASE`, `HOTDATA_WORKSPACE`, and `HOTDATA_API_URL` injected. Pass a database id as a group positional (`hotdata databases <id> run ...`, sandbox-style) or via `--database <id>`; omit both to auto-create a scratch database using `--description` / `--schema` / `--table` / `--expires-at`. Use this to launch an agent or child process whose API access is scoped to a single database. The minted JWT carries `database`, `workspaces`, `permissions:["read","write"]`, `source:"database_token"`. The session is persisted at `~/.hotdata/database_session.json` (mode `0600`); the child's exit code is propagated.
212215

213216
Example:
214217

src/api.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,33 @@ impl ApiClient {
5050

5151
// Auth source precedence:
5252
//
53-
// 1. `HOTDATA_SANDBOX_TOKEN` env var — a `sandbox run` child
53+
// 1. `HOTDATA_DATABASE_TOKEN` env var — a `databases run` child
54+
// is executing with the parent's credentials scrubbed and a
55+
// database-scoped JWT injected. Refresh in-memory via
56+
// `HOTDATA_DATABASE_REFRESH_TOKEN` near expiry; never write
57+
// to disk (the child's FS may not be writable).
58+
// 2. `HOTDATA_SANDBOX_TOKEN` env var — a `sandbox run` child
5459
// is executing with the parent's credentials scrubbed.
5560
// Refresh in-memory via `HOTDATA_SANDBOX_REFRESH_TOKEN` if
5661
// the JWT is close to expiry; never write to disk (the
5762
// child's FS may not be writable).
58-
// 2. `~/.hotdata/sandbox_session.json` — the user ran
63+
// 3. `~/.hotdata/sandbox_session.json` — the user ran
5964
// `hotdata sandbox set <id>` (or `sandbox new` / `sandbox
6065
// run` in the parent shell). The sandbox JWT is the active
6166
// bearer for *every* command until `sandbox set` (with no
6267
// id) clears the file.
63-
// 3. `~/.hotdata/session.json` + optional api_key fallback —
68+
// 4. `~/.hotdata/session.json` + optional api_key fallback —
6469
// normal user-scoped CLI session.
6570
let api_url = profile_config.api_url.to_string();
66-
let access_token = if std::env::var("HOTDATA_SANDBOX_TOKEN").is_ok() {
71+
let access_token = if std::env::var("HOTDATA_DATABASE_TOKEN").is_ok() {
72+
match crate::database_session::refresh_from_env(&api_url) {
73+
Some(t) => t,
74+
None => {
75+
eprintln!("{}", "error: HOTDATA_DATABASE_TOKEN is empty".red());
76+
std::process::exit(1);
77+
}
78+
}
79+
} else if std::env::var("HOTDATA_SANDBOX_TOKEN").is_ok() {
6780
match crate::sandbox_session::refresh_from_env(&api_url) {
6881
Some(t) => t,
6982
None => {
@@ -118,7 +131,9 @@ impl ApiClient {
118131
}
119132
profile_config.sandbox
120133
}),
121-
database_id: workspace_id.and_then(|ws| crate::config::load_current_database("default", ws)),
134+
database_id: std::env::var("HOTDATA_DATABASE").ok().or_else(|| {
135+
workspace_id.and_then(|ws| crate::config::load_current_database("default", ws))
136+
}),
122137
}
123138
}
124139

src/command.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,34 @@ pub enum DatabasesCommands {
632632
#[command(subcommand)]
633633
command: Option<DatabaseTablesCommands>,
634634
},
635+
636+
/// Run a command with a database-scoped token. Creates a new database unless --database is given.
637+
Run {
638+
/// Existing database id to scope the token to (omit to auto-create a database)
639+
#[arg(long)]
640+
database: Option<String>,
641+
642+
/// Description for the auto-created database (only used when --database is omitted)
643+
#[arg(long)]
644+
description: Option<String>,
645+
646+
/// Schema for tables declared in the auto-created database (default: public)
647+
#[arg(long, default_value = "public")]
648+
schema: String,
649+
650+
/// Table to declare in the auto-created database (repeatable)
651+
#[arg(long = "table")]
652+
tables: Vec<String>,
653+
654+
/// When the auto-created database expires. Accepts a relative duration
655+
/// (e.g. 24h, 7d, 90m) or an RFC 3339 timestamp. Defaults to 24h when omitted.
656+
#[arg(long)]
657+
expires_at: Option<String>,
658+
659+
/// Command to execute (everything after `--`)
660+
#[arg(trailing_var_arg = true, required = true)]
661+
cmd: Vec<String>,
662+
},
635663
}
636664

637665
#[derive(Subcommand)]

0 commit comments

Comments
 (0)