Skip to main content

unitycatalog_delta_api/
backend.rs

1//! The `DeltaBackend` port: the narrow backend interface the Delta business logic
2//! calls.
3//!
4//! All Delta *semantics* — the managed-table contract, the `updateTable` action
5//! dispatcher, `loadTable` list construction, credential→config mapping, and
6//! commit arbitration — live in [`crate::handler`] and are expressed in terms of
7//! this trait. A server implements only these operations over its own storage,
8//! credential vending, and authorization; it never re-implements any Delta
9//! semantics.
10//!
11//! The trait is generic over a context `Cx` (mangrove's `RequestContext`,
12//! lakekeeper's `RequestMetadata`, …) threaded from the axum router. All types
13//! exchanged here are the crate's own — the Delta wire DTOs, [`crate::column`],
14//! and the coordinate/spec structs below — never a server's resource model.
15//!
16//! # Authorization contract
17//!
18//! Authorization goes through **one** hook, [`DeltaBackend::authorize`], which the
19//! handler calls with a [`DeltaAction`] before every user-facing operation. The
20//! data methods below (`resolve_table`, `create_table_row`, `delete_table`,
21//! `vend_*`, `resolve_staging_*`, …) are **pure data access and must not perform
22//! their own authorization** — the handler has already authorized the operation.
23//! (An implementor is of course free to enforce coarse storage-level guards, but
24//! the caller-facing access decision belongs in `authorize`.) See [`crate::authz`].
25
26use std::collections::BTreeMap;
27
28use async_trait::async_trait;
29
30use serde::Deserialize;
31
32use crate::authz::DeltaAction;
33use crate::column::Column;
34use crate::coordinator::CommitCoordinator;
35use crate::error::DeltaBackendError;
36use crate::models::{DeltaDataSourceFormat, DeltaTableType};
37
38/// Result of a [`DeltaBackend`] operation.
39pub type BackendResult<T> = Result<T, DeltaBackendError>;
40
41/// A fully-qualified table coordinate.
42///
43/// Deserializes from the router's `{catalog}/{schema}/{table}` path parameters.
44#[derive(Debug, Clone, Deserialize)]
45pub struct TableRef {
46    pub catalog: String,
47    pub schema: String,
48    pub table: String,
49}
50
51impl TableRef {
52    /// The dotted `catalog.schema.table` full name.
53    pub fn full_name(&self) -> String {
54        format!("{}.{}.{}", self.catalog, self.schema, self.table)
55    }
56}
57
58/// A schema coordinate (the parent of table / staging-table creation).
59///
60/// Deserializes from the router's `{catalog}/{schema}` path parameters.
61#[derive(Debug, Clone, Deserialize)]
62pub struct SchemaRef {
63    pub catalog: String,
64    pub schema: String,
65}
66
67/// A table resolved by the backend, in the crate's portable shape.
68#[derive(Debug, Clone)]
69pub struct ResolvedTable {
70    /// The table UUID (as a string). `None` for tables the backend does not
71    /// assign an id to (which the Delta API rejects downstream).
72    pub table_id: Option<String>,
73    /// The table's storage root location.
74    pub location: String,
75    /// The Delta table type, or `None` for table types the Delta API cannot
76    /// serve (views, metric views, …), which `loadTable` rejects with 400.
77    pub table_type: Option<DeltaTableType>,
78    /// The table's data source format, if known. Commit-coordinator state is
79    /// only attached to MANAGED tables whose format is
80    /// [`DeltaDataSourceFormat::Delta`].
81    pub data_source_format: Option<DeltaDataSourceFormat>,
82    pub columns: Vec<Column>,
83    pub properties: BTreeMap<String, String>,
84    /// Creation time, epoch milliseconds.
85    pub created_at_ms: Option<i64>,
86    /// Last-update time, epoch milliseconds. Surfaced as `updated_time`.
87    pub updated_at_ms: Option<i64>,
88    /// The backend's monotonic per-object version. Drives the etag and the
89    /// `assert-etag` compare-and-swap (see [`etag_of`]). A backend that does not
90    /// track versions leaves this `0`.
91    pub version: u64,
92}
93
94/// The etag for a resolved table: `etag-<version>`.
95///
96/// This is the single definition of the table etag, shared by the handler (which
97/// hands it to the client on `loadTable`) and by any backend enforcing the
98/// `assert-etag` compare-and-swap in [`DeltaBackend::update_table_row`], so the
99/// asserted etag and the compared etag are always derived identically. The
100/// version is the backend's monotonic per-object counter, bumped on every
101/// mutation — a purpose-built etag that maps directly to the store's
102/// version-precondition CAS.
103pub fn etag_of(table: &ResolvedTable) -> String {
104    etag_of_version(table.version)
105}
106
107/// The etag formula over the version it depends on, for backends that hold the
108/// version without a full [`ResolvedTable`]. See [`etag_of`].
109pub fn etag_of_version(version: u64) -> String {
110    format!("etag-{version}")
111}
112
113/// A staging-table reservation (uuid + managed location) allocated before a
114/// managed `createTable`.
115#[derive(Debug, Clone)]
116pub struct StagingReservation {
117    /// The reservation UUID the created table adopts.
118    pub table_id: String,
119    /// The reservation's name, as the backend keys it.
120    ///
121    /// Carried so a backend that consumes the reservation during
122    /// [`create_table_row`](DeltaBackend::create_table_row) can delete it by its
123    /// store key without a second lookup.
124    pub name: String,
125    /// The managed location under which the client writes the initial commit.
126    pub location: String,
127    /// The principal that created the reservation, for the creator-match check.
128    pub created_by: Option<String>,
129    /// Whether the reservation has already been finalized into a table.
130    pub stage_committed: bool,
131}
132
133/// The access level for a vended storage credential.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum CredentialAccess {
136    Read,
137    ReadWrite,
138}
139
140/// A credential vended by the backend, in a cloud-neutral shape the handler maps
141/// onto the wire `DeltaStorageCredential`.
142#[derive(Debug, Clone)]
143pub struct VendedCredential {
144    /// The prefix / URL the credential applies to.
145    pub url: String,
146    /// Expiration, epoch milliseconds.
147    pub expiration_time_ms: i64,
148    pub kind: VendedCredentialKind,
149}
150
151/// Cloud-provider-specific credential material.
152#[derive(Debug, Clone)]
153pub enum VendedCredentialKind {
154    /// AWS (and R2, which reuses the S3-shaped fields).
155    S3 {
156        access_key_id: String,
157        secret_access_key: String,
158        session_token: Option<String>,
159    },
160    /// Azure user-delegation SAS.
161    AzureSas { sas_token: String },
162    /// GCS OAuth token.
163    GcsOauth { oauth_token: String },
164    /// No recognized credential material (an empty config is vended).
165    None,
166}
167
168/// Specification for persisting a new Delta table row. The handler has already
169/// validated the contract and derived the UC-shaped columns + stored properties.
170#[derive(Debug, Clone)]
171pub struct CreateTableSpec {
172    pub at: SchemaRef,
173    pub name: String,
174    pub table_type: DeltaTableType,
175    pub location: String,
176    pub comment: Option<String>,
177    pub columns: Vec<Column>,
178    pub properties: BTreeMap<String, String>,
179    /// For MANAGED tables, the adopted staging reservation id; `None` for EXTERNAL
180    /// (the backend assigns the id).
181    pub table_id: Option<String>,
182    /// For MANAGED tables, the staging reservation being adopted, already resolved
183    /// and authorized by the handler.
184    ///
185    /// When `Some`, `create_table_row` must **atomically consume this reservation
186    /// and create the table** adopting its id (`table_id` above equals
187    /// `adopt_staging.table_id`) — a transactional backend does both in one
188    /// transaction, closing the orphaned-reservation race. `None` for EXTERNAL
189    /// tables, which have no reservation.
190    pub adopt_staging: Option<StagingReservation>,
191}
192
193/// Specification for persisting metadata changes to an existing table (the
194/// non-commit half of `updateTable`). The handler has already applied the
195/// canonical-order action dispatch to produce the new columns/properties/comment.
196#[derive(Debug, Clone)]
197pub struct UpdateTableSpec {
198    pub table_id: String,
199    pub columns: Vec<Column>,
200    pub properties: BTreeMap<String, String>,
201    /// The new comment. Unlike `columns`/`properties` (full replacement
202    /// snapshots), this is a delta: `None` leaves the stored comment unchanged
203    /// (the wire has no clear-comment action); `Some(c)` sets it.
204    pub comment: Option<String>,
205    /// The `assert-etag` precondition, when the client sent one.
206    ///
207    /// `Some(etag)` means the write is a **compare-and-swap**: the backend must
208    /// verify the table's current etag still equals `etag` *at write time* and
209    /// return [`DeltaBackendError::UpdateRequirementConflict`] on mismatch —
210    /// closing the read-modify-write race. `None` means no `assert-etag` was
211    /// asserted and the write is unconditional. (`assert-table-uuid` is checked in
212    /// the handler and never reaches the port.)
213    ///
214    /// [`DeltaBackendError::UpdateRequirementConflict`]: crate::error::DeltaBackendError::UpdateRequirementConflict
215    pub expected_etag: Option<String>,
216}
217
218/// Optional Delta operations a backend may or may not support.
219///
220/// Reported by [`DeltaBackend::capabilities`] and used by `getConfig` to advertise
221/// only the endpoints the backend actually serves — so a discovery-driven client is
222/// never pointed at an operation the backend will `501` on. All flags default to
223/// the conservative `false`; a backend opts in by overriding `capabilities()`.
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
225pub struct DeltaCapabilities {
226    /// Whether `renameTable` is served. The port models rename as optionally
227    /// [`NotImplemented`](crate::error::DeltaBackendError::NotImplemented) (see
228    /// [`DeltaBackend::rename_table`]); a backend whose `rename_table` returns
229    /// `NotImplemented` must leave this `false` so `getConfig` does not advertise an
230    /// endpoint that 501s.
231    pub rename: bool,
232}
233
234/// The backend port. See the module docs.
235#[async_trait]
236pub trait DeltaBackend<Cx = ()>: Send + Sync + 'static {
237    /// The optional operations this backend serves, used by `getConfig` to advertise
238    /// a capability-accurate endpoint list. Defaults to [`DeltaCapabilities::default`]
239    /// (all off), so a backend advertises an optional operation only by opting in.
240    fn capabilities(&self) -> DeltaCapabilities {
241        DeltaCapabilities::default()
242    }
243
244    /// Authorize an action before the handler performs it.
245    ///
246    /// The single authorization seam (see the [module authz contract](self)):
247    /// the handler calls this with a [`DeltaAction`] at the top of every
248    /// user-facing operation. Deny by returning
249    /// [`DeltaBackendError::PermissionDenied`] (→ 403) or
250    /// [`DeltaBackendError::Unauthenticated`] (→ 401).
251    async fn authorize(&self, action: DeltaAction<'_>, cx: &Cx) -> BackendResult<()>;
252
253    /// Confirm the catalog exists (for `getConfig`). Missing → [`DeltaBackendError::NotFound`].
254    async fn catalog_exists(&self, catalog: &str, cx: &Cx) -> BackendResult<()>;
255
256    /// Resolve a table by 3-part name. Missing → [`DeltaBackendError::NotFound`].
257    async fn resolve_table(&self, table: &TableRef, cx: &Cx) -> BackendResult<ResolvedTable>;
258
259    /// Validate that an EXTERNAL table location lies within a registered external
260    /// location.
261    async fn validate_external_location(&self, location: &str, cx: &Cx) -> BackendResult<()>;
262
263    /// Persist a new Delta table row and return it resolved.
264    ///
265    /// When [`spec.adopt_staging`](CreateTableSpec::adopt_staging) is `Some`, this
266    /// call **atomically consumes that staging reservation and creates the table**
267    /// adopting its id (a transactional backend does both in one transaction,
268    /// closing the orphaned-reservation race; a non-transactional backend does the
269    /// two steps back-to-back with the same window as before).
270    async fn create_table_row(
271        &self,
272        spec: CreateTableSpec,
273        cx: &Cx,
274    ) -> BackendResult<ResolvedTable>;
275
276    /// Persist metadata changes to an existing table and return it resolved.
277    ///
278    /// When [`spec.expected_etag`](UpdateTableSpec::expected_etag) is `Some`, the
279    /// write is a **compare-and-swap** against that etag (see the field docs):
280    /// return [`DeltaBackendError::UpdateRequirementConflict`] if the table's
281    /// current etag no longer matches. Returns the refreshed table so the handler
282    /// need not re-resolve it.
283    ///
284    /// [`DeltaBackendError::UpdateRequirementConflict`]: crate::error::DeltaBackendError::UpdateRequirementConflict
285    async fn update_table_row(
286        &self,
287        spec: UpdateTableSpec,
288        cx: &Cx,
289    ) -> BackendResult<ResolvedTable>;
290
291    /// Delete a table.
292    async fn delete_table(&self, table: &TableRef, cx: &Cx) -> BackendResult<()>;
293
294    /// Rename a table within the same catalog + schema. Backends without rename
295    /// support return [`DeltaBackendError::NotImplemented`].
296    async fn rename_table(&self, from: &TableRef, to_name: &str, cx: &Cx) -> BackendResult<()>;
297
298    /// Allocate a staging reservation (uuid + managed location) under a schema.
299    async fn allocate_staging(
300        &self,
301        at: &SchemaRef,
302        name: &str,
303        cx: &Cx,
304    ) -> BackendResult<StagingReservation>;
305
306    /// Resolve a staging reservation by its managed location.
307    async fn resolve_staging_by_location(
308        &self,
309        location: &str,
310        cx: &Cx,
311    ) -> BackendResult<StagingReservation>;
312
313    /// Resolve a staging reservation by its uuid.
314    async fn resolve_staging_by_id(
315        &self,
316        table_id: &str,
317        cx: &Cx,
318    ) -> BackendResult<StagingReservation>;
319
320    /// Vend a credential for a table's location at the given access level.
321    async fn vend_table_credential(
322        &self,
323        table_id: &str,
324        access: CredentialAccess,
325        cx: &Cx,
326    ) -> BackendResult<VendedCredential>;
327
328    /// Vend a credential for an arbitrary path at the given access level.
329    async fn vend_path_credential(
330        &self,
331        location: &str,
332        access: CredentialAccess,
333        cx: &Cx,
334    ) -> BackendResult<VendedCredential>;
335
336    /// The commit coordinator backing this backend's Delta commit log.
337    fn commit_coordinator(&self) -> &dyn CommitCoordinator;
338}