use std::sync::Arc;
use crate::WyrdClient;
use arrow_schema::SchemaRef;
use serde::{Deserialize, Serialize};
use wyrd_spec::ids::CardUid;
use wyrd_spec::vala::api::{
BifrostTableDescription, CompactionTypeWire, FieldSpec, PhysicalLayoutWire,
RegisterTableRequest, RegisterTableResponse,
};
use wyrd_spec::vala::ids::RunId;
use crate::bifrost::query::BifrostClientError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedTable {
pub table_uid: String,
pub fingerprint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(into = "TableConfigWire", try_from = "TableConfigWire")]
pub struct TableConfig {
namespace: String,
name: String,
user_schema: SchemaRef,
physical_layout: Option<PhysicalLayoutWire>,
compaction_target_file_size_bytes: Option<u64>,
compaction_type: Option<CompactionTypeWire>,
resolved: Option<ResolvedTable>,
}
impl TableConfig {
pub fn from_arrow(fqn: &str, schema: SchemaRef) -> Result<Self, BifrostClientError> {
let (namespace, name) = split_fqn(fqn)?;
reject_reserved_columns(&schema)?;
wyrd_types::arrow_schema_to_fieldspec(&schema)?;
Ok(Self {
namespace,
name,
user_schema: schema,
physical_layout: None,
compaction_target_file_size_bytes: None,
compaction_type: None,
resolved: None,
})
}
pub fn from_json_schema(
fqn: &str,
schema: &serde_json::Value,
) -> Result<Self, BifrostClientError> {
let arrow = wyrd_types::json_schema_to_arrow(schema)?;
Self::from_arrow(fqn, Arc::new(arrow))
}
pub fn from_model<T: schemars::JsonSchema>(fqn: &str) -> Result<Self, BifrostClientError> {
let schema = serde_json::to_value(schemars::schema_for!(T)).map_err(|error| {
wyrd_queue::WyrdQueueError::SchemaParse(format!(
"model schema is not representable as JSON: {error}"
))
})?;
Self::from_json_schema(fqn, &schema)
}
pub async fn describe_with(client: &WyrdClient, fqn: &str) -> Result<Self, BifrostClientError> {
let (namespace, name) = split_fqn(fqn)?;
let description = crate::bifrost::query::QueryClient::new(client)
.describe_table(&namespace, &name)
.await?;
Self::from_description(&description)
}
pub async fn describe(fqn: &str) -> Result<Self, BifrostClientError> {
let client = WyrdClient::from_global().map_err(BifrostClientError::from)?;
Self::describe_with(&client, fqn).await
}
pub(crate) fn from_description(
description: &BifrostTableDescription,
) -> Result<Self, BifrostClientError> {
let user_schema = wyrd_types::fieldspec_to_arrow(&description.user_fields)?;
Ok(Self {
namespace: description.entry.namespace.clone(),
name: description.entry.name.clone(),
user_schema: Arc::new(user_schema),
physical_layout: Some(description.physical_layout.clone()),
compaction_target_file_size_bytes: description.compaction_target_file_size_bytes,
compaction_type: description.compaction_type,
resolved: Some(ResolvedTable {
table_uid: description.entry.table_uid.clone(),
fingerprint: description.entry.fingerprint.clone(),
}),
})
}
#[must_use]
pub fn with_layout(mut self, layout: PhysicalLayoutWire) -> Self {
self.physical_layout = Some(layout);
self
}
#[must_use]
pub fn with_compaction_target_file_size_bytes(mut self, bytes: u64) -> Self {
self.compaction_target_file_size_bytes = Some(bytes);
self
}
#[must_use]
pub fn compaction_target_file_size_bytes(&self) -> Option<u64> {
self.compaction_target_file_size_bytes
}
#[must_use]
pub fn with_compaction_type(mut self, compaction_type: CompactionTypeWire) -> Self {
self.compaction_type = Some(compaction_type);
self
}
#[must_use]
pub fn compaction_type(&self) -> Option<CompactionTypeWire> {
self.compaction_type
}
#[must_use]
pub fn fqn(&self) -> String {
format!("{}.{}", self.namespace, self.name)
}
#[must_use]
pub fn namespace(&self) -> &str {
&self.namespace
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn user_schema(&self) -> &SchemaRef {
&self.user_schema
}
#[must_use]
pub fn resolved(&self) -> Option<&ResolvedTable> {
self.resolved.as_ref()
}
pub(crate) fn register_request(&self) -> RegisterTableRequest {
RegisterTableRequest {
namespace: self.namespace.clone(),
name: self.name.clone(),
fields: wyrd_types::arrow_schema_to_fieldspec(&self.user_schema)
.expect("every TableConfig constructor admits only a wire-representable schema"),
physical_layout: self.physical_layout.clone(),
compaction_target_file_size_bytes: self.compaction_target_file_size_bytes,
compaction_type: self.compaction_type,
}
}
pub(crate) fn resolve(&mut self, response: &RegisterTableResponse) {
self.resolved = Some(ResolvedTable {
table_uid: response.table_uid.clone(),
fingerprint: response.fingerprint.clone(),
});
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TableConfigWire {
namespace: String,
name: String,
fields: Vec<FieldSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
physical_layout: Option<PhysicalLayoutWire>,
#[serde(default, skip_serializing_if = "Option::is_none")]
compaction_target_file_size_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
compaction_type: Option<CompactionTypeWire>,
#[serde(default, skip_serializing_if = "Option::is_none")]
resolved: Option<ResolvedTable>,
}
impl From<TableConfig> for TableConfigWire {
fn from(config: TableConfig) -> Self {
Self {
namespace: config.namespace,
name: config.name,
fields: wyrd_types::arrow_schema_to_fieldspec(&config.user_schema)
.expect("every TableConfig constructor admits only a wire-representable schema"),
physical_layout: config.physical_layout,
compaction_target_file_size_bytes: config.compaction_target_file_size_bytes,
compaction_type: config.compaction_type,
resolved: config.resolved,
}
}
}
impl TryFrom<TableConfigWire> for TableConfig {
type Error = BifrostClientError;
fn try_from(wire: TableConfigWire) -> Result<Self, Self::Error> {
Ok(Self {
namespace: wire.namespace,
name: wire.name,
user_schema: Arc::new(wyrd_types::fieldspec_to_arrow(&wire.fields)?),
physical_layout: wire.physical_layout,
compaction_target_file_size_bytes: wire.compaction_target_file_size_bytes,
compaction_type: wire.compaction_type,
resolved: wire.resolved,
})
}
}
#[derive(Debug, Clone)]
pub struct WriterTable {
fqn: Arc<str>,
schema: SchemaRef,
}
impl WriterTable {
#[must_use]
pub(crate) fn new(fqn: &str, schema: SchemaRef) -> Self {
Self {
fqn: Arc::from(fqn),
schema,
}
}
#[must_use]
pub fn fqn(&self) -> &str {
&self.fqn
}
#[must_use]
pub fn user_schema(&self) -> &SchemaRef {
&self.schema
}
}
#[derive(Debug, Clone, Default)]
pub struct Correlation {
pub card_uid: Option<CardUid>,
pub run_id: Option<RunId>,
}
pub(crate) fn split_fqn(fqn: &str) -> Result<(String, String), BifrostClientError> {
let (namespace, name) = fqn.rsplit_once('.').ok_or_else(|| {
wyrd_queue::WyrdQueueError::SchemaParse(format!(
"table `{fqn}` is not `<namespace>.<name>`"
))
})?;
if namespace.is_empty() || name.is_empty() {
return Err(wyrd_queue::WyrdQueueError::SchemaParse(format!(
"table `{fqn}` is not `<namespace>.<name>`"
))
.into());
}
Ok((namespace.to_owned(), name.to_owned()))
}
fn reject_reserved_columns(schema: &SchemaRef) -> Result<(), BifrostClientError> {
for field in schema.fields() {
let name = field.name().as_str();
if wyrd_queue::is_reserved_column(name) {
return Err(wyrd_queue::WyrdQueueError::ReservedColumn(format!(
"column `{name}` is server-owned and must not be declared"
))
.into());
}
}
Ok(())
}