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