Skip to main content

chronix_query/
lib.rs

1//! # chronix-query
2//!
3//! Query engine for the Chronix time-series database.
4//!
5//! This crate implements the full query pipeline:
6//!
7//! 1. **Planning** — [`QueryPlan`] and [`QueryBuilder`] for fluent query construction
8//! 2. **Pruning** — Multi-level segment elimination (time, bloom, stats)
9//! 3. **Projection** — Read only the columns needed by the query
10//! 4. **Filtering** — Vectorized predicate evaluation on Arrow arrays
11//! 5. **Deduplication** — Sort-merge dedup for overlapping segments
12//! 6. **Aggregation** — count, sum, min, max, avg, first, last
13//! 7. **Downsampling** — Time-bucket aggregation
14//! 8. **Window functions** — row_number, rank, lead, lag, delta, rate, irate,
15//!    moving average, cumulative sum, increase
16//!
17//! All results are returned as Apache Arrow `RecordBatch` for zero-copy
18//! interoperability with data processing ecosystems.
19
20#![warn(missing_docs)]
21#![deny(unsafe_code)]
22#![allow(clippy::module_name_repetitions)]
23
24pub mod aggregate;
25pub mod convert;
26pub mod dedup;
27pub mod downsample;
28pub mod encoded_pushdown;
29pub mod error;
30pub mod filter;
31pub mod memory;
32pub mod plan;
33pub mod pruning;
34pub mod window;
35
36pub use aggregate::AggFn;
37pub use aggregate::{AggResult, DecimalTrack, Num};
38pub use convert::points_to_record_batch;
39pub use encoded_pushdown::{ColumnPredicate, EncodedDomainEvaluator, RowGroupVerdict};
40pub use error::QueryError;
41pub use memory::MemoryTracker;
42pub use plan::{FieldPredicate, QueryBuilder, QueryPlan, ZoneMapOp};
43pub use window::WindowFn;
44
45/// The Arrow type a schema column decodes to — one answer, in one place.
46///
47/// It was four copies of this match (the scan schema, the SQL table
48/// provider, the server's schema endpoint, the segment reader), each ending
49/// in a catch-all that mapped anything it did not name to a string. That is
50/// harmless while every unnamed type *is* a string, and turns a decimal
51/// column into `Utf8` the moment one is not.
52///
53/// `Timestamp` maps to `Int64`, the storage representation. The SQL layer
54/// presents the time column as `Timestamp(Nanosecond)` and converts on the
55/// way out; see `chronix::sql`.
56#[must_use]
57pub fn column_type_to_arrow(ct: chronix_core::ColumnType) -> arrow::datatypes::DataType {
58    use arrow::datatypes::DataType;
59    use chronix_core::ColumnType;
60    match ct {
61        ColumnType::Timestamp | ColumnType::I64 => DataType::Int64,
62        ColumnType::U64 => DataType::UInt64,
63        ColumnType::F64 => DataType::Float64,
64        ColumnType::Bool => DataType::Boolean,
65        ColumnType::String => DataType::Utf8,
66        ColumnType::Decimal { scale } => DataType::Decimal128(
67            chronix_core::DECIMAL_PRECISION,
68            i8::try_from(scale).unwrap_or(0),
69        ),
70    }
71}
72
73/// Consolidated Arrow column → `f64` extraction.
74///
75/// Supports `Float64Array`, `Int64Array`, `UInt64Array` and
76/// `Decimal128Array`. Returns `None` for null values or unsupported types.
77///
78/// # The one place a decimal becomes approximate
79///
80/// Storing or aggregating a `Decimal128` column keeps it exact: the segment
81/// holds the mantissa, `sum`/`min`/`max`/`first`/`last` fold in `i128`, and
82/// SQL sees `Decimal128(38, s)`.
83///
84/// What comes through here is the other kind of question — a moving average,
85/// a percentile, a forecast, an anomaly score, a chart's downsample, a
86/// PromQL sample. Those are defined in floating point and their answers are
87/// estimates whatever the input was, so converting is honest. Keeping it in
88/// one named function leaves one boundary to audit rather than an `as f64`
89/// in every module.
90#[allow(clippy::cast_precision_loss)]
91pub fn extract_f64(col: &dyn arrow::array::Array, index: usize) -> Option<f64> {
92    use arrow::array::{Decimal128Array, Float64Array, Int64Array, UInt64Array};
93    if col.is_null(index) {
94        return None;
95    }
96    col.as_any()
97        .downcast_ref::<Float64Array>()
98        .map(|a| a.value(index))
99        .or_else(|| {
100            col.as_any()
101                .downcast_ref::<Int64Array>()
102                .map(|a| a.value(index) as f64)
103        })
104        .or_else(|| {
105            col.as_any()
106                .downcast_ref::<UInt64Array>()
107                .map(|a| a.value(index) as f64)
108        })
109        .or_else(|| {
110            col.as_any().downcast_ref::<Decimal128Array>().map(|a| {
111                let scale = u32::try_from(a.scale()).unwrap_or(0);
112                a.value(index) as f64 / chronix_core::pow10(scale).unwrap_or(1) as f64
113            })
114        })
115}