Skip to content

Commit 51d96b2

Browse files
feat(cli): add database usage, backup list, restore, and connection rotate
Extends the database group with the remaining Management API surface: - database usage <database> [--from --to]: operations and storage metrics for a period - database backup list <database> [--limit]: platform-created backups with retention metadata - database restore <database> --backup <id> [--source-database <db>] --confirm <database-id>: destructive in-place restore; status goes to recovering and connections/credentials are preserved - database connection rotate <connection> --confirm <connection-id>: mints new credentials and prints the new one-time URL exactly once on stdout, matching the connection create output contract Restore and rotate follow the exact-id --confirm convention because they are destructive (restore overwrites data; rotate revokes the previous credentials). All commands support --json with structured error codes (DATABASE_BACKUPS_UNSUPPORTED, DATABASE_BACKUP_NOT_FOUND, DATABASE_RESTORE_CONFLICT) for agent consumption. Spec-first: command-spec.md and resource-model.md updated alongside the implementation.
1 parent f152fe3 commit 51d96b2

11 files changed

Lines changed: 1970 additions & 16 deletions

File tree

docs/product/command-spec.md

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,91 @@ prisma-cli database create my-db --branch feature/foo --region eu-central-1
861861
prisma-cli database create my-db --json
862862
```
863863

864+
## `prisma-cli database usage <database> --project <id-or-name> --branch <git-name> --from <iso-date> --to <iso-date>`
865+
866+
Purpose:
867+
868+
- show usage metrics for one Prisma Postgres database
869+
870+
Behavior:
871+
872+
- requires auth and resolved project context; accepts `--project <id-or-name>` as an explicit fallback
873+
- resolves `<database>` by exact database id or exact database name inside the resolved project
874+
- `--branch <git-name>` narrows name resolution when the same database name exists on multiple Branches
875+
- `--from <iso-date>` and `--to <iso-date>` bound the reporting period; when omitted, the platform defaults the period to the current month so far
876+
- the CLI validates that `--from` and `--to` parse as dates and that `--from` is not after `--to` before calling the API
877+
- reports operations used (`ops`) and storage used (`GiB`) for the period, plus the resolved period bounds and the generation timestamp
878+
- read-only; never prints or returns connection strings, passwords, or endpoint secrets
879+
- fails with `DATABASE_NOT_FOUND` or `DATABASE_AMBIGUOUS` when the target cannot be selected safely
880+
881+
Examples:
882+
883+
```bash
884+
prisma-cli database usage db_123
885+
prisma-cli database usage acme-production --from 2026-06-01 --to 2026-06-30
886+
prisma-cli database usage db_123 --json
887+
```
888+
889+
## `prisma-cli database backup`
890+
891+
Manage backups for a database. `backup` is nested under `database` because
892+
backups exist only in the context of one Prisma Postgres database, following
893+
the same parent-noun/subordinate-noun/action shape as `database connection`.
894+
The platform creates backups automatically; the first slice is read-only.
895+
896+
### `prisma-cli database backup list <database> --limit <n>`
897+
898+
Purpose:
899+
900+
- list backups for a database
901+
902+
Behavior:
903+
904+
- requires auth and resolved project context; accepts `--project <id-or-name>` as an explicit fallback
905+
- resolves `<database>` by exact database id or exact database name inside the resolved project
906+
- supports `--branch <git-name>` to narrow database name resolution
907+
- `--limit <n>` caps the number of returned backups; the platform accepts 1 to 100 and defaults to 25
908+
- lists backup id, type (`full` or `incremental`), status (`running`, `completed`, `failed`, or `unknown`), size when available, and created timestamp
909+
- includes the platform's backup retention window in days
910+
- read-only; never prints or returns secret values
911+
- fails with `DATABASE_BACKUPS_UNSUPPORTED` when the platform reports backups are not available for the database (for example remote/BYO databases)
912+
- fails with `DATABASE_NOT_FOUND` or `DATABASE_AMBIGUOUS` when the target cannot be selected safely
913+
914+
Examples:
915+
916+
```bash
917+
prisma-cli database backup list db_123
918+
prisma-cli database backup list acme-production --limit 50
919+
prisma-cli database backup list db_123 --json
920+
```
921+
922+
## `prisma-cli database restore <database> --backup <backup-id> --source-database <database> --confirm <database-id>`
923+
924+
Purpose:
925+
926+
- restore a database's data from a backup
927+
928+
Behavior:
929+
930+
- requires auth and resolved project context; accepts `--project <id-or-name>` as an explicit fallback
931+
- restores into the resolved `<database>` (the target): all current data is immediately and irreversibly overwritten with the backup contents; connections and credentials are preserved, so no new connection URL is printed
932+
- resolves `<database>` by exact database id or exact database name inside the resolved project; `--branch <git-name>` narrows name resolution
933+
- `--backup <backup-id>` is required and names the backup to restore from; ids come from `database backup list`
934+
- by default the backup must belong to the target database; `--source-database <database>` restores from another database's backup, for example a production backup into a scratch database, and requires access to both databases' projects
935+
- requires `--confirm <database-id>` where the value exactly matches the resolved target database id; `--yes` does not satisfy this confirmation
936+
- the restore runs asynchronously: the target database status becomes `recovering` until the restore completes, and `database show` reports the current status; `nextSteps` includes the show command
937+
- fails with `DATABASE_RESTORE_CONFLICT` when the target database is provisioning or already recovering
938+
- fails with `DATABASE_NOT_FOUND` or `DATABASE_AMBIGUOUS` when a database target cannot be selected safely
939+
- never prints or returns connection strings, passwords, or endpoint secrets
940+
941+
Examples:
942+
943+
```bash
944+
prisma-cli database restore db_123 --backup bkp_456 --confirm db_123
945+
prisma-cli database restore scratch --backup bkp_456 --source-database acme-production --confirm db_789
946+
prisma-cli database restore db_123 --backup bkp_456 --confirm db_123 --json
947+
```
948+
864949
## `prisma-cli database remove <database> --confirm <database-id>`
865950

866951
Purpose:
@@ -891,7 +976,8 @@ valid in the context of a Prisma Postgres database. The subgroup mirrors the
891976
`project env <action>` shape: the parent command names the resource family,
892977
the nested noun names the subordinate resource, and the final token is the
893978
action. There is no `database connection show` command because connection
894-
strings are one-time-view secrets.
979+
strings are one-time-view secrets: the platform returns them only from
980+
`connection create` and `connection rotate`, never from read endpoints.
895981

896982
### `prisma-cli database connection list <database>`
897983

@@ -944,6 +1030,34 @@ prisma-cli database connection create db_123 --name readonly
9441030
prisma-cli database connection create db_123 --json
9451031
```
9461032

