posthog_cli/experimental/query/
mod.rs

1use std::collections::HashMap;
2
3use anyhow::Error;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::invocation_context::context;
8
9pub mod command;
10
11// TODO - we could formalise a lot of this and move it into posthog-rs, tbh
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct QueryRequest {
15    pub query: Query,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub refresh: Option<QueryRefresh>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(tag = "kind")]
22pub enum Query {
23    HogQLQuery { query: String },
24    HogQLMetadata(MetadataQuery),
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum QueryRefresh {
30    Blocking,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MetadataQuery {
35    pub language: MetadataLanguage,
36    pub query: String,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub source: Option<Box<Query>>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub enum MetadataLanguage {
43    #[serde(rename = "hogQL")]
44    HogQL,
45}
46
47pub type HogQLQueryResult = Result<HogQLQueryResponse, HogQLQueryErrorResponse>;
48
49#[derive(Debug, Clone, Deserialize, Serialize)]
50pub struct HogQLQueryResponse {
51    pub cache_key: Option<String>,
52    pub cache_target_age: Option<String>,
53    pub clickhouse: Option<String>, // Clickhouse query text
54    #[serde(default, deserialize_with = "null_is_empty")]
55    pub columns: Vec<String>, // Columns returned from the query
56    pub error: Option<String>,
57    #[serde(default, deserialize_with = "null_is_empty")]
58    pub explain: Vec<String>,
59    #[serde(default, rename = "hasMore", deserialize_with = "null_is_false")]
60    pub has_more: bool,
61    pub hogql: Option<String>, // HogQL query text
62    #[serde(default, deserialize_with = "null_is_false")]
63    pub is_cached: bool,
64    pub last_refresh: Option<String>, // Last time the query was refreshed
65    pub next_allowed_client_refresh_time: Option<String>, // Next time the client can refresh the query
66    pub offset: Option<i64>,                              // Offset of the response rows
67    pub limit: Option<i64>,                               // Limit of the query
68    pub query: Option<String>,                            // Query text
69    #[serde(default, deserialize_with = "null_is_empty")]
70    pub types: Vec<(String, String)>,
71    #[serde(default, deserialize_with = "null_is_empty")]
72    pub results: Vec<Vec<Value>>,
73    #[serde(default, deserialize_with = "null_is_empty")]
74    pub timings: Vec<Timing>,
75    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
76    pub other: HashMap<String, Value>,
77}
78
79#[derive(Debug, Clone, Deserialize, Serialize)]
80pub struct HogQLQueryErrorResponse {
81    pub code: String,
82    pub detail: String,
83    #[serde(rename = "type")]
84    pub error_type: String,
85    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
86    pub other: HashMap<String, Value>,
87}
88
89#[derive(Debug, Clone, Deserialize, Serialize)]
90pub struct Timing {
91    pub k: String,
92    pub t: f64,
93}
94
95#[derive(Debug, Clone, Deserialize, Serialize)]
96pub struct MetadataResponse {
97    #[serde(default, deserialize_with = "null_is_empty")]
98    pub errors: Vec<Notice>,
99    #[serde(default, deserialize_with = "null_is_empty")]
100    pub notices: Vec<Notice>,
101    #[serde(default, deserialize_with = "null_is_empty")]
102    pub warnings: Vec<Notice>,
103    #[serde(default, rename = "isUsingIndices")]
104    pub is_using_indices: Option<IndicesUsage>,
105    #[serde(default, deserialize_with = "null_is_false", rename = "isValid")]
106    pub is_valid: bool,
107    #[serde(default, deserialize_with = "null_is_false")]
108    pub is_valid_view: bool,
109    #[serde(default, deserialize_with = "null_is_empty")]
110    pub table_names: Vec<String>,
111    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
112    pub other: HashMap<String, Value>,
113}
114
115#[derive(Debug, Clone, Deserialize, Serialize)]
116#[serde(rename_all = "lowercase")]
117pub enum IndicesUsage {
118    Undecisive,
119    No,
120    Partial,
121    Yes,
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize)]
125pub struct Notice {
126    pub message: String,
127    #[serde(flatten)]
128    pub span: Option<NoticeSpan>,
129    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
130    pub other: HashMap<String, Value>,
131}
132
133#[derive(Debug, Clone, Deserialize, Serialize)]
134pub struct NoticeSpan {
135    pub start: usize,
136    pub end: usize,
137}
138
139fn null_is_empty<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
140where
141    D: serde::Deserializer<'de>,
142    T: serde::Deserialize<'de>,
143{
144    let opt = Option::deserialize(deserializer)?;
145    match opt {
146        Some(v) => Ok(v),
147        None => Ok(Vec::new()),
148    }
149}
150
151fn null_is_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
152where
153    D: serde::Deserializer<'de>,
154{
155    let opt = Option::deserialize(deserializer)?;
156    match opt {
157        Some(v) => Ok(v),
158        None => Ok(false),
159    }
160}
161
162pub fn run_query(to_run: &str) -> Result<HogQLQueryResult, Error> {
163    let client = &context().client;
164    let request = QueryRequest {
165        query: Query::HogQLQuery {
166            query: to_run.to_string(),
167        },
168        refresh: Some(QueryRefresh::Blocking),
169    };
170
171    let response = client.post("query")?.json(&request).send()?;
172
173    let code = response.status();
174    let body = response.text()?;
175
176    let value: Value = serde_json::from_str(&body)?;
177
178    if !code.is_success() {
179        let error: HogQLQueryErrorResponse = serde_json::from_value(value)?;
180        return Ok(Err(error));
181    }
182
183    // NOTE: We don't do any pagination here, because the HogQLQuery runner doesn't support it
184    let response: HogQLQueryResponse = serde_json::from_value(value)?;
185    Ok(Ok(response))
186}
187
188pub fn check_query(to_run: &str) -> Result<MetadataResponse, Error> {
189    let client = &context().client;
190
191    let query = MetadataQuery {
192        language: MetadataLanguage::HogQL,
193        query: to_run.to_string(),
194        source: None, // TODO - allow for this to be set? Idk if it matters much
195    };
196
197    let query = Query::HogQLMetadata(query);
198
199    let request = QueryRequest {
200        query,
201        refresh: None,
202    };
203
204    let response = client.post("query")?.json(&request).send()?;
205
206    let code = response.status();
207    let body = response.text()?;
208
209    let value: Value = serde_json::from_str(&body)?;
210
211    if !code.is_success() {
212        let error: MetadataResponse = serde_json::from_value(value)?;
213        return Ok(error);
214    }
215
216    let response: MetadataResponse = serde_json::from_value(value)?;
217
218    Ok(response)
219}
220
221impl std::error::Error for HogQLQueryErrorResponse {}
222
223impl std::fmt::Display for HogQLQueryErrorResponse {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        write!(f, "{} ({}): {}", self.error_type, self.code, self.detail)
226    }
227}