1use std::ops::Range;
5
6use serde::{Deserialize, Serialize};
7use spiral_contract::safelong::SafeUnsignedLong;
8use uuid::Uuid;
9
10pub use super::api::{
11 FilterClause, FilterSupport, FragmentReceipt, PlannedSplit, PreparedWriteReceipt, RowRange,
12 Scan, ScanInfo, ScanMetricsSnapshot, ScanPlan, Split, SplitTake, StorageCounters, StorageScope,
13 TableVersion, WindowCounters,
14};
15use super::emit::Emit;
16use super::error::ScandalError;
17use super::expression::{Expression, NamedExpression};
18use super::profile::ExecutionOptions;
19
20pub const MAX_TAKE_POSITIONS: usize = 65_536;
22pub const MAX_TAKES: usize = 256;
24
25impl Default for StorageCounters {
26 fn default() -> Self {
27 Self {
28 scope: StorageScope::Complete,
29 bytes: SafeUnsignedLong(0),
30 requests: SafeUnsignedLong(0),
31 read_time_ns: SafeUnsignedLong(0),
32 }
33 }
34}
35
36impl Default for WindowCounters {
37 fn default() -> Self {
38 Self {
39 visited: SafeUnsignedLong(0),
40 pruned_statistics: SafeUnsignedLong(0),
41 }
42 }
43}
44
45impl Default for ScanMetricsSnapshot {
46 fn default() -> Self {
47 Self {
48 storage: None,
49 windows: None,
50 fragments_considered: SafeUnsignedLong(0),
51 fragments_pruned: SafeUnsignedLong(0),
52 files_considered: SafeUnsignedLong(0),
53 files_pruned: SafeUnsignedLong(0),
54 output_rows: SafeUnsignedLong(0),
55 }
56 }
57}
58
59impl ScanMetricsSnapshot {
60 pub fn add(&mut self, other: &Self) {
62 self.storage = match (self.storage.take(), other.storage.clone()) {
63 (Some(mut storage), Some(other)) => {
64 storage.bytes.0 += other.bytes.0;
65 storage.requests.0 += other.requests.0;
66 storage.read_time_ns.0 += other.read_time_ns.0;
67 Some(storage)
68 }
69 (storage, other) => storage.or(other),
70 };
71 self.windows = match (self.windows.take(), other.windows.clone()) {
72 (Some(mut windows), Some(other)) => {
73 windows.visited.0 += other.visited.0;
74 windows.pruned_statistics.0 += other.pruned_statistics.0;
75 Some(windows)
76 }
77 (windows, other) => windows.or(other),
78 };
79 self.fragments_considered.0 += other.fragments_considered.0;
80 self.fragments_pruned.0 += other.fragments_pruned.0;
81 self.files_considered.0 += other.files_considered.0;
82 self.files_pruned.0 += other.files_pruned.0;
83 self.output_rows.0 += other.output_rows.0;
84 }
85}
86
87#[derive(Clone, Debug, Default, Deserialize, Serialize)]
89#[serde(rename_all = "camelCase", deny_unknown_fields)]
90pub struct ScanOptions {
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub execution: Option<ExecutionOptions>,
94 #[serde(default, skip_serializing_if = "is_false")]
96 pub collect_metrics: bool,
97 #[serde(default)]
100 pub partitioned: bool,
101 pub max_streams: Option<usize>,
104 pub columns: Option<Vec<String>>,
106 pub projection: Option<Vec<NamedExpression>>,
108 pub filter: Option<Expression>,
110 #[serde(default, with = "super::safelong::option_u64")]
112 pub limit: Option<u64>,
113 pub batch_rows: Option<usize>,
115 pub split_token: Option<String>,
117 pub row_range: Option<ScanRowRange>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub emit: Option<Emit>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub row_id_column: Option<String>,
131 #[serde(
134 default,
135 skip_serializing_if = "Option::is_none",
136 with = "super::safelong::option_ranges"
137 )]
138 pub row_ids: Option<Vec<Range<u64>>>,
139}
140
141#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
143#[serde(rename_all = "camelCase", deny_unknown_fields)]
144pub struct ScanRowRange {
145 #[serde(with = "super::safelong::u64")]
147 pub start: u64,
148 #[serde(with = "super::safelong::u64")]
150 pub end: u64,
151}
152
153impl ScanRowRange {
154 pub fn validate(self) -> Result<(), ScandalError> {
156 if self.start > self.end {
157 return Err(ScandalError::InvalidArgument {
158 message: "rowRange start exceeds end".into(),
159 });
160 }
161 Ok(())
162 }
163}
164
165fn is_false(value: &bool) -> bool {
166 !value
167}
168
169impl ScanOptions {
170 pub fn stream_count(&self) -> Result<usize, ScandalError> {
172 if let Some(execution) = &self.execution {
173 execution.validate()?;
174 }
175 if let Some(ranges) = &self.row_ids
176 && ranges.iter().any(|range| range.start > range.end)
177 {
178 return Err(ScandalError::InvalidArgument {
179 message: "a rowIds range starts after its end".into(),
180 });
181 }
182 if let Some(range) = self.row_range {
183 range.validate()?;
184 if self.split_token.is_some() {
185 return Err(ScandalError::InvalidArgument {
186 message: "rowRange and splitToken are mutually exclusive".into(),
187 });
188 }
189 }
190 let count = self.max_streams.unwrap_or(4);
191 if !(1..=256).contains(&count) {
192 return Err(ScandalError::InvalidArgument {
193 message: "maxStreams must be between 1 and 256".into(),
194 });
195 }
196 Ok(
197 if self.partitioned
198 && self.limit.is_none()
199 && self.split_token.is_none()
200 && self.row_range.is_none()
201 {
202 count
203 } else {
204 1
205 },
206 )
207 }
208}
209
210#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
212#[serde(rename_all = "camelCase")]
213pub struct TableSplit {
214 pub token: String,
216 #[serde(default, with = "super::safelong::option_u64")]
217 pub estimated_bytes: Option<u64>,
218 #[serde(default, with = "super::safelong::option_u64")]
219 pub estimated_rows: Option<u64>,
220}
221
222impl ScanInfo {
223 pub fn require_exact_filter(&self) -> Result<(), ScandalError> {
225 require_exact_filter(&self.filter_clauses)
226 }
227}
228
229pub fn require_exact_filter(clauses: &[FilterClause]) -> Result<(), ScandalError> {
231 if let Some(clause) = clauses
232 .iter()
233 .find(|clause| clause.support != FilterSupport::Exact)
234 {
235 return Err(ScandalError::InvalidArgument {
236 message: clause
237 .reason
238 .clone()
239 .unwrap_or_else(|| "scan predicate cannot be enforced exactly".to_owned()),
240 });
241 }
242 Ok(())
243}
244
245impl Scan {
246 pub fn new(table: TableVersion) -> Self {
248 Self {
249 table,
250 columns: None,
251 projection: None,
252 filter: None,
253 limit: None,
254 batch_rows: None,
255 collect_metrics: None,
256 execution: None,
257 emit: None,
258 row_range: None,
259 row_id_column: None,
260 row_ids: None,
261 }
262 }
263
264 pub fn collects_metrics(&self) -> bool {
266 self.collect_metrics.unwrap_or(false)
267 }
268}
269
270impl RowRange {
271 pub fn new(rows: Range<u64>) -> Self {
273 Self {
274 start: SafeUnsignedLong(rows.start),
275 end: SafeUnsignedLong(rows.end),
276 }
277 }
278
279 pub fn range(&self) -> Range<u64> {
281 self.start.0..self.end.0
282 }
283}
284
285#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
287#[serde(rename_all = "camelCase")]
288pub struct TableWriteResult {
289 pub resource_id: Uuid,
291 pub version_id: Uuid,
293}
294
295#[cfg(test)]
296mod tests {
297 use super::{ScanOptions, ScanRowRange, TableSplit};
298
299 #[test]
300 fn default_options_and_lossless_split_estimates_round_trip() {
301 let options: ScanOptions = serde_json::from_str("{}").unwrap();
302 assert!(!options.partitioned);
303 assert!(!options.collect_metrics);
304 assert!(
305 serde_json::to_value(&options)
306 .unwrap()
307 .get("collectMetrics")
308 .is_none()
309 );
310 assert_eq!(options.stream_count().unwrap(), 1);
311 let mut parallel = ScanOptions {
312 partitioned: true,
313 max_streams: Some(3),
314 ..Default::default()
315 };
316 assert_eq!(parallel.stream_count().unwrap(), 3);
317 parallel.limit = Some(1);
318 assert_eq!(parallel.stream_count().unwrap(), 1);
319 parallel.max_streams = Some(0);
320 assert!(parallel.stream_count().is_err());
321 parallel.max_streams = Some(257);
322 assert!(parallel.stream_count().is_err());
323 assert!(options.filter.is_none());
324 let split = TableSplit {
325 token: "exact".to_owned(),
326 estimated_rows: Some(u64::MAX),
327 estimated_bytes: Some(12),
328 };
329 let json = serde_json::to_value(split).unwrap();
330 assert_eq!(json["estimatedRows"], u64::MAX.to_string());
331 assert_eq!(json["estimatedBytes"], 12);
332 let decoded: TableSplit = serde_json::from_value(json).unwrap();
333 assert_eq!(decoded.estimated_rows, Some(u64::MAX));
334 }
335 #[test]
336 fn row_ranges_encode_losslessly_and_force_ordered_execution() {
337 let options = ScanOptions {
338 partitioned: true,
339 max_streams: Some(4),
340 row_range: Some(ScanRowRange {
341 start: 3,
342 end: u64::MAX,
343 }),
344 ..Default::default()
345 };
346 let json = serde_json::to_value(&options).unwrap();
347 assert_eq!(
348 json["rowRange"],
349 serde_json::json!({"start": 3, "end": "18446744073709551615"})
350 );
351 let decoded: ScanOptions = serde_json::from_value(json).unwrap();
352 assert_eq!(decoded.row_range, options.row_range);
353 assert_eq!(decoded.stream_count().unwrap(), 1);
354 assert!(
355 ScanOptions {
356 split_token: Some("split".into()),
357 ..options.clone()
358 }
359 .stream_count()
360 .is_err()
361 );
362 assert!(
363 ScanOptions {
364 row_range: Some(ScanRowRange { start: 4, end: 3 }),
365 ..options
366 }
367 .stream_count()
368 .is_err()
369 );
370 }
371}