Skip to content

Commit ed397ef

Browse files
authored
Add stable multi-key pipeline sorting (#70)
1 parent f19aa55 commit ed397ef

16 files changed

Lines changed: 932 additions & 161 deletions

File tree

crates/hubuum-filter/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The current native stages provide:
6666
- typed boolean predicates with explicit casts, null/missing tests, fanout,
6767
quoted JSON literals, and `NOT`/`AND`/`OR` composition;
6868
- projection and selector-based value extraction;
69-
- line or field sorting with string, numeric, and IP casts;
69+
- stable multi-key sorting with strict casts, fanout reduction, and null order;
7070
- head, tail, and count;
7171
- grouping, aggregation, collapse, and array unroll; and
7272
- JQ-compatible transforms through the in-process `jaq` evaluator.

crates/hubuum-filter/src/eval.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use regex::Regex;
22

33
use crate::error::PipelineError;
4-
use crate::model::{OutputEnvelope, OutputShape, PipeStage, SortCast};
4+
use crate::model::{OutputEnvelope, OutputShape, PipeStage};
55
use crate::settings::PipelineSettings;
66
use crate::verbs::collection::{
77
aggregate_envelope, collapse_groups, count_envelope, group_envelope, limit_envelope,
8-
sort_envelope, unroll_envelope,
8+
sort_columns_envelope, sort_whole_envelope, unroll_envelope,
99
};
1010
use crate::verbs::jq::jq_envelope;
1111
use crate::verbs::project::{project_envelope, value_envelope};
@@ -53,7 +53,7 @@ impl PipeStage {
5353
| Self::TypedReject(_)
5454
| Self::Truthy(_)
5555
| Self::Columns(_)
56-
| Self::SortColumn { .. }
56+
| Self::SortColumns(_)
5757
| Self::Group(_)
5858
| Self::Aggregate(_)
5959
| Self::CollapseGroups
@@ -116,15 +116,9 @@ fn apply_semantic_stage(
116116
PipeStage::Head { count, offset } => limit_envelope(envelope, *count, *offset, false),
117117
PipeStage::Tail(count) => limit_envelope(envelope, *count, 0, true),
118118
PipeStage::Count => count_envelope(envelope),
119-
PipeStage::SortLines { descending } => {
120-
sort_envelope(envelope, None, *descending, SortCast::Auto)
121-
}
119+
PipeStage::SortLines { descending } => sort_whole_envelope(envelope, *descending),
122120
PipeStage::Columns(columns) => project_envelope(envelope, columns),
123-
PipeStage::SortColumn {
124-
selector,
125-
descending,
126-
cast,
127-
} => sort_envelope(envelope, Some(selector), *descending, *cast),
121+
PipeStage::SortColumns(spec) => sort_columns_envelope(envelope, spec),
128122
PipeStage::Group(keys) => group_envelope(envelope, keys),
129123
PipeStage::Aggregate(spec) => aggregate_envelope(envelope, spec),
130124
PipeStage::CollapseGroups => collapse_groups(envelope),

crates/hubuum-filter/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ mod tests;
1717
pub use error::PipelineError;
1818
pub use eval::{apply_pipeline, apply_pipeline_with_settings};
1919
pub use model::{
20-
AggregateFunction, AggregateSpec, GroupKey, OutputEnvelope, OutputName, OutputShape, PipeStage,
21-
ProjectTerm, SortCast,
20+
AggregateFunction, AggregateSpec, GroupKey, NullOrder, OutputEnvelope, OutputName, OutputShape,
21+
PipeStage, ProjectTerm, SortCast, SortDirection, SortKey, SortReduction, SortSpec,
2222
};
2323
pub use parse::split_pipeline;
2424
pub use pipeline::Pipeline;

crates/hubuum-filter/src/model.rs

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,12 @@ pub enum PipeStage {
1717
Truthy(Option<Selector>),
1818
Reject(String),
1919
TypedReject(Predicate),
20-
Head {
21-
count: usize,
22-
offset: usize,
23-
},
20+
Head { count: usize, offset: usize },
2421
Tail(usize),
2522
Count,
26-
SortLines {
27-
descending: bool,
28-
},
23+
SortLines { descending: bool },
2924
Columns(Vec<ProjectTerm>),
30-
SortColumn {
31-
selector: Selector,
32-
descending: bool,
33-
cast: SortCast,
34-
},
25+
SortColumns(SortSpec),
3526
Group(Vec<GroupKey>),
3627
Aggregate(AggregateSpec),
3728
CollapseGroups,
@@ -113,7 +104,7 @@ impl PipeStage {
113104
Self::Head { .. } => "L",
114105
Self::Tail(_) => "tail",
115106
Self::Count => "C",
116-
Self::SortLines { .. } | Self::SortColumn { .. } => "S",
107+
Self::SortLines { .. } | Self::SortColumns(_) => "S",
117108
Self::Columns(_) => "P",
118109
Self::Group(_) => "G",
119110
Self::Aggregate(_) => "A",
@@ -135,7 +126,7 @@ impl PipeStage {
135126
| Self::Value(_) => STRUCTURED_SHAPES,
136127
Self::Head { .. } | Self::Tail(_) | Self::SortLines { .. } => COLLECTION_SHAPES,
137128
Self::Columns(_) => PROJECT_SHAPES,
138-
Self::SortColumn { .. } | Self::Unroll(_) => STRUCTURED_COLLECTION_SHAPES,
129+
Self::SortColumns(_) | Self::Unroll(_) => STRUCTURED_COLLECTION_SHAPES,
139130
Self::Group(_) => GROUP_INPUT_SHAPES,
140131
Self::Aggregate(_) | Self::CollapseGroups => GROUPS_ONLY,
141132
}
@@ -199,7 +190,7 @@ impl PipeStage {
199190
unreachable!("validated input shape")
200191
}
201192
},
202-
Self::SortColumn { .. } | Self::Unroll(_) => match input {
193+
Self::SortColumns(_) | Self::Unroll(_) => match input {
203194
OutputShape::Empty => EMPTY_ONLY,
204195
OutputShape::Rows => ROWS_ONLY,
205196
OutputShape::Values => VALUES_ONLY,
@@ -389,6 +380,128 @@ pub enum SortCast {
389380
String,
390381
Number,
391382
Ip,
383+
Boolean,
384+
DateTime,
385+
Version,
386+
Natural,
387+
}
388+
389+
impl Display for SortCast {
390+
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
391+
formatter.write_str(match self {
392+
Self::Auto => "auto",
393+
Self::String => "str",
394+
Self::Number => "num",
395+
Self::Ip => "ip",
396+
Self::Boolean => "bool",
397+
Self::DateTime => "datetime",
398+
Self::Version => "version",
399+
Self::Natural => "natural",
400+
})
401+
}
402+
}
403+
404+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
405+
pub enum SortDirection {
406+
#[default]
407+
Ascending,
408+
Descending,
409+
}
410+
411+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
412+
pub enum SortReduction {
413+
#[default]
414+
First,
415+
Min,
416+
Max,
417+
}
418+
419+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
420+
pub enum NullOrder {
421+
First,
422+
#[default]
423+
Last,
424+
}
425+
426+
#[derive(Debug, Clone, PartialEq, Eq)]
427+
pub struct SortKey {
428+
selector: Selector,
429+
direction: SortDirection,
430+
cast: SortCast,
431+
reduction: SortReduction,
432+
null_order: NullOrder,
433+
}
434+
435+
impl SortKey {
436+
pub fn new(selector: impl Into<String>) -> Result<Self, PipelineError> {
437+
Ok(Self {
438+
selector: Selector::new(selector)?,
439+
direction: SortDirection::Ascending,
440+
cast: SortCast::Auto,
441+
reduction: SortReduction::First,
442+
null_order: NullOrder::Last,
443+
})
444+
}
445+
446+
pub fn selector(&self) -> &Selector {
447+
&self.selector
448+
}
449+
450+
pub fn direction(&self) -> SortDirection {
451+
self.direction
452+
}
453+
454+
pub fn cast(&self) -> SortCast {
455+
self.cast
456+
}
457+
458+
pub fn reduction(&self) -> SortReduction {
459+
self.reduction
460+
}
461+
462+
pub fn null_order(&self) -> NullOrder {
463+
self.null_order
464+
}
465+
466+
pub fn with_direction(mut self, direction: SortDirection) -> Self {
467+
self.direction = direction;
468+
self
469+
}
470+
471+
pub fn with_cast(mut self, cast: SortCast) -> Self {
472+
self.cast = cast;
473+
self
474+
}
475+
476+
pub fn with_reduction(mut self, reduction: SortReduction) -> Self {
477+
self.reduction = reduction;
478+
self
479+
}
480+
481+
pub fn with_null_order(mut self, null_order: NullOrder) -> Self {
482+
self.null_order = null_order;
483+
self
484+
}
485+
}
486+
487+
#[derive(Debug, Clone, PartialEq, Eq)]
488+
pub struct SortSpec {
489+
keys: Vec<SortKey>,
490+
}
491+
492+
impl SortSpec {
493+
pub fn new(keys: Vec<SortKey>) -> Result<Self, PipelineError> {
494+
if keys.is_empty() {
495+
return Err(PipelineError::Pipe(
496+
"Pipe stage 'S' requires at least one sort key".to_string(),
497+
));
498+
}
499+
Ok(Self { keys })
500+
}
501+
502+
pub fn keys(&self) -> &[SortKey] {
503+
&self.keys
504+
}
392505
}
393506

394507
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]

0 commit comments

Comments
 (0)