1#[path = "../api/scandal/contract.rs"]
7pub mod api;
8pub mod blob;
9pub mod emit;
10pub mod error;
11pub mod expression;
12pub mod functions;
13pub mod ordered;
14pub mod plan;
15pub mod profile;
16pub mod read_cache;
17mod safelong;
18mod storage;
19pub mod types;
20pub mod vector;
21
22use std::borrow::Cow;
23use std::ops::Range;
24use std::time::{Duration, SystemTime, UNIX_EPOCH};
25
26use arrow_array::RecordBatch;
27use arrow_schema::{Schema, SchemaRef};
28use async_trait::async_trait;
29use bytes::Bytes;
30use futures::stream::BoxStream;
31use serde::{Deserialize, Serialize};
32pub use storage::{GcsSession, S3Session, StorageConfig};
33use url::Url;
34use uuid::Uuid;
35pub use vortex_array as vortex;
36
37use self::blob::{Blob, BlobPart, BlobStat, BlobUpload};
38pub use self::error::ScandalError;
39use self::expression::NamedExpression;
40use self::functions::FunctionInfo;
41pub use self::types::{
42 FilterClause, FragmentReceipt, PlannedSplit, PreparedWriteReceipt, RowRange, Scan, ScanInfo,
43 ScanMetricsSnapshot, ScanPlan, Split, SplitTake, TableVersion,
44};
45
46pub mod wire;
47
48#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51pub struct ScandalConfig {
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub spfs_endpoint: Option<Url>,
56 #[serde(default)]
59 pub local_files: bool,
60 #[serde(default)]
62 pub provider_roots: bool,
63 #[serde(default)]
67 pub refuse_custom_endpoints: bool,
68}
69
70pub use self::api::{
71 AlignedBase, AlignedFragment, AppendRowsRequest, ArrowSchema, AttachColumnsRequest,
72 AttachedColumns, ChangePlan, DeleteRowsRequest, DirectRead, EditBase, GenesisBase,
73 PatchRowsRequest, PlanChangesRequest, ReadLegacyTableRequest, RowIdRange, SchemaChange,
74 SearchVectorsRequest, SortColumn, StorageBinding, StorageCredential, SuccessorBase,
75 TableBinding, WriteAlignedRequest, WriteBase, WriteProperties,
76};
77
78impl StorageBinding {
79 pub fn resource(resource: Uuid) -> Self {
81 Self::new(format!("spiral://{resource}/"))
82 }
83
84 pub fn new(root_uri: impl Into<String>) -> Self {
86 Self {
87 root_uri: root_uri.into(),
88 root_alias: None,
89 credential: None,
90 endpoint: None,
91 expires_at: None,
92 direct_read: None,
93 }
94 }
95
96 pub fn remaining_lifetime(&self) -> Option<Duration> {
99 let expires_at = Duration::from_secs(self.expires_at?.0);
100 let now = SystemTime::now()
101 .duration_since(UNIX_EPOCH)
102 .unwrap_or_default();
103 Some(expires_at.saturating_sub(now))
104 }
105
106 pub fn credential(&self) -> Option<&str> {
110 self.credential
111 .as_ref()
112 .map(|credential| credential.0.as_str())
113 }
114}
115
116impl TableBinding {
117 pub fn binding(&self) -> Cow<'_, StorageBinding> {
119 self.storage.as_ref().map_or_else(
120 || Cow::Owned(StorageBinding::resource(self.table_id)),
121 Cow::Borrowed,
122 )
123 }
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize)]
129pub struct ResolvedUri {
130 pub root_uri: Url,
132 pub storage: StorageBinding,
134}
135
136#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
138#[serde(
139 tag = "type",
140 rename_all = "camelCase",
141 rename_all_fields = "camelCase"
142)]
143pub enum ResourceAccess {
144 Read,
146 Attempt { attempt_id: Uuid },
148 Publication,
150}
151
152#[async_trait]
157pub trait UriResolver: Send + Sync {
158 async fn resolve(&self, uri: &Url) -> Result<ResolvedUri, ScandalError>;
161
162 async fn resolve_for(
164 &self,
165 uri: &Url,
166 access: ResourceAccess,
167 ) -> Result<ResolvedUri, ScandalError> {
168 if access != ResourceAccess::Read {
169 return Err(ScandalError::PermissionDenied {
170 operation: "write".into(),
171 resource: uri.to_string(),
172 });
173 }
174 self.resolve(uri).await
175 }
176}
177
178impl ArrowSchema {
179 pub fn from_schema(schema: &Schema) -> Result<Self, ScandalError> {
181 serde_json::to_value(schema)
182 .map(Self)
183 .map_err(|error| ScandalError::Internal {
184 message: error.to_string(),
185 })
186 }
187
188 pub fn to_schema(&self) -> Result<Schema, ScandalError> {
190 serde_json::from_value(self.0.clone()).map_err(|error| ScandalError::InvalidArgument {
191 message: format!("invalid Arrow schema: {error}"),
192 })
193 }
194}
195
196impl WriteBase {
197 pub fn genesis(schema: &Schema) -> Result<Self, ScandalError> {
199 Ok(Self::Genesis(GenesisBase {
200 schema: ArrowSchema::from_schema(schema)?,
201 }))
202 }
203
204 pub fn append(version_id: Uuid) -> Self {
206 Self::Append(SuccessorBase { version_id })
207 }
208
209 pub fn overwrite(version_id: Uuid) -> Self {
211 Self::Overwrite(SuccessorBase { version_id })
212 }
213
214 pub fn base_version(&self) -> Result<Option<Uuid>, ScandalError> {
216 match self {
217 Self::Genesis(_) => Ok(None),
218 Self::Append(base) | Self::Overwrite(base) => Ok(Some(base.version_id)),
219 Self::Delete(base) => Ok(Some(base.version_id)),
220 Self::Edit(base) => Ok(Some(base.version_id)),
221 Self::Aligned(base) => Ok(base.version_id),
222 Self::Unknown { tag, .. } => Err(ScandalError::InvalidArgument {
223 message: format!("unknown write base `{tag}`"),
224 }),
225 }
226 }
227}
228
229#[derive(Clone, Debug)]
231pub struct SplitBatch {
232 pub batch: RecordBatch,
233 pub next_row: u64,
236 pub metrics: Option<ScanMetricsSnapshot>,
240}
241
242pub type SplitStream = BoxStream<'static, Result<SplitBatch, ScandalError>>;
243pub type RecordBatchStream = BoxStream<'static, Result<RecordBatch, ScandalError>>;
244pub type ByteStream = BoxStream<'static, Result<Bytes, ScandalError>>;
245
246pub struct SplitRead {
248 pub schema: SchemaRef,
249 pub batches: SplitStream,
250}
251
252pub struct Evaluation {
254 pub schema: SchemaRef,
255 pub batches: RecordBatchStream,
256}
257
258#[async_trait]
262pub trait Scandal: Send + Sync {
263 fn resolves_resources(&self) -> bool {
265 false
266 }
267
268 async fn read_legacy_table(
270 &self,
271 _request: &ReadLegacyTableRequest,
272 ) -> Result<ByteStream, ScandalError> {
273 Err(ScandalError::InvalidArgument {
274 message: "Legacy reads are unavailable on this implementation.".to_owned(),
275 })
276 }
277
278 async fn list_functions(&self) -> Result<Vec<FunctionInfo>, ScandalError>;
280
281 async fn search_vectors(
284 &self,
285 _request: &SearchVectorsRequest,
286 ) -> Result<Evaluation, ScandalError> {
287 Err(unsupported("vector search"))
288 }
289
290 async fn plan_scan(
293 &self,
294 scan: &Scan,
295 storage: &StorageBinding,
296 ) -> Result<ScanPlan, ScandalError>;
297
298 async fn read_scan(
300 &self,
301 scan: &Scan,
302 split: &Split,
303 storage: &StorageBinding,
304 ) -> Result<SplitRead, ScandalError>;
305
306 async fn take_rows(
312 &self,
313 scan: &Scan,
314 takes: &[SplitTake],
315 storage: &StorageBinding,
316 ) -> Result<SplitRead, ScandalError>;
317
318 async fn read_blob(
320 &self,
321 blob: &Blob,
322 range: Option<Range<u64>>,
323 storage: &StorageBinding,
324 ) -> Result<ByteStream, ScandalError>;
325
326 async fn stat_blob(
328 &self,
329 blob: &Blob,
330 storage: &StorageBinding,
331 ) -> Result<BlobStat, ScandalError>;
332
333 async fn write_blob(
335 &self,
336 table: &TableBinding,
337 media_type: &str,
338 max_bytes: u64,
339 input: ByteStream,
340 ) -> Result<Blob, ScandalError>;
341
342 async fn pack_blobs(
347 &self,
348 table: &TableBinding,
349 attempt: Option<Uuid>,
350 schema: SchemaRef,
351 input: RecordBatchStream,
352 ) -> Result<Vec<RecordBatch>, ScandalError>;
353
354 async fn start_blob_upload(
358 &self,
359 table: &TableBinding,
360 attempt: Uuid,
361 media_type: &str,
362 part_bytes: Option<u64>,
363 ) -> Result<BlobUpload, ScandalError>;
364
365 async fn upload_blob_part(
368 &self,
369 upload: &BlobUpload,
370 part: u32,
371 input: ByteStream,
372 storage: &StorageBinding,
373 ) -> Result<BlobPart, ScandalError>;
374
375 async fn complete_blob_upload(
378 &self,
379 upload: &BlobUpload,
380 parts: &[BlobPart],
381 storage: &StorageBinding,
382 ) -> Result<Blob, ScandalError>;
383
384 async fn abort_blob_upload(
386 &self,
387 upload: &BlobUpload,
388 storage: &StorageBinding,
389 ) -> Result<(), ScandalError>;
390
391 async fn append_rows(
397 &self,
398 table: &TableBinding,
399 base: Option<Uuid>,
400 attempt: Option<Uuid>,
401 schema: SchemaRef,
402 input: RecordBatchStream,
403 ) -> Result<Vec<FragmentReceipt>, ScandalError>;
404
405 async fn append_partition(
407 &self,
408 request: &AppendRowsRequest,
409 schema: SchemaRef,
410 input: RecordBatchStream,
411 ) -> Result<Vec<FragmentReceipt>, ScandalError> {
412 if request
413 .properties
414 .as_ref()
415 .is_some_and(|properties| !properties.ordering.is_empty())
416 {
417 return Err(ScandalError::InvalidArgument {
418 message: "this backend does not support ordered writes".into(),
419 });
420 }
421 self.append_rows(&request.table, request.base, request.attempt, schema, input)
422 .await
423 }
424
425 async fn patch_rows(
428 &self,
429 _request: &PatchRowsRequest,
430 _schema: SchemaRef,
431 _input: RecordBatchStream,
432 ) -> Result<Vec<FragmentReceipt>, ScandalError> {
433 Err(unsupported("patching rows"))
434 }
435
436 async fn delete_rows(
439 &self,
440 _request: &DeleteRowsRequest,
441 _schema: SchemaRef,
442 _input: RecordBatchStream,
443 ) -> Result<Vec<FragmentReceipt>, ScandalError> {
444 Err(unsupported("deleting rows by row ID"))
445 }
446
447 async fn attach_columns(
451 &self,
452 _request: &AttachColumnsRequest,
453 _schema: SchemaRef,
454 _input: RecordBatchStream,
455 ) -> Result<AttachedColumns, ScandalError> {
456 Err(unsupported("attaching columns"))
457 }
458
459 async fn write_aligned(
462 &self,
463 _request: &WriteAlignedRequest,
464 _schema: SchemaRef,
465 _input: RecordBatchStream,
466 ) -> Result<AlignedFragment, ScandalError> {
467 Err(unsupported("aligned writes"))
468 }
469
470 async fn plan_changes(
472 &self,
473 _request: &PlanChangesRequest,
474 ) -> Result<ChangePlan, ScandalError> {
475 Err(unsupported("change plans"))
476 }
477
478 async fn finalize_write(
480 &self,
481 table: &TableBinding,
482 base: &WriteBase,
483 receipts: &[FragmentReceipt],
484 ) -> Result<PreparedWriteReceipt, ScandalError>;
485
486 async fn bind_projection(
488 &self,
489 input: &Schema,
490 projection: &[NamedExpression],
491 ) -> Result<SchemaRef, ScandalError>;
492
493 async fn evaluate(
496 &self,
497 projection: &[NamedExpression],
498 input_schema: SchemaRef,
499 input: RecordBatchStream,
500 tables: &[TableBinding],
501 ) -> Result<Evaluation, ScandalError>;
502}
503
504fn unsupported(operation: &str) -> ScandalError {
506 ScandalError::InvalidArgument {
507 message: format!("{operation} is unavailable on this implementation"),
508 }
509}