Skip to content

feat(restore): Restore command group and Phase 4 apply support - #570

Open
guptadev21 wants to merge 10 commits into
feat/database-standby-phase-3from
feat/managed-database-phase-4
Open

feat(restore): Restore command group and Phase 4 apply support#570
guptadev21 wants to merge 10 commits into
feat/database-standby-phase-3from
feat/managed-database-phase-4

Conversation

@guptadev21

@guptadev21 guptadev21 commented Aug 21, 2026

Copy link
Copy Markdown
Member

Adds the client half of Managed Database Phase 4 — restore into a live
database, and the major-version migration expressed as a restore from an old
data directory.

Stacked on #569 (Phase 3, standby topology). Depends on
rapyuta-io-sdk-v2#66, which this branch's lock pins.

What lands

  • rio database restore create|list|inspect, plus Restore in the apply
    kind registry so a restore can run from a manifest. A restore belongs to
    exactly one database — the API nests it as /v2/databases/{name}/restores/
    — so the commands are nested too, and each takes --database.
    rio backup deliberately stays top-level: a backup really is a top-level
    API resource, where database is only a list filter.
  • --source dataDirectory with --source-version is the major-version
    migration path; it is not a separate command, because the transfer is a
    logical dump-and-load rather than a physical copy.
  • The archive to restore is named directly with --file-upload, by GUID or
    filename. A backup produces many archives over its life, and only the caller
    knows which one they mean.
  • A restore.yaml sample manifest, so rio explain restore works.

Corrections to the earlier Phase 4 commits

These were cut on 2026-08-19 and the apiserver moved after them.

  • No restore delete. The apiserver removed the route: a restore runs to a
    terminal phase and stays as an audit record, and deleting the target
    database is what stops one still in flight. rio restore delete was calling
    a route that answers 404. delete_object now raises rather than 404-ing
    silently, so an apply teardown that reaches a Restore fails loudly.
  • SDK pin. The lock named a rev from before RestoreStatus.step and the
    Stopped phase existed, so reading a stopped restore would have raised on
    parse. The dependency string also still named the Phase 3 branch while
    [tool.uv.sources] named Phase 4; they now agree.
  • Step and archive columns. A backup or restore holds one phase for
    minutes, so the phase alone cannot tell progress from a stall — that is what
    step is for. The archive count says whether a restore has anything to
    read, and rio backup inspect now carries the file-upload GUIDs that
    --file-upload takes. Without the SDK's BackupStatus.fileUploads those
    GUIDs were unreachable except through rio device uploads list, which broke
    the documented flow end to end.

Verification

uv run ruff check . clean, uv run pytest tests/unit 301 passed. New unit
coverage for both list columns.

Not covered by unit tests, which are all httpx-mocked and so cannot catch a
route that no longer exists — needs an ephemeral env on the Phase 4 apiserver:

  1. Create a database with two logical databases and write rows into both.
  2. Back it up; confirm rio backup inspect prints status.fileUploads.
  3. Drop one logical database, restore it by --file-upload, and confirm the
    other is untouched.
  4. Confirm rio database delete under a running restore leaves it Stopped
    and that the list renders it.
  5. Migrate a v17 data directory into a v18 database with
    --source dataDirectory --source-version 17.

🤖 Generated with Claude Code

https://claude.ai/code/session_019jJvnkwSKGtksZ5gq3PLmq

guptadev21 and others added 6 commits August 19, 2026 16:33
A restore loads logical databases into a live, running database from a backup,
or from an old on-device data directory — the latter being how a major-version
migration is done, since the transfer is a logical dump and load rather than a
physical copy. It never creates a database, and never touches logical databases
outside the ones it is given.

Restore is a sub-resource of Database, so every command takes the target with
--database rather than guessing it: the route itself is nested.

`rio apply` gains the Restore kind. Its dependencies are the target database and,
for a backup source, the source backup — so a manifest directory applies in the
right order. Update raises NotImplementedError: a restore is one-shot and its
spec is immutable, so re-applying an existing one must not re-run it.

