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    match query {
47        QueryCommand::Editor {
48            no_print,
49            debug,
50            execute,
51        } => {
52            // Given this is an interactive command, we're happy enough to not join the capture handle
53            context().capture_command_invoked("query_editor");
54            let res = start_query_editor(*debug)?;
55            if !no_print {
56                println!("Final query: {res}");
57            }
58            if *execute {
59                let res = run_query(&res)??;
60                for result in res.results {
61                    println!("{}", serde_json::to_string(&result)?);
62                }
63            }
64        }
65        QueryCommand::Run { query, debug } => {
66            // Given this is an interactive command, we're happy enough to not join the capture handle
67            context().capture_command_invoked("query_run");
68            let res = run_query(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            context().capture_command_invoked("query_check");
79            let res = check_query(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}