use std::ops::Range;
use serde::{Deserialize, Serialize};
use spiral_contract::safelong::SafeUnsignedLong;
use uuid::Uuid;
pub use super::api::{
FilterClause, FilterSupport, FragmentReceipt, PlannedSplit, PreparedWriteReceipt, RowRange,
Scan, ScanInfo, ScanMetricsSnapshot, ScanPlan, Split, SplitTake, StorageCounters, StorageScope,
TableVersion, WindowCounters,
};
use super::emit::Emit;
use super::error::ScandalError;
use super::expression::{Expression, NamedExpression};
pub const MAX_TAKE_POSITIONS: usize = 65_536;
pub const MAX_TAKES: usize = 256;
impl Default for StorageCounters {
fn default() -> Self {
Self {
scope: StorageScope::Complete,
bytes: SafeUnsignedLong(0),
requests: SafeUnsignedLong(0),
read_time_ns: SafeUnsignedLong(0),
}
}
}
impl Default for WindowCounters {
fn default() -> Self {
Self {
visited: SafeUnsignedLong(0),
pruned_statistics: SafeUnsignedLong(0),
}
}
}
impl Default for ScanMetricsSnapshot {
fn default() -> Self {
Self {
storage: None,
windows: None,
fragments_considered: SafeUnsignedLong(0),
fragments_pruned: SafeUnsignedLong(0),
files_considered: SafeUnsignedLong(0),
files_pruned: SafeUnsignedLong(0),
output_rows: SafeUnsignedLong(0),
}
}
}
impl ScanMetricsSnapshot {
pub fn add(&mut self, other: &Self) {
self.storage = match (self.storage.take(), other.storage.clone()) {
(Some(mut storage), Some(other)) => {
storage.bytes.0 += other.bytes.0;
storage.requests.0 += other.requests.0;
storage.read_time_ns.0 += other.read_time_ns.0;
Some(storage)
}
(storage, other) => storage.or(other),
};
self.windows = match (self.windows.take(), other.windows.clone()) {
(Some(mut windows), Some(other)) => {
windows.visited.0 += other.visited.0;
windows.pruned_statistics.0 += other.pruned_statistics.0;
Some(windows)
}
(windows, other) => windows.or(other),
};
self.fragments_considered.0 += other.fragments_considered.0;
self.fragments_pruned.0 += other.fragments_pruned.0;
self.files_considered.0 += other.files_considered.0;
self.files_pruned.0 += other.files_pruned.0;
self.output_rows.0 += other.output_rows.0;
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ScanOptions {
#[serde(default, skip_serializing_if = "is_false")]
pub collect_metrics: bool,
#[serde(default)]
pub partitioned: bool,
pub max_streams: Option<usize>,
pub columns: Option<Vec<String>>,
pub projection: Option<Vec<NamedExpression>>,
pub filter: Option<Expression>,
#[serde(default, with = "super::safelong::option_u64")]
pub limit: Option<u64>,
pub batch_rows: Option<usize>,
pub split_token: Option<String>,
pub row_range: Option<ScanRowRange>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub emit: Option<Emit>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub row_id_column: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "super::safelong::option_ranges"
)]
pub row_ids: Option<Vec<Range<u64>>>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ScanRowRange {
#[serde(with = "super::safelong::u64")]
pub start: u64,
#[serde(with = "super::safelong::u64")]
pub end: u64,
}
impl ScanRowRange {
pub fn validate(self) -> Result<(), ScandalError> {
if self.start > self.end {
return Err(ScandalError::InvalidArgument {
message: "rowRange start exceeds end".into(),
});
}
Ok(())
}
}
fn is_false(value: &bool) -> bool {
!value
}
impl ScanOptions {
pub fn stream_count(&self) -> Result<usize, ScandalError> {
if let Some(ranges) = &self.row_ids
&& ranges.iter().any(|range| range.start > range.end)
{
return Err(ScandalError::InvalidArgument {
message: "a rowIds range starts after its end".into(),
});
}
if let Some(range) = self.row_range {
range.validate()?;
if self.split_token.is_some() {
return Err(ScandalError::InvalidArgument {
message: "rowRange and splitToken are mutually exclusive".into(),
});
}
}
let count = self.max_streams.unwrap_or(4);
if !(1..=256).contains(&count) {
return Err(ScandalError::InvalidArgument {
message: "maxStreams must be between 1 and 256".into(),
});
}
Ok(
if self.partitioned
&& self.limit.is_none()
&& self.split_token.is_none()
&& self.row_range.is_none()
{
count
} else {
1
},
)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TableSplit {
pub token: String,
#[serde(default, with = "super::safelong::option_u64")]
pub estimated_bytes: Option<u64>,
#[serde(default, with = "super::safelong::option_u64")]
pub estimated_rows: Option<u64>,
}
impl ScanInfo {
pub fn require_exact_filter(&self) -> Result<(), ScandalError> {
require_exact_filter(&self.filter_clauses)
}
}
pub fn require_exact_filter(clauses: &[FilterClause]) -> Result<(), ScandalError> {
if let Some(clause) = clauses
.iter()
.find(|clause| clause.support != FilterSupport::Exact)
{
return Err(ScandalError::InvalidArgument {
message: clause
.reason
.clone()
.unwrap_or_else(|| "scan predicate cannot be enforced exactly".to_owned()),
});
}
Ok(())
}
impl Scan {
pub fn new(table: TableVersion) -> Self {
Self {
table,
columns: None,
projection: None,
filter: None,
limit: None,
batch_rows: None,
collect_metrics: None,
emit: None,
row_range: None,
row_id_column: None,
row_ids: None,
}
}
pub fn collects_metrics(&self) -> bool {
self.collect_metrics.unwrap_or(false)
}
}
impl RowRange {
pub fn new(rows: Range<u64>) -> Self {
Self {
start: SafeUnsignedLong(rows.start),
end: SafeUnsignedLong(rows.end),
}
}
pub fn range(&self) -> Range<u64> {
self.start.0..self.end.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TableWriteResult {
pub resource_id: Uuid,
pub version_id: Uuid,
}
#[cfg(test)]
mod tests {
use super::{ScanOptions, ScanRowRange, TableSplit};
#[test]
fn default_options_and_lossless_split_estimates_round_trip() {
let options: ScanOptions = serde_json::from_str("{}").unwrap();
assert!(!options.partitioned);
assert!(!options.collect_metrics);
assert!(
serde_json::to_value(&options)
.unwrap()
.get("collectMetrics")
.is_none()
);
assert_eq!(options.stream_count().unwrap(), 1);
let mut parallel = ScanOptions {
partitioned: true,
max_streams: Some(3),
..Default::default()
};
assert_eq!(parallel.stream_count().unwrap(), 3);
parallel.limit = Some(1);
assert_eq!(parallel.stream_count().unwrap(), 1);
parallel.max_streams = Some(0);
assert!(parallel.stream_count().is_err());
parallel.max_streams = Some(257);
assert!(parallel.stream_count().is_err());
assert!(options.filter.is_none());
let split = TableSplit {
token: "exact".to_owned(),
estimated_rows: Some(u64::MAX),
estimated_bytes: Some(12),
};
let json = serde_json::to_value(split).unwrap();
assert_eq!(json["estimatedRows"], u64::MAX.to_string());
assert_eq!(json["estimatedBytes"], 12);
let decoded: TableSplit = serde_json::from_value(json).unwrap();
assert_eq!(decoded.estimated_rows, Some(u64::MAX));
}
#[test]
fn row_ranges_encode_losslessly_and_force_ordered_execution() {
let options = ScanOptions {
partitioned: true,
max_streams: Some(4),
row_range: Some(ScanRowRange {
start: 3,
end: u64::MAX,
}),
..Default::default()
};
let json = serde_json::to_value(&options).unwrap();
assert_eq!(
json["rowRange"],
serde_json::json!({"start": 3, "end": "18446744073709551615"})
);
let decoded: ScanOptions = serde_json::from_value(json).unwrap();
assert_eq!(decoded.row_range, options.row_range);
assert_eq!(decoded.stream_count().unwrap(), 1);
assert!(
ScanOptions {
split_token: Some("split".into()),
..options.clone()
}
.stream_count()
.is_err()
);
assert!(
ScanOptions {
row_range: Some(ScanRowRange { start: 4, end: 3 }),
..options
}
.stream_count()
.is_err()
);
}
}