1033+
### `prisma-cli database connection rotate <connection> --confirm <connection-id>`
1034+
1035+
Purpose:
1036+
1037+
- rotate a database connection's credentials and print the new one-time connection URL
1038+
1039+
Behavior:
1040+
1041+
- requires auth
1042+
- treats `<connection>` as the connection id to rotate
1043+
- requires `--confirm <connection-id>` where the value exactly matches the connection id; `--yes` does not satisfy this confirmation, because rotation revokes the previous credentials (best-effort) and breaks clients still using them
1044+
- mints new credentials for the same connection; the connection id and name are unchanged
1045+
- the new connection URL is a one-time-view secret with the same output contract as `database connection create`:
1046+
- in default human mode, stderr shows a short rotation summary and stdout contains exactly one line, the raw new connection URL
1047+
- human stderr does not repeat, label, or wrap the connection URL
1048+
- `--verbose` adds human-only metadata rows such as database and connection id on stderr before the URL is written to stdout
1049+
- `--quiet` suppresses successful stderr output and leaves stdout as exactly the raw connection URL
1050+
- in `--json`, `result.connectionString` contains the raw one-time URL exactly once
1051+
- no `DATABASE_URL=` or `DIRECT_URL=` formatting is added; consumers decide how to store the URL
1052+
- fails with `DATABASE_CONNECTION_NOT_FOUND` when the connection does not exist
1053+
1054+
Examples:
1055+
1056+
```bash
1057+
prisma-cli database connection rotate conn_123 --confirm conn_123
1058+
prisma-cli database connection rotate conn_123 --confirm conn_123 --json
1059+
```
1060+
9471061
### `prisma-cli database connection remove <connection> --confirm <connection-id>`
9481062

9491063
Purpose:

docs/product/resource-model.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,26 @@ top-level target-context group is `branch`, not `env`.
156156
resource.
157157

