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        query::{check_query, run_query, MetadataResponse, Notice},
10    },
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(host: &str, query: &QueryCommand) -> Result<(), Error> {
46    let creds = load_token()?;
47
48    match query {
49        QueryCommand::Editor {
50            no_print,
51            debug,
52            execute,
53        } => {
54            let res = start_query_editor(host, creds.clone(), *debug)?;
55            if !no_print {
56                println!("Final query: {}", res);
57            }
58            if *execute {
59                let query_endpoint = format!("{}/api/environments/{}/query", host, creds.env_id);
60                let res = run_query(&query_endpoint, &creds.token, &res)??;
61                for result in res.results {
62                    println!("{}", serde_json::to_string(&result)?);
63                }
64            }
65        }
66        QueryCommand::Run { query, debug } => {
67            let query_endpoint = format!("{}/api/environments/{}/query", host, creds.env_id);
68            let res = run_query(&query_endpoint, &creds.token, query)??;
69            if *debug {
70                println!("{}", serde_json::to_string_pretty(&res)?);
71            } else {
72                for result in res.results {
73                    println!("{}", serde_json::to_string(&result)?);
74                }
75            }
76        }
77        QueryCommand::Check { query, raw } => {
78            let query_endpoint = format!("{}/api/environments/{}/query", host, creds.env_id);
79            let res = check_query(&query_endpoint, &creds.token, query)?;
80            if *raw {
81                println!("{}", serde_json::to_string_pretty(&res)?);
82            } else {
83                pretty_print_check_response(query, res)?;
84            }
85        }
86    }
87
88    Ok(())
89}
90
91#[derive(thiserror::Error, Debug, Diagnostic)]
92#[error("Query checked")]
93#[diagnostic()]
94struct CheckDiagnostic {
95    #[source_code]
96    source_code: String,
97
98    #[related]
99    errors: Vec<CheckError>,
100    #[related]
101    warnings: Vec<CheckWarning>,
102    #[related]
103    notices: Vec<CheckNotice>,
104}
105
106#[derive(thiserror::Error, Debug, Diagnostic)]
107#[error("Error")]
108#[diagnostic(severity(Error))]
109struct CheckError {
110    #[help]
111    message: String,
112    #[label]
113    err_span: SourceSpan,
114}
115
116#[derive(thiserror::Error, Debug, Diagnostic)]
117#[error("Warning")]
118#[diagnostic(severity(Warning))]
119struct CheckWarning {
120    #[help]
121    message: String,
122    #[label]
123    err_span: SourceSpan,
124}
125
126#[derive(thiserror::Error, Debug, Diagnostic)]
127#[error("Notice")]
128#[diagnostic(severity(Info))]
129struct CheckNotice {
130    #[help]
131    message: String,
132    #[label]
133    err_span: SourceSpan,
134}
135
136// We use miette to pretty print notices, warnings and errors across the original query.
137fn pretty_print_check_response(query: &str, res: MetadataResponse) -> Result<(), Error> {
138    let errors = res.errors.into_iter().map(CheckError::from).collect();
139    let warnings = res.warnings.into_iter().map(CheckWarning::from).collect();
140    let notices = res.notices.into_iter().map(CheckNotice::from).collect();
141
142    let diagnostic: miette::Error = CheckDiagnostic {
143        source_code: query.to_string(),
144        errors,
145        warnings,
146        notices,
147    }
148    .into();
149
150    println!("{:?}", diagnostic);
151
152    Ok(())
153}
154
155impl From<Notice> for CheckNotice {
156    fn from(notice: Notice) -> Self {
157        let (start, len) = match notice.span {
158            Some(span) => (span.start, span.end - span.start),
159            None => (0, 0),
160        };
161        Self {
162            message: notice.message,
163            err_span: SourceSpan::new(start.into(), len),
164        }
165    }
166}
167
168impl From<Notice> for CheckWarning {
169    fn from(notice: Notice) -> Self {
170        let (start, len) = match notice.span {
171            Some(span) => (span.start, span.end - span.start),
172            None => (0, 0),
173        };
174        Self {
175            message: notice.message,
176            err_span: SourceSpan::new(start.into(), len),
177        }
178    }
179}
180
181impl From<Notice> for CheckError {
182    fn from(notice: Notice) -> Self {
183        let (start, len) = match notice.span {
184            Some(span) => (span.start, span.end - span.start),
185            None => (0, 0),
186        };
187        Self {
188            message: notice.message,
189            err_span: SourceSpan::new(start.into(), len),
190        }
191    }
192}