Skip to content

feat(databases): add fork subcommand - #227

Merged
eddietejeda merged 3 commits into
mainfrom
feat/databases-fork
Jul 15, 2026
Merged

eddietejeda merged 3 commits into
mainfrom
feat/databases-fork

Conversation

@eddietejeda

@eddietejeda eddietejeda commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds hotdata databases fork [DATABASE], wrapping the POST /v1/databases/{id}/fork API. It creates an independent deep copy of a managed database — same schemas, tables, and data — that diverges freely from the source afterwards (writes to one never affect the other).

hotdata databases fork [DATABASE] [--name <label>] [--expires-at <dur|ts>] [-o table|json|yaml]

Fork is a public API and is generated into the hotdata Rust SDK, so the CLI calls it through the same typed ergonomic handle as every other database op:

api.client().databases().fork(&db.id, ForkDatabaseRequest { name, expires_at })

This PR bumps the SDK dependency hotdata 0.8.1 → 0.9.1 (the release that adds the fork_database method and its ergonomic wrapper).

Design decisions

  • Source defaults to the current database. The positional resolves by id, catalog, or name via the existing resolve_database helper, matching attach/detach. So hotdata databases fork just forks whatever you're working in.
  • --name defaults to <source>-fork. An unnamed fork inherits both the source's display name and its catalog alias from the server, which would leave two rows in databases list identical in every user-facing column. The client-side default keeps them distinguishable.
  • Expiry is always surfaced. With --expires-at omitted, a still-future source expiry is silently inherited, so the resolved expires_at is always printed (never when none).
  • Sets the fork as the current database on success (like create), since the flagship workflow is "fork to experiment, then throw away." The source is left unchanged and this is stated in the output.
  • Error handling surfaces the API error body verbatim, with an added recovery hint on the parquet-backed-source 400 (fork_error_exit).

SDK-bump fallout

The 0.9.1 bump also pulls in the now-required default_schema field on the database detail/summary/create SDK models. Test fixtures in databases.rs and indexes.rs that mock those responses gain the field; no production code change beyond fork.

Testing

  • Unit tests (name-default logic, typed happy-path via databases().fork(), 400 error-body mapping). Full suite: 287 passed, 0 failed. cargo fmt --check clean; release build green.
  • End-to-end against the live API with the release binary: bare fork of current DB, fork by id/name, all three output formats, data integrity (identical rows), independence (mutating the fork left the source untouched), inherited-expiry, ambiguous-catalog error, nonexistent-source error.
  • Fuzzed malformed --expires-at (banana, 0h, 1.5h, overflow, past timestamps), hostile --name (empty, unicode, SQL-injection string, oversized, control chars), and hostile source ids (traversal, quotes, junk). No crashes, no orphaned databases, no injection leakage.

Notes

  • Pre-existing (not addressed here): a literal path-traversal source like ../workspaces prints error parsing response: missing field ... instead of a clean not-found. It reproduces on databases show/delete — the SDK's typed GET handle normalizes ../. Cosmetic, affects the whole name_or_id command family; worth a separate fix.
  • cargo clippy -D warnings reports pre-existing errors on main unrelated to this change.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the fork subcommand, request builder, raw-seam usage, and tests. Consistent with existing conventions (mirrors create), correct request/response handling, and well covered by the new tests. LGTM.

Add `hotdata databases fork [DATABASE]` wrapping the
`POST /v1/databases/{id}/fork` API: create an independent deep copy of a
managed database — same schemas, tables, and data, diverging freely
afterwards.

- Source positional defaults to the current database; resolves by id,
  catalog, or name like `attach`/`detach`.
- `--name` defaults to `<source>-fork` so the fork and source stay
  distinguishable in `databases list` (an unnamed fork otherwise inherits
  both the source's display name and catalog alias).
- `--expires-at` accepts a relative duration or RFC 3339 timestamp; when
  omitted a still-future source expiry is inherited, so the resolved
  expiry is always printed ("never" when none).
- Sets the fork as the current database on success; source is unchanged.
- Routes through the raw `post_raw` seam (the endpoint is excluded from
  the public SDK); surfaces the API error body, with a recovery hint on
  the pre-DuckLake (parquet-backed) 400.

Validated end-to-end against production and fuzzed (malformed expiry,
hostile names, hostile source ids); no crashes or orphaned databases.
@eddietejeda
eddietejeda force-pushed the feat/databases-fork branch from 077a85c to 0ec9f26 Compare July 15, 2026 21:58
claude[bot]
claude Bot previously approved these changes Jul 15, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the fork subcommand. Clean implementation that mirrors the existing create command's structure (raw post_raw seam, spinner, current-database save, output formats). fork_database_request name-defaulting and field omission are correct and well covered by the three new unit tests. Error surfacing and the DuckLake hint degrade gracefully. No blocking issues.

@codecov

codecov Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 46.58385% with 86 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/commands/databases.rs 49.66% 76 Missing ⚠️
src/main.rs 0.00% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

claude[bot]
claude Bot previously approved these changes Jul 15, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the fork subcommand. Consistent with existing database command patterns (create/attach/detach), well-documented, and covered by meaningful unit tests (name-default logic, happy-path POST, 400 error body). Error handling, spinner lifecycle, and source resolution all check out. LGTM.

The fork endpoint is now public and generated into the SDK, so the CLI
calls it through the same ergonomic `databases()` handle as
create/get/list/delete instead of the raw `post_raw` seam:

    api.client().databases().fork(&db.id, ForkDatabaseRequest { .. })

- fork_database_request now builds the typed ForkDatabaseRequest (double
  -option fields) rather than raw JSON; behaviour (default name to
  `<source>-fork`, omit fields to inherit) is unchanged.
- fork_error_exit preserves the verbatim API error plus the parquet
  -backed-source recovery hint, now driven off the mapped ApiError.
- Bump hotdata 0.8.1 -> 0.9.1 (adds fork + ergonomic wrapper).

The bump also pulls in the `default_schema` field now required on the
database detail/summary/create SDK models, so the affected test fixtures
in databases.rs and indexes.rs gain it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the fork subcommand end to end: request builder, orchestration, error path, dispatch wiring, docs, and the SDK-bump fixture updates. Mirrors the existing create flow faithfully and is well-tested. No blocking issues.

@eddietejeda
eddietejeda merged commit 2f02cb4 into main Jul 15, 2026
14 checks passed
@eddietejeda
eddietejeda deleted the feat/databases-fork branch July 15, 2026 23:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant