1use async_trait::async_trait;
16use buffa::Enumeration;
17
18use unitycatalog_common::models::staging_tables::v1::{CreateStagingTableRequest, StagingTable};
19use unitycatalog_common::models::tables::v1::{
20 Column as UcColumn, CreateTableRequest, DataSourceFormat, DeleteTableRequest, GetTableRequest,
21 Table, TableType,
22};
23use unitycatalog_common::models::temporary_credentials::v1::{
24 GenerateTemporaryPathCredentialsRequest, GenerateTemporaryTableCredentialsRequest,
25 TemporaryCredential, generate_temporary_path_credentials_request::Operation as PathOp,
26 generate_temporary_table_credentials_request::Operation as TableOp,
27 temporary_credential::Credentials,
28};
29use unitycatalog_common::models::{ResourceIdent, ResourceName, ResourceRef};
30use unitycatalog_common::store::Precondition;
31
32use unitycatalog_delta_api::authz::DeltaAction;
33use unitycatalog_delta_api::backend::{
34 BackendResult, CreateTableSpec, CredentialAccess, DeltaBackend, DeltaCapabilities,
35 ResolvedTable, SchemaRef, StagingReservation, TableRef, UpdateTableSpec, VendedCredential,
36 VendedCredentialKind,
37};
38use unitycatalog_delta_api::column::{
39 Column as CrateColumn, ColumnTypeName as CrateColumnTypeName,
40};
41use unitycatalog_delta_api::coordinator::{CommitCoordinator, ProvidesCommitCoordinator};
42use unitycatalog_delta_api::error::DeltaBackendError;
43use unitycatalog_delta_api::models::DeltaTableType;
44
45use crate::api::RequestContext;
46use crate::api::staging_tables::find_staging_table_by_location;
47use crate::api::tables::TableHandler;
48use crate::codegen::staging_tables::StagingTableHandler;
49use crate::codegen::temporary_credentials::TemporaryCredentialHandler;
50use crate::policy::{Permission, Policy, Principal};
51use crate::services::ServerHandler;
52use crate::services::location::StorageLocationUrl;
53use crate::services::object_store::validate_external_storage_location;
54use crate::store::{ResourceStore, ResourceStoreReader};
55use crate::{Error, Result};
56
57fn to_backend_err(e: Error) -> DeltaBackendError {
67 match &e {
68 Error::NotFound | Error::ResourceStore { .. } => DeltaBackendError::NotFound(e.to_string()),
69 Error::Common { source } => match source.error_code() {
72 "RESOURCE_ALREADY_EXISTS" => DeltaBackendError::AlreadyExists(e.to_string()),
73 "INVALID_PARAMETER_VALUE" => DeltaBackendError::InvalidArgument(e.to_string()),
74 "PERMISSION_DENIED" => DeltaBackendError::PermissionDenied(e.to_string()),
75 "COMMIT_VERSION_CONFLICT" => DeltaBackendError::CommitVersionConflict(e.to_string()),
76 "RESOURCE_CONFLICT" => DeltaBackendError::UpdateRequirementConflict(e.to_string()),
79 "RESOURCE_EXHAUSTED" => DeltaBackendError::ResourceExhausted(e.to_string()),
80 _ => DeltaBackendError::NotFoundGeneric(e.to_string()),
81 },
82 Error::NotAllowed => DeltaBackendError::PermissionDenied(e.to_string()),
83 Error::Unauthenticated => DeltaBackendError::Unauthenticated(e.to_string()),
84 Error::AlreadyExists => DeltaBackendError::AlreadyExists(e.to_string()),
85 Error::CommitVersionConflict(m) => DeltaBackendError::CommitVersionConflict(m.clone()),
86 Error::UpdateRequirementConflict(m) => {
87 DeltaBackendError::UpdateRequirementConflict(m.clone())
88 }
89 Error::ResourceExhausted(m) => DeltaBackendError::ResourceExhausted(m.clone()),
90 Error::InvalidArgument(m) => DeltaBackendError::InvalidArgument(m.clone()),
91 Error::InvalidIdentifier(_) | Error::MissingRecipient => {
92 DeltaBackendError::InvalidArgument(e.to_string())
93 }
94 Error::NotImplemented(w) => DeltaBackendError::NotImplemented(w),
95 _ => DeltaBackendError::Internal(e.to_string()),
96 }
97}
98
99fn parse_etag_version(etag: &str) -> Option<u64> {
105 etag.strip_prefix("etag-").and_then(|v| v.parse().ok())
106}
107
108fn uc_column_to_crate(c: UcColumn) -> CrateColumn {
114 CrateColumn {
115 name: c.name,
116 type_text: c.type_text,
117 type_json: c.type_json,
118 position: c.position,
119 type_name: CrateColumnTypeName::from(c.type_name.to_i32()),
120 comment: c.comment,
121 nullable: c.nullable,
122 partition_index: c.partition_index,
123 }
124}
125
126fn crate_column_to_uc(c: CrateColumn) -> UcColumn {
133 UcColumn {
134 name: c.name,
135 type_text: c.type_text,
136 type_json: c.type_json,
137 position: c.position,
138 type_name: (c.type_name as i32).into(),
139 comment: c.comment,
140 nullable: c.nullable,
141 partition_index: c.partition_index,
142 ..Default::default()
143 }
144}
145
146fn to_uc_table_type(t: DeltaTableType) -> TableType {
151 match t {
152 DeltaTableType::Managed => TableType::Managed,
153 DeltaTableType::External => TableType::External,
154 }
155}
156
157fn to_delta_format(f: i32) -> Option<unitycatalog_delta_api::models::DeltaDataSourceFormat> {
160 use unitycatalog_delta_api::models::DeltaDataSourceFormat as F;
161 match DataSourceFormat::from_i32(f)? {
162 DataSourceFormat::DELTA => Some(F::Delta),
163 DataSourceFormat::ICEBERG => Some(F::Iceberg),
164 DataSourceFormat::HUDI => Some(F::Hudi),
165 DataSourceFormat::PARQUET => Some(F::Parquet),
166 DataSourceFormat::CSV => Some(F::Csv),
167 DataSourceFormat::JSON => Some(F::Json),
168 DataSourceFormat::ORC => Some(F::Orc),
169 DataSourceFormat::AVRO => Some(F::Avro),
170 DataSourceFormat::TEXT => Some(F::Text),
171 DataSourceFormat::UNITY_CATALOG => Some(F::UnityCatalog),
172 DataSourceFormat::DELTASHARING => Some(F::Deltasharing),
173 DataSourceFormat::DATA_SOURCE_FORMAT_UNSPECIFIED => None,
174 }
175}
176
177fn table_to_resolved(table: Table, version: u64) -> ResolvedTable {
186 let table_type = match table.table_type.as_known() {
187 Some(TableType::MANAGED) => Some(DeltaTableType::Managed),
188 Some(TableType::EXTERNAL) => Some(DeltaTableType::External),
189 _ => None,
190 };
191 ResolvedTable {
192 table_id: table.table_id,
193 location: table.storage_location.unwrap_or_default(),
194 table_type,
195 data_source_format: to_delta_format(table.data_source_format.to_i32()),
196 columns: table.columns.into_iter().map(uc_column_to_crate).collect(),
197 properties: table.properties.into_iter().collect(),
198 created_at_ms: table.created_at,
199 updated_at_ms: table.updated_at,
200 version,
201 }
202}
203
204fn staging_to_reservation(st: StagingTable) -> StagingReservation {
205 StagingReservation {
206 table_id: st.id,
207 name: st.name,
208 location: st.staging_location,
209 created_by: st.created_by,
210 stage_committed: st.stage_committed,
211 }
212}
213
214fn to_vended_credential(creds: &TemporaryCredential, url: String) -> VendedCredential {
216 let kind = match &creds.credentials {
217 Some(Credentials::AwsTempCredentials(aws)) => VendedCredentialKind::S3 {
218 access_key_id: aws.access_key_id.clone(),
219 secret_access_key: aws.secret_access_key.clone(),
220 session_token: (!aws.session_token.is_empty()).then(|| aws.session_token.clone()),
221 },
222 Some(Credentials::AzureUserDelegationSas(az)) => VendedCredentialKind::AzureSas {
223 sas_token: az.sas_token.clone(),
224 },
225 Some(Credentials::GcpOauthToken(gcp)) => VendedCredentialKind::GcsOauth {
226 oauth_token: gcp.oauth_token.clone(),
227 },
228 Some(Credentials::R2TempCredentials(r2)) => VendedCredentialKind::S3 {
230 access_key_id: r2.access_key_id.clone(),
231 secret_access_key: r2.secret_access_key.clone(),
232 session_token: (!r2.session_token.is_empty()).then(|| r2.session_token.clone()),
233 },
234 _ => VendedCredentialKind::None,
235 };
236 VendedCredential {
237 url,
238 expiration_time_ms: creds.expiration_time,
239 kind,
240 }
241}
242
243fn to_table_op(access: CredentialAccess) -> i32 {
244 match access {
245 CredentialAccess::Read => TableOp::Read as i32,
246 CredentialAccess::ReadWrite => TableOp::ReadWrite as i32,
247 }
248}
249
250fn to_path_op(access: CredentialAccess) -> i32 {
251 match access {
252 CredentialAccess::Read => PathOp::PathRead as i32,
253 CredentialAccess::ReadWrite => PathOp::PathReadWrite as i32,
254 }
255}
256
257impl ServerHandler<RequestContext> {
262 async fn get_staging_by_id(&self, table_id: &str) -> Result<StagingTable> {
264 let uuid = uuid::Uuid::parse_str(table_id)
265 .map_err(|_| Error::invalid_argument("table_id is not a valid UUID"))?;
266 let ident = ResourceIdent::StagingTable(ResourceRef::Uuid(uuid));
267 let staging: StagingTable = self.get(&ident).await?.0.try_into()?;
268 Ok(staging)
269 }
270}
271
272#[async_trait]
273impl DeltaBackend<RequestContext> for ServerHandler<RequestContext> {
274 fn capabilities(&self) -> DeltaCapabilities {
275 DeltaCapabilities { rename: true }
277 }
278
279 async fn catalog_exists(&self, catalog: &str, _cx: &RequestContext) -> BackendResult<()> {
280 let ident = ResourceIdent::catalog(ResourceName::new([catalog]));
281 self.get(&ident)
282 .await
283 .map_err(Error::from)
284 .map_err(to_backend_err)?;
285 Ok(())
286 }
287
288 async fn resolve_table(
289 &self,
290 table: &TableRef,
291 cx: &RequestContext,
292 ) -> BackendResult<ResolvedTable> {
293 let t = TableHandler::get_table(
294 self,
295 GetTableRequest {
296 full_name: table.full_name(),
297 include_delta_metadata: None,
298 include_browse: None,
299 include_manifest_capabilities: None,
300 ..Default::default()
301 },
302 cx.clone(),
303 )
304 .await
305 .map_err(to_backend_err)?;
306 let version = match t.table_id.as_deref() {
309 Some(id) => match uuid::Uuid::parse_str(id) {
310 Ok(uuid) => {
311 let ident = ResourceIdent::Table(ResourceRef::Uuid(uuid));
312 self.get_versioned(&ident)
313 .await
314 .map(|(_, _, v)| v)
315 .map_err(Error::from)
316 .map_err(to_backend_err)?
317 }
318 Err(_) => 0,
319 },
320 None => 0,
321 };
322 Ok(table_to_resolved(t, version))
323 }
324
325 async fn authorize(&self, action: DeltaAction<'_>, cx: &RequestContext) -> BackendResult<()> {
326 match action {
327 DeltaAction::CreateTable {
328 at,
329 name,
330 table_type,
331 } => {
332 let create_action = CreateTableRequest {
335 name: name.to_string(),
336 catalog_name: at.catalog.clone(),
337 schema_name: at.schema.clone(),
338 table_type: to_uc_table_type(table_type).into(),
339 data_source_format: DataSourceFormat::Delta.into(),
340 ..Default::default()
341 };
342 self.check_required(&create_action, cx)
343 .await
344 .map_err(to_backend_err)
345 }
346 DeltaAction::WriteTable { table_id, .. } => {
347 let uuid = uuid::Uuid::parse_str(table_id).map_err(|_| {
348 DeltaBackendError::InvalidArgument("table id is not a valid UUID".into())
349 })?;
350 let ident = ResourceIdent::Table(ResourceRef::Uuid(uuid));
351 self.authorize_checked(&ident, &Permission::Write, cx)
352 .await
353 .map_err(to_backend_err)
354 }
355 DeltaAction::AdoptStaging { reservation } => {
356 let principal = match cx.recipient() {
361 Principal::User(name) => Some(name.clone()),
362 Principal::Anonymous => None,
363 };
364 if reservation.created_by.as_deref() != principal.as_deref() {
365 return Err(DeltaBackendError::PermissionDenied(
366 "caller is not the creator of the staging table".to_string(),
367 ));
368 }
369 Ok(())
370 }
371 DeltaAction::ReadTable { .. }
378 | DeltaAction::DeleteTable { .. }
379 | DeltaAction::RenameTable { .. }
380 | DeltaAction::VendTableCredential { .. }
381 | DeltaAction::VendPathCredential { .. }
382 | DeltaAction::CreateStaging { .. } => Ok(()),
383 _ => Err(DeltaBackendError::PermissionDenied(
387 "unrecognized Delta action".to_string(),
388 )),
389 }
390 }
391
392 async fn validate_external_location(
393 &self,
394 location: &str,
395 _cx: &RequestContext,
396 ) -> BackendResult<()> {
397 let parsed = StorageLocationUrl::parse(location)
398 .map_err(Error::from)
399 .map_err(to_backend_err)?;
400 validate_external_storage_location(self, &parsed)
401 .await
402 .map_err(to_backend_err)
403 }
404
405 async fn create_table_row(
406 &self,
407 spec: CreateTableSpec,
408 _cx: &RequestContext,
409 ) -> BackendResult<ResolvedTable> {
410 let adopt_ident = spec.adopt_staging.as_ref().map(|reservation| {
411 ResourceIdent::staging_table(ResourceName::new([reservation.name.as_str()]))
412 });
413 let table = Table {
414 name: spec.name,
415 catalog_name: spec.at.catalog,
416 schema_name: spec.at.schema,
417 table_type: to_uc_table_type(spec.table_type).into(),
418 data_source_format: DataSourceFormat::Delta.into(),
419 columns: spec.columns.into_iter().map(crate_column_to_uc).collect(),
420 storage_location: Some(spec.location),
421 comment: spec.comment,
422 properties: spec.properties.into_iter().collect(),
423 table_id: spec.table_id,
424 ..Default::default()
425 };
426 let (resource, _, version) = match adopt_ident {
438 Some(ident) => {
439 self.replace_atomically_versioned(&ident, table.into())
440 .await
441 }
442 None => self.create_versioned(table.into()).await,
443 }
444 .map_err(Error::from)
445 .map_err(to_backend_err)?;
446 let stored: Table = resource
447 .try_into()
448 .map_err(Error::from)
449 .map_err(to_backend_err)?;
450 Ok(table_to_resolved(stored, version))
451 }
452
453 async fn update_table_row(
454 &self,
455 spec: UpdateTableSpec,
456 _cx: &RequestContext,
457 ) -> BackendResult<ResolvedTable> {
458 let uuid = uuid::Uuid::parse_str(&spec.table_id).map_err(|_| {
459 DeltaBackendError::InvalidArgument("table id is not a valid UUID".into())
460 })?;
461 let ident = ResourceIdent::Table(ResourceRef::Uuid(uuid));
462 let (resource, _, version) = self
465 .get_versioned(&ident)
466 .await
467 .map_err(Error::from)
468 .map_err(to_backend_err)?;
469 let mut table: Table = resource
470 .try_into()
471 .map_err(Error::from)
472 .map_err(to_backend_err)?;
473 let precondition = match &spec.expected_etag {
479 Some(expected) => {
480 let expected_version = parse_etag_version(expected).ok_or_else(|| {
481 DeltaBackendError::UpdateRequirementConflict(
482 "assert-etag failed: table has been modified".into(),
483 )
484 })?;
485 if expected_version != version {
486 return Err(DeltaBackendError::UpdateRequirementConflict(
487 "assert-etag failed: table has been modified".into(),
488 ));
489 }
490 Precondition::Version(expected_version)
491 }
492 None => Precondition::Any,
493 };
494 table.columns = spec.columns.into_iter().map(crate_column_to_uc).collect();
495 table.properties = spec.properties.into_iter().collect();
496 if let Some(comment) = spec.comment {
499 table.comment = Some(comment);
500 }
501 let (updated_resource, _, new_version) = self
502 .update_checked(&ident, table.into(), precondition)
503 .await
504 .map_err(Error::from)
505 .map_err(to_backend_err)?;
506 let updated: Table = updated_resource
507 .try_into()
508 .map_err(Error::from)
509 .map_err(to_backend_err)?;
510 Ok(table_to_resolved(updated, new_version))
511 }
512
513 async fn delete_table(&self, table: &TableRef, cx: &RequestContext) -> BackendResult<()> {
514 TableHandler::delete_table(
515 self,
516 DeleteTableRequest {
517 full_name: table.full_name(),
518 ..Default::default()
519 },
520 cx.clone(),
521 )
522 .await
523 .map_err(to_backend_err)
524 }
525
526 async fn rename_table(
527 &self,
528 from: &TableRef,
529 to_name: &str,
530 _cx: &RequestContext,
531 ) -> BackendResult<()> {
532 let from_ident = ResourceIdent::table(ResourceName::new([
537 from.catalog.as_str(),
538 from.schema.as_str(),
539 from.table.as_str(),
540 ]));
541 let new_name = ResourceName::new([from.catalog.as_str(), from.schema.as_str(), to_name]);
542 self.rename(&from_ident, &new_name, Precondition::Any)
543 .await
544 .map_err(Error::from)
545 .map_err(to_backend_err)?;
546 Ok(())
547 }
548
549 async fn allocate_staging(
550 &self,
551 at: &SchemaRef,
552 name: &str,
553 cx: &RequestContext,
554 ) -> BackendResult<StagingReservation> {
555 let staging = StagingTableHandler::create_staging_table(
556 self,
557 CreateStagingTableRequest {
558 name: name.to_string(),
559 catalog_name: at.catalog.clone(),
560 schema_name: at.schema.clone(),
561 ..Default::default()
562 },
563 cx.clone(),
564 )
565 .await
566 .map_err(to_backend_err)?;
567 Ok(staging_to_reservation(staging))
568 }
569
570 async fn resolve_staging_by_location(
571 &self,
572 location: &str,
573 _cx: &RequestContext,
574 ) -> BackendResult<StagingReservation> {
575 let staging = find_staging_table_by_location(self, location)
576 .await
577 .map_err(to_backend_err)?;
578 Ok(staging_to_reservation(staging))
579 }
580
581 async fn resolve_staging_by_id(
582 &self,
583 table_id: &str,
584 _cx: &RequestContext,
585 ) -> BackendResult<StagingReservation> {
586 let staging = self
587 .get_staging_by_id(table_id)
588 .await
589 .map_err(to_backend_err)?;
590 Ok(staging_to_reservation(staging))
591 }
592
593 async fn vend_table_credential(
594 &self,
595 table_id: &str,
596 access: CredentialAccess,
597 cx: &RequestContext,
598 ) -> BackendResult<VendedCredential> {
599 let creds = self
600 .generate_temporary_table_credentials(
601 GenerateTemporaryTableCredentialsRequest {
602 table_id: table_id.to_string(),
603 operation: to_table_op(access).into(),
604 ..Default::default()
605 },
606 cx.clone(),
607 )
608 .await
609 .map_err(to_backend_err)?;
610 let url = creds.url.clone();
611 Ok(to_vended_credential(&creds, url))
612 }
613
614 async fn vend_path_credential(
615 &self,
616 location: &str,
617 access: CredentialAccess,
618 cx: &RequestContext,
619 ) -> BackendResult<VendedCredential> {
620 let creds = self
621 .generate_temporary_path_credentials(
622 GenerateTemporaryPathCredentialsRequest {
623 url: location.to_string(),
624 operation: to_path_op(access).into(),
625 dry_run: Some(false),
626 ..Default::default()
627 },
628 cx.clone(),
629 )
630 .await
631 .map_err(to_backend_err)?;
632 let url = creds.url.clone();
633 Ok(to_vended_credential(&creds, url))
634 }
635
636 fn commit_coordinator(&self) -> &dyn CommitCoordinator {
637 ProvidesCommitCoordinator::commit_coordinator(self)
638 }
639}