diff --git a/packages/common/api-helper/build/src/macro_util.rs b/packages/common/api-helper/build/src/macro_util.rs index 03d556e0e7..20841ad68a 100644 --- a/packages/common/api-helper/build/src/macro_util.rs +++ b/packages/common/api-helper/build/src/macro_util.rs @@ -237,7 +237,7 @@ pub fn __deserialize_query(route: &Url) -> GlobalRes } #[doc(hidden)] -#[tracing::instrument(skip_all, name="setup_ctx")] +#[tracing::instrument(skip_all, name = "setup_ctx")] pub async fn __with_ctx< A: auth::ApiAuth + Send, DB: chirp_workflow::db::Database + Sync + 'static, diff --git a/packages/common/clickhouse-inserter/src/error.rs b/packages/common/clickhouse-inserter/src/error.rs index 9d389ecabb..eb5a600f98 100644 --- a/packages/common/clickhouse-inserter/src/error.rs +++ b/packages/common/clickhouse-inserter/src/error.rs @@ -2,15 +2,15 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum Error { - #[error("failed to send event to ClickHouse inserter")] - ChannelSendError, + #[error("failed to send event to ClickHouse inserter")] + ChannelSendError, - #[error("serialization error: {0}")] - SerializationError(#[source] serde_json::Error), + #[error("serialization error: {0}")] + SerializationError(#[source] serde_json::Error), - #[error("failed to build reqwest client: {0}")] - ReqwestBuildError(#[source] reqwest::Error), + #[error("failed to build reqwest client: {0}")] + ReqwestBuildError(#[source] reqwest::Error), - #[error("failed to spawn background task")] - TaskSpawnError, -} \ No newline at end of file + #[error("failed to spawn background task")] + TaskSpawnError, +} diff --git a/packages/common/clickhouse-user-query/src/builder.rs b/packages/common/clickhouse-user-query/src/builder.rs index 8b2c854e58..57a0d2d5ac 100644 --- a/packages/common/clickhouse-user-query/src/builder.rs +++ b/packages/common/clickhouse-user-query/src/builder.rs @@ -8,200 +8,261 @@ use crate::schema::{PropertyType, Schema}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserDefinedQueryBuilder { - where_clause: String, - bind_values: Vec, + where_clause: String, + bind_values: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] enum BindValue { - Bool(bool), - String(String), - Number(f64), - ArrayString(Vec), + Bool(bool), + String(String), + Number(f64), + ArrayString(Vec), } impl UserDefinedQueryBuilder { - pub fn new(schema: &Schema, expr: &QueryExpr) -> Result { - let mut builder = QueryBuilder::new(schema); - let where_clause = builder.build_where_clause(expr)?; - - if where_clause.trim().is_empty() { - return Err(UserQueryError::EmptyQuery); - } - - Ok(Self { - where_clause, - bind_values: builder.bind_values, - }) - } - - pub fn bind_to(&self, mut query: Query) -> Query { - for bind_value in &self.bind_values { - query = match bind_value { - BindValue::Bool(v) => query.bind(*v), - BindValue::String(v) => query.bind(v), - BindValue::Number(v) => query.bind(*v), - BindValue::ArrayString(v) => query.bind(v), - }; - } - query - } - - pub fn where_expr(&self) -> &str { - &self.where_clause - } + pub fn new(schema: &Schema, expr: &QueryExpr) -> Result { + let mut builder = QueryBuilder::new(schema); + let where_clause = builder.build_where_clause(expr)?; + + if where_clause.trim().is_empty() { + return Err(UserQueryError::EmptyQuery); + } + + Ok(Self { + where_clause, + bind_values: builder.bind_values, + }) + } + + pub fn bind_to(&self, mut query: Query) -> Query { + for bind_value in &self.bind_values { + query = match bind_value { + BindValue::Bool(v) => query.bind(*v), + BindValue::String(v) => query.bind(v), + BindValue::Number(v) => query.bind(*v), + BindValue::ArrayString(v) => query.bind(v), + }; + } + query + } + + pub fn where_expr(&self) -> &str { + &self.where_clause + } } struct QueryBuilder<'a> { - schema: &'a Schema, - bind_values: Vec, + schema: &'a Schema, + bind_values: Vec, } impl<'a> QueryBuilder<'a> { - fn new(schema: &'a Schema) -> Self { - Self { - schema, - bind_values: Vec::new(), - } - } - - fn build_where_clause(&mut self, expr: &QueryExpr) -> Result { - match expr { - QueryExpr::And { exprs } => { - if exprs.is_empty() { - return Err(UserQueryError::EmptyQuery); - } - let clauses: Result> = exprs - .iter() - .map(|e| self.build_where_clause(e)) - .collect(); - Ok(format!("({})", clauses?.join(" AND "))) - } - QueryExpr::Or { exprs } => { - if exprs.is_empty() { - return Err(UserQueryError::EmptyQuery); - } - let clauses: Result> = exprs - .iter() - .map(|e| self.build_where_clause(e)) - .collect(); - Ok(format!("({})", clauses?.join(" OR "))) - } - QueryExpr::BoolEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Bool)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Bool(*value)); - Ok(format!("{} = ?", column)) - } - QueryExpr::BoolNotEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Bool)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Bool(*value)); - Ok(format!("{} != ?", column)) - } - QueryExpr::StringEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::String)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::String(value.clone())); - Ok(format!("{} = ?", column)) - } - QueryExpr::StringNotEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::String)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::String(value.clone())); - Ok(format!("{} != ?", column)) - } - QueryExpr::ArrayContains { property, subproperty, values } => { - if values.is_empty() { - return Err(UserQueryError::EmptyArrayValues("ArrayContains".to_string())); - } - self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::ArrayString(values.clone())); - Ok(format!("hasAny({}, ?)", column)) - } - QueryExpr::ArrayDoesNotContain { property, subproperty, values } => { - if values.is_empty() { - return Err(UserQueryError::EmptyArrayValues("ArrayDoesNotContain".to_string())); - } - self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::ArrayString(values.clone())); - Ok(format!("NOT hasAny({}, ?)", column)) - } - QueryExpr::NumberEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} = ?", column)) - } - QueryExpr::NumberNotEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} != ?", column)) - } - QueryExpr::NumberLess { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} < ?", column)) - } - QueryExpr::NumberLessOrEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} <= ?", column)) - } - QueryExpr::NumberGreater { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} > ?", column)) - } - QueryExpr::NumberGreaterOrEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} >= ?", column)) - } - } - } - - fn validate_property_access( - &self, - property: &str, - subproperty: &Option, - expected_type: &PropertyType, - ) -> Result<()> { - let prop = self.schema.get_property(property) - .ok_or_else(|| UserQueryError::PropertyNotFound(property.to_string()))?; - - if subproperty.is_some() && !prop.supports_subproperties { - return Err(UserQueryError::SubpropertiesNotSupported(property.to_string())); - } - - if &prop.ty != expected_type { - return Err(UserQueryError::PropertyTypeMismatch( - property.to_string(), - expected_type.type_name().to_string(), - prop.ty.type_name().to_string(), - )); - } - - Ok(()) - } - - fn build_column_reference(&self, property: &str, subproperty: &Option) -> Result { - let property_ident = Identifier(property); - - match subproperty { - Some(subprop) => { - // For ClickHouse Map access, use string literal syntax - Ok(format!("{}[{}]", property_ident.0, format!("'{}'", subprop.replace("'", "\\'")))) - } - None => Ok(property_ident.0.to_string()), - } - } -} + fn new(schema: &'a Schema) -> Self { + Self { + schema, + bind_values: Vec::new(), + } + } + + fn build_where_clause(&mut self, expr: &QueryExpr) -> Result { + match expr { + QueryExpr::And { exprs } => { + if exprs.is_empty() { + return Err(UserQueryError::EmptyQuery); + } + let clauses: Result> = + exprs.iter().map(|e| self.build_where_clause(e)).collect(); + Ok(format!("({})", clauses?.join(" AND "))) + } + QueryExpr::Or { exprs } => { + if exprs.is_empty() { + return Err(UserQueryError::EmptyQuery); + } + let clauses: Result> = + exprs.iter().map(|e| self.build_where_clause(e)).collect(); + Ok(format!("({})", clauses?.join(" OR "))) + } + QueryExpr::BoolEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Bool)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Bool(*value)); + Ok(format!("{} = ?", column)) + } + QueryExpr::BoolNotEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Bool)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Bool(*value)); + Ok(format!("{} != ?", column)) + } + QueryExpr::StringEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::String)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::String(value.clone())); + Ok(format!("{} = ?", column)) + } + QueryExpr::StringNotEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::String)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::String(value.clone())); + Ok(format!("{} != ?", column)) + } + QueryExpr::ArrayContains { + property, + subproperty, + values, + } => { + if values.is_empty() { + return Err(UserQueryError::EmptyArrayValues( + "ArrayContains".to_string(), + )); + } + self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values + .push(BindValue::ArrayString(values.clone())); + Ok(format!("hasAny({}, ?)", column)) + } + QueryExpr::ArrayDoesNotContain { + property, + subproperty, + values, + } => { + if values.is_empty() { + return Err(UserQueryError::EmptyArrayValues( + "ArrayDoesNotContain".to_string(), + )); + } + self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values + .push(BindValue::ArrayString(values.clone())); + Ok(format!("NOT hasAny({}, ?)", column)) + } + QueryExpr::NumberEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} = ?", column)) + } + QueryExpr::NumberNotEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} != ?", column)) + } + QueryExpr::NumberLess { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} < ?", column)) + } + QueryExpr::NumberLessOrEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} <= ?", column)) + } + QueryExpr::NumberGreater { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} > ?", column)) + } + QueryExpr::NumberGreaterOrEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} >= ?", column)) + } + } + } + fn validate_property_access( + &self, + property: &str, + subproperty: &Option, + expected_type: &PropertyType, + ) -> Result<()> { + let prop = self + .schema + .get_property(property) + .ok_or_else(|| UserQueryError::PropertyNotFound(property.to_string()))?; + + if subproperty.is_some() && !prop.supports_subproperties { + return Err(UserQueryError::SubpropertiesNotSupported( + property.to_string(), + )); + } + + if &prop.ty != expected_type { + return Err(UserQueryError::PropertyTypeMismatch( + property.to_string(), + expected_type.type_name().to_string(), + prop.ty.type_name().to_string(), + )); + } + + Ok(()) + } + + fn build_column_reference( + &self, + property: &str, + subproperty: &Option, + ) -> Result { + let property_ident = Identifier(property); + + match subproperty { + Some(subprop) => { + // For ClickHouse Map access, use string literal syntax + Ok(format!( + "{}[{}]", + property_ident.0, + format!("'{}'", subprop.replace("'", "\\'")) + )) + } + None => Ok(property_ident.0.to_string()), + } + } +} diff --git a/packages/common/clickhouse-user-query/src/error.rs b/packages/common/clickhouse-user-query/src/error.rs index 5dbdc30d53..5b4fc009cb 100644 --- a/packages/common/clickhouse-user-query/src/error.rs +++ b/packages/common/clickhouse-user-query/src/error.rs @@ -2,23 +2,23 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum UserQueryError { - #[error("Property '{0}' not found in schema")] - PropertyNotFound(String), - - #[error("Property '{0}' does not support subproperties")] - SubpropertiesNotSupported(String), - - #[error("Property '{0}' type mismatch: expected {1}, got {2}")] - PropertyTypeMismatch(String, String, String), - - #[error("Invalid property or subproperty name '{0}': must contain only alphanumeric characters and underscores")] - InvalidPropertyName(String), - - #[error("Empty query expression")] - EmptyQuery, - - #[error("Empty array values in {0} operation")] - EmptyArrayValues(String), + #[error("Property '{0}' not found in schema")] + PropertyNotFound(String), + + #[error("Property '{0}' does not support subproperties")] + SubpropertiesNotSupported(String), + + #[error("Property '{0}' type mismatch: expected {1}, got {2}")] + PropertyTypeMismatch(String, String, String), + + #[error("Invalid property or subproperty name '{0}': must contain only alphanumeric characters and underscores")] + InvalidPropertyName(String), + + #[error("Empty query expression")] + EmptyQuery, + + #[error("Empty array values in {0} operation")] + EmptyArrayValues(String), } -pub type Result = std::result::Result; \ No newline at end of file +pub type Result = std::result::Result; diff --git a/packages/common/clickhouse-user-query/src/lib.rs b/packages/common/clickhouse-user-query/src/lib.rs index be7896fec7..09c602a4ae 100644 --- a/packages/common/clickhouse-user-query/src/lib.rs +++ b/packages/common/clickhouse-user-query/src/lib.rs @@ -57,4 +57,4 @@ pub use schema::{Property, PropertyType, Schema}; pub mod builder; pub mod error; pub mod query; -pub mod schema; \ No newline at end of file +pub mod schema; diff --git a/packages/common/clickhouse-user-query/src/query.rs b/packages/common/clickhouse-user-query/src/query.rs index c6970e6525..9fb5489e28 100644 --- a/packages/common/clickhouse-user-query/src/query.rs +++ b/packages/common/clickhouse-user-query/src/query.rs @@ -3,71 +3,70 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum QueryExpr { - And { - exprs: Vec, - }, - Or { - exprs: Vec, - }, - BoolEqual { - property: String, - subproperty: Option, - value: bool, - }, - BoolNotEqual { - property: String, - subproperty: Option, - value: bool, - }, - StringEqual { - property: String, - subproperty: Option, - value: String, - }, - StringNotEqual { - property: String, - subproperty: Option, - value: String, - }, - ArrayContains { - property: String, - subproperty: Option, - values: Vec, - }, - ArrayDoesNotContain { - property: String, - subproperty: Option, - values: Vec, - }, - NumberEqual { - property: String, - subproperty: Option, - value: f64, - }, - NumberNotEqual { - property: String, - subproperty: Option, - value: f64, - }, - NumberLess { - property: String, - subproperty: Option, - value: f64, - }, - NumberLessOrEqual { - property: String, - subproperty: Option, - value: f64, - }, - NumberGreater { - property: String, - subproperty: Option, - value: f64, - }, - NumberGreaterOrEqual { - property: String, - subproperty: Option, - value: f64, - }, + And { + exprs: Vec, + }, + Or { + exprs: Vec, + }, + BoolEqual { + property: String, + subproperty: Option, + value: bool, + }, + BoolNotEqual { + property: String, + subproperty: Option, + value: bool, + }, + StringEqual { + property: String, + subproperty: Option, + value: String, + }, + StringNotEqual { + property: String, + subproperty: Option, + value: String, + }, + ArrayContains { + property: String, + subproperty: Option, + values: Vec, + }, + ArrayDoesNotContain { + property: String, + subproperty: Option, + values: Vec, + }, + NumberEqual { + property: String, + subproperty: Option, + value: f64, + }, + NumberNotEqual { + property: String, + subproperty: Option, + value: f64, + }, + NumberLess { + property: String, + subproperty: Option, + value: f64, + }, + NumberLessOrEqual { + property: String, + subproperty: Option, + value: f64, + }, + NumberGreater { + property: String, + subproperty: Option, + value: f64, + }, + NumberGreaterOrEqual { + property: String, + subproperty: Option, + value: f64, + }, } - diff --git a/packages/common/clickhouse-user-query/src/schema.rs b/packages/common/clickhouse-user-query/src/schema.rs index 3f45dea3e7..8938402710 100644 --- a/packages/common/clickhouse-user-query/src/schema.rs +++ b/packages/common/clickhouse-user-query/src/schema.rs @@ -3,70 +3,69 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Schema { - pub properties: Vec, + pub properties: Vec, } impl Schema { - pub fn new(properties: Vec) -> Result { - // All property validation happens in Property::new() - Ok(Self { properties }) - } - - pub fn get_property(&self, name: &str) -> Option<&Property> { - self.properties.iter().find(|p| p.name == name) - } + pub fn new(properties: Vec) -> Result { + // All property validation happens in Property::new() + Ok(Self { properties }) + } + + pub fn get_property(&self, name: &str) -> Option<&Property> { + self.properties.iter().find(|p| p.name == name) + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Property { - pub name: String, - pub supports_subproperties: bool, - pub ty: PropertyType, + pub name: String, + pub supports_subproperties: bool, + pub ty: PropertyType, } impl Property { - pub fn new(name: String, supports_subproperties: bool, ty: PropertyType) -> Result { - validate_property_name(&name)?; - Ok(Self { - name, - supports_subproperties, - ty, - }) - } + pub fn new(name: String, supports_subproperties: bool, ty: PropertyType) -> Result { + validate_property_name(&name)?; + Ok(Self { + name, + supports_subproperties, + ty, + }) + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum PropertyType { - Bool, - String, - Number, - ArrayString, + Bool, + String, + Number, + ArrayString, } impl PropertyType { - pub fn type_name(&self) -> &'static str { - match self { - PropertyType::Bool => "bool", - PropertyType::String => "string", - PropertyType::Number => "number", - PropertyType::ArrayString => "array[string]", - } - } + pub fn type_name(&self) -> &'static str { + match self { + PropertyType::Bool => "bool", + PropertyType::String => "string", + PropertyType::Number => "number", + PropertyType::ArrayString => "array[string]", + } + } } fn validate_property_name(name: &str) -> Result<()> { - if name.is_empty() { - return Err(UserQueryError::InvalidPropertyName(name.to_string())); - } - - if !name.chars().all(|c| c.is_alphanumeric() || c == '_') { - return Err(UserQueryError::InvalidPropertyName(name.to_string())); - } - - if name.chars().next().unwrap().is_numeric() { - return Err(UserQueryError::InvalidPropertyName(name.to_string())); - } - - Ok(()) -} + if name.is_empty() { + return Err(UserQueryError::InvalidPropertyName(name.to_string())); + } + + if !name.chars().all(|c| c.is_alphanumeric() || c == '_') { + return Err(UserQueryError::InvalidPropertyName(name.to_string())); + } + if name.chars().next().unwrap().is_numeric() { + return Err(UserQueryError::InvalidPropertyName(name.to_string())); + } + + Ok(()) +} diff --git a/packages/common/clickhouse-user-query/tests/builder_tests.rs b/packages/common/clickhouse-user-query/tests/builder_tests.rs index 71db71ce2f..70c9a2dae8 100644 --- a/packages/common/clickhouse-user-query/tests/builder_tests.rs +++ b/packages/common/clickhouse-user-query/tests/builder_tests.rs @@ -1,211 +1,228 @@ use clickhouse_user_query::*; fn create_test_schema() -> Schema { - Schema::new(vec![ - Property::new("prop_a".to_string(), false, PropertyType::String).unwrap(), - Property::new("prop_b".to_string(), true, PropertyType::String).unwrap(), - Property::new("bool_prop".to_string(), false, PropertyType::Bool).unwrap(), - Property::new("number_prop".to_string(), false, PropertyType::Number).unwrap(), - Property::new("array_prop".to_string(), false, PropertyType::ArrayString).unwrap(), - ]).unwrap() + Schema::new(vec![ + Property::new("prop_a".to_string(), false, PropertyType::String).unwrap(), + Property::new("prop_b".to_string(), true, PropertyType::String).unwrap(), + Property::new("bool_prop".to_string(), false, PropertyType::Bool).unwrap(), + Property::new("number_prop".to_string(), false, PropertyType::Number).unwrap(), + Property::new("array_prop".to_string(), false, PropertyType::ArrayString).unwrap(), + ]) + .unwrap() } #[test] fn test_simple_string_equal() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_a".to_string(), - subproperty: None, - value: "foo".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "prop_a = ?"); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_a".to_string(), + subproperty: None, + value: "foo".to_string(), + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "prop_a = ?"); } #[test] fn test_subproperty_access() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_b".to_string(), - subproperty: Some("sub".to_string()), - value: "bar".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "prop_b['sub'] = ?"); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_b".to_string(), + subproperty: Some("sub".to_string()), + value: "bar".to_string(), + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "prop_b['sub'] = ?"); } #[test] fn test_and_query() { - let schema = create_test_schema(); - let query = QueryExpr::And { - exprs: vec![ - QueryExpr::StringEqual { - property: "prop_a".to_string(), - subproperty: None, - value: "foo".to_string(), - }, - QueryExpr::BoolEqual { - property: "bool_prop".to_string(), - subproperty: None, - value: true, - }, - ], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "(prop_a = ? AND bool_prop = ?)"); + let schema = create_test_schema(); + let query = QueryExpr::And { + exprs: vec![ + QueryExpr::StringEqual { + property: "prop_a".to_string(), + subproperty: None, + value: "foo".to_string(), + }, + QueryExpr::BoolEqual { + property: "bool_prop".to_string(), + subproperty: None, + value: true, + }, + ], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "(prop_a = ? AND bool_prop = ?)"); } #[test] fn test_array_contains() { - let schema = create_test_schema(); - let query = QueryExpr::ArrayContains { - property: "array_prop".to_string(), - subproperty: None, - values: vec!["val1".to_string(), "val2".to_string()], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "hasAny(array_prop, ?)"); + let schema = create_test_schema(); + let query = QueryExpr::ArrayContains { + property: "array_prop".to_string(), + subproperty: None, + values: vec!["val1".to_string(), "val2".to_string()], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "hasAny(array_prop, ?)"); } #[test] fn test_property_not_found() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "nonexistent".to_string(), - subproperty: None, - value: "foo".to_string(), - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::PropertyNotFound(_)))); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "nonexistent".to_string(), + subproperty: None, + value: "foo".to_string(), + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!(result, Err(UserQueryError::PropertyNotFound(_)))); } #[test] fn test_type_mismatch() { - let schema = create_test_schema(); - let query = QueryExpr::BoolEqual { - property: "prop_a".to_string(), // This is a string property - subproperty: None, - value: true, - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::PropertyTypeMismatch(_, _, _)))); + let schema = create_test_schema(); + let query = QueryExpr::BoolEqual { + property: "prop_a".to_string(), // This is a string property + subproperty: None, + value: true, + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + result, + Err(UserQueryError::PropertyTypeMismatch(_, _, _)) + )); } #[test] fn test_subproperties_not_supported() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_a".to_string(), // This doesn't support subproperties - subproperty: Some("sub".to_string()), - value: "foo".to_string(), - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::SubpropertiesNotSupported(_)))); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_a".to_string(), // This doesn't support subproperties + subproperty: Some("sub".to_string()), + value: "foo".to_string(), + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + result, + Err(UserQueryError::SubpropertiesNotSupported(_)) + )); } #[test] fn test_invalid_property_name() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop-with-dashes".to_string(), - subproperty: None, - value: "foo".to_string(), - }; - - // Invalid property names are now caught as "not found" since schema validation - // happens at schema creation time, not query time - let builder_result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(builder_result, Err(UserQueryError::PropertyNotFound(_)))); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop-with-dashes".to_string(), + subproperty: None, + value: "foo".to_string(), + }; + + // Invalid property names are now caught as "not found" since schema validation + // happens at schema creation time, not query time + let builder_result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + builder_result, + Err(UserQueryError::PropertyNotFound(_)) + )); } #[test] fn test_subproperty_with_special_chars() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_b".to_string(), // This supports subproperties - subproperty: Some("sub-with-dashes".to_string()), - value: "foo".to_string(), - }; - - // Subproperties with special characters should work fine with Identifier escaping - let builder_result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(builder_result.is_ok()); - - let builder = builder_result.unwrap(); - assert_eq!(builder.where_expr(), "prop_b['sub-with-dashes'] = ?"); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_b".to_string(), // This supports subproperties + subproperty: Some("sub-with-dashes".to_string()), + value: "foo".to_string(), + }; + + // Subproperties with special characters should work fine with Identifier escaping + let builder_result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(builder_result.is_ok()); + + let builder = builder_result.unwrap(); + assert_eq!(builder.where_expr(), "prop_b['sub-with-dashes'] = ?"); } #[test] fn test_empty_array_values() { - let schema = create_test_schema(); - let query = QueryExpr::ArrayContains { - property: "array_prop".to_string(), - subproperty: None, - values: vec![], - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::EmptyArrayValues(_)))); + let schema = create_test_schema(); + let query = QueryExpr::ArrayContains { + property: "array_prop".to_string(), + subproperty: None, + values: vec![], + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!(result, Err(UserQueryError::EmptyArrayValues(_)))); } #[test] fn test_number_greater() { - let schema = create_test_schema(); - let query = QueryExpr::NumberGreater { - property: "number_prop".to_string(), - subproperty: None, - value: 42.5, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "number_prop > ?"); + let schema = create_test_schema(); + let query = QueryExpr::NumberGreater { + property: "number_prop".to_string(), + subproperty: None, + value: 42.5, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "number_prop > ?"); } #[test] fn test_number_less_or_equal() { - let schema = create_test_schema(); - let query = QueryExpr::NumberLessOrEqual { - property: "number_prop".to_string(), - subproperty: None, - value: 100.0, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "number_prop <= ?"); + let schema = create_test_schema(); + let query = QueryExpr::NumberLessOrEqual { + property: "number_prop".to_string(), + subproperty: None, + value: 100.0, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "number_prop <= ?"); } #[test] fn test_number_with_subproperty() { - let schema = Schema::new(vec![ - Property::new("metrics".to_string(), true, PropertyType::Number).unwrap(), - ]).unwrap(); - - let query = QueryExpr::NumberEqual { - property: "metrics".to_string(), - subproperty: Some("score".to_string()), - value: 95.5, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "metrics['score'] = ?"); + let schema = Schema::new(vec![Property::new( + "metrics".to_string(), + true, + PropertyType::Number, + ) + .unwrap()]) + .unwrap(); + + let query = QueryExpr::NumberEqual { + property: "metrics".to_string(), + subproperty: Some("score".to_string()), + value: 95.5, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "metrics['score'] = ?"); } #[test] fn test_number_type_mismatch() { - let schema = create_test_schema(); - let query = QueryExpr::NumberGreater { - property: "prop_a".to_string(), // This is a String type, not Number - subproperty: None, - value: 42.0, - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::PropertyTypeMismatch(_, _, _)))); -} \ No newline at end of file + let schema = create_test_schema(); + let query = QueryExpr::NumberGreater { + property: "prop_a".to_string(), // This is a String type, not Number + subproperty: None, + value: 42.0, + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + result, + Err(UserQueryError::PropertyTypeMismatch(_, _, _)) + )); +} diff --git a/packages/common/clickhouse-user-query/tests/integration_tests.rs b/packages/common/clickhouse-user-query/tests/integration_tests.rs index b612afc190..81132e3c77 100644 --- a/packages/common/clickhouse-user-query/tests/integration_tests.rs +++ b/packages/common/clickhouse-user-query/tests/integration_tests.rs @@ -2,60 +2,67 @@ use clickhouse::{Client, Row}; use clickhouse_user_query::*; use serde::Deserialize; use serde_json; -use testcontainers::{runners::AsyncRunner, ContainerAsync, GenericImage, core::ContainerPort}; +use testcontainers::{core::ContainerPort, runners::AsyncRunner, ContainerAsync, GenericImage}; #[derive(Row, Deserialize)] struct UserRow { - id: String, + id: String, } struct TestSetup { - client: Client, - _container: ContainerAsync, + client: Client, + _container: ContainerAsync, } impl TestSetup { - async fn new() -> Self { - let clickhouse_image = GenericImage::new("clickhouse/clickhouse-server", "23.8-alpine") - .with_exposed_port(ContainerPort::Tcp(8123)) - .with_exposed_port(ContainerPort::Tcp(9000)); - - let container = clickhouse_image.start().await.expect("Failed to start ClickHouse container"); - - let port = container.get_host_port_ipv4(8123).await.expect("Failed to get port"); - let client = Client::default() - .with_url(format!("http://localhost:{}", port)); - - // Wait for ClickHouse to be ready and create test table - let setup = Self { - client, - _container: container, - }; - - // Wait for ClickHouse to fully start up - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - setup.setup_test_data().await; - setup - } - - async fn setup_test_data(&self) { - // Create test table with sample data - self.client - .query("CREATE TABLE IF NOT EXISTS test_users ( + async fn new() -> Self { + let clickhouse_image = GenericImage::new("clickhouse/clickhouse-server", "23.8-alpine") + .with_exposed_port(ContainerPort::Tcp(8123)) + .with_exposed_port(ContainerPort::Tcp(9000)); + + let container = clickhouse_image + .start() + .await + .expect("Failed to start ClickHouse container"); + + let port = container + .get_host_port_ipv4(8123) + .await + .expect("Failed to get port"); + let client = Client::default().with_url(format!("http://localhost:{}", port)); + + // Wait for ClickHouse to be ready and create test table + let setup = Self { + client, + _container: container, + }; + + // Wait for ClickHouse to fully start up + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + setup.setup_test_data().await; + setup + } + + async fn setup_test_data(&self) { + // Create test table with sample data + self.client + .query( + "CREATE TABLE IF NOT EXISTS test_users ( id String, active Bool, metadata Map(String, String), tags Array(String), age UInt32, score Float64 - ) ENGINE = Memory") - .execute() - .await - .expect("Failed to create test table"); - - // Insert test data - self.client + ) ENGINE = Memory", + ) + .execute() + .await + .expect("Failed to create test table"); + + // Insert test data + self.client .query("INSERT INTO test_users VALUES ('user1', true, {'region': 'us-east', 'tier': 'premium'}, ['verified', 'premium'], 25, 95.5), ('user2', false, {'region': 'us-west', 'tier': 'basic'}, ['basic'], 30, 67.2), @@ -63,357 +70,417 @@ impl TestSetup { .execute() .await .expect("Failed to insert test data"); - } + } } #[tokio::test] async fn test_simple_query_execution() { - let setup = TestSetup::new().await; - - // Create schema - let schema = Schema::new(vec![ - Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), - ]).unwrap(); - - // Create query - let query_expr = QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }; - - // Build query - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - // Execute query - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 and user3 (active users) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create schema + let schema = Schema::new(vec![Property::new( + "active".to_string(), + false, + PropertyType::Bool, + ) + .unwrap()]) + .unwrap(); + + // Create query + let query_expr = QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }; + + // Build query + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + // Execute query + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 and user3 (active users) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_subproperty_query_execution() { - let setup = TestSetup::new().await; - - // Create schema with map support - let schema = Schema::new(vec![ - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - ]).unwrap(); - - // Query for premium tier users - let query_expr = QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("tier".to_string()), - value: "premium".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 and user3 (premium tier) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create schema with map support + let schema = Schema::new(vec![Property::new( + "metadata".to_string(), + true, + PropertyType::String, + ) + .unwrap()]) + .unwrap(); + + // Query for premium tier users + let query_expr = QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("tier".to_string()), + value: "premium".to_string(), + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 and user3 (premium tier) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_array_contains_query_execution() { - let setup = TestSetup::new().await; - - // Create schema with array support - let schema = Schema::new(vec![ - Property::new("tags".to_string(), false, PropertyType::ArrayString).unwrap(), - ]).unwrap(); - - // Query for users with specific tags - let query_expr = QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: None, - values: vec!["verified".to_string(), "beta".to_string()], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 and user3 (have verified) and user3 (has beta) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create schema with array support + let schema = Schema::new(vec![Property::new( + "tags".to_string(), + false, + PropertyType::ArrayString, + ) + .unwrap()]) + .unwrap(); + + // Query for users with specific tags + let query_expr = QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: None, + values: vec!["verified".to_string(), "beta".to_string()], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 and user3 (have verified) and user3 (has beta) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_complex_and_or_query_execution() { - let setup = TestSetup::new().await; - - // Create comprehensive schema - let schema = Schema::new(vec![ - Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - Property::new("tags".to_string(), false, PropertyType::ArrayString).unwrap(), - ]).unwrap(); - - // Complex query: (active = true AND metadata['tier'] = 'premium') OR tags contains 'beta' - let query_expr = QueryExpr::Or { - exprs: vec![ - QueryExpr::And { - exprs: vec![ - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("tier".to_string()), - value: "premium".to_string(), - }, - ], - }, - QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: None, - values: vec!["beta".to_string()], - }, - ], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return: - // - user1 (active=true AND tier=premium) - // - user3 (active=true AND tier=premium AND has beta tag) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create comprehensive schema + let schema = Schema::new(vec![ + Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), + Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), + Property::new("tags".to_string(), false, PropertyType::ArrayString).unwrap(), + ]) + .unwrap(); + + // Complex query: (active = true AND metadata['tier'] = 'premium') OR tags contains 'beta' + let query_expr = QueryExpr::Or { + exprs: vec![ + QueryExpr::And { + exprs: vec![ + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("tier".to_string()), + value: "premium".to_string(), + }, + ], + }, + QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: None, + values: vec!["beta".to_string()], + }, + ], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return: + // - user1 (active=true AND tier=premium) + // - user3 (active=true AND tier=premium AND has beta tag) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_sql_injection_protection() { - let setup = TestSetup::new().await; - - // Create schema - let schema = Schema::new(vec![ - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - ]).unwrap(); - - // Attempt SQL injection in subproperty - let query_expr = QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("'; DROP TABLE test_users; --".to_string()), - value: "malicious".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - // Verify the query builds safely with proper escaping - let where_clause = builder.where_expr(); - assert!(where_clause.contains("metadata['\\'; DROP TABLE test_users; --']")); - assert!(where_clause.contains("= ?")); - - // Execute the query - it should run safely and return no results - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution should succeed safely") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return no results (not drop the table) - assert_eq!(result.len(), 0); - - // Verify table still exists by running a simple query - let table_check: Vec = setup.client - .query("SELECT id FROM test_users LIMIT 1") - .fetch_all::() - .await - .expect("Table should still exist") - .into_iter() - .map(|user| user.id) - .collect(); - - assert!(!table_check.is_empty(), "Table should not have been dropped"); + let setup = TestSetup::new().await; + + // Create schema + let schema = Schema::new(vec![Property::new( + "metadata".to_string(), + true, + PropertyType::String, + ) + .unwrap()]) + .unwrap(); + + // Attempt SQL injection in subproperty + let query_expr = QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("'; DROP TABLE test_users; --".to_string()), + value: "malicious".to_string(), + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + // Verify the query builds safely with proper escaping + let where_clause = builder.where_expr(); + assert!(where_clause.contains("metadata['\\'; DROP TABLE test_users; --']")); + assert!(where_clause.contains("= ?")); + + // Execute the query - it should run safely and return no results + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution should succeed safely") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return no results (not drop the table) + assert_eq!(result.len(), 0); + + // Verify table still exists by running a simple query + let table_check: Vec = setup + .client + .query("SELECT id FROM test_users LIMIT 1") + .fetch_all::() + .await + .expect("Table should still exist") + .into_iter() + .map(|user| user.id) + .collect(); + + assert!( + !table_check.is_empty(), + "Table should not have been dropped" + ); } #[tokio::test] async fn test_json_serialization_roundtrip() { - let setup = TestSetup::new().await; - - // Create schema - let schema = Schema::new(vec![ - Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - ]).unwrap(); - - // Create complex query - let original_query = QueryExpr::And { - exprs: vec![ - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("tier".to_string()), - value: "premium".to_string(), - }, - ], - }; - - // Serialize to JSON - let json = serde_json::to_string(&original_query).unwrap(); - - // Deserialize from JSON - let deserialized_query: QueryExpr = serde_json::from_str(&json).unwrap(); - - // Build queries from both and verify they're identical - let original_builder = UserDefinedQueryBuilder::new(&schema, &original_query).unwrap(); - let deserialized_builder = UserDefinedQueryBuilder::new(&schema, &deserialized_query).unwrap(); - - assert_eq!(original_builder.where_expr(), deserialized_builder.where_expr()); - - // Execute both queries and verify results are the same - let query1 = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", original_builder.where_expr())); - let query1 = original_builder.bind_to(query1); - - let query2 = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", deserialized_builder.where_expr())); - let query2 = deserialized_builder.bind_to(query2); - - let result1: Vec = query1 - .fetch_all::() - .await - .unwrap() - .into_iter() - .map(|user| user.id) - .collect(); - - let result2: Vec = query2 - .fetch_all::() - .await - .unwrap() - .into_iter() - .map(|user| user.id) - .collect(); - - assert_eq!(result1, result2); - assert_eq!(result1.len(), 2); // user1 and user3 + let setup = TestSetup::new().await; + + // Create schema + let schema = Schema::new(vec![ + Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), + Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), + ]) + .unwrap(); + + // Create complex query + let original_query = QueryExpr::And { + exprs: vec![ + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("tier".to_string()), + value: "premium".to_string(), + }, + ], + }; + + // Serialize to JSON + let json = serde_json::to_string(&original_query).unwrap(); + + // Deserialize from JSON + let deserialized_query: QueryExpr = serde_json::from_str(&json).unwrap(); + + // Build queries from both and verify they're identical + let original_builder = UserDefinedQueryBuilder::new(&schema, &original_query).unwrap(); + let deserialized_builder = UserDefinedQueryBuilder::new(&schema, &deserialized_query).unwrap(); + + assert_eq!( + original_builder.where_expr(), + deserialized_builder.where_expr() + ); + + // Execute both queries and verify results are the same + let query1 = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + original_builder.where_expr() + )); + let query1 = original_builder.bind_to(query1); + + let query2 = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + deserialized_builder.where_expr() + )); + let query2 = deserialized_builder.bind_to(query2); + + let result1: Vec = query1 + .fetch_all::() + .await + .unwrap() + .into_iter() + .map(|user| user.id) + .collect(); + + let result2: Vec = query2 + .fetch_all::() + .await + .unwrap() + .into_iter() + .map(|user| user.id) + .collect(); + + assert_eq!(result1, result2); + assert_eq!(result1.len(), 2); // user1 and user3 } #[tokio::test] async fn test_numeric_query_execution() { - let setup = TestSetup::new().await; - - // Create schema with number support - let schema = Schema::new(vec![ - Property::new("score".to_string(), false, PropertyType::Number).unwrap(), - ]).unwrap(); - - // Query for users with score greater than 80 - let query_expr = QueryExpr::NumberGreater { - property: "score".to_string(), - subproperty: None, - value: 80.0, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 (95.5) and user3 (88.9), but not user2 (67.2) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); - assert!(!result.contains(&"user2".to_string())); + let setup = TestSetup::new().await; + + // Create schema with number support + let schema = Schema::new(vec![Property::new( + "score".to_string(), + false, + PropertyType::Number, + ) + .unwrap()]) + .unwrap(); + + // Query for users with score greater than 80 + let query_expr = QueryExpr::NumberGreater { + property: "score".to_string(), + subproperty: None, + value: 80.0, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 (95.5) and user3 (88.9), but not user2 (67.2) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); + assert!(!result.contains(&"user2".to_string())); } #[tokio::test] async fn test_numeric_less_or_equal_query() { - let setup = TestSetup::new().await; - - // Create schema with number support - let schema = Schema::new(vec![ - Property::new("score".to_string(), false, PropertyType::Number).unwrap(), - ]).unwrap(); - - // Query for users with score <= 90 - let query_expr = QueryExpr::NumberLessOrEqual { - property: "score".to_string(), - subproperty: None, - value: 90.0, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user2 (67.2) and user3 (88.9), but not user1 (95.5) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user2".to_string())); - assert!(result.contains(&"user3".to_string())); - assert!(!result.contains(&"user1".to_string())); -} \ No newline at end of file + let setup = TestSetup::new().await; + + // Create schema with number support + let schema = Schema::new(vec![Property::new( + "score".to_string(), + false, + PropertyType::Number, + ) + .unwrap()]) + .unwrap(); + + // Query for users with score <= 90 + let query_expr = QueryExpr::NumberLessOrEqual { + property: "score".to_string(), + subproperty: None, + value: 90.0, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user2 (67.2) and user3 (88.9), but not user1 (95.5) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user2".to_string())); + assert!(result.contains(&"user3".to_string())); + assert!(!result.contains(&"user1".to_string())); +} diff --git a/packages/common/clickhouse-user-query/tests/query_tests.rs b/packages/common/clickhouse-user-query/tests/query_tests.rs index a5834df7b5..ea162d4033 100644 --- a/packages/common/clickhouse-user-query/tests/query_tests.rs +++ b/packages/common/clickhouse-user-query/tests/query_tests.rs @@ -2,164 +2,178 @@ use clickhouse_user_query::*; #[test] fn test_query_expr_serde() { - let query = QueryExpr::StringEqual { - property: "user_id".to_string(), - subproperty: None, - value: "12345".to_string(), - }; - - // Test serialization - let json = serde_json::to_string_pretty(&query).unwrap(); - - assert!(json.contains(r#""string_equal""#)); - assert!(json.contains(r#""property": "user_id""#)); - assert!(json.contains(r#""value": "12345""#)); - - // Test deserialization - let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); - match deserialized { - QueryExpr::StringEqual { property, value, .. } => { - assert_eq!(property, "user_id"); - assert_eq!(value, "12345"); - } - _ => panic!("Expected StringEqual"), - } + let query = QueryExpr::StringEqual { + property: "user_id".to_string(), + subproperty: None, + value: "12345".to_string(), + }; + + // Test serialization + let json = serde_json::to_string_pretty(&query).unwrap(); + + assert!(json.contains(r#""string_equal""#)); + assert!(json.contains(r#""property": "user_id""#)); + assert!(json.contains(r#""value": "12345""#)); + + // Test deserialization + let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); + match deserialized { + QueryExpr::StringEqual { + property, value, .. + } => { + assert_eq!(property, "user_id"); + assert_eq!(value, "12345"); + } + _ => panic!("Expected StringEqual"), + } } #[test] fn test_complex_query_serde() { - let query = QueryExpr::And { - exprs: vec![ - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: Some("category".to_string()), - values: vec!["premium".to_string(), "verified".to_string()], - }, - ], - }; - - let json = serde_json::to_string_pretty(&query).unwrap(); - - assert!(json.contains(r#""and""#)); - assert!(json.contains(r#""bool_equal""#)); - assert!(json.contains(r#""array_contains""#)); - - let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); - match deserialized { - QueryExpr::And { exprs } => { - assert_eq!(exprs.len(), 2); - } - _ => panic!("Expected And expression"), - } + let query = QueryExpr::And { + exprs: vec![ + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: Some("category".to_string()), + values: vec!["premium".to_string(), "verified".to_string()], + }, + ], + }; + + let json = serde_json::to_string_pretty(&query).unwrap(); + + assert!(json.contains(r#""and""#)); + assert!(json.contains(r#""bool_equal""#)); + assert!(json.contains(r#""array_contains""#)); + + let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); + match deserialized { + QueryExpr::And { exprs } => { + assert_eq!(exprs.len(), 2); + } + _ => panic!("Expected And expression"), + } } #[test] fn test_query_expr_creation() { - let query = QueryExpr::And { - exprs: vec![ - QueryExpr::StringEqual { - property: "user_id".to_string(), - subproperty: None, - value: "12345".to_string(), - }, - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - ], - }; - - match query { - QueryExpr::And { exprs } => { - assert_eq!(exprs.len(), 2); - } - _ => panic!("Expected And expression"), - } + let query = QueryExpr::And { + exprs: vec![ + QueryExpr::StringEqual { + property: "user_id".to_string(), + subproperty: None, + value: "12345".to_string(), + }, + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + ], + }; + + match query { + QueryExpr::And { exprs } => { + assert_eq!(exprs.len(), 2); + } + _ => panic!("Expected And expression"), + } } #[test] fn test_subproperty_query() { - let query = QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("key".to_string()), - value: "value".to_string(), - }; - - match query { - QueryExpr::StringEqual { property, subproperty, value } => { - assert_eq!(property, "metadata"); - assert_eq!(subproperty, Some("key".to_string())); - assert_eq!(value, "value"); - } - _ => panic!("Expected StringEqual expression"), - } + let query = QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("key".to_string()), + value: "value".to_string(), + }; + + match query { + QueryExpr::StringEqual { + property, + subproperty, + value, + } => { + assert_eq!(property, "metadata"); + assert_eq!(subproperty, Some("key".to_string())); + assert_eq!(value, "value"); + } + _ => panic!("Expected StringEqual expression"), + } } #[test] fn test_array_query() { - let query = QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: None, - values: vec!["premium".to_string(), "verified".to_string()], - }; - - match query { - QueryExpr::ArrayContains { property, values, .. } => { - assert_eq!(property, "tags"); - assert_eq!(values.len(), 2); - assert!(values.contains(&"premium".to_string())); - } - _ => panic!("Expected ArrayContains expression"), - } + let query = QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: None, + values: vec!["premium".to_string(), "verified".to_string()], + }; + + match query { + QueryExpr::ArrayContains { + property, values, .. + } => { + assert_eq!(property, "tags"); + assert_eq!(values.len(), 2); + assert!(values.contains(&"premium".to_string())); + } + _ => panic!("Expected ArrayContains expression"), + } } #[test] fn test_numeric_query() { - let query = QueryExpr::NumberGreater { - property: "score".to_string(), - subproperty: None, - value: 85.5, - }; - - match query { - QueryExpr::NumberGreater { property, value, .. } => { - assert_eq!(property, "score"); - assert_eq!(value, 85.5); - } - _ => panic!("Expected NumberGreater expression"), - } + let query = QueryExpr::NumberGreater { + property: "score".to_string(), + subproperty: None, + value: 85.5, + }; + + match query { + QueryExpr::NumberGreater { + property, value, .. + } => { + assert_eq!(property, "score"); + assert_eq!(value, 85.5); + } + _ => panic!("Expected NumberGreater expression"), + } } #[test] fn test_numeric_query_serde() { - let query = QueryExpr::NumberLessOrEqual { - property: "metrics".to_string(), - subproperty: Some("latency".to_string()), - value: 100.0, - }; - - // Test serialization - let json = serde_json::to_string_pretty(&query).unwrap(); - - assert!(json.contains(r#""number_less_or_equal""#)); - assert!(json.contains(r#""property": "metrics""#)); - assert!(json.contains(r#""subproperty": "latency""#)); - assert!(json.contains(r#""value": 100.0"#)); - - // Test deserialization - let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); - match deserialized { - QueryExpr::NumberLessOrEqual { property, subproperty, value } => { - assert_eq!(property, "metrics"); - assert_eq!(subproperty, Some("latency".to_string())); - assert_eq!(value, 100.0); - } - _ => panic!("Expected NumberLessOrEqual"), - } -} \ No newline at end of file + let query = QueryExpr::NumberLessOrEqual { + property: "metrics".to_string(), + subproperty: Some("latency".to_string()), + value: 100.0, + }; + + // Test serialization + let json = serde_json::to_string_pretty(&query).unwrap(); + + assert!(json.contains(r#""number_less_or_equal""#)); + assert!(json.contains(r#""property": "metrics""#)); + assert!(json.contains(r#""subproperty": "latency""#)); + assert!(json.contains(r#""value": 100.0"#)); + + // Test deserialization + let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); + match deserialized { + QueryExpr::NumberLessOrEqual { + property, + subproperty, + value, + } => { + assert_eq!(property, "metrics"); + assert_eq!(subproperty, Some("latency".to_string())); + assert_eq!(value, 100.0); + } + _ => panic!("Expected NumberLessOrEqual"), + } +} diff --git a/packages/common/clickhouse-user-query/tests/schema_tests.rs b/packages/common/clickhouse-user-query/tests/schema_tests.rs index 16c4a5bbfb..7e14d310d4 100644 --- a/packages/common/clickhouse-user-query/tests/schema_tests.rs +++ b/packages/common/clickhouse-user-query/tests/schema_tests.rs @@ -2,26 +2,27 @@ use clickhouse_user_query::*; #[test] fn test_schema_creation() { - let schema = Schema::new(vec![ - Property::new("valid_name".to_string(), false, PropertyType::String).unwrap(), - Property::new("another_valid_123".to_string(), true, PropertyType::Bool).unwrap(), - ]).unwrap(); - - assert_eq!(schema.properties.len(), 2); - assert!(schema.get_property("valid_name").is_some()); - assert!(schema.get_property("nonexistent").is_none()); + let schema = Schema::new(vec![ + Property::new("valid_name".to_string(), false, PropertyType::String).unwrap(), + Property::new("another_valid_123".to_string(), true, PropertyType::Bool).unwrap(), + ]) + .unwrap(); + + assert_eq!(schema.properties.len(), 2); + assert!(schema.get_property("valid_name").is_some()); + assert!(schema.get_property("nonexistent").is_none()); } #[test] fn test_invalid_property_name() { - let result = Property::new("invalid-name".to_string(), false, PropertyType::String); - assert!(result.is_err()); + let result = Property::new("invalid-name".to_string(), false, PropertyType::String); + assert!(result.is_err()); } #[test] fn test_property_type_names() { - assert_eq!(PropertyType::Bool.type_name(), "bool"); - assert_eq!(PropertyType::String.type_name(), "string"); - assert_eq!(PropertyType::Number.type_name(), "number"); - assert_eq!(PropertyType::ArrayString.type_name(), "array[string]"); -} \ No newline at end of file + assert_eq!(PropertyType::Bool.type_name(), "bool"); + assert_eq!(PropertyType::String.type_name(), "string"); + assert_eq!(PropertyType::Number.type_name(), "number"); + assert_eq!(PropertyType::ArrayString.type_name(), "array[string]"); +} diff --git a/packages/common/config/src/config/server/rivet/mod.rs b/packages/common/config/src/config/server/rivet/mod.rs index 9ddaedf7f8..1e53d14b94 100644 --- a/packages/common/config/src/config/server/rivet/mod.rs +++ b/packages/common/config/src/config/server/rivet/mod.rs @@ -177,7 +177,7 @@ impl Rivet { } } -impl Rivet { +impl Rivet { pub fn default_cluster_id(&self) -> GlobalResult { if let Some(default_cluster_id) = self.default_cluster_id { ensure!( @@ -190,7 +190,9 @@ impl Rivet { // Return default development clusters AccessKind::Development => Ok(default_dev_cluster::CLUSTER_ID), // No cluster configured - AccessKind::Public | AccessKind::Private => bail!("`default_cluster_id` not configured"), + AccessKind::Public | AccessKind::Private => { + bail!("`default_cluster_id` not configured") + } } } } diff --git a/packages/common/pools/src/db/redis.rs b/packages/common/pools/src/db/redis.rs index 6e377f8c25..5ef0bfa3dc 100644 --- a/packages/common/pools/src/db/redis.rs +++ b/packages/common/pools/src/db/redis.rs @@ -65,4 +65,4 @@ pub async fn setup(config: Config) -> Result, Error> tracing::debug!("redis connected"); Ok(redis) -} \ No newline at end of file +} diff --git a/packages/common/pools/src/error.rs b/packages/common/pools/src/error.rs index 7425a3c692..77617c5c42 100644 --- a/packages/common/pools/src/error.rs +++ b/packages/common/pools/src/error.rs @@ -74,7 +74,7 @@ pub enum Error { } impl From for Error { - fn from(err: global_error::GlobalError) -> Self { - Error::Global(err) - } + fn from(err: global_error::GlobalError) -> Self { + Error::Global(err) + } } diff --git a/packages/common/pools/src/pools.rs b/packages/common/pools/src/pools.rs index bc5623411e..04fbaa7fa9 100644 --- a/packages/common/pools/src/pools.rs +++ b/packages/common/pools/src/pools.rs @@ -46,8 +46,9 @@ impl Pools { let clickhouse_inserter = if let Some(vector_http) = config.server.as_ref().and_then(|x| x.vector_http.as_ref()) { - let inserter = clickhouse_inserter::create_inserter(&vector_http.host, vector_http.port) - .map_err(Error::BuildClickHouseInserter)?; + let inserter = + clickhouse_inserter::create_inserter(&vector_http.host, vector_http.port) + .map_err(Error::BuildClickHouseInserter)?; Some(inserter) } else { None diff --git a/packages/common/pools/src/prelude.rs b/packages/common/pools/src/prelude.rs index 48d7a39030..b729a9f96d 100644 --- a/packages/common/pools/src/prelude.rs +++ b/packages/common/pools/src/prelude.rs @@ -4,7 +4,7 @@ pub use redis; pub use sqlx; pub use crate::{ - ClickHouseInserterHandle, ClickHousePool, CrdbPool, FdbPool, NatsPool, RedisPool, SqlitePool, __sql_query, - __sql_query_as, __sql_query_as_raw, sql_execute, sql_fetch, sql_fetch_all, sql_fetch_many, - sql_fetch_one, sql_fetch_optional, + ClickHouseInserterHandle, ClickHousePool, CrdbPool, FdbPool, NatsPool, RedisPool, SqlitePool, + __sql_query, __sql_query_as, __sql_query_as_raw, sql_execute, sql_fetch, sql_fetch_all, + sql_fetch_many, sql_fetch_one, sql_fetch_optional, }; diff --git a/packages/common/util/core/src/format.rs b/packages/common/util/core/src/format.rs index 7b95d1b9a3..82bc65b15c 100644 --- a/packages/common/util/core/src/format.rs +++ b/packages/common/util/core/src/format.rs @@ -162,7 +162,7 @@ pub fn duration(ms: i64, relative: bool) -> String { let hours = (ms % 86_400_000) / 3_600_000; let minutes = (ms % 3_600_000) / 60_000; let seconds = (ms % 60_000) / 1_000; - + if days > 0 { parts.push(format!("{days}d")); } @@ -181,5 +181,5 @@ pub fn duration(ms: i64, relative: bool) -> String { parts.push("ago".to_string()); } - parts.join(" ") + parts.join(" ") } diff --git a/packages/core/api/status/src/route/actor.rs b/packages/core/api/status/src/route/actor.rs index 441a7c4ba4..d19258840a 100644 --- a/packages/core/api/status/src/route/actor.rs +++ b/packages/core/api/status/src/route/actor.rs @@ -231,7 +231,10 @@ pub async fn status( _ => { bail_with!( INTERNAL_STATUS_CHECK_FAILED, - error = format!("unknown request error: {:?} {:?}", content.status, content.content) + error = format!( + "unknown request error: {:?} {:?}", + content.status, content.content + ) ); } }, @@ -277,7 +280,7 @@ pub async fn status( .instrument(tracing::info_span!("actor_destroy_request", base_path=%config.base_path)) .await { - Ok(_res) => {}, + Ok(_res) => {} Err(rivet_api::apis::Error::ResponseError(content)) => match content.entity { Some(Status400(body)) | Some(Status403(body)) @@ -293,7 +296,10 @@ pub async fn status( _ => { bail_with!( INTERNAL_STATUS_CHECK_FAILED, - error = format!("unknown request error: {:?} {:?}", content.status, content.content) + error = format!( + "unknown request error: {:?} {:?}", + content.status, content.content + ) ); } }, diff --git a/packages/core/services/dynamic-config/src/ops/get_config.rs b/packages/core/services/dynamic-config/src/ops/get_config.rs index 45025494f8..c27e206d13 100644 --- a/packages/core/services/dynamic-config/src/ops/get_config.rs +++ b/packages/core/services/dynamic-config/src/ops/get_config.rs @@ -19,12 +19,11 @@ pub async fn get_config(ctx: &OperationCtx, input: &Input) -> GlobalResult>()? - .timestamp_millis(), - }) - .to_workflow::() - .tag("client_id", client_id) - .send() - .await; + let res = ctx + .signal(pegboard::workflows::client::Drain { + drain_timeout_ts: unwrap_with!( + body.drain_complete_ts, + API_BAD_BODY, + error = "missing `drain_complete_ts`" + ) + .parse::>()? + .timestamp_millis(), + }) + .to_workflow::() + .tag("client_id", client_id) + .send() + .await; if let Some(WorkflowError::WorkflowNotFound) = res.as_workflow_error() { tracing::warn!( @@ -151,7 +152,8 @@ pub async fn toggle_drain_client( res?; } } else { - let res = ctx.signal(pegboard::workflows::client::Undrain {}) + let res = ctx + .signal(pegboard::workflows::client::Undrain {}) .to_workflow::() .tag("client_id", client_id) .send() diff --git a/packages/edge/infra/guard/core/src/metrics.rs b/packages/edge/infra/guard/core/src/metrics.rs index ae1f3c0ced..6a91107e66 100644 --- a/packages/edge/infra/guard/core/src/metrics.rs +++ b/packages/edge/infra/guard/core/src/metrics.rs @@ -1,5 +1,5 @@ use lazy_static::lazy_static; -use rivet_metrics::{prometheus::*, REGISTRY, BUCKETS}; +use rivet_metrics::{prometheus::*, BUCKETS, REGISTRY}; lazy_static! { // MARK: Internal diff --git a/packages/toolchain/cli/src/commands/function/endpoint.rs b/packages/toolchain/cli/src/commands/function/endpoint.rs index f3c13bdcab..0a07b305a4 100644 --- a/packages/toolchain/cli/src/commands/function/endpoint.rs +++ b/packages/toolchain/cli/src/commands/function/endpoint.rs @@ -61,4 +61,3 @@ async fn get_route( Ok(matching_route) } - diff --git a/packages/toolchain/cli/src/commands/function/list.rs b/packages/toolchain/cli/src/commands/function/list.rs index 45a8a81689..7a1af78029 100644 --- a/packages/toolchain/cli/src/commands/function/list.rs +++ b/packages/toolchain/cli/src/commands/function/list.rs @@ -35,4 +35,3 @@ impl Opts { Ok(()) } } - diff --git a/packages/toolchain/cli/src/commands/route/endpoint.rs b/packages/toolchain/cli/src/commands/route/endpoint.rs index d941854370..d3afc7c24e 100644 --- a/packages/toolchain/cli/src/commands/route/endpoint.rs +++ b/packages/toolchain/cli/src/commands/route/endpoint.rs @@ -41,10 +41,10 @@ impl Opts { pub async fn execute(&self) -> Result<()> { let ctx = crate::util::login::load_or_login().await?; let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?; - + // Get existing route if it exists let route = get_route(&ctx, &env, &self.name).await?; - + // Parse selector tags let selector_tags = self .selector_tags @@ -55,20 +55,26 @@ impl Opts { // Build route update body let mut update_route_body = models::RoutesUpdateRouteBody { - hostname: route.as_ref().map(|r| r.hostname.clone()).unwrap_or_else(|| { - // Default hostname is project-env.domain - format!( - "{}-{}.{}", - ctx.project.name_id, - env, - ctx.bootstrap - .domains - .job - .as_ref() - .expect("bootstrap.domains.job") - ) - }), - path: route.as_ref().map(|r| r.path.clone()).unwrap_or_else(|| "/".to_string()), + hostname: route + .as_ref() + .map(|r| r.hostname.clone()) + .unwrap_or_else(|| { + // Default hostname is project-env.domain + format!( + "{}-{}.{}", + ctx.project.name_id, + env, + ctx.bootstrap + .domains + .job + .as_ref() + .expect("bootstrap.domains.job") + ) + }), + path: route + .as_ref() + .map(|r| r.path.clone()) + .unwrap_or_else(|| "/".to_string()), route_subpaths: route.as_ref().map(|r| r.route_subpaths).unwrap_or(true), strip_prefix: route.as_ref().map(|r| r.strip_prefix).unwrap_or(true), target: Box::new(models::RoutesRouteTarget { @@ -124,15 +130,24 @@ impl Opts { Result::Ok(_) => { println!( "Successfully {} route: {}{}", - if route.is_some() { "updated" } else { "created" }, - update_route_body.hostname, + if route.is_some() { + "updated" + } else { + "created" + }, + update_route_body.hostname, update_route_body.path ); Ok(()) } Err(err) => { - eprintln!("Failed to {}: {}", - if route.is_some() { "update route" } else { "create route" }, + eprintln!( + "Failed to {}: {}", + if route.is_some() { + "update route" + } else { + "create route" + }, err ); Err(err.into()) @@ -142,7 +157,11 @@ impl Opts { } // Helper function to get route if it exists -async fn get_route(ctx: &ToolchainCtx, env: &str, route_id: &str) -> Result> { +async fn get_route( + ctx: &ToolchainCtx, + env: &str, + route_id: &str, +) -> Result> { let routes_response = apis::routes_api::routes_list( &ctx.openapi_config_cloud, Some(&ctx.project.name_id.to_string()), @@ -158,4 +177,4 @@ async fn get_route(ctx: &ToolchainCtx, env: &str, route_id: &str) -> Result opts.execute().await, } } -} \ No newline at end of file +} diff --git a/packages/toolchain/cli/src/util/deploy.rs b/packages/toolchain/cli/src/util/deploy.rs index 4b6a9e585c..a292551cdb 100644 --- a/packages/toolchain/cli/src/util/deploy.rs +++ b/packages/toolchain/cli/src/util/deploy.rs @@ -432,4 +432,3 @@ async fn create_function_route( Ok(()) } -