use std::cmp::Ordering;
use std::collections::HashSet;
use std::sync::Arc;
use futures::StreamExt;
use tracing::instrument;
use super::pipeline::{ScanPipeline, build_field_state};
use super::resolved::ResolvedTableContext;
use crate::exec::field_path::FieldPath;
use crate::exec::operators::SortDirection;
use crate::exec::ordering::{OutputOrdering, SortProperty};
use crate::exec::permission::{
PhysicalPermission, convert_permission_to_physical_runtime, should_check_perms,
validate_record_user_access,
};
use crate::exec::{
AccessMode, CombineAccessModes, ContextLevel, ExecOperator, ExecutionContext, FlowResult,
OperatorMetrics, ValueBatch, ValueBatchStream, buffer_stream, monitor_stream,
};
use crate::expr::{ControlFlow, ControlFlowExt};
use crate::iam::Action;
use crate::val::{RecordId, TableName, Value};
#[derive(Debug, Clone)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum MergeMode {
ById(SortDirection),
ByIndexKey {
path: FieldPath,
direction: SortDirection,
},
ByIndexKeyDedup {
path: FieldPath,
direction: SortDirection,
},
}
impl MergeMode {
fn direction(&self) -> SortDirection {
match self {
MergeMode::ById(d)
| MergeMode::ByIndexKey {
direction: d,
..
}
| MergeMode::ByIndexKeyDedup {
direction: d,
..
} => *d,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum MergeKey {
Rid(RecordId),
Field(Value),
}
impl PartialOrd for MergeKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for MergeKey {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(MergeKey::Rid(a), MergeKey::Rid(b)) => a.cmp(b),
(MergeKey::Field(a), MergeKey::Field(b)) => a.cmp(b),
_ => Ordering::Equal,
}
}
}
fn extract_merge_key(mode: &MergeMode, value: &Value) -> Option<MergeKey> {
match mode {
MergeMode::ById(_) => match value {
Value::Object(obj) => match obj.get("id") {
Some(Value::RecordId(rid)) => Some(MergeKey::Rid(rid.clone())),
_ => None,
},
_ => None,
},
MergeMode::ByIndexKey {
path,
..
}
| MergeMode::ByIndexKeyDedup {
path,
..
} => Some(MergeKey::Field(path.extract(value).into_owned())),
}
}
fn extract_rid(value: &Value) -> Option<RecordId> {
match value {
Value::Object(obj) => match obj.get("id") {
Some(Value::RecordId(rid)) => Some(rid.clone()),
_ => None,
},
_ => None,
}
}
#[derive(Debug)]
pub struct UnionIndexScan {
pub(crate) table_name: TableName,
pub(crate) inputs: Vec<Arc<dyn ExecOperator>>,
pub(crate) needed_fields: Option<HashSet<String>>,
pub(crate) resolved: Option<ResolvedTableContext>,
pub(crate) merge: Option<MergeMode>,
pub(crate) downstream_topk: bool,
pub(crate) metrics: Arc<OperatorMetrics>,
}
impl UnionIndexScan {
pub(crate) fn new(
table_name: TableName,
inputs: Vec<Arc<dyn ExecOperator>>,
needed_fields: Option<HashSet<String>>,
) -> Self {
Self {
table_name,
inputs,
needed_fields,
resolved: None,
merge: None,
downstream_topk: false,
metrics: Arc::new(OperatorMetrics::new()),
}
}
pub(crate) fn with_resolved(mut self, resolved: ResolvedTableContext) -> Self {
self.resolved = Some(resolved);
self
}
pub(crate) fn with_merge_by_id(mut self, direction: SortDirection) -> Self {
self.merge = Some(MergeMode::ById(direction));
self
}
pub(crate) fn with_merge_by_index_key(
mut self,
path: FieldPath,
direction: SortDirection,
) -> Self {
self.merge = Some(MergeMode::ByIndexKey {
path,
direction,
});
self
}
pub(crate) fn with_merge_by_index_key_dedup(
mut self,
path: FieldPath,
direction: SortDirection,
) -> Self {
self.merge = Some(MergeMode::ByIndexKeyDedup {
path,
direction,
});
self
}
pub(crate) fn with_downstream_topk(mut self) -> Self {
self.downstream_topk = true;
self
}
}
impl ExecOperator for UnionIndexScan {
fn name(&self) -> &'static str {
"UnionIndexScan"
}
fn attrs(&self) -> Vec<(String, String)> {
let mut attrs = vec![
("table".to_string(), self.table_name.to_string()),
("branches".to_string(), self.inputs.len().to_string()),
];
match &self.merge {
Some(MergeMode::ById(dir)) => {
attrs.push(("merge_by_id".to_string(), format!("{dir:?}")));
}
Some(MergeMode::ByIndexKey {
path,
direction,
}) => {
attrs.push(("merge_by_index_key".to_string(), format!("{path} {direction:?}")));
}
Some(MergeMode::ByIndexKeyDedup {
path,
direction,
}) => {
attrs.push((
"merge_by_index_key_dedup".to_string(),
format!("{path} {direction:?}"),
));
}
None => {}
}
if self.downstream_topk {
attrs.push(("downstream_topk".to_string(), "true".to_string()));
}
attrs
}
fn required_context(&self) -> ContextLevel {
self.inputs
.iter()
.map(|input| input.required_context())
.max()
.unwrap_or(ContextLevel::Database)
}
fn access_mode(&self) -> AccessMode {
self.inputs.iter().map(|input| input.access_mode()).combine_all()
}
fn children(&self) -> Vec<&Arc<dyn ExecOperator>> {
self.inputs.iter().collect()
}
fn metrics(&self) -> Option<&OperatorMetrics> {
Some(&self.metrics)
}
fn output_ordering(&self) -> OutputOrdering {
match &self.merge {
Some(MergeMode::ById(direction)) => OutputOrdering::Sorted(vec![SortProperty {
path: FieldPath::field("id"),
direction: *direction,
collate: false,
numeric: false,
}]),
Some(MergeMode::ByIndexKey {
path,
direction,
})
| Some(MergeMode::ByIndexKeyDedup {
path,
direction,
}) => OutputOrdering::Sorted(vec![SortProperty {
path: path.clone(),
direction: *direction,
collate: false,
numeric: false,
}]),
None => OutputOrdering::Unordered,
}
}
#[instrument(name = "UnionIndexScan::execute", level = "trace", skip_all)]
fn execute(&self, ctx: &ExecutionContext) -> FlowResult<ValueBatchStream> {
if self.inputs.is_empty() {
return Ok(monitor_stream(
Box::pin(futures::stream::empty()),
"UnionIndexScan",
&self.metrics,
));
}
let db_ctx = ctx.database()?.clone();
validate_record_user_access(&db_ctx)?;
let check_perms = should_check_perms(&db_ctx, Action::View)?;
let mut sub_streams: Vec<ValueBatchStream> = Vec::with_capacity(self.inputs.len());
for input in &self.inputs {
let stream = input.execute(ctx)?;
let sub_stream = if self.merge.is_some() || self.downstream_topk {
stream
} else {
buffer_stream(
stream,
input.access_mode(),
input.cardinality_hint(),
ctx.root().ctx.config.operator_buffer_size,
)
};
sub_streams.push(sub_stream);
}
let table_name = self.table_name.clone();
let needed_fields = self.needed_fields.clone();
let resolved = self.resolved.clone();
let merge = self.merge.clone();
let ctx = ctx.clone();
let stream: ValueBatchStream = Box::pin(async_stream::try_stream! {
let db_ctx = ctx.database().context("UnionIndexScan requires database context")?;
let version = ctx.version_stamp();
let (select_permission, field_state) = if let Some(ref res) = resolved {
let perm = res.select_permission(check_perms);
let fs = res.field_state_for_projection(needed_fields.as_ref());
(perm, fs)
} else {
let table_def = db_ctx
.get_table_def(&table_name, version)
.await
.context("Failed to get table")?;
if table_def.is_none() {
Err(ControlFlow::Err(anyhow::Error::new(crate::err::Error::TbNotFound {
name: table_name.clone(),
})))?;
}
let select_permission = if check_perms {
let catalog_perm = match &table_def {
Some(def) => def.permissions.select.clone(),
None => crate::catalog::Permission::None,
};
convert_permission_to_physical_runtime(&catalog_perm, ctx.ctx())
.await
.context("Failed to convert permission")?
} else {
PhysicalPermission::Allow
};
let field_state = build_field_state(
&ctx, &table_name, check_perms, needed_fields.as_ref(),
).await?;
(select_permission, field_state)
};
if matches!(select_permission, PhysicalPermission::Deny) {
return;
}
let mut pipeline = ScanPipeline::new(
select_permission, None, field_state,
check_perms, None, 0,
);
if let Some(merge_mode) = merge {
let direction = merge_mode.direction();
let k = sub_streams.len();
let mut buffers: Vec<Vec<Value>> = Vec::with_capacity(k);
let mut positions: Vec<usize> = Vec::with_capacity(k);
for stream in &mut sub_streams {
if let Some(batch_result) = stream.next().await {
let batch: ValueBatch = batch_result?;
buffers.push(batch.values);
positions.push(0);
} else {
buffers.push(Vec::new());
positions.push(0);
}
}
let mut last_rid: Option<RecordId> = None;
let needs_rid_dedup = matches!(merge_mode, MergeMode::ByIndexKeyDedup { .. });
let mut seen_rids: HashSet<RecordId> = HashSet::new();
loop {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(
anyhow::anyhow!(crate::err::Error::QueryCancelled),
))?;
}
let mut best_idx: Option<usize> = None;
let mut best_key: Option<MergeKey> = None;
for i in 0..k {
if positions[i] >= buffers[i].len() {
continue; }
let key = match extract_merge_key(
&merge_mode,
&buffers[i][positions[i]],
) {
Some(k) => k,
None => continue,
};
let is_better = match &best_key {
None => true,
Some(prev) => match direction {
SortDirection::Asc => key.cmp(prev) == Ordering::Less,
SortDirection::Desc => key.cmp(prev) == Ordering::Greater,
},
};
if is_better {
best_idx = Some(i);
best_key = Some(key);
}
}
let Some(idx) = best_idx else {
break; };
let value = buffers[idx][positions[idx]].clone();
positions[idx] += 1;
if positions[idx] >= buffers[idx].len() {
buffers[idx].clear();
positions[idx] = 0;
if let Some(batch_result) = sub_streams[idx].next().await {
let batch: ValueBatch = batch_result?;
buffers[idx] = batch.values;
}
}
if matches!(merge_mode, MergeMode::ById(_))
&& let Some(MergeKey::Rid(ref rid)) = best_key
{
if last_rid.as_ref() == Some(rid) {
continue;
}
last_rid = Some(rid.clone());
}
if needs_rid_dedup
&& let Some(rid) = extract_rid(&value)
&& !seen_rids.insert(rid)
{
continue;
}
let mut batch = vec![value];
let cont = pipeline.process_batch(&mut batch, &ctx).await?;
if !batch.is_empty() {
yield ValueBatch { values: batch };
}
if !cont {
return;
}
}
} else {
let mut seen: HashSet<RecordId> = HashSet::new();
for mut sub_stream in sub_streams {
while let Some(batch_result) = sub_stream.next().await {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(
anyhow::anyhow!(crate::err::Error::QueryCancelled),
))?;
}
let batch: ValueBatch = batch_result?;
let mut deduped: Vec<Value> = batch
.values
.into_iter()
.filter(|v| match extract_rid(v) {
Some(rid) => seen.insert(rid),
None => true, })
.collect();
if !deduped.is_empty() {
let cont = pipeline.process_batch(&mut deduped, &ctx).await?;
if !deduped.is_empty() {
yield ValueBatch { values: deduped };
}
if !cont {
return;
}
}
}
}
}
});
Ok(monitor_stream(stream, "UnionIndexScan", &self.metrics))
}
}