158158
The beta package exposes `database` as the canonical database management group.
159-
The first slice manages Prisma Postgres database metadata and one-time-view
159+
It manages Prisma Postgres database metadata, usage, backups, and one-time-view
160160
connection strings:
161161

162162
- `database list`
163163
- `database show <database>`
164164
- `database create <name>`
165+
- `database usage <database>`
166+
- `database restore <database>`
165167
- `database remove <database>`
168+
- `database backup list <database>`
166169
- `database connection list <database>`
167170
- `database connection create <database>`
171+
- `database connection rotate <connection>`
168172
- `database connection remove <connection>`
169173

170-
The `database connection` subgroup is nested because connection strings exist
171-
only for databases. It follows the same parent-noun/subordinate-noun/action
172-
shape as `project env <action>`. There is no `database connection show` command:
173-
connection strings are secrets and the platform returns them only during create
174-
operations.
174+
The `database connection` and `database backup` subgroups are nested because
175+
connections and backups exist only for databases. They follow the same
176+
parent-noun/subordinate-noun/action shape as `project env <action>`. There is
177+
no `database connection show` command: connection strings are secrets and the
178+
platform returns them only from create and rotate operations.
175179

176180
Rules:
177181

@@ -185,6 +189,13 @@ Rules:
185189
or return secret values
186190
- database and database connection removal require exact id confirmation with
187191
`--confirm <id>`; `--yes` is not sufficient
192+
- database restore and connection rotation are equally destructive (restore
193+
overwrites the target's data; rotation revokes the previous credentials) and
194+
require the same exact id confirmation with `--confirm <id>`
195+
- backups are platform-created; the CLI lists them and restores from them but
196+
never creates or deletes them in the current slice
197+
- `database usage` and `database backup list` are read-only and never print
198+
secret values
188199
- preview Branch setup writes branch-scoped `DATABASE_URL` and `DIRECT_URL` overrides, not separate app bindings
189200
- first production deploy setup writes production `DATABASE_URL` and `DIRECT_URL` env vars before the App has a live deployment
190201
- database setup never overwrites an existing branch-scoped `DATABASE_URL`

packages/cli/fixtures/mock-api.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,5 +183,45 @@
183183
"createdAt": "2026-06-02T00:00:00.000Z",
184184
"connectionString": "postgresql://secret-production.example.prisma.io/postgres"
185185
}
186+
],
187+
"databaseBackups": [
188+
{
189+
"id": "bkp_101",
190+
"databaseId": "db_123",
191+
"backupType": "full",
192+
"status": "completed",
193+
"size": 1048576,
194+
"createdAt": "2026-06-20T00:00:00.000Z"
195+
},
196+
{
197+
"id": "bkp_102",
198+
"databaseId": "db_123",
199+
"backupType": "incremental",
200+
"status": "completed",
201+
"createdAt": "2026-06-21T00:00:00.000Z"
202+
},
203+
{
204+
"id": "bkp_201",
205+
"databaseId": "db_456",
206+
"backupType": "full",
207+
"status": "completed",
208+
"size": 5242880,
209+
"createdAt": "2026-06-22T00:00:00.000Z"
210+
}
211+
],
212+
"databaseBackupRetentionDays": 35,
213+
"databaseUsage": [
214+
{
215+
"databaseId": "db_123",
216+
"period": {
217+
"start": "2026-06-01T00:00:00.000Z",
218+
"end": "2026-06-30T23:59:59.999Z"
219+
},
220+
"metrics": {
221+
"operations": { "used": 12500, "unit": "ops" },
222+
"storage": { "used": 1.25, "unit": "GiB" }
223+
},
224+
"generatedAt": "2026-07-01T00:00:00.000Z"
225+
}
186226
]
187227
}

packages/cli/src/adapters/mock-api.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ interface DatabaseConnectionRecord {
6969
connectionString?: string;
7070
}
7171

