systemprompt_cli/commands/cloud/
db.rs1use 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}