--if-exists without --clean warns rather than silently doing nothing; pg_restore
rejects that combination, so the device drops the flag.
Resolves rapyuta-io-sdk-v2 to feat/managed-database-phase-4 (3e29debe), which
carries the Restore model and client methods the restore command group needs.
A backup restore now names the uploaded archive directly, by file-upload GUID or
filename — the two columns `rio device uploads list` prints. --backup stays as
provenance and no longer resolves anything, so it is optional.
The apiserver removed the restore delete route: a restore runs to a terminal
phase and stays as an audit record, and deleting the target database is what
stops one still in flight. `rio restore delete` therefore called a route that
answers 404.

delete_object now raises rather than silently 404-ing, so an apply teardown
that reaches a Restore fails loudly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jJvnkwSKGtksZ5gq3PLmq
The lock pinned an SDK rev from before RestoreStatus.step and the Stopped
phase existed, so reading a stopped restore would have raised on parse. The
bump also brings BackupStatus.fileUploads, which is where the file-upload
GUID a restore is addressed by is published.

The dependency string still named the Phase 3 branch while
[tool.uv.sources] named Phase 4; they now agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jJvnkwSKGtksZ5gq3PLmq
A backup or restore holds one phase for minutes, so the phase alone cannot
tell progress from a stall — that is what the step column is for. The archive
count tells the operator whether a restore has anything to read, and
`rio backup inspect` now carries the file-upload GUIDs that
`rio restore create --file-upload` takes.

Adds the restore manifest `rio explain` was missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jJvnkwSKGtksZ5gq3PLmq
@guptadev21
guptadev21 requested review from a team as code owners August 21, 2026 07:35
@guptadev21
guptadev21 requested review from ankitrgadiya and smrutisenapati and removed request for a team August 21, 2026 07:35
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

🤖 Pull Request Artifacts (#32949953673) 🎉

guptadev21 and others added 4 commits August 24, 2026 12:08
A restore is not a first-class resource: it belongs to exactly one database,
the v2 API nests it as /v2/databases/{name}/restores/, and every command
already required --database to name the target. Registering it at the top
level put the command tree at a different altitude from the resource tree.

The package moves to riocli/database/restore/ to match how a nested group is
laid out elsewhere in the repo (rio device tools).

Wiring and help text only. The apply kind registry is keyed by manifest kind,
not command path, so `rio apply` and `rio explain restore` are unchanged, as
are every command's options.

`rio backup` deliberately stays top-level: a backup really is a top-level API
resource, where `database` is only a list filter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jJvnkwSKGtksZ5gq3PLmq
Model.delete() catches only HttpNotFoundError, so delete_object raising
NotImplementedError aborted `rio delete -f` for every other resource in the
same bundle. A restore is an audit record with no delete route, so skipping it
is the correct outcome. update_object keeps raising -- that path is handled and
correctly reports the restore as existing rather than re-running it.

Drops the Archives column from `rio backup list` and the manifest's reference
to status.fileUploads: the field is gone from the v2 API, so the column would
always read 0. Also corrects the manifest's claim that backupName and
backupRunID are provenance only -- backupName selects the restore image's
major version, both are derived from the upload's metadata, and targetTime is
now honoured when the device still holds the backup locally.
Lists a database's uploaded backup archives. There was no way to see them
before: `rio device uploads list` needs a device name and has no filters, so
archives were unreachable once the uploading device or the Backup record was
gone.

The Upload ID it prints is what `rio database restore create --file-upload`
takes, which is the whole point -- finding an archive to restore was previously
guesswork.

Picks up list_database_uploads from the pinned SDK branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jJvnkwSKGtksZ5gq3PLmq
Archives now outlive their backup, so this is the only thing that removes one.
Takes the database and the upload GUID that `rio database upload list` prints.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jJvnkwSKGtksZ5gq3PLmq
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