72+
interface DatabaseBackupRecord {
73+
id: string;
74+
databaseId: string;
75+
backupType: string;
76+
status: string;
77+
size?: number;
78+
createdAt: string;
79+
}
80+
81+
interface DatabaseUsageRecord {
82+
databaseId: string;
83+
period: { start: string; end: string };
84+
metrics: {
85+
operations: { used: number; unit: string };
86+
storage: { used: number; unit: string };
87+
};
88+
generatedAt: string;
89+
}
90+
7291
interface MockApiData {
7392
providers: ProviderRecord[];
7493
users: UserRecord[];
@@ -79,6 +98,9 @@ interface MockApiData {
7998
deployments: DeploymentRecord[];
8099
databases?: DatabaseRecord[];
81100
databaseConnections?: DatabaseConnectionRecord[];
101+
databaseBackups?: DatabaseBackupRecord[];
102+
databaseUsage?: DatabaseUsageRecord[];
103+
databaseBackupRetentionDays?: number;
82104
}
83105

84106
export class MockApi {
@@ -317,6 +339,116 @@ export class MockApi {
317339
);
318340
return connection;
319341
}
342+
343+
getDatabaseUsage(
344+
databaseId: string,
345+
period?: { from?: string; to?: string },
346+
): {
347+
period: { start: string; end: string };
348+
metrics: {
349+
operations: { used: number; unit: string };
350+
storage: { used: number; unit: string };
351+
};
352+
generatedAt: string;
353+
} {
354+
const usage = (this.data.databaseUsage ?? []).find(
355+
(record) => record.databaseId === databaseId,
356+
);
357+
const defaults = usage ?? {
358+
databaseId,
359+
period: {
360+
start: "2026-06-01T00:00:00.000Z",
361+
end: "2026-06-30T23:59:59.999Z",
362+
},
363+
metrics: {
364+
operations: { used: 0, unit: "ops" },
365+
storage: { used: 0, unit: "GiB" },
366+
},
367+
generatedAt: "2026-07-01T00:00:00.000Z",
368+
};
369+
370+
return {
371+
period: {
372+
start: period?.from ?? defaults.period.start,
373+
end: period?.to ?? defaults.period.end,
374+
},
375+
metrics: defaults.metrics,
376+
generatedAt: defaults.generatedAt,
377+
};
378+
}
379+
380+
listDatabaseBackups(
381+
databaseId: string,
382+
limit?: number,
383+
): {
384+
backups: Array<{
385+
id: string;
386+
backupType: string;
387+
status: string;
388+
size: number | null;
389+
createdAt: string;
390+
}>;
391+
retentionDays: number | null;
392+
hasMore: boolean;
393+
} {
394+
const backups = (this.data.databaseBackups ?? []).filter(
395+
(backup) => backup.databaseId === databaseId,
396+
);
397+
const limited = limit === undefined ? backups : backups.slice(0, limit);
398+
399+
return {
400+
backups: limited.map((backup) => ({
401+
id: backup.id,
402+
backupType: backup.backupType,
403+
status: backup.status,
404+
size: backup.size ?? null,
405+
createdAt: backup.createdAt,
406+
})),
407+
retentionDays: this.data.databaseBackupRetentionDays ?? null,
408+
hasMore: limited.length < backups.length,
409+
};
410+
}
411+
412+
restoreDatabase(input: {
413+
targetDatabaseId: string;
414+
sourceDatabaseId: string;
415+
backupId: string;
416+
}):
417+
| { outcome: "restored"; database: DatabaseRecord }
418+
| { outcome: "target-not-found" }
419+
| { outcome: "backup-not-found" } {
420+
const target = this.getDatabase(input.targetDatabaseId);
421+
if (!target) {
422+
return { outcome: "target-not-found" };
423+
}
424+
425+
const backup = (this.data.databaseBackups ?? []).find(
426+
(candidate) =>
427+
candidate.id === input.backupId &&
428+
candidate.databaseId === input.sourceDatabaseId,
429+
);
430+
if (!backup) {
431+
return { outcome: "backup-not-found" };
432+
}
433+
434+
target.status = "recovering";
435+
return { outcome: "restored", database: target };
436+
}
437+
438+
rotateDatabaseConnection(
439+
connectionId: string,
440+
):
441+
| { connection: DatabaseConnectionRecord; connectionString: string }
442+
| undefined {
443+
const connection = this.getDatabaseConnection(connectionId);
444+
if (!connection) {
445+
return undefined;
446+
}
447+
448+
const connectionString = `postgresql://rotated-${connection.databaseId}-${connection.id}.example.prisma.io/postgres`;
449+
connection.connectionString = connectionString;
450+
return { connection, connectionString };
451+
}
320452
}
321453

322454
export type {

0 commit comments

Comments
 (0)