Skip to main content

systemprompt_cli/commands/infrastructure/db/
mod.rs

1//! `db` CLI command group: schema inspection, queries, and migration tooling.
2//!
3//! [`execute`] runs commands against the invocation's
4//! [`CommandContext`](crate::context::CommandContext): migration variants are
5//! routed to the profile or standalone dispatcher depending on whether the
6//! invocation is database-scoped, and the remaining subcommands share the
7//! context's pool. Subcommands cover ad-hoc queries, schema introspection,
8//! migration apply/down/repair/squash, and the schema doctor.
9
10mod admin;
11mod admin_migrate;
12mod admin_migrate_down;
13mod admin_migrate_mark_applied;
14mod admin_migrate_plan;
15mod admin_migrate_repair;
16mod admin_migrate_status;
17mod admin_migrations;
18mod admin_squash;
19mod commands;
20mod dispatch;
21mod doctor;
22mod helpers;
23mod introspect;
24mod query;
25mod schema;
26mod types;
27
28use anyhow::{Context, Result, bail};
29use std::sync::Arc;
30use systemprompt_database::{DatabaseAdminService, DbPool, QueryExecutor};
31
32use crate::cli_settings::CliConfig;
33use crate::context::CommandContext;
34use crate::shared::render_result;
35use dispatch::{dispatch_profile_migration, dispatch_standalone_migration};
36
37pub use commands::{DbCommands, MigrationsCommands};
38pub use types::*;
39
40pub async fn execute(cmd: DbCommands, ctx: &CommandContext) -> Result<()> {
41    let config = &ctx.cli;
42    let Some(cmd) = (match ctx.database_context() {
43        Some(db_ctx) => dispatch_standalone_migration(cmd, db_ctx, config).await?,
44        None => dispatch_profile_migration(cmd, config).await?,
45    }) else {
46        return Ok(());
47    };
48
49    let (pool, admin_service, query_executor) = connect_services(ctx).await?;
50
51    match cmd {
52        DbCommands::Query {
53            sql,
54            limit,
55            offset,
56            format: _,
57        } => run_query(&query_executor, &sql, limit, offset, config).await,
58        DbCommands::Execute { sql, format: _ } => run_write(&query_executor, &sql, config).await,
59        DbCommands::Tables { filter } => {
60            schema::execute_tables(&admin_service, filter, config).await
61        },
62        DbCommands::Describe { table_name } => {
63            schema::execute_describe(&admin_service, &table_name, config).await
64        },
65        DbCommands::Info => schema::execute_info(&admin_service, config).await,
66        DbCommands::Migrate { .. }
67        | DbCommands::MigrateDown { .. }
68        | DbCommands::MigrateSquash { .. }
69        | DbCommands::MigrateRepair { .. }
70        | DbCommands::MigrateMarkApplied { .. } => unreachable!(),
71        DbCommands::Migrations { cmd } => {
72            admin::execute_migrations(ctx.app_context().await?, cmd, config).await
73        },
74        DbCommands::MigratePlan { extension, json } => {
75            admin::execute_migrate_plan(
76                ctx.app_context().await?,
77                extension.as_deref(),
78                json,
79                config,
80            )
81            .await
82        },
83        DbCommands::MigrateStatus { extension, json } => {
84            admin::execute_migrate_status(
85                ctx.app_context().await?,
86                extension.as_deref(),
87                json,
88                config,
89            )
90            .await
91        },
92        DbCommands::AssignAdmin { user } => {
93            if ctx.is_database_scoped() {
94                bail!("assign-admin requires full profile context");
95            }
96            admin::execute_assign_admin(ctx.app_context().await?, &user, config).await
97        },
98        DbCommands::Status => admin::execute_status(&admin_service, config).await,
99        DbCommands::Validate => schema::execute_validate(&admin_service, config).await,
100        DbCommands::Count { table_name } => {
101            schema::execute_count(&admin_service, &table_name, config).await
102        },
103        DbCommands::Indexes { table } => {
104            introspect::execute_indexes(&admin_service, table, config).await
105        },
106        DbCommands::Size => introspect::execute_size(&admin_service, config).await,
107        DbCommands::Doctor => doctor::execute_doctor(&pool, config).await,
108    }
109}
110
111async fn connect_services(
112    ctx: &CommandContext,
113) -> Result<(DbPool, DatabaseAdminService, QueryExecutor)> {
114    let pool = ctx
115        .db_pool()
116        .await
117        .context("Failed to connect to database. Check your profile configuration.")?;
118    let write_pool = pool
119        .write_pool_arc()
120        .context("Database must be PostgreSQL")?;
121    let admin_service = DatabaseAdminService::new(Arc::clone(&write_pool));
122    let query_executor = QueryExecutor::new(write_pool);
123    Ok((pool, admin_service, query_executor))
124}
125
126async fn run_query(
127    executor: &QueryExecutor,
128    sql: &str,
129    limit: Option<u32>,
130    offset: Option<u32>,
131    config: &CliConfig,
132) -> Result<()> {
133    let params = query::QueryParams { sql, limit, offset };
134    let result = query::execute_query(executor, &params, config).await?;
135    render_result(&result, config);
136    Ok(())
137}
138
139async fn run_write(executor: &QueryExecutor, sql: &str, config: &CliConfig) -> Result<()> {
140    let result = query::execute_write(executor, sql, config).await?;
141    render_result(&result, config);
142    Ok(())
143}