Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[workspace]
members = [
"crates/core",
]
resolver = "2"
4 changes: 4 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[package]
name = "asap-control-core"
version = "0.1.0"
edition = "2021"
305 changes: 305 additions & 0 deletions crates/core/src/intent_algebra/expr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
use std::rc::Rc;
use std::time::Duration;

use crate::types::AccuracyTarget;
use super::schema::{HasSchema, L3Schema, SchemaCatalog};

// ── Stub leaf / supporting types ──────────────────────────────────────────────
// Full definitions will be added as the respective layers are implemented.

/// A row-level filter predicate (WHERE clause / PromQL label matcher).
#[derive(Debug, Clone)] pub struct Predicate;
/// One item in a SELECT projection list.
#[derive(Debug, Clone)] pub struct ProjectItem;
/// A GROUP BY key reference.
#[derive(Debug, Clone)] pub struct GroupKey;
/// A reference to a column by name.
#[derive(Debug, Clone)] pub struct ColumnRef;
/// A set of partitioning keys (sharding hint for L5 stage allocator).
#[derive(Debug, Clone)] pub struct PartitionKeys;
/// One key in an ORDER BY clause.
#[derive(Debug, Clone)] pub struct SortKey;
/// An analytic window frame (ROWS / RANGE BETWEEN …).
#[derive(Debug, Clone)] pub struct WindowFrame;
/// PromQL vector-match modifiers (`on`/`ignoring` + `group_left`/`group_right`).
#[derive(Debug, Clone)] pub struct VectorMatch;
/// Reference to a metric by name (PromQL / OTLP).
#[derive(Debug, Clone)] pub struct MetricRef;
/// Closed time interval for a time-series scan.
#[derive(Debug, Clone)] pub struct TimeRange;
/// Label matchers applied to a time-series scan.
#[derive(Debug, Clone)] pub struct LabelFilter;
/// Reference to a relational table by name.
#[derive(Debug, Clone)] pub struct TableRef;
/// Join key specification (USING / ON column reference).
#[derive(Debug, Clone)] pub struct JoinKey;

