1use anyhow::Error;
2use clap::Subcommand;
3use miette::{Diagnostic, SourceSpan};
4
5use crate::{
6 tui::query::start_query_editor,
7 utils::{
8 auth::load_token,
9 posthog::capture_command_invoked,
10 query::{check_query, run_query, MetadataResponse, Notice},
11 },
12};
13
14#[derive(Debug, Subcommand)]
15pub enum QueryCommand {
16 Editor {
18 #[arg(long, default_value = "false")]
19 no_print: bool,
21 #[arg(long, default_value = "false")]
22 debug: bool,
24 #[arg(long, default_value = "false")]
25 execute: bool,
27 },
28 Run {
30 query: String,
32 #[arg(long)]
33 debug: bool,
35 },
36 Check {
38 query: String,
40 #[arg(long)]
42 raw: bool,
43 },
44}
45
46pub fn query_command(host: Option<String>, query: &QueryCommand) -> Result<(), Error> {
47 let creds = load_token()?;
48 let host = creds.get_host(host.as_deref());
49
50 match query {
51 QueryCommand::Editor {
52 no_print,
53 debug,
54 execute,
55 } => {
56 let handle = capture_command_invoked("query_editor", Some(creds.env_id.clone()));
58 let res = start_query_editor(&host, creds.clone(), *debug)?;
59 if !no_print {
60 println!("Final query: {res}");
61 }
62 if *execute {
63 let query_endpoint = format!("{}/api/environments/{}/query", host, creds.env_id);
64 let res = run_query(&query_endpoint, &creds.token, &res)??;
65 for result in res.results {
66 println!("{}", serde_json::to_string(&result)?);
67 }
68 }
69 let _ = handle.join();
70 }
71 QueryCommand::Run { query, debug } => {
72 let handle = capture_command_invoked("query_run", Some(creds.env_id.clone()));
74 let query_endpoint = format!("{}/api/environments/{}/query", host, creds.env_id);
75 let res = run_query(&query_endpoint, &creds.token, query)??;
76 if *debug {
77 println!("{}", serde_json::to_string_pretty(&res)?);
78 } else {
79 for result in res.results {
80 println!("{}", serde_json::to_string(&result)?);
81 }
82 }
83 let _ = handle.join();
84 }
85 QueryCommand::Check { query, raw } => {
86 let handle = capture_command_invoked("query_check", Some(creds.env_id.clone()));
87 let query_endpoint = format!("{}/api/environments/{}/query", host, creds.env_id);
88 let res = check_query(&query_endpoint, &creds.token, query)?;
89 if *raw {
90 println!("{}", serde_json::to_string_pretty(&res)?);
91 } else {
92 pretty_print_check_response(query, res)?;
93 }
94 let _ = handle.join();
95 }
96 }
97
98 Ok(())
99}
100
101#[derive(thiserror::Error, Debug, Diagnostic)]
102#[error("Query checked")]
103#[diagnostic()]
104struct CheckDiagnostic {
105 #[source_code]
106 source_code: String,
107
108 #[related]
109 errors: Vec<CheckError>,
110 #[related]
111 warnings: Vec<CheckWarning>,
112 #[related]
113 notices: Vec<CheckNotice>,
114}
115
116#[derive(thiserror::Error, Debug, Diagnostic)]
117#[error("Error")]
118#[diagnostic(severity(Error))]
119struct CheckError {
120 #[help]
121 message: String,
122 #[label]
123 err_span: SourceSpan,
124}
125
126#[derive(thiserror::Error, Debug, Diagnostic)]
127#[error("Warning")]
128#[diagnostic(severity(Warning))]
129struct CheckWarning {
130 #[help]
131 message: String,
132 #[label]
133 err_span: SourceSpan,
134}
135
136#[derive(thiserror::Error, Debug, Diagnostic)]
137#[error("Notice")]
138#[diagnostic(severity(Info))]
139struct CheckNotice {
140 #[help]
141 message: String,
142 #[label]
143 err_span: SourceSpan,
144}
145
146fn pretty_print_check_response(query: &str, res: MetadataResponse) -> Result<(), Error> {
148 let errors = res.errors.into_iter().map(CheckError::from).collect();
149 let warnings = res.warnings.into_iter().map(CheckWarning::from).collect();
150 let notices = res.notices.into_iter().map(CheckNotice::from).collect();
151
152 let diagnostic: miette::Error = CheckDiagnostic {
153 source_code: query.to_string(),
154 errors,
155 warnings,
156 notices,
157 }
158 .into();
159
160 println!("{diagnostic:?}");
161
162 Ok(())
163}
164
165impl From<Notice> for CheckNotice {
166 fn from(notice: Notice) -> Self {
167 let (start, len) = match notice.span {
168 Some(span) => (span.start, span.end - span.start),
169 None => (0, 0),
170 };
171 Self {
172 message: notice.message,
173 err_span: SourceSpan::new(start.into(), len),
174 }
175 }
176}
177
178impl From<Notice> for CheckWarning {
179 fn from(notice: Notice) -> Self {
180 let (start, len) = match notice.span {
181 Some(span) => (span.start, span.end - span.start),
182 None => (0, 0),
183 };
184 Self {
185 message: notice.message,
186 err_span: SourceSpan::new(start.into(), len),
187 }
188 }
189}
190
191impl From<Notice> for CheckError {
192 fn from(notice: Notice) -> Self {
193 let (start, len) = match notice.span {
194 Some(span) => (span.start, span.end - span.start),
195 None => (0, 0),
196 };
197 Self {
198 message: notice.message,
199 err_span: SourceSpan::new(start.into(), len),
200 }
201 }
202}