Skip to main content

systemprompt_cli/commands/infrastructure/db/
mod.rs

1mod admin;
2mod helpers;
3mod introspect;
4mod query;
5mod schema;
6mod types;
7
8use anyhow::{bail, Context, Result};
9use clap::Subcommand;
10use std::sync::Arc;
11use systemprompt_database::{DatabaseAdminService, QueryExecutor};
12use systemprompt_runtime::{AppContext, DatabaseContext};
13
14use crate::cli_settings::CliConfig;
15use crate::shared::render_result;
16
17pub use types::*;
18
19#[derive(Debug, Subcommand)]
20pub enum DbCommands {
21    #[command(about = "Execute SQL query (read-only)")]
22    Query {
23        sql: String,
24        #[arg(long)]
25        limit: Option<u32>,
26        #[arg(long)]
27        offset: Option<u32>,
28        #[arg(long)]
29        format: Option<String>,
30    },
31    #[command(about = "Execute write operation (INSERT, UPDATE, DELETE)")]
32    Execute {
33        sql: String,
34        #[arg(long)]
35        format: Option<String>,
36    },
37    #[command(about = "List all tables with row counts and sizes")]
38    Tables {
39        #[arg(long, help = "Filter tables by pattern")]
40        filter: Option<String>,
41    },
42    #[command(about = "Describe table schema with columns and indexes")]
43    Describe { table_name: String },
44    #[command(about = "Show database information")]
45    Info,
46    #[command(about = "Run database migrations")]
47    Migrate,
48    #[command(about = "Show migration status and history")]
49    Migrations {
50        #[command(subcommand)]
51        cmd: MigrationsCommands,
52    },
53    #[command(about = "Assign admin role to a user")]
54    AssignAdmin { user: String },
55    #[command(about = "Show database connection status")]
56    Status,
57    #[command(about = "Validate database schema against expected tables")]
58    Validate,
59    #[command(about = "Get row count for a table")]
60    Count { table_name: String },
61    #[command(about = "List all indexes")]
62    Indexes {
63        #[arg(long, help = "Filter by table name")]
64        table: Option<String>,
65    },
66    #[command(about = "Show database and table sizes")]
67    Size,
68}
69
70#[derive(Debug, Subcommand)]
71pub enum MigrationsCommands {
72    #[command(about = "Show migration status for all extensions")]
73    Status,
74    #[command(about = "Show migration history for an extension")]
75    History {
76        #[arg(help = "Extension ID")]
77        extension: String,
78    },
79}
80
81struct DatabaseTool {
82    ctx: AppContext,
83    admin_service: DatabaseAdminService,
84    query_executor: QueryExecutor,
85}
86
87impl DatabaseTool {
88    async fn new() -> Result<Self> {
89        let ctx = AppContext::new()
90            .await
91            .context("Failed to connect to database. Check your profile configuration.")?;
92        let pool = ctx.db_pool().pool_arc()?;
93        let admin_service = DatabaseAdminService::new(Arc::clone(&pool));
94        let query_executor = QueryExecutor::new(pool);
95        Ok(Self {
96            ctx,
97            admin_service,
98            query_executor,
99        })
100    }
101}
102
103pub async fn execute(cmd: DbCommands, config: &CliConfig) -> Result<()> {
104    if matches!(cmd, DbCommands::Migrate) {
105        return admin::execute_migrate(config).await;
106    }
107
108    let db = DatabaseTool::new().await?;
109
110    match cmd {
111        DbCommands::Query {
112            sql,
113            limit,
114            offset,
115            format: _,
116        } => {
117            let params = query::QueryParams {
118                sql: &sql,
119                limit,
120                offset,
121            };
122            let result = query::execute_query(&db.query_executor, &params, config).await?;
123            render_result(&result);
124            Ok(())
125        },
126        DbCommands::Execute { sql, format: _ } => {
127            let result = query::execute_write(&db.query_executor, &sql, config).await?;
128            render_result(&result);
129            Ok(())
130        },
131        DbCommands::Tables { filter } => {
132            schema::execute_tables(&db.admin_service, filter, config).await
133        },
134        DbCommands::Describe { table_name } => {
135            schema::execute_describe(&db.admin_service, &table_name, config).await
136        },
137        DbCommands::Info => schema::execute_info(&db.admin_service, config).await,
138        DbCommands::Migrate => unreachable!(),
139        DbCommands::Migrations { cmd } => admin::execute_migrations(&db.ctx, cmd, config).await,
140        DbCommands::AssignAdmin { user } => {
141            admin::execute_assign_admin(&db.ctx, &user, config).await
142        },
143        DbCommands::Status => admin::execute_status(&db.admin_service, config).await,
144        DbCommands::Validate => schema::execute_validate(&db.admin_service, config).await,
145        DbCommands::Count { table_name } => {
146            schema::execute_count(&db.admin_service, &table_name, config).await
147        },
148        DbCommands::Indexes { table } => {
149            introspect::execute_indexes(&db.admin_service, table, config).await
150        },
151        DbCommands::Size => introspect::execute_size(&db.admin_service, config).await,
152    }
153}
154
155pub async fn execute_with_db(
156    cmd: DbCommands,
157    db_ctx: &DatabaseContext,
158    config: &CliConfig,
159) -> Result<()> {
160    let pool = db_ctx
161        .db_pool()
162        .pool_arc()
163        .context("Database must be PostgreSQL")?;
164    let admin_service = DatabaseAdminService::new(Arc::clone(&pool));
165    let query_executor = QueryExecutor::new(pool);
166
167    match cmd {
168        DbCommands::Query {
169            sql,
170            limit,
171            offset,
172            format: _,
173        } => {
174            let params = query::QueryParams {
175                sql: &sql,
176                limit,
177                offset,
178            };
179            let result = query::execute_query(&query_executor, &params, config).await?;
180            render_result(&result);
181            Ok(())
182        },
183        DbCommands::Execute { sql, format: _ } => {
184            let result = query::execute_write(&query_executor, &sql, config).await?;
185            render_result(&result);
186            Ok(())
187        },
188        DbCommands::Tables { filter } => {
189            schema::execute_tables(&admin_service, filter, config).await
190        },
191        DbCommands::Describe { table_name } => {
192            schema::execute_describe(&admin_service, &table_name, config).await
193        },
194        DbCommands::Info => schema::execute_info(&admin_service, config).await,
195        DbCommands::Migrate => admin::execute_migrate_standalone(db_ctx, config).await,
196        DbCommands::Migrations { cmd } => {
197            admin::execute_migrations_standalone(db_ctx, cmd, config).await
198        },
199        DbCommands::AssignAdmin { .. } => {
200            bail!("assign-admin requires full profile context")
201        },
202        DbCommands::Status => admin::execute_status(&admin_service, config).await,
203        DbCommands::Validate => schema::execute_validate(&admin_service, config).await,
204        DbCommands::Count { table_name } => {
205            schema::execute_count(&admin_service, &table_name, config).await
206        },
207        DbCommands::Indexes { table } => {
208            introspect::execute_indexes(&admin_service, table, config).await
209        },
210        DbCommands::Size => introspect::execute_size(&admin_service, config).await,
211    }
212}