posthog_cli/experimental/query/
command.rs

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