Skip to content

Commit d8b8946

Browse files
authored
feat(databases)!: lineage walks the whole fork family (#288)
* feat(databases): lineage --tree walks the whole fork family The lineage endpoint returns one ancestral path plus one generation of direct forks, so a fork of a fork is invisible from its grandparent. --tree assembles the full family client-side: walk down from root_id, one lineage request per database — N+1, acceptable for the small fork trees the feature targets — and render it with proper rail glyphs (│/├─/└─), the queried database marked as before. - the queried database's already-fetched lineage seeds the walk and is reused, so its endpoint is hit exactly once - a node whose own lineage can't be fetched (e.g. a deleted generation) degrades to forks_unknown instead of aborting, and the known descent toward the queried database is grafted so it always appears - a visited-set guards against server-side cycles; per-node fork pages keep the '⋯ N more — see --forks-limit' marker, applied per database - -o json/yaml return {database_id, root_id, tree} with a recursive node shape; forks_unknown / unlisted_forks appear only when set - docs: skill synopsis + lineage bullet cover --tree * fix(databases): don't show an ancestor's downstream fork edge as its own Live verification surfaced the server's ancestor-entry semantics: each ancestors[] entry carries the fork edge BELOW it — the snapshot of that ancestor the next generation copied, and when — while forks[] entries carry their own creation edge. The tree walk took the root's entry from the ancestor chain verbatim, so the root row claimed a fork event it never had ('— root, forked …, snapshot 0'). Shift the chain metadata one generation down so every tree row carries its own creation edge (the forks-list convention), and strip it from the top of the chain: a root has no fork event, and below a truncation the edge above is unknown. As a bonus the grafted rows on the deleted-generation path now show their true fork dates too. Live-verified against production (create → fork → fork → lineage --tree from every node → delete middle → --tree again → deleted): the walk renders grandchildren, the server 404s lineage for deleted ids, and the graft path keeps the queried database visible under the deleted generation with '⋯ other forks unknown'. * feat(databases)!: lineage always walks the whole fork family Drop flat mode and the --tree flag: 'databases lineage' now renders the full family tree by default. The tree is an informational superset of the flat view — same ancestors (grafted even past deleted generations), same direct forks with paging — plus every deeper generation, and it displays each row's own creation edge instead of the server's queried-database-relative ancestor annotations, which made the flat view label the child row with the grandchild's fork date. BREAKING: the -o json/yaml shape changes from {ancestors, ancestors_truncated, forks, fork_count} to {database_id, root_id, tree} with a recursive tree node. The flat shape shipped in 0.29.0 (today); this is the cheapest moment to change it. Trade-offs accepted: one request per database instead of one total (fork families are small in practice), no atomic snapshot, and per-node degradation ('⋯ forks unknown') instead of one all-or-nothing response. ancestors_truncated is no longer carried: the walk detects truncation structurally (the top of the ancestor chain won't match root_id). Live-verified: create → fork → lineage renders the tree by default; --tree is rejected; cleanup done. * fix(databases): graft the descent past a paginated fork page too The review caught a hole in the walk: descent was grafted only when a node's lineage fetch FAILED, not when it succeeded with a truncated fork page. A root with more forks than the page (server clamps to 100, newest-first) could page out the very generation that leads to the queried database — the tree then rendered without its '← this database' row and exited 0, breaking the always-marked contract the skill doc states. Now the Ok arm grafts the known descent child whenever the listed page didn't include it, counting it out of the unlisted tally. Regression test: fork_count 3, page of 1, queried database off the page. Also split the Err arm's diagnostics: a 404 stays quiet (the expected shape of a deleted generation), while any other failure (auth, rate limit, transport) still renders '⋯ forks unknown' but says why on stderr, naming the id — so a failed request no longer masquerades as a deleted-generation artifact. --------- Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent 9867597 commit d8b8946

4 files changed

