use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use super::common::MetabaseId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DatasetQuery {
pub database: MetabaseId,
#[serde(rename = "type")]
pub query_type: String,
pub query: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Vec<QueryParameter>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<QueryConstraints>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NativeQuery {
pub query: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
#[serde(rename = "template-tags")]
pub template_tags: HashMap<String, TemplateTag>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collection: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QueryParameter {
pub id: String,
#[serde(rename = "type")]
pub parameter_type: String,
pub value: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemplateTag {
pub id: String,
pub name: String,
#[serde(rename = "display-name")]
pub display_name: String,
#[serde(rename = "type")]
pub tag_type: String,
#[serde(default)]
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QueryConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_results: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_execution_time: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QueryResult {
pub data: QueryData,
pub database_id: MetabaseId,
pub started_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finished_at: Option<DateTime<Utc>>,
pub json_query: Value,
pub status: QueryStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub row_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub running_time: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum QueryStatus {
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QueryData {
pub cols: Vec<Column>,
pub rows: Vec<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub native_form: Option<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub insights: Vec<Insight>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Column {
pub name: String,
pub display_name: String,
pub base_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub effective_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub semantic_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub field_ref: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Insight {
#[serde(rename = "type")]
pub insight_type: String,
pub value: Value,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExecuteQueryRequest {
pub dataset_query: DatasetQuery,
#[serde(skip_serializing_if = "Option::is_none")]
pub visualization_settings: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExecuteNativeQueryRequest {
pub database: MetabaseId,
pub native: NativeQuery,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Vec<QueryParameter>>,
}
impl DatasetQuery {
pub fn builder(database: MetabaseId) -> DatasetQueryBuilder {
DatasetQueryBuilder::new(database)
}
}
impl NativeQuery {
pub fn new(sql: impl Into<String>) -> Self {
Self {
query: sql.into(),
template_tags: HashMap::new(),
collection: None,
}
}
pub fn builder(sql: impl Into<String>) -> NativeQueryBuilder {
NativeQueryBuilder::new(sql)
}
pub fn with_param(mut self, name: &str, value: Value) -> Self {
let tag = TemplateTag {
id: uuid::Uuid::new_v4().to_string(),
name: name.to_string(),
display_name: name.to_string(),
tag_type: match &value {
Value::String(_) => "text",
Value::Number(_) => "number",
Value::Bool(_) => "text",
_ => "text",
}
.to_string(),
required: false,
default: Some(value),
};
self.template_tags.insert(name.to_string(), tag);
self
}
}
pub struct DatasetQueryBuilder {
database: MetabaseId,
query_type: String,
query: Value,
parameters: Option<Vec<QueryParameter>>,
constraints: Option<QueryConstraints>,
}
impl DatasetQueryBuilder {
pub fn new(database: MetabaseId) -> Self {
Self {
database,
query_type: "query".to_string(),
query: Value::Null,
parameters: None,
constraints: None,
}
}
pub fn query_type(mut self, query_type: impl Into<String>) -> Self {
self.query_type = query_type.into();
self
}
pub fn query(mut self, query: Value) -> Self {
self.query = query;
self
}
pub fn parameters(mut self, params: Vec<QueryParameter>) -> Self {
self.parameters = Some(params);
self
}
pub fn constraints(mut self, constraints: QueryConstraints) -> Self {
self.constraints = Some(constraints);
self
}
pub fn build(self) -> DatasetQuery {
DatasetQuery {
database: self.database,
query_type: self.query_type,
query: self.query,
parameters: self.parameters,
constraints: self.constraints,
}
}
}
pub struct NativeQueryBuilder {
query: String,
template_tags: HashMap<String, TemplateTag>,
collection: Option<String>,
}
impl NativeQueryBuilder {
pub fn new(sql: impl Into<String>) -> Self {
Self {
query: sql.into(),
template_tags: HashMap::new(),
collection: None,
}
}
pub fn add_param(mut self, name: &str, param_type: &str, value: Value) -> Self {
let tag = TemplateTag {
id: uuid::Uuid::new_v4().to_string(),
name: name.to_string(),
display_name: name.to_string(),
tag_type: param_type.to_string(),
required: false,
default: Some(value),
};
self.template_tags.insert(name.to_string(), tag);
self
}
pub fn add_text_param(self, name: &str, value: &str) -> Self {
self.add_param(name, "text", Value::String(value.to_string()))
}
pub fn add_number_param(self, name: &str, value: f64) -> Self {
self.add_param(name, "number", serde_json::json!(value))
}
pub fn add_date_param(self, name: &str, value: &str) -> Self {
self.add_param(name, "date", Value::String(value.to_string()))
}
pub fn with_params(mut self, params: HashMap<String, Value>) -> Self {
for (name, value) in params {
let param_type = match &value {
Value::String(_) => "text",
Value::Number(_) => "number",
Value::Bool(_) => "text",
_ => "text",
};
let tag = TemplateTag {
id: uuid::Uuid::new_v4().to_string(),
name: name.clone(),
display_name: name.clone(),
tag_type: param_type.to_string(),
required: false,
default: Some(value),
};
self.template_tags.insert(name, tag);
}
self
}
pub fn collection(mut self, collection: impl Into<String>) -> Self {
self.collection = Some(collection.into());
self
}
pub fn build(self) -> NativeQuery {
NativeQuery {
query: self.query,
template_tags: self.template_tags,
collection: self.collection,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_dataset_query_builder() {
let query = DatasetQuery::builder(MetabaseId(1))
.query_type("native")
.query(json!({"query": "SELECT * FROM users"}))
.build();
assert_eq!(query.database, MetabaseId(1));
assert_eq!(query.query_type, "native");
assert_eq!(query.query, json!({"query": "SELECT * FROM users"}));
}
#[test]
fn test_native_query() {
let mut template_tags = HashMap::new();
template_tags.insert(
"date".to_string(),
TemplateTag {
id: "test-id".to_string(),
name: "date".to_string(),
display_name: "Date".to_string(),
tag_type: "date".to_string(),
required: true,
default: None,
},
);
let native = NativeQuery {
query: "SELECT * FROM orders WHERE created_at > {{date}}".to_string(),
template_tags,
collection: None,
};
assert_eq!(native.template_tags.len(), 1);
assert!(native.template_tags.contains_key("date"));
assert!(native.template_tags["date"].required);
}
#[test]
fn test_native_query_builder() {
let query = NativeQuery::builder("SELECT * FROM orders WHERE status = {{status}}")
.add_text_param("status", "completed")
.build();
assert_eq!(
query.query,
"SELECT * FROM orders WHERE status = {{status}}"
);
assert!(query.template_tags.contains_key("status"));
assert_eq!(query.template_tags["status"].tag_type, "text");
assert_eq!(
query.template_tags["status"].default,
Some(json!("completed"))
);
}
#[test]
fn test_query_result() {
let result = QueryResult {
data: QueryData {
cols: vec![Column {
name: "id".to_string(),
display_name: "ID".to_string(),
base_type: "type/Integer".to_string(),
effective_type: None,
semantic_type: None,
field_ref: None,
}],
rows: vec![vec![json!(1)], vec![json!(2)]],
native_form: None,
insights: vec![],
},
database_id: MetabaseId(1),
started_at: Utc::now(),
finished_at: Some(Utc::now()),
json_query: json!({}),
status: QueryStatus::Completed,
row_count: Some(2),
running_time: Some(150),
};
assert_eq!(result.status, QueryStatus::Completed);
assert_eq!(result.row_count, Some(2));
assert_eq!(result.data.rows.len(), 2);
}
#[test]
fn test_query_constraints() {
let constraints = QueryConstraints {
max_results: Some(1000),
max_execution_time: Some(60),
};
assert_eq!(constraints.max_results, Some(1000));
assert_eq!(constraints.max_execution_time, Some(60));
}
}