posthog_cli/commands/
query.rs

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    /// Start the interactive query editor
17    Editor {
18        #[arg(long, default_value = "false")]
19        /// Don't print the final query to stdout
20        no_print: bool,
21        #[arg(long, default_value = "false")]
22        /// Print out query debug information, as well as showing query results
23        debug: bool,
24        #[arg(long, default_value = "false")]
25        /// Run the final query and print the results as json lines to stdout
26        execute: bool,
27    },
28    /// Run a query directly, and print the results as json lines to stdout
29    Run {
30        /// The query to run
31        query: String,
32        #[arg(long)]
33        /// Print the returned json, rather than just the results
34        debug: bool,
35    },
36    /// Syntax and type-check a query, without running it
37    Check {
38        /// The query to check
39        query: String,
40        /// Print the raw response from the server as json
41        #[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            // Given this is an interactive command, we're happy enough to not join the capture handle
57            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            // Given this is an interactive command, we're happy enough to not join the capture handle
73            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
146// We use miette to pretty print notices, warnings and errors across the original query.
147fn 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}