#[path = "../api/scandal/contract.rs"]
pub mod api;
pub mod blob;
pub mod emit;
pub mod error;
pub mod expression;
pub mod functions;
pub mod ordered;
pub mod read_cache;
mod safelong;
mod storage;
pub mod types;
use std::borrow::Cow;
use std::ops::Range;
use arrow_array::RecordBatch;
use arrow_schema::{Schema, SchemaRef};
use async_trait::async_trait;
use bytes::Bytes;
use futures::stream::BoxStream;
use serde::{Deserialize, Serialize};
pub use storage::{GcsSession, S3Session, StorageConfig};
use url::Url;
use uuid::Uuid;
pub use vortex_array as vortex;
use self::blob::{Blob, BlobPart, BlobStat, BlobUpload};
pub use self::error::ScandalError;
use self::expression::NamedExpression;
use self::functions::FunctionInfo;
pub use self::types::{
FilterClause, FragmentReceipt, PlannedSplit, PreparedWriteReceipt, RowRange, Scan, ScanInfo,
ScanMetricsSnapshot, ScanPlan, Split, SplitTake, TableVersion,
};
pub mod wire;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ScandalConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spfs_endpoint: Option<Url>,
#[serde(default)]
pub local_files: bool,
#[serde(default)]
pub provider_roots: bool,
#[serde(default)]
pub refuse_custom_endpoints: bool,
}
pub use self::api::{
AppendRowsRequest, ArrowSchema, AttachColumnsRequest, AttachedColumns, ChangePlan,
DeleteRowsRequest, EditBase, GenesisBase, PatchRowsRequest, PlanChangesRequest,
ReadLegacyTableRequest, RowIdRange, SchemaChange, SortColumn, StorageBinding,
StorageCredential, SuccessorBase, TableBinding, WriteBase, WriteProperties,
};
impl StorageBinding {
pub fn resource(resource: Uuid) -> Self {
Self::new(format!("spiral://{resource}/"))
}
pub fn new(root_uri: impl Into<String>) -> Self {
Self {
root_uri: root_uri.into(),
root_alias: None,
credential: None,
}
}
pub fn credential(&self) -> Option<&str> {
self.credential
.as_ref()
.map(|credential| credential.0.as_str())
}
}
impl TableBinding {
pub fn binding(&self) -> Cow<'_, StorageBinding> {
self.storage.as_ref().map_or_else(
|| Cow::Owned(StorageBinding::resource(self.table_id)),
Cow::Borrowed,
)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ResolvedUri {
pub root_uri: Url,
pub storage: StorageBinding,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(
tag = "type",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum ResourceAccess {
Read,
Attempt { attempt_id: Uuid },
Publication,
}
#[async_trait]
pub trait UriResolver: Send + Sync {
async fn resolve(&self, uri: &Url) -> Result<ResolvedUri, ScandalError>;
async fn resolve_for(
&self,
uri: &Url,
access: ResourceAccess,
) -> Result<ResolvedUri, ScandalError> {
if access != ResourceAccess::Read {
return Err(ScandalError::PermissionDenied {
operation: "write".into(),
resource: uri.to_string(),
});
}
self.resolve(uri).await
}
}
impl ArrowSchema {
pub fn from_schema(schema: &Schema) -> Result<Self, ScandalError> {
serde_json::to_value(schema)
.map(Self)
.map_err(|error| ScandalError::Internal {
message: error.to_string(),
})
}
pub fn to_schema(&self) -> Result<Schema, ScandalError> {
serde_json::from_value(self.0.clone()).map_err(|error| ScandalError::InvalidArgument {
message: format!("invalid Arrow schema: {error}"),
})
}
}
impl WriteBase {
pub fn genesis(schema: &Schema) -> Result<Self, ScandalError> {
Ok(Self::Genesis(GenesisBase {
schema: ArrowSchema::from_schema(schema)?,
}))
}
pub fn append(version_id: Uuid) -> Self {
Self::Append(SuccessorBase { version_id })
}
pub fn overwrite(version_id: Uuid) -> Self {
Self::Overwrite(SuccessorBase { version_id })
}
pub fn base_version(&self) -> Result<Option<Uuid>, ScandalError> {
match self {
Self::Genesis(_) => Ok(None),
Self::Append(base) | Self::Overwrite(base) => Ok(Some(base.version_id)),
Self::Delete(base) => Ok(Some(base.version_id)),
Self::Edit(base) => Ok(Some(base.version_id)),
Self::Unknown { tag, .. } => Err(ScandalError::InvalidArgument {
message: format!("unknown write base `{tag}`"),
}),
}
}
}
#[derive(Clone, Debug)]
pub struct SplitBatch {
pub batch: RecordBatch,
pub next_row: u64,
pub metrics: Option<ScanMetricsSnapshot>,
}
pub type SplitStream = BoxStream<'static, Result<SplitBatch, ScandalError>>;
pub type RecordBatchStream = BoxStream<'static, Result<RecordBatch, ScandalError>>;
pub type ByteStream = BoxStream<'static, Result<Bytes, ScandalError>>;
pub struct SplitRead {
pub schema: SchemaRef,
pub batches: SplitStream,
}
pub struct Evaluation {
pub schema: SchemaRef,
pub batches: RecordBatchStream,
}
#[async_trait]
pub trait Scandal: Send + Sync {
fn resolves_resources(&self) -> bool {
false
}
async fn read_legacy_table(
&self,
_request: &ReadLegacyTableRequest,
) -> Result<ByteStream, ScandalError> {
Err(ScandalError::InvalidArgument {
message: "Legacy reads are unavailable on this implementation.".to_owned(),
})
}
async fn list_functions(&self) -> Result<Vec<FunctionInfo>, ScandalError>;
async fn plan_scan(
&self,
scan: &Scan,
storage: &StorageBinding,
) -> Result<ScanPlan, ScandalError>;
async fn read_scan(
&self,
scan: &Scan,
split: &Split,
storage: &StorageBinding,
) -> Result<SplitRead, ScandalError>;
async fn take_rows(
&self,
scan: &Scan,
takes: &[SplitTake],
storage: &StorageBinding,
) -> Result<SplitRead, ScandalError>;
async fn read_blob(
&self,
blob: &Blob,
range: Option<Range<u64>>,
storage: &StorageBinding,
) -> Result<ByteStream, ScandalError>;
async fn stat_blob(
&self,
blob: &Blob,
storage: &StorageBinding,
) -> Result<BlobStat, ScandalError>;
async fn write_blob(
&self,
table: &TableBinding,
media_type: &str,
max_bytes: u64,
input: ByteStream,
) -> Result<Blob, ScandalError>;
async fn pack_blobs(
&self,
table: &TableBinding,
attempt: Option<Uuid>,
schema: SchemaRef,
input: RecordBatchStream,
) -> Result<Vec<RecordBatch>, ScandalError>;
async fn start_blob_upload(
&self,
table: &TableBinding,
attempt: Uuid,
media_type: &str,
part_bytes: Option<u64>,
) -> Result<BlobUpload, ScandalError>;
async fn upload_blob_part(
&self,
upload: &BlobUpload,
part: u32,
input: ByteStream,
storage: &StorageBinding,
) -> Result<BlobPart, ScandalError>;
async fn complete_blob_upload(
&self,
upload: &BlobUpload,
parts: &[BlobPart],
storage: &StorageBinding,
) -> Result<Blob, ScandalError>;
async fn abort_blob_upload(
&self,
upload: &BlobUpload,
storage: &StorageBinding,
) -> Result<(), ScandalError>;
async fn append_rows(
&self,
table: &TableBinding,
base: Option<Uuid>,
attempt: Option<Uuid>,
schema: SchemaRef,
input: RecordBatchStream,
) -> Result<Vec<FragmentReceipt>, ScandalError>;
async fn append_partition(
&self,
request: &AppendRowsRequest,
schema: SchemaRef,
input: RecordBatchStream,
) -> Result<Vec<FragmentReceipt>, ScandalError> {
if request
.properties
.as_ref()
.is_some_and(|properties| !properties.ordering.is_empty())
{
return Err(ScandalError::InvalidArgument {
message: "this backend does not support ordered writes".into(),
});
}
self.append_rows(&request.table, request.base, request.attempt, schema, input)
.await
}
async fn patch_rows(
&self,
_request: &PatchRowsRequest,
_schema: SchemaRef,
_input: RecordBatchStream,
) -> Result<Vec<FragmentReceipt>, ScandalError> {
Err(unsupported("patching rows"))
}
async fn delete_rows(
&self,
_request: &DeleteRowsRequest,
_schema: SchemaRef,
_input: RecordBatchStream,
) -> Result<Vec<FragmentReceipt>, ScandalError> {
Err(unsupported("deleting rows by row ID"))
}
async fn attach_columns(
&self,
_request: &AttachColumnsRequest,
_schema: SchemaRef,
_input: RecordBatchStream,
) -> Result<AttachedColumns, ScandalError> {
Err(unsupported("attaching columns"))
}
async fn plan_changes(
&self,
_request: &PlanChangesRequest,
) -> Result<ChangePlan, ScandalError> {
Err(unsupported("change plans"))
}
async fn finalize_write(
&self,
table: &TableBinding,
base: &WriteBase,
receipts: &[FragmentReceipt],
) -> Result<PreparedWriteReceipt, ScandalError>;
async fn bind_projection(
&self,
input: &Schema,
projection: &[NamedExpression],
) -> Result<SchemaRef, ScandalError>;
async fn evaluate(
&self,
projection: &[NamedExpression],
input_schema: SchemaRef,
input: RecordBatchStream,
tables: &[TableBinding],
) -> Result<Evaluation, ScandalError>;
}
fn unsupported(operation: &str) -> ScandalError {
ScandalError::InvalidArgument {
message: format!("{operation} is unavailable on this implementation"),
}
}