mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
chore: add unit tests (#7487)
### **PR Type** Tests, Enhancement ___ ### **Description** - Add extensive unit tests across common meta modules - Refactor LokiError mapping and tag filtering logic - Simplify record_batch usage and string interpolation - Fix histogram interval functions and cache invalidation
This commit is contained in:
@@ -312,4 +312,34 @@ mod tests {
|
||||
let response = HttpResponse::json(payload);
|
||||
assert_eq!(response.status(), http::StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_fields_not_serialized_when_none() {
|
||||
let response = HttpResponse {
|
||||
code: 200,
|
||||
message: "ok".to_string(),
|
||||
id: None,
|
||||
name: None,
|
||||
error_detail: None,
|
||||
trace_id: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&response).unwrap();
|
||||
assert!(!serialized.contains("id"));
|
||||
assert!(!serialized.contains("name"));
|
||||
assert!(!serialized.contains("error_detail"));
|
||||
assert!(!serialized.contains("trace_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_response_builder_chaining() {
|
||||
let mut response = HttpResponse::message(http::StatusCode::OK, "test");
|
||||
response
|
||||
.with_id("id-1".into())
|
||||
.with_name("name-1".into())
|
||||
.with_trace_id("trace-1".into());
|
||||
|
||||
assert_eq!(response.id, Some("id-1".into()));
|
||||
assert_eq!(response.name, Some("name-1".into()));
|
||||
assert_eq!(response.trace_id, Some("trace-1".into()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,4 +650,47 @@ mod tests {
|
||||
assert_eq!(request.message.message_id, "test-id");
|
||||
assert_eq!(request.subscription, "test-subscription");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ingestion_response_skip_empty_status_and_error() {
|
||||
let response = IngestionResponse {
|
||||
code: 200,
|
||||
status: vec![],
|
||||
error: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&response).unwrap();
|
||||
assert!(!serialized.contains("status"));
|
||||
assert!(!serialized.contains("error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_status_skip_empty_error() {
|
||||
let status = RecordStatus {
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
error: "".to_string(),
|
||||
};
|
||||
let serialized = serde_json::to_string(&status).unwrap();
|
||||
assert!(!serialized.contains("error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hec_status_conversion() {
|
||||
// Test all variants
|
||||
let success: HecResponse = HecStatus::Success.into();
|
||||
assert_eq!(success.text, "Success");
|
||||
assert_eq!(success.code, 200);
|
||||
|
||||
let invalid_format: HecResponse = HecStatus::InvalidFormat.into();
|
||||
assert_eq!(invalid_format.text, "Invalid data format");
|
||||
assert_eq!(invalid_format.code, 400);
|
||||
|
||||
let invalid_index: HecResponse = HecStatus::InvalidIndex.into();
|
||||
assert_eq!(invalid_index.text, "Incorrect index");
|
||||
assert_eq!(invalid_index.code, 400);
|
||||
|
||||
let custom: HecResponse = HecStatus::Custom("Test error".to_string(), 418).into();
|
||||
assert_eq!(custom.text, "Test error");
|
||||
assert_eq!(custom.code, 418);
|
||||
}
|
||||
}
|
||||
|
||||
+56
-33
@@ -114,41 +114,37 @@ impl From<LokiError> for actix_web::HttpResponse {
|
||||
fn from(error: LokiError) -> Self {
|
||||
use actix_web::HttpResponse;
|
||||
|
||||
match error {
|
||||
// Client errors (400)
|
||||
LokiError::InvalidTimestamp { message } => HttpResponse::BadRequest()
|
||||
if let LokiError::Ingestion { .. } = &error {
|
||||
return HttpResponse::InternalServerError()
|
||||
.content_type("text/plain")
|
||||
.body(format!("invalid timestamp: {message}")),
|
||||
LokiError::InvalidLabels { message } => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(format!("invalid labels: {message}")),
|
||||
LokiError::EmptyStream => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body("empty stream data"),
|
||||
LokiError::UnsupportedContentType { content_type } => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(format!("unsupported content type: {content_type}")),
|
||||
LokiError::UnsupportedContentEncoding { encoding } => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(format!("unsupported content encoding: {encoding}")),
|
||||
LokiError::ProtobufDecode { source } => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(format!("failed to decode protobuf: {source}")),
|
||||
LokiError::SnappyDecompression { source } => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(format!("failed to decompress snappy: {source}")),
|
||||
LokiError::JsonParse { source } => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(format!("failed to parse JSON: {source}")),
|
||||
LokiError::GzipDecompression { source } => HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(format!("failed to decompress gzip: {source}")),
|
||||
|
||||
// Server errors (500)
|
||||
LokiError::Ingestion { .. } => HttpResponse::InternalServerError()
|
||||
.content_type("text/plain")
|
||||
.body("internal server error during log ingestion"),
|
||||
.body("internal server error during log ingestion");
|
||||
}
|
||||
let body = match error {
|
||||
LokiError::InvalidTimestamp { message } => format!("invalid timestamp: {message}"),
|
||||
LokiError::InvalidLabels { message } => format!("invalid labels: {message}"),
|
||||
LokiError::EmptyStream => "empty stream data".to_string(),
|
||||
LokiError::UnsupportedContentType { content_type } => {
|
||||
format!("unsupported content type: {content_type}")
|
||||
}
|
||||
LokiError::UnsupportedContentEncoding { encoding } => {
|
||||
format!("unsupported content encoding: {encoding}")
|
||||
}
|
||||
LokiError::ProtobufDecode { source } => {
|
||||
format!("failed to decode protobuf: {source}")
|
||||
}
|
||||
LokiError::SnappyDecompression { source } => {
|
||||
format!("failed to decompress snappy: {source}")
|
||||
}
|
||||
LokiError::JsonParse { source } => format!("failed to parse JSON: {source}"),
|
||||
LokiError::GzipDecompression { source } => {
|
||||
format!("failed to decompress gzip: {source}")
|
||||
}
|
||||
LokiError::Ingestion { .. } => unreachable!("Already tested above"),
|
||||
};
|
||||
|
||||
HttpResponse::BadRequest()
|
||||
.content_type("text/plain")
|
||||
.body(body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,4 +253,31 @@ mod tests {
|
||||
assert_eq!(request.streams[0].values[0].line, "log message");
|
||||
assert!(request.streams[0].values[0].structured_metadata.is_some());
|
||||
}
|
||||
#[test]
|
||||
fn test_loki_entry_default() {
|
||||
let entry = LokiEntry::default();
|
||||
assert_eq!(entry.timestamp, "");
|
||||
assert_eq!(entry.line, "");
|
||||
assert!(entry.structured_metadata.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loki_stream_structure() {
|
||||
let mut labels = HashMap::new();
|
||||
labels.insert("service".to_string(), "api".to_string());
|
||||
|
||||
let entry = LokiEntry {
|
||||
timestamp: "123".to_string(),
|
||||
line: "log".to_string(),
|
||||
structured_metadata: None,
|
||||
};
|
||||
|
||||
let stream = LokiStream {
|
||||
stream: labels.clone(),
|
||||
values: vec![entry],
|
||||
};
|
||||
|
||||
assert_eq!(stream.stream, labels);
|
||||
assert_eq!(stream.values.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,17 +67,16 @@ impl RumExtraData {
|
||||
}
|
||||
|
||||
fn filter_tags(data: &HashMap<String, String>) -> HashMap<String, serde_json::Value> {
|
||||
match data.get("ootags").or_else(|| data.get("o2tags")) {
|
||||
Some(tags) => tags
|
||||
.split(',')
|
||||
.map(|tag| {
|
||||
let key_val: Vec<_> = tag.split(':').collect();
|
||||
(key_val[0].to_string(), key_val[1].into())
|
||||
})
|
||||
.collect(),
|
||||
|
||||
None => HashMap::default(),
|
||||
}
|
||||
data.get("ootags")
|
||||
.or_else(|| data.get("o2tags"))
|
||||
.map_or_else(HashMap::default, |tags| {
|
||||
tags.split(',')
|
||||
.map(|tag| {
|
||||
let key_val: Vec<_> = tag.split(':').collect();
|
||||
(key_val[0].to_string(), key_val[1].into())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn extractor(
|
||||
@@ -101,21 +100,27 @@ impl RumExtraData {
|
||||
user_agent_hashmap.extend(tags);
|
||||
{
|
||||
let headers = req.headers();
|
||||
let conn_info = req.connection_info().clone();
|
||||
let ip_address = match headers.contains_key("X-Forwarded-For")
|
||||
|| headers.contains_key("Forwarded")
|
||||
{
|
||||
true => conn_info.realip_remote_addr().unwrap(),
|
||||
false => conn_info.peer_addr().unwrap(),
|
||||
// Borrow checker can't reliably tell that RefCell has been dropped
|
||||
// before an await, even if `drop` is directly used
|
||||
// This little contraption helps avoid clippy issues
|
||||
let ip_address = {
|
||||
let conn_info = req.connection_info();
|
||||
match headers.contains_key("X-Forwarded-For") || headers.contains_key("Forwarded") {
|
||||
true => conn_info.realip_remote_addr(),
|
||||
false => conn_info.peer_addr(),
|
||||
}
|
||||
.unwrap()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
user_agent_hashmap.insert("ip".into(), ip_address.into());
|
||||
let ip = match parse_ip_addr(ip_address) {
|
||||
let ip = match parse_ip_addr(&ip_address) {
|
||||
Ok((ip, _)) => ip,
|
||||
// Default to ipv4 loopback address
|
||||
Err(_) => IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
|
||||
};
|
||||
|
||||
user_agent_hashmap.insert("ip".into(), ip_address.into());
|
||||
|
||||
let maxminddb_client = MAXMIND_DB_CLIENT.read().await;
|
||||
let geo_info = if let Some(client) = maxminddb_client.as_ref() {
|
||||
if let Ok(city_info) = client.city_reader.lookup::<maxminddb::geoip2::City>(ip) {
|
||||
@@ -151,11 +156,10 @@ impl RumExtraData {
|
||||
let user_agent = req
|
||||
.headers()
|
||||
.get("User-Agent")
|
||||
.map(|v| v.to_str().unwrap_or(""))
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let ua_parser = UA_PARSER.clone();
|
||||
let parsed_user_agent = ua_parser.parse(user_agent);
|
||||
let parsed_user_agent = (*UA_PARSER).parse(user_agent);
|
||||
|
||||
user_agent_hashmap.insert(
|
||||
"user_agent".into(),
|
||||
|
||||
@@ -361,3 +361,127 @@ impl ClusterInfoResponse {
|
||||
region_entry.clusters.insert(cluster_name, cluster_info);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use config::meta::cluster::Node;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_organization_setting_defaults() {
|
||||
let setting = OrganizationSetting::default();
|
||||
assert_eq!(setting.trace_id_field_name, "trace_id");
|
||||
assert_eq!(setting.span_id_field_name, "span_id");
|
||||
assert!(!setting.toggle_ingestion_logs);
|
||||
assert!(!setting.enable_streaming_search);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_list_response_add_node() {
|
||||
let node = Node {
|
||||
name: "node-1".into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut response = NodeListResponse::new();
|
||||
response.add_node(node.clone(), "us-east".into(), "cluster-a".into());
|
||||
|
||||
let nodes = &response
|
||||
.regions
|
||||
.get("us-east")
|
||||
.unwrap()
|
||||
.clusters
|
||||
.get("cluster-a")
|
||||
.unwrap();
|
||||
assert_eq!(nodes.len(), 1);
|
||||
assert_eq!(nodes[0].name, "node-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_list_response_add_nodes() {
|
||||
let node1 = Node {
|
||||
name: "node-1".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let node2 = Node {
|
||||
name: "node-2".into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut response = NodeListResponse::new();
|
||||
response.add_nodes(vec![
|
||||
(node1.clone(), "us-west".into(), "cluster-x".into()),
|
||||
(node2.clone(), "us-west".into(), "cluster-x".into()),
|
||||
]);
|
||||
|
||||
let nodes = &response
|
||||
.regions
|
||||
.get("us-west")
|
||||
.unwrap()
|
||||
.clusters
|
||||
.get("cluster-x")
|
||||
.unwrap();
|
||||
assert_eq!(nodes.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cluster_info_response_add() {
|
||||
let mut response = ClusterInfoResponse::default();
|
||||
let info = ClusterInfo { pending_jobs: 5 };
|
||||
|
||||
response.add_cluster_info(info.clone(), "cluster-1".into(), "eu".into());
|
||||
|
||||
let cluster = response
|
||||
.regions
|
||||
.get("eu")
|
||||
.unwrap()
|
||||
.clusters
|
||||
.get("cluster-1")
|
||||
.unwrap();
|
||||
assert_eq!(cluster.pending_jobs, 5);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
#[test]
|
||||
fn test_organization_invite_response_data_serialization() {
|
||||
let data = OrganizationInviteResponseData {
|
||||
valid_members: Some(vec!["user1@example.com".into(), "user2@example.com".into()]),
|
||||
existing_members: Some(vec!["existing@example.com".into()]),
|
||||
invalid_email: Some("bad-email".into()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&data).unwrap();
|
||||
assert!(json.contains("user1@example.com"));
|
||||
assert!(json.contains("bad-email"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
#[test]
|
||||
fn test_organization_invites_creation() {
|
||||
let invites = OrganizationInvites {
|
||||
invites: vec!["a@example.com".into(), "b@example.com".into()],
|
||||
role: UserRole::Member,
|
||||
};
|
||||
|
||||
assert_eq!(invites.invites.len(), 2);
|
||||
assert_eq!(invites.role, UserRole::Member);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
#[test]
|
||||
fn test_organization_invite_user_record() {
|
||||
let record = OrganizationInviteUserRecord {
|
||||
email: "user@example.com".into(),
|
||||
first_name: "First".into(),
|
||||
last_name: "Last".into(),
|
||||
role: "admin".into(),
|
||||
status: InviteStatus::Pending,
|
||||
expires_at: 999999,
|
||||
is_external: true,
|
||||
};
|
||||
|
||||
assert_eq!(record.status, InviteStatus::Pending);
|
||||
assert!(record.is_external);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright 2025 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
|
||||
@@ -259,6 +259,7 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod enterprise_tests {
|
||||
use arrow::array::record_batch;
|
||||
use arrow_schema::{DataType, Field};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use o2_enterprise::enterprise::common::streaming_agg_cache::{
|
||||
@@ -267,37 +268,14 @@ mod enterprise_tests {
|
||||
|
||||
#[test]
|
||||
fn test_calculate_record_batches_deltas() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow::{
|
||||
array::{Int64Array, StringArray},
|
||||
datatypes::Schema,
|
||||
};
|
||||
|
||||
// Create a simple schema for testing
|
||||
let schema = Arc::new(Schema::new(vec![
|
||||
Field::new("status", DataType::Utf8, false),
|
||||
Field::new("count", DataType::Int64, false),
|
||||
]));
|
||||
|
||||
// Create test record batches
|
||||
let batch1 = arrow::array::RecordBatch::try_new(
|
||||
schema.clone(),
|
||||
vec![
|
||||
Arc::new(StringArray::from(vec!["200", "404"])),
|
||||
Arc::new(Int64Array::from(vec![100, 50])),
|
||||
],
|
||||
let batch1 = record_batch!(
|
||||
("status", Utf8, ["200", "404"]),
|
||||
("count", Int64, [100, 50])
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let batch2 = arrow::array::RecordBatch::try_new(
|
||||
schema.clone(),
|
||||
vec![
|
||||
Arc::new(StringArray::from(vec!["200", "500"])),
|
||||
Arc::new(Int64Array::from(vec![80, 20])),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let batch2 =
|
||||
record_batch!(("status", Utf8, ["200", "500"]), ("count", Int64, [80, 20])).unwrap();
|
||||
|
||||
// Test case: Query: 10:00 - 16:00, Cache: 11:00 - 12:00, 14:00 - 15:00
|
||||
// Expected Deltas: 10:00 - 11:00, 12:00 - 14:00, 15:00 - 16:00
|
||||
|
||||
@@ -19,6 +19,8 @@ pub const TRACES: &str = "traces";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -31,13 +33,6 @@ mod tests {
|
||||
#[test]
|
||||
fn test_service_constants_are_unique() {
|
||||
let services = [LOGS, METRICS, TRACES];
|
||||
for i in 0..services.len() {
|
||||
for j in (i + 1)..services.len() {
|
||||
assert_ne!(
|
||||
services[i], services[j],
|
||||
"Service constants should be unique"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(HashSet::from(services).len() == services.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,12 @@ mod tests {
|
||||
total_fields: 1,
|
||||
};
|
||||
|
||||
assert!(stream.uds_schema.is_some());
|
||||
assert_eq!(stream.uds_schema.unwrap().len(), 1);
|
||||
assert!(stream.uds_schema.is_some_and(|schema| schema.len() == 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_stream_delete_fields_default() {
|
||||
let delete_fields = StreamDeleteFields::default();
|
||||
assert!(delete_fields.fields.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,4 +157,14 @@ mod tests {
|
||||
assert!(serialized.contains("orgId"));
|
||||
assert!(serialized.contains("streamName"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_syslog_routes() {
|
||||
let routes = SyslogRoutes { routes: vec![] };
|
||||
assert!(routes.routes.is_empty());
|
||||
|
||||
let serialized = serde_json::to_string(&routes).unwrap();
|
||||
let deserialized: SyslogRoutes = serde_json::from_str(&serialized).unwrap();
|
||||
assert!(deserialized.routes.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,4 +305,12 @@ mod test_telemetry {
|
||||
add_zo_info(&mut props).await;
|
||||
assert!(!tel.base_info.is_empty())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_telemetry_send_track_event_without_base_info_or_zo_data() {
|
||||
let mut telemetry = Telemetry::new();
|
||||
telemetry
|
||||
.send_track_event("test_event", None, false, false)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn record_batches_to_json_rows(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arrow::{
|
||||
array::{Int32Array, StringArray, UInt64Array},
|
||||
array::{Int32Array, StringArray, record_batch},
|
||||
datatypes::{DataType, Field, Schema},
|
||||
record_batch::RecordBatch,
|
||||
};
|
||||
@@ -67,23 +67,10 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_batch() -> RecordBatch {
|
||||
let schema = Schema::new(vec![
|
||||
Field::new("id", DataType::Int32, false),
|
||||
Field::new("name", DataType::Utf8, false),
|
||||
Field::new("value", DataType::UInt64, false),
|
||||
]);
|
||||
|
||||
let id_array = Int32Array::from(vec![1, 2, 3]);
|
||||
let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
|
||||
let value_array = UInt64Array::from(vec![100, 200, 300]);
|
||||
|
||||
RecordBatch::try_new(
|
||||
schema.into(),
|
||||
vec![
|
||||
std::sync::Arc::new(id_array),
|
||||
std::sync::Arc::new(name_array),
|
||||
std::sync::Arc::new(value_array),
|
||||
],
|
||||
record_batch!(
|
||||
("id", Int32, [1, 2, 3]),
|
||||
("name", Utf8, ["Alice", "Bob", "Charlie"]),
|
||||
("value", UInt64, [100, 200, 300])
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -53,8 +53,10 @@ fn read_cpu_cgroup_v1() -> usize {
|
||||
&& !val.is_empty()
|
||||
&& !val.to_lowercase().starts_with("max")
|
||||
{
|
||||
let columns = val.split(' ').collect::<Vec<&str>>();
|
||||
return columns[0].parse::<usize>().unwrap_or_default();
|
||||
return val
|
||||
.split_once(' ')
|
||||
.and_then(|(v, _)| v.parse::<usize>().ok())
|
||||
.unwrap_or_default();
|
||||
};
|
||||
0
|
||||
}
|
||||
|
||||
Vendored
+9
-12
@@ -62,16 +62,13 @@ pub async fn invalidate_cached_response_by_stream_min_ts(
|
||||
|
||||
let filtered_responses = responses
|
||||
.iter()
|
||||
.filter(|meta| meta.response_end_time >= stream_min_ts)
|
||||
.cloned()
|
||||
.filter_map(|mut meta| {
|
||||
if meta.response_end_time >= stream_min_ts {
|
||||
if meta.response_start_time < stream_min_ts {
|
||||
meta.response_start_time = stream_min_ts;
|
||||
}
|
||||
Some(meta) // Keep the entry after updating
|
||||
} else {
|
||||
None // Remove the entry
|
||||
.map(|mut meta| {
|
||||
if meta.response_start_time < stream_min_ts {
|
||||
meta.response_start_time = stream_min_ts;
|
||||
}
|
||||
meta
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -726,10 +723,10 @@ fn handle_histogram(
|
||||
.map(|v| v.trim().trim_matches(|v| (v == '\'' || v == '"')))
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
attrs.get(1).map_or_else(
|
||||
|| generate_histogram_interval(q_time_range),
|
||||
|v| v.to_string(),
|
||||
)
|
||||
attrs
|
||||
.get(1)
|
||||
.map_or_else(|| generate_histogram_interval(q_time_range), |v| *v)
|
||||
.to_string()
|
||||
};
|
||||
|
||||
*origin_sql = origin_sql.replace(
|
||||
|
||||
@@ -129,6 +129,7 @@ impl TreeNodeRewriter for HistogramToDatebin {
|
||||
format!("{} second", self.histogram_interval)
|
||||
} else {
|
||||
generate_histogram_interval(Some((self.start_time, self.end_time)))
|
||||
.to_string()
|
||||
};
|
||||
cast(
|
||||
Expr::Literal(ScalarValue::from(interval)),
|
||||
|
||||
+48
-30
@@ -1492,7 +1492,7 @@ impl VisitorMut for HistogramIntervalVisitor {
|
||||
|
||||
fn pre_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow<Self::Break> {
|
||||
if let Expr::Function(func) = expr
|
||||
&& func.name.to_string().to_lowercase() == "histogram"
|
||||
&& func.name.to_string().eq_ignore_ascii_case("histogram")
|
||||
{
|
||||
if let FunctionArguments::List(list) = &func.args {
|
||||
let mut args = list.args.iter();
|
||||
@@ -1505,7 +1505,7 @@ impl VisitorMut for HistogramIntervalVisitor {
|
||||
.trim_matches(|v| v == '\'' || v == '"')
|
||||
.to_string()
|
||||
} else {
|
||||
generate_histogram_interval(self.time_range)
|
||||
generate_histogram_interval(self.time_range).to_string()
|
||||
};
|
||||
self.interval = match convert_histogram_interval_to_seconds(&interval) {
|
||||
Ok(v) => Some(v),
|
||||
@@ -1643,12 +1643,12 @@ impl VisitorMut for TrackTotalHitsVisitor {
|
||||
|
||||
fn generate_table_reference(idents: &[Ident]) -> (TableReference, String) {
|
||||
if idents.len() == 2 {
|
||||
let table_name = idents[0].value.clone();
|
||||
let table_name = idents[0].value.as_str();
|
||||
let field_name = idents[1].value.clone();
|
||||
(TableReference::from(table_name), field_name)
|
||||
} else {
|
||||
let stream_type = idents[0].value.clone();
|
||||
let table_name = idents[1].value.clone();
|
||||
let stream_type = idents[0].value.as_str();
|
||||
let table_name = idents[1].value.as_str();
|
||||
let field_name = idents[2].value.clone();
|
||||
(TableReference::partial(stream_type, table_name), field_name)
|
||||
}
|
||||
@@ -1717,11 +1717,14 @@ macro_rules! intervals {
|
||||
(@unit m) => { chrono::Duration::minutes };
|
||||
}
|
||||
|
||||
pub fn generate_histogram_interval(time_range: Option<(i64, i64)>) -> String {
|
||||
if time_range.is_none() || time_range.unwrap().eq(&(0, 0)) {
|
||||
return "1 hour".to_string();
|
||||
pub fn generate_histogram_interval(time_range: Option<(i64, i64)>) -> &'static str {
|
||||
let Some((start, end)) = time_range else {
|
||||
return "1 hour";
|
||||
};
|
||||
if (start, end).eq(&(0, 0)) {
|
||||
return "1 hour";
|
||||
}
|
||||
let duration = time_range.map(|r| r.1 - r.0).unwrap();
|
||||
let duration = end - start;
|
||||
|
||||
const INTERVALS: [(i64, &str); 10] = intervals![
|
||||
(h, 24 * 60, "1 day"),
|
||||
@@ -1738,35 +1741,39 @@ pub fn generate_histogram_interval(time_range: Option<(i64, i64)>) -> String {
|
||||
|
||||
for (time, interval) in INTERVALS.iter() {
|
||||
if duration >= *time {
|
||||
return interval.to_string();
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
"10 second".to_string()
|
||||
"10 second"
|
||||
}
|
||||
|
||||
pub fn convert_histogram_interval_to_seconds(interval: &str) -> Result<i64, Error> {
|
||||
let interval = interval.trim();
|
||||
let (num, unit) = interval
|
||||
let pos = interval
|
||||
.find(|c: char| !c.is_numeric())
|
||||
.map(|pos| interval.split_at(pos))
|
||||
.ok_or_else(|| Error::Message(format!("Invalid interval format: '{interval}'")))?;
|
||||
|
||||
let seconds = match unit.trim().to_lowercase().as_str() {
|
||||
"second" | "seconds" | "s" | "secs" | "sec" => num.parse::<i64>(),
|
||||
"minute" | "minutes" | "m" | "mins" | "min" => num.parse::<i64>().map(|n| n * 60),
|
||||
"hour" | "hours" | "h" | "hrs" | "hr" => num.parse::<i64>().map(|n| n * 3600),
|
||||
"day" | "days" | "d" => num.parse::<i64>().map(|n| n * 86400),
|
||||
let (num_str, unit_str) = interval.split_at(pos);
|
||||
let num = num_str.parse::<i64>().map_err(|_| {
|
||||
Error::ErrorCode(ErrorCodes::SearchSQLNotValid(
|
||||
"Invalid number format".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let unit = unit_str.trim().to_lowercase();
|
||||
let seconds = match unit.as_str() {
|
||||
"second" | "seconds" | "s" | "secs" | "sec" => num,
|
||||
"minute" | "minutes" | "m" | "mins" | "min" => num * 60,
|
||||
"hour" | "hours" | "h" | "hrs" | "hr" => num * 3600,
|
||||
"day" | "days" | "d" => num * 86400,
|
||||
_ => {
|
||||
return Err(Error::ErrorCode(ErrorCodes::SearchSQLNotValid(
|
||||
"Unsupported histogram interval unit".to_string(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
seconds.map_err(|_| {
|
||||
Error::ErrorCode(ErrorCodes::SearchSQLNotValid(
|
||||
"Invalid number format".to_string(),
|
||||
))
|
||||
})
|
||||
|
||||
Ok(seconds)
|
||||
}
|
||||
|
||||
pub fn pickup_where(sql: &str, meta: Option<MetaSql>) -> Result<Option<String>, Error> {
|
||||
@@ -1786,17 +1793,28 @@ pub fn pickup_where(sql: &str, meta: Option<MetaSql>) -> Result<Option<String>,
|
||||
return Ok(None);
|
||||
};
|
||||
let mut where_str = caps.get(1).unwrap().as_str().to_string();
|
||||
if !meta.group_by.is_empty() {
|
||||
where_str = where_str[0..where_str.to_lowercase().rfind(" group ").unwrap()].to_string();
|
||||
let where_str_lower = where_str.to_lowercase();
|
||||
|
||||
let clause_to_strip = if !meta.group_by.is_empty() {
|
||||
Some(" group ")
|
||||
} else if meta.having {
|
||||
where_str = where_str[0..where_str.to_lowercase().rfind(" having ").unwrap()].to_string();
|
||||
Some(" having ")
|
||||
} else if !meta.order_by.is_empty() {
|
||||
where_str = where_str[0..where_str.to_lowercase().rfind(" order ").unwrap()].to_string();
|
||||
} else if meta.limit > 0 || where_str.to_lowercase().ends_with(" limit 0") {
|
||||
where_str = where_str[0..where_str.to_lowercase().rfind(" limit ").unwrap()].to_string();
|
||||
Some(" order ")
|
||||
} else if meta.limit > 0 || where_str_lower.ends_with(" limit 0") {
|
||||
Some(" limit ")
|
||||
} else if meta.offset > 0 {
|
||||
where_str = where_str[0..where_str.to_lowercase().rfind(" offset ").unwrap()].to_string();
|
||||
Some(" offset ")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(clause) = clause_to_strip
|
||||
&& let Some(pos) = where_str_lower.rfind(clause)
|
||||
{
|
||||
where_str.truncate(pos);
|
||||
}
|
||||
|
||||
Ok(Some(where_str))
|
||||
}
|
||||
|
||||
|
||||
@@ -320,7 +320,7 @@ mod tests {
|
||||
if include_fts_field {
|
||||
fields.push(Field::new("content", DataType::Utf8, false));
|
||||
let content: Vec<String> = (0..num_rows)
|
||||
.map(|i| format!("Test content number {}", i))
|
||||
.map(|i| format!("Test content number {i}"))
|
||||
.collect();
|
||||
columns.push(Arc::new(StringArray::from(content)));
|
||||
}
|
||||
|
||||
@@ -442,8 +442,8 @@ mod tests {
|
||||
];
|
||||
|
||||
for (i, blob_type) in blob_types.iter().enumerate() {
|
||||
let blob_data = format!("blob data {}", i).into_bytes();
|
||||
let blob_tag = format!("blob_{}", i);
|
||||
let blob_data = format!("blob data {i}").into_bytes();
|
||||
let blob_tag = format!("blob_{i}");
|
||||
|
||||
writer
|
||||
.add_blob(&blob_data, blob_type.clone(), blob_tag.clone())
|
||||
@@ -456,7 +456,7 @@ mod tests {
|
||||
assert_eq!(writer.blobs_metadata[i].blob_type, *expected_type);
|
||||
assert_eq!(
|
||||
writer.blobs_metadata[i].properties.get("blob_tag"),
|
||||
Some(&format!("blob_{}", i))
|
||||
Some(&format!("blob_{i}"))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -495,7 +495,7 @@ mod tests {
|
||||
let caching_directory = CachingDirectory::new(ram_directory);
|
||||
|
||||
// Should be able to debug print
|
||||
let debug_str = format!("{:?}", caching_directory);
|
||||
let debug_str = format!("{caching_directory:?}");
|
||||
assert!(debug_str.contains("CachingDirectory"));
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ mod tests {
|
||||
let handle = caching_directory.get_file_handle(test_path)?;
|
||||
|
||||
// Should be able to debug print
|
||||
let debug_str = format!("{:?}", handle);
|
||||
let debug_str = format!("{handle:?}");
|
||||
assert!(debug_str.contains("CachingFileHandle"));
|
||||
assert!(debug_str.contains("debug_test"));
|
||||
|
||||
|
||||
@@ -603,9 +603,9 @@ mod tests {
|
||||
for i in 0..10 {
|
||||
let cache_clone = cache.clone();
|
||||
let handle = thread::spawn(move || {
|
||||
let path = PathBuf::from(format!("file_{}.term", i));
|
||||
let path = PathBuf::from(format!("file_{i}.term"));
|
||||
let range = 0..10;
|
||||
let data = OwnedBytes::new(format!("data_{:04}", i).into_bytes());
|
||||
let data = OwnedBytes::new(format!("data_{i:04}").into_bytes());
|
||||
|
||||
cache_clone.put_slice(path.clone(), range.clone(), data.clone());
|
||||
let retrieved = cache_clone.get_slice(&path, range);
|
||||
|
||||
@@ -179,7 +179,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
let data = result.unwrap();
|
||||
assert!(data.len() > 0); // Should have some data
|
||||
assert!(!data.is_empty()); // Should have some data
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -188,7 +188,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
let data = result.unwrap();
|
||||
assert!(data.len() > 0); // Should have some data
|
||||
assert!(!data.is_empty()); // Should have some data
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -197,7 +197,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
let data = result.unwrap();
|
||||
assert!(data.len() > 0); // Should have some data
|
||||
assert!(!data.is_empty()); // Should have some data
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -206,7 +206,7 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
let data = result.unwrap();
|
||||
assert!(data.len() > 0); // Should have some data
|
||||
assert!(!data.is_empty()); // Should have some data
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -302,7 +302,7 @@ mod tests {
|
||||
|
||||
for ext in test_extensions {
|
||||
let result = get_file_from_empty_puffin_dir_with_ext(ext);
|
||||
assert!(result.is_ok(), "Failed to get file with extension: {}", ext);
|
||||
assert!(result.is_ok(), "Failed to get file with extension: {ext}");
|
||||
|
||||
let data = result.unwrap();
|
||||
// Data should be valid OwnedBytes
|
||||
@@ -440,7 +440,7 @@ mod tests {
|
||||
// Should be able to get files using the utility function
|
||||
for ext in ALLOWED_FILE_EXT {
|
||||
let data = get_file_from_empty_puffin_dir_with_ext(ext).unwrap();
|
||||
assert!(data.len() > 0);
|
||||
assert!(!data.is_empty());
|
||||
}
|
||||
|
||||
for ext in EMPTY_FILE_EXT {
|
||||
|
||||
@@ -153,7 +153,7 @@ mod tests {
|
||||
|
||||
for i in 0..5 {
|
||||
let (index, reader) = create_test_index();
|
||||
let key = format!("key_{}", i);
|
||||
let key = format!("key_{i}");
|
||||
cache.put(key.clone(), (index, reader));
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ mod tests {
|
||||
|
||||
// Verify all entries can be retrieved
|
||||
for i in 0..5 {
|
||||
let key = format!("key_{}", i);
|
||||
let key = format!("key_{i}");
|
||||
assert!(cache.get(&key).is_some());
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
// Fill cache beyond capacity
|
||||
for i in 0..15 {
|
||||
let (index, reader) = create_test_index();
|
||||
let key = format!("key_{}", i);
|
||||
let key = format!("key_{i}");
|
||||
cache.put(key, (index, reader));
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ mod tests {
|
||||
// Fill cache to capacity
|
||||
for i in 0..20 {
|
||||
let (index, reader) = create_test_index();
|
||||
let key = format!("key_{}", i);
|
||||
let key = format!("key_{i}");
|
||||
cache.put(key, (index, reader));
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ mod tests {
|
||||
let cache_clone = cache.clone();
|
||||
let handle = thread::spawn(move || {
|
||||
let (index, reader) = create_test_index();
|
||||
let key = format!("concurrent_key_{}", i);
|
||||
let key = format!("concurrent_key_{i}");
|
||||
|
||||
// Put entry
|
||||
cache_clone.put(key.clone(), (index.clone(), reader.clone()));
|
||||
@@ -278,7 +278,7 @@ mod tests {
|
||||
let cache_clone = cache.clone();
|
||||
let handle = thread::spawn(move || {
|
||||
let (index, reader) = create_test_index();
|
||||
let key = format!("eviction_key_{}", i);
|
||||
let key = format!("eviction_key_{i}");
|
||||
cache_clone.put(key, (index, reader));
|
||||
|
||||
// Small delay to increase chance of concurrent operations
|
||||
@@ -318,7 +318,7 @@ mod tests {
|
||||
// Add many entries
|
||||
for i in 0..200 {
|
||||
let (index, reader) = create_test_index();
|
||||
let key = format!("stress_key_{}", i);
|
||||
let key = format!("stress_key_{i}");
|
||||
cache.put(key, (index, reader));
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ mod tests {
|
||||
// Add many entries without triggering eviction
|
||||
for i in 0..500 {
|
||||
let (index, reader) = create_test_index();
|
||||
let key = format!("large_key_{}", i);
|
||||
let key = format!("large_key_{i}");
|
||||
cache.put(key, (index, reader));
|
||||
}
|
||||
|
||||
|
||||
@@ -488,8 +488,8 @@ mod tests {
|
||||
for i in 0..10 {
|
||||
let writer_clone = writer.clone();
|
||||
let handle = thread::spawn(move || {
|
||||
let path = format!("file_{}.txt", i);
|
||||
let data = format!("data for file {}", i);
|
||||
let path = format!("file_{i}.txt");
|
||||
let data = format!("data for file {i}");
|
||||
writer_clone
|
||||
.atomic_write(Path::new(&path), data.as_bytes())
|
||||
.unwrap();
|
||||
@@ -506,9 +506,9 @@ mod tests {
|
||||
|
||||
// All files should be readable
|
||||
for i in 0..10 {
|
||||
let path = format!("file_{}.txt", i);
|
||||
let path = format!("file_{i}.txt");
|
||||
let data = writer.atomic_read(Path::new(&path)).unwrap();
|
||||
let expected = format!("data for file {}", i);
|
||||
let expected = format!("data for file {i}");
|
||||
assert_eq!(data, expected.as_bytes());
|
||||
}
|
||||
}
|
||||
@@ -556,7 +556,7 @@ mod tests {
|
||||
];
|
||||
|
||||
for filename in &special_files {
|
||||
let data = format!("data for {}", filename);
|
||||
let data = format!("data for {filename}");
|
||||
writer
|
||||
.atomic_write(Path::new(filename), data.as_bytes())
|
||||
.unwrap();
|
||||
@@ -568,7 +568,7 @@ mod tests {
|
||||
for filename in &special_files {
|
||||
assert!(files.contains(&PathBuf::from(filename)));
|
||||
let read_data = writer.atomic_read(Path::new(filename)).unwrap();
|
||||
let expected = format!("data for {}", filename);
|
||||
let expected = format!("data for {filename}");
|
||||
assert_eq!(read_data, expected.as_bytes());
|
||||
}
|
||||
}
|
||||
@@ -698,7 +698,7 @@ mod tests {
|
||||
// Test all allowed extensions
|
||||
let allowed_extensions = ["term", "idx", "pos", "fast"];
|
||||
for ext in &allowed_extensions {
|
||||
let filename = format!("test.{}", ext);
|
||||
let filename = format!("test.{ext}");
|
||||
writer.atomic_write(Path::new(&filename), b"data").unwrap();
|
||||
}
|
||||
|
||||
|
||||
+12
-11
@@ -810,12 +810,14 @@ pub async fn list_users(
|
||||
if is_list_all {
|
||||
let (org, id) = org_user.key().split_once('/').unwrap();
|
||||
if let Some(org_record) = organization::get_org(org).await {
|
||||
let user_org = user_orgs.entry(id.to_string()).or_insert(vec![]);
|
||||
user_org.push(OrgRoleMapping {
|
||||
org_id: org.to_string(),
|
||||
role: org_user.value().role.clone(),
|
||||
org_name: org_record.name,
|
||||
});
|
||||
user_orgs
|
||||
.entry(id.to_string())
|
||||
.or_default()
|
||||
.push(OrgRoleMapping {
|
||||
org_id: org.to_string(),
|
||||
role: org_user.value().role.clone(),
|
||||
org_name: org_record.name,
|
||||
});
|
||||
}
|
||||
} else if org_user.key().starts_with(&format!("{org_id}/"))
|
||||
&& let Some(user) = get_user(Some(org_id), org_user.value().email.as_str()).await
|
||||
@@ -935,7 +937,7 @@ pub async fn remove_user_from_org(
|
||||
}
|
||||
|
||||
if !user.organizations.is_empty() {
|
||||
let mut orgs = user.clone().organizations;
|
||||
let orgs = &mut user.organizations;
|
||||
if orgs.len() == 1 {
|
||||
if orgs[0].role.eq(&UserRole::ServiceAccount) && user.is_external {
|
||||
return Ok(HttpResponse::Forbidden().json(MetaHttpResponse::error(
|
||||
@@ -978,7 +980,7 @@ pub async fn remove_user_from_org(
|
||||
),
|
||||
));
|
||||
}
|
||||
if org.name.eq(&org_id.to_string()) {
|
||||
if org.name.eq(org_id) {
|
||||
let user_role = &org.role;
|
||||
is_service_account = user_role.eq(&UserRole::ServiceAccount);
|
||||
_user_fga_role =
|
||||
@@ -989,8 +991,7 @@ pub async fn remove_user_from_org(
|
||||
};
|
||||
}
|
||||
}
|
||||
orgs.retain(|x| !x.name.eq(&org_id.to_string()));
|
||||
user.organizations = orgs;
|
||||
orgs.retain(|x| !x.name.eq(org_id));
|
||||
let resp = db::org_users::remove(org_id, &email_id).await;
|
||||
// special case as we cache flattened user struct
|
||||
if resp.is_ok() {
|
||||
@@ -1069,7 +1070,7 @@ pub fn is_user_from_org(orgs: Vec<UserOrg>, org_id: &str) -> (bool, UserOrg) {
|
||||
(false, get_default_user_org())
|
||||
} else {
|
||||
let mut local_orgs = orgs;
|
||||
local_orgs.retain(|org| !org.name.eq(&org_id.to_string()));
|
||||
local_orgs.retain(|org| !org.name.eq(org_id));
|
||||
if local_orgs.is_empty() {
|
||||
(false, get_default_user_org())
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user