Skip to main content

systemprompt_cli/commands/cloud/
db.rs

1use anyhow::{anyhow, Context, Result};
2use clap::Subcommand;
3use systemprompt_cloud::{ProfilePath, ProjectContext};
4use systemprompt_runtime::DatabaseContext;
5
6use crate::cli_settings::CliConfig;
7use crate::commands::infrastructure::db;
8
9#[derive(Debug, Subcommand)]
10pub enum CloudDbCommands {
11    #[command(about = "Run migrations on cloud database")]
12    Migrate {
13        #[arg(long, help = "Profile name")]
14        profile: String,
15    },
16
17    #[command(about = "Execute SQL query (read-only) on cloud database")]
18    Query {
19        #[arg(long, help = "Profile name")]
20        profile: String,
21        sql: String,
22        #[arg(long)]
23        limit: Option<u32>,
24        #[arg(long)]
25        offset: Option<u32>,
26        #[arg(long)]
27        format: Option<String>,
28    },
29
30    #[command(about = "Execute write operation on cloud database")]
31    Execute {
32        #[arg(long, help = "Profile name")]
33        profile: String,
34        sql: String,
35        #[arg(long)]
36        format: Option<String>,
37    },
38
39    #[command(about = "Validate cloud database schema")]
40    Validate {
41        #[arg(long, help = "Profile name")]
42        profile: String,
43    },
44
45    #[command(about = "Show cloud database connection status")]
46    Status {
47        #[arg(long, help = "Profile name")]
48        profile: String,
49    },
50
51    #[command(about = "Show cloud database info")]
52    Info {
53        #[arg(long, help = "Profile name")]
54        profile: String,
55    },
56
57    #[command(about = "List all tables in cloud database")]
58    Tables {
59        #[arg(long, help = "Profile name")]
60        profile: String,
61        #[arg(long, help = "Filter tables by pattern")]
62        filter: Option<String>,
63    },
64
65    #[command(about = "Describe table schema in cloud database")]
66    Describe {
67        #[arg(long, help = "Profile name")]
68        profile: String,
69        table_name: String,
70    },
71
72    #[command(about = "Get row count for a table in cloud database")]
73    Count {
74        #[arg(long, help = "Profile name")]
75        profile: String,
76        table_name: String,
77    },
78
79    #[command(about = "List all indexes in cloud database")]
80    Indexes {
81        #[arg(long, help = "Profile name")]
82        profile: String,
83        #[arg(long, help = "Filter by table name")]
84        table: Option<String>,
85    },
86
87    #[command(about = "Show cloud database and table sizes")]
88    Size {
89        #[arg(long, help = "Profile name")]
90        profile: String,
91    },
92}
93
94impl CloudDbCommands {
95    fn profile_name(&self) -> &str {
96        match self {
97            Self::Migrate { profile }
98            | Self::Query { profile, .. }
99            | Self::Execute { profile, .. }
100            | Self::Validate { profile }
101            | Self::Status { profile }
102            | Self::Info { profile }
103            | Self::Tables { profile, .. }
104            | Self::Describe { profile, .. }
105            | Self::Count { profile, .. }
106            | Self::Indexes { profile, .. }
107            | Self::Size { profile } => profile,
108        }
109    }
110
111    fn into_db_command(self) -> db::DbCommands {
112        match self {
113            Self::Migrate { .. } => db::DbCommands::Migrate,
114            Self::Query {
115                sql,
116                limit,
117                offset,
118                format,
119                ..
120            } => db::DbCommands::Query {
121                sql,
122                limit,
123                offset,
124                format,
125            },
126            Self::Execute { sql, format, .. } => db::DbCommands::Execute { sql, format },
127            Self::Validate { .. } => db::DbCommands::Validate,
128            Self::Status { .. } => db::DbCommands::Status,
129            Self::Info { .. } => db::DbCommands::Info,
130            Self::Tables { filter, .. } => db::DbCommands::Tables { filter },
131            Self::Describe { table_name, .. } => db::DbCommands::Describe { table_name },
132            Self::Count { table_name, .. } => db::DbCommands::Count { table_name },
133            Self::Indexes { table, .. } => db::DbCommands::Indexes { table },
134            Self::Size { .. } => db::DbCommands::Size,
135        }
136    }
137}
138
139pub async fn execute(cmd: CloudDbCommands, config: &CliConfig) -> Result<()> {
140    let profile_name = cmd.profile_name().to_string();
141    let db_url = load_cloud_database_url(&profile_name)?;
142    let db_ctx = DatabaseContext::from_url(&db_url).await?;
143    let db_cmd = cmd.into_db_command();
144
145    db::execute_with_db(db_cmd, &db_ctx, config).await
146}
147
148fn load_cloud_database_url(profile_name: &str) -> Result<String> {
149    let ctx = ProjectContext::discover();
150    let profile_dir = ctx.profile_dir(profile_name);
151
152    if !profile_dir.exists() {
153        return Err(anyhow!("Profile '{}' not found", profile_name));
154    }
155
156    let secrets_path = ProfilePath::Secrets.resolve(&profile_dir);
157    if !secrets_path.exists() {
158        return Err(anyhow!(
159            "No secrets.json found for profile '{}'",
160            profile_name
161        ));
162    }
163
164    let secrets_content = std::fs::read_to_string(&secrets_path)
165        .with_context(|| format!("Failed to read {}", secrets_path.display()))?;
166
167    let secrets: serde_json::Value =
168        serde_json::from_str(&secrets_content).with_context(|| "Failed to parse secrets.json")?;
169
170    secrets["database_url"]
171        .as_str()
172        .map(String::from)
173        .ok_or_else(|| {
174            anyhow!(
175                "No database_url in secrets.json for profile '{}'",
176                profile_name
177            )
178        })
179}