Lines changed: 573 additions & 88 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ The full command surface. The top level has eight groups — `auth`, `workspaces
146146
| `databases show` | Show details for an instant database |
147147
| `databases create` | Create a new instant database |
148148
| `databases fork` | Fork a database into a new, independent database |
149-
| `databases lineage` | Show a database's fork ancestry and direct forks |
149+
| `databases lineage` | Show a database's whole fork family tree |
150150
| `databases attach` | Attach a catalog so its tables are queryable |
151151
| `databases detach` | Detach a previously attached catalog |
152152
| `databases use` | Set the current (default) database |

‎skills/hotdata/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ hotdata databases tables remove <table> [--database <id>] [--schema public] [--w
121121
- `count` — the total number of instant databases in the workspace, across **all** pages (`list` shows one page). Prints a bare integer by default so it drops straight into scripts (`$(hotdata databases count)`); `--output json|yaml` render `{"count": N}` / `count: N`.
122122
- `create` — creates a new instant database. `--name` is an optional human-readable display name. `--catalog` sets the SQL alias used in queries (`SELECT … FROM <catalog>.schema.table`); must be `[a-z_][a-z0-9_]*`. `--expires-at` accepts relative durations (`24h`, `7d`, `90m`) or an RFC 3339 timestamp; omitting means no expiry. Repeat `--table` to declare tables up front.
123123
- `fork` — creates a new instant database that is an independent deep copy of an existing one (same schemas, tables, and data); the source is left unchanged and the two diverge freely afterwards. The source defaults to the active database; pass the database `<id>` to fork another. `--name` defaults to `<source>-fork` (so the two stay distinguishable in `list`); `--expires-at` accepts a relative duration or RFC 3339 timestamp, and when omitted a still-future source expiry is carried over. The fork becomes the active database on success. The fork answers to the **same catalog alias** as its source inside its own scope; catalogs attached to the source are **re-attached** to the fork, but indexes are **not** carried over. Only databases created with the current (DuckLake) storage engine can be forked — older parquet-backed databases return an error. The fork's output (and its `databases <id>` view) records **`forked_from`** provenance: source id, the source's name at fork time, the copied snapshot, and when.
124-
- `lineage` — renders a database's fork family tree (defaults to the active database): the ancestor chain down from the root, the queried database marked `← this database`, and its **direct** forks one level below (a fork of a fork appears in its own parent's lineage). Lineage is a historical record, not a live link — the databases stay independent, and a **deleted** generation stays in the chain (marked `deleted`). Forks made before the server recorded lineage carry none. `--forks-limit <n>` pages the direct-fork list (server clamps to 1–100); a truncated list closes with `⋯ N more`, and `-o json`/`yaml` always report the true `fork_count`.
124+
- `lineage` — renders a database's **whole fork family tree**, walked from the root down (defaults to the active database; one lineage request per database, fine for the small families forks produce in practice): every reachable generation, with the queried database marked `← this database`. Lineage is a historical record, not a live link — the databases stay independent, and a **deleted** generation stays in the chain (marked `deleted`), though its own fork list can't be enumerated: such branches end with `⋯ forks unknown`. Forks made before the server recorded lineage carry none. `--forks-limit <n>` pages each database's direct-fork list (server clamps to 1–100); a truncated branch closes with `⋯ N more`. `-o json`/`yaml` return `{database_id, root_id, tree}` with a recursive `tree` node.
125125
- `use` — saves the database **id** as the active database. Subsequent `databases tables` and `databases context` commands use it automatically. Note that a successful `fork` also updates this: the fork becomes the active database.
126126
- `unset` — clears the active database from config.
127127
- `<id>` — inspect one database (returns id, catalog, name, expires_at; a fork also shows its `forked_from` record).

‎skills/hotdata/references/WORKFLOWS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ hotdata databases fork --expires-at 24h # deep copy; becomes the active databa
140140
hotdata databases load --catalog sales --table orders --file ./risky.parquet # hits the fork
141141
```
142142

143-
**Capture both ids.** After the fork, both databases answer to the same catalog alias (here `sales`), so ids are the only unambiguous way to refer to either one — the source id comes from `databases list` up front, the fork id from the `fork` output. The shared alias means experimental SQL runs unchanged against the fork. Attached catalogs are re-attached to the fork; indexes are not carried over. When done, keep the fork (`databases use <source_id>` to switch back to the source) or delete it (`databases remove <fork_id>`). If the ids get lost, `hotdata databases lineage` recovers the pair: it renders the fork tree (ancestors and direct forks), and `databases <id>` shows a fork's `forked_from` record. Only DuckLake-backed databases can be forked — see `fork` in the main skill for details.
143+
**Capture both ids.** After the fork, both databases answer to the same catalog alias (here `sales`), so ids are the only unambiguous way to refer to either one — the source id comes from `databases list` up front, the fork id from the `fork` output. The shared alias means experimental SQL runs unchanged against the fork. Attached catalogs are re-attached to the fork; indexes are not carried over. When done, keep the fork (`databases use <source_id>` to switch back to the source) or delete it (`databases remove <fork_id>`). If the ids get lost, `hotdata databases lineage` recovers the pair: it renders the whole fork family tree, and `databases <id>` shows a fork's `forked_from` record. Only DuckLake-backed databases can be forked — see `fork` in the main skill for details.
144144

145145
---
146146

0 commit comments

Comments
 (0)