// ── Enum supporting types ─────────────────────────────────────────────────────

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JoinKind {
Inner,
Left,
Right,
Full,
Cross,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SetOpKind {
Union,
Intersect,
Except,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BinaryOpKind {
// Arithmetic
Add, Sub, Mul, Div, Mod,
// Comparison
Eq, NotEq, Lt, LtEq, Gt, GtEq,
// Boolean / PromQL set operators
And, Or, Unless,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WindowFuncKind {
RowNumber, Rank, DenseRank,
Lag, Lead,
FirstValue, LastValue,
NthValue(u64),
Sum, Avg, Count, Min, Max,
}

/// Which data model a `Source` or `AggIntent` operates over.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataModel {
TimeSeries,
Tabular,
/// Agnostic β€” works over either data model.
Any,
}

// ── Time window kind ──────────────────────────────────────────────────────────

/// The lifecycle / flush semantics of a streaming time window.
/// Used by `QueryExpr::TimeWindow`; distinct from SQL analytic frames
/// (`QueryExpr::WindowFunc`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimeWindowKind {
/// Non-overlapping fixed-size windows.
Tumbling,
/// Overlapping windows advancing by `slide` interval.
Sliding,
/// Windows that open on activity and close after a gap of inactivity.
Session,
}

// ── Leaf data source ──────────────────────────────────────────────────────────

/// The leaf data source of a query. Carried by `QueryExpr::Scan` to keep
/// L3 data-model-agnostic: everything above `Scan` (`Filter`, `Aggregate`,
/// etc.) works identically regardless of `Source` variant.
#[derive(Debug, Clone)]
pub enum Source {
/// Time-series input β€” deployment-model-asapquery / asaplifecycle shape.
TimeSeries {
metric: MetricRef,
time: TimeRange,
labels: LabelFilter,
},
/// Tabular input β€” deployment-model-asapfusion / future-OLAP shape.
Table {
table_ref: TableRef,
columns: Vec<ColumnRef>,
},
/// Recursive join over sources (multi-table tabular queries).
Join {
left: Box<Source>,
right: Box<Source>,
on: JoinKey,
},
}

impl Source {
pub fn data_model(&self) -> DataModel {
match self {
Source::TimeSeries { .. } => DataModel::TimeSeries,
Source::Table { .. } | Source::Join { .. } => DataModel::Tabular,
}
}
}

// ── Aggregation intent ────────────────────────────────────────────────────────

/// What to compute, not how. Sketch type and parameters are chosen by L4
/// rules; `AggIntent` is the L3 statement of intent only.
///
/// Heavy-hitter top-k (`TopK`) is a first-class intent because dedicated
/// sketch primitives (SpaceSaving, CMS-with-heap) compute it in one pass.
/// Generic ordering+limit stays as `QueryExpr::Sort + QueryExpr::Limit`.
#[derive(Debug, Clone)]
pub enum AggIntent {
// ── Data-model-agnostic ───────────────────────────────────────────────────
Count { accuracy: AccuracyTarget },
Sum,
Min,
Max,
Quantile { q: f64, accuracy: AccuracyTarget },
/// Heavy-hitter top-k. Distinct from generic `Sort + Limit` β€” a
/// dedicated sketch (SpaceSaving, CMS-with-heap) computes it as a single
/// primitive. Recognised by L1β†’L2β†’L3 lowering on `ORDER BY count DESC
/// LIMIT k` / PromQL `topk(k, …)`.
TopK { k: usize, by: Vec<ColumnRef>, accuracy: AccuracyTarget },
Cardinality { accuracy: AccuracyTarget },

// ── Time-series streaming derivatives ────────────────────────────────────
// Include PromQL counter-reset adjustment; not equivalent to Sum/Count
// over a Window. Kept distinct so delta-set aggregators bind directly.
Rate { window: Duration },
Increase { window: Duration },
}

impl AggIntent {
/// Which data model this intent semantically requires. L4 rules consult
/// this to skip non-applicable intents (e.g. `Rate` over a `Source::Table`).
pub fn requires(&self) -> DataModel {
todo!()
}

/// Output column type β€” used by L3 schema derivation for `Aggregate`.
pub fn output_type(&self, _input: &super::schema::L3Field) -> super::schema::L3DataType {
todo!()
}
}

// ── L3 DAG node ───────────────────────────────────────────────────────────────

/// A node in the L3 DAG. Wraps the expression and its derived output schema
/// so that every edge implicitly carries a typed schema: holding an
/// `Rc<L3Node>` gives you both the child expression and the schema of the
/// data flowing on that edge.
#[derive(Debug, Clone)]
pub struct L3Node {
pub expr: QueryExpr,
/// Output schema of `expr` β€” the schema of the data flowing on the edge
/// leading *from* this node to its parent(s).
pub schema: L3Schema,
}

// ── L3 intent algebra IR ──────────────────────────────────────────────────────

/// Language- and deployment-independent intent-only IR. No sketch types,
/// no sketch parameters, no language-specific operators. Traversing from
/// the root node yields a DAG; shared sub-expressions appear as multiple
/// `Rc` references to the same `L3Node`.
#[derive(Debug, Clone)]
pub enum QueryExpr {
// ── Base relations ────────────────────────────────────────────────────────
/// Outermost leaf. `source` carries the data-model-specific leaf shape.
Scan { source: Source, predicates: Vec<Predicate> },
/// Reference to a named `LetBinding` sub-expression; resolved at plan time.
Ref(String),

// ── Filtering & projection ────────────────────────────────────────────────
/// Οƒ β€” row-level filter. Output schema = child schema (unchanged).
Filter { child: Rc<L3Node>, pred: Predicate },
/// Ο€ β€” column projection. Output schema = child schema projected to `cols`.
Project { child: Rc<L3Node>, cols: Vec<ProjectItem> },

// ── Aggregation ───────────────────────────────────────────────────────────
/// Ξ³ + Ξ± β€” GROUP BY + aggregate intents. Concrete operator (HashAgg /
/// SortAgg / SketchAgg) chosen by L4; `aggs` carry intent only.
Aggregate {
child: Rc<L3Node>,
by: Vec<GroupKey>,
aggs: Vec<AggIntent>,
having: Option<Predicate>,
},

// ── Time / streaming windows ──────────────────────────────────────────────
/// ψ β€” tumbling / sliding / session window over the time axis. Defines
/// the flush / reset lifecycle for aggregates in its sub-DAG. SQL analytic
/// frames are a different node (`WindowFunc`).
TimeWindow {
child: Rc<L3Node>,
kind: TimeWindowKind,
size: Duration,
slide: Option<Duration>,
},

// ── Distributed-execution structure ───────────────────────────────────────
/// Logical-only partitioning marker. Output schema = child schema.
/// Carries a sharding hint for the L5 stage allocator.
Partition { child: Rc<L3Node>, keys: PartitionKeys },
/// Ξ΄ β€” SQL `DISTINCT` / row deduplication.
Distinct { child: Rc<L3Node>, cols: Vec<ColumnRef> },
/// βŠ• β€” exact union of sub-results from independent stages or shards.
/// Sketch unions are a separate node in `SummaryExpr` because they carry
/// sketch-family / params type constraints.
Merge { children: Vec<Rc<L3Node>> },

// ── Joins ─────────────────────────────────────────────────────────────────
/// Logical join. L4 picks the physical alternative (HashJoin /
/// SortMergeJoin / SketchJoin) based on selectivity, memory budget, and
/// accuracy target.
Join {
kind: JoinKind,
left: Rc<L3Node>,
right: Rc<L3Node>,
pred: Option<Predicate>,
},

// ── Set operators ─────────────────────────────────────────────────────────
SetOp {
kind: SetOpKind,
all: bool,
left: Rc<L3Node>,
right: Rc<L3Node>,
},

// ── Ordering & limiting ───────────────────────────────────────────────────
/// Generic order-by for non-heavy-hitter cases (`ORDER BY name LIMIT 10`).
/// Heavy-hitter shapes lower to `AggIntent::TopK` instead.
Sort { child: Rc<L3Node>, keys: Vec<SortKey> },
Limit { child: Rc<L3Node>, n: u64, offset: u64 },

// ── Subquery / CTE ────────────────────────────────────────────────────────
Subquery { child: Rc<L3Node>, alias: String },
/// SQL `WITH name AS (expr) … body`; lowering target for PromQL
/// recording-rule bindings. The `expr` sub-DAG may be referenced N times
/// via `Ref(name)` in `body`, giving the DAG its fan-in.
LetBinding {
name: String,
expr: Rc<L3Node>,
body: Rc<L3Node>,
},

// ── Analytic window functions ─────────────────────────────────────────────
/// SQL `OVER (PARTITION BY … ORDER BY … ROWS BETWEEN …)`.
/// Distinct from `TimeWindow` β€” that is a streaming window over the time
/// axis; this is an analytic frame over already-grouped rows.
WindowFunc {
child: Rc<L3Node>,
func: WindowFuncKind,
partition_by: Vec<GroupKey>,
order_by: Vec<SortKey>,
frame: Option<WindowFrame>,
},

// ── Binary composition ────────────────────────────────────────────────────
/// Arithmetic / comparison / boolean composition (PromQL binary ops
/// including `and` / `or` / `unless`, SQL boolean composition).
BinaryOp {
op: BinaryOpKind,
lhs: Rc<L3Node>,
rhs: Rc<L3Node>,
vector_match: Option<VectorMatch>,
},
}

impl HasSchema for QueryExpr {
fn output_schema(&self, _input_schemas: &[&L3Schema], _catalog: &SchemaCatalog) -> L3Schema {
todo!()
}
}
10 changes: 10 additions & 0 deletions crates/core/src/intent_algebra/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pub mod expr;
pub mod schema;

pub use expr::{
AggIntent, BinaryOpKind, ColumnRef, DataModel, GroupKey, JoinKey, JoinKind, L3Node,
LabelFilter, MetricRef, PartitionKeys, Predicate, ProjectItem, QueryExpr, SetOpKind,
SortKey, Source, TableRef, TimeRange, TimeWindowKind, VectorMatch, WindowFrame,
WindowFuncKind,
};
pub use schema::{HasSchema, L3DataType, L3Field, L3Schema, SchemaCatalog};
53 changes: 53 additions & 0 deletions crates/core/src/intent_algebra/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/// Opaque handle to the external data-source catalog (Prometheus metric
/// metadata, SQL `information_schema`, DataFusion catalog). Used only by
/// `Scan` schema derivation to resolve leaf column types; all other nodes
/// derive their output schemas purely from their input schemas.
pub struct SchemaCatalog;

// ── Data types ────────────────────────────────────────────────────────────────

/// Column types that may appear on an L3 DAG edge.
/// L4 extends this set with `L4DataType::Sketch`; L3 edges never carry
/// sketch-state columns.
#[derive(Debug, Clone, PartialEq)]
pub enum L3DataType {
Int64,
Float64,
Utf8,
Boolean,
Timestamp,
Duration,
/// Key→Value map (e.g. PromQL label set encoded as a column).
Map(Box<L3DataType>, Box<L3DataType>),
List(Box<L3DataType>),
}

// ── Schema ────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone)]
pub struct L3Field {
pub name: String,
pub dtype: L3DataType,
pub nullable: bool,
}

/// Schema carried on every edge of the L3 DAG. Describes the columns
/// flowing between two operators. Type-checked at plan construction time:
/// a node whose predicate references a column absent from its child's
/// `L3Schema` is a plan-time error.
#[derive(Debug, Clone)]
pub struct L3Schema {
pub fields: Vec<L3Field>,
/// Index into `fields` for the time axis, if any.
/// PromQL `Scan` leaves always carry one; SQL leaves may or may not.
pub time_index: Option<usize>,
}

// ── Schema derivation trait ───────────────────────────────────────────────────

/// Implemented by `QueryExpr` to compute the output schema of a node given
/// its children's output schemas. The `L3Node` wrapper stores the derived
/// schema so derivation runs once at construction, not on every traversal.
pub trait HasSchema {
fn output_schema(&self, input_schemas: &[&L3Schema], catalog: &SchemaCatalog) -> L3Schema;
}
4 changes: 4 additions & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod intent_algebra;
pub mod sketch_algebra;
pub mod types;
pub mod workload;
Loading