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.
134pub use olai_storage_credentials::AccessLevel as CredentialAccess;
135
136/// A credential vended by the backend: the catalog-independent model, plus the
137/// location it covers as the catalog spells it, which the handler sends as the
138/// wire `prefix`.
139#[derive(Debug, Clone)]
140pub struct VendedCredential {
141    /// The prefix / URL the credential applies to.
142    pub url: String,
143    /// The credential.
144    pub credential: olai_storage_credentials::VendedCredential,
145}
146
147/// Specification for persisting a new Delta table row. The handler has already
148/// validated the contract and derived the UC-shaped columns + stored properties.
149#[derive(Debug, Clone)]
150pub struct CreateTableSpec {
151    pub at: SchemaRef,
152    pub name: String,
153    pub table_type: DeltaTableType,
154    pub location: String,
155    pub comment: Option<String>,
156    pub columns: Vec<Column>,
157    pub properties: BTreeMap<String, String>,
158    /// For MANAGED tables, the adopted staging reservation id; `None` for EXTERNAL
159    /// (the backend assigns the id).
160    pub table_id: Option<String>,
161    /// For MANAGED tables, the staging reservation being adopted, already resolved
162    /// and authorized by the handler.
163    ///
164    /// When `Some`, `create_table_row` must **atomically consume this reservation
165    /// and create the table** adopting its id (`table_id` above equals
166    /// `adopt_staging.table_id`) — a transactional backend does both in one
167    /// transaction, closing the orphaned-reservation race. `None` for EXTERNAL
168    /// tables, which have no reservation.
169    pub adopt_staging: Option<StagingReservation>,
170}
171
172/// Specification for persisting metadata changes to an existing table (the
173/// non-commit half of `updateTable`). The handler has already applied the
174/// canonical-order action dispatch to produce the new columns/properties/comment.
175#[derive(Debug, Clone)]
176pub struct UpdateTableSpec {
177    pub table_id: String,
178    pub columns: Vec<Column>,
179    pub properties: BTreeMap<String, String>,
180    /// The new comment. Unlike `columns`/`properties` (full replacement
181    /// snapshots), this is a delta: `None` leaves the stored comment unchanged
182    /// (the wire has no clear-comment action); `Some(c)` sets it.
183    pub comment: Option<String>,
184    /// The `assert-etag` precondition, when the client sent one.
185    ///
186    /// `Some(etag)` means the write is a **compare-and-swap**: the backend must
187    /// verify the table's current etag still equals `etag` *at write time* and
188    /// return [`DeltaBackendError::UpdateRequirementConflict`] on mismatch —
189    /// closing the read-modify-write race. `None` means no `assert-etag` was
190    /// asserted and the write is unconditional. (`assert-table-uuid` is checked in
191    /// the handler and never reaches the port.)
192    ///
193    /// [`DeltaBackendError::UpdateRequirementConflict`]: crate::error::DeltaBackendError::UpdateRequirementConflict
194    pub expected_etag: Option<String>,
195}
196
197/// Optional Delta operations a backend may or may not support.
198///
199/// Reported by [`DeltaBackend::capabilities`] and used by `getConfig` to advertise
200/// only the endpoints the backend actually serves — so a discovery-driven client is
201/// never pointed at an operation the backend will `501` on. All flags default to
202/// the conservative `false`; a backend opts in by overriding `capabilities()`.
203#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
204pub struct DeltaCapabilities {
205    /// Whether `renameTable` is served. The port models rename as optionally
206    /// [`NotImplemented`](crate::error::DeltaBackendError::NotImplemented) (see
207    /// [`DeltaBackend::rename_table`]); a backend whose `rename_table` returns
208    /// `NotImplemented` must leave this `false` so `getConfig` does not advertise an
209    /// endpoint that 501s.
210    pub rename: bool,
211}
212
213/// The backend port. See the module docs.
214#[async_trait]
215pub trait DeltaBackend<Cx = ()>: Send + Sync + 'static {
216    /// The optional operations this backend serves, used by `getConfig` to advertise
217    /// a capability-accurate endpoint list. Defaults to [`DeltaCapabilities::default`]
218    /// (all off), so a backend advertises an optional operation only by opting in.
219    fn capabilities(&self) -> DeltaCapabilities {
220        DeltaCapabilities::default()
221    }
222
223    /// Authorize an action before the handler performs it.
224    ///
225    /// The single authorization seam (see the [module authz contract](self)):
226    /// the handler calls this with a [`DeltaAction`] at the top of every
227    /// user-facing operation. Deny by returning
228    /// [`DeltaBackendError::PermissionDenied`] (→ 403) or
229    /// [`DeltaBackendError::Unauthenticated`] (→ 401).
230    async fn authorize(&self, action: DeltaAction<'_>, cx: &Cx) -> BackendResult<()>;
231
232    /// Confirm the catalog exists (for `getConfig`). Missing → [`DeltaBackendError::NotFound`].
233    async fn catalog_exists(&self, catalog: &str, cx: &Cx) -> BackendResult<()>;
234
235    /// Resolve a table by 3-part name. Missing → [`DeltaBackendError::NotFound`].
236    async fn resolve_table(&self, table: &TableRef, cx: &Cx) -> BackendResult<ResolvedTable>;
237
238    /// Validate that an EXTERNAL table location lies within a registered external
239    /// location.
240    async fn validate_external_location(&self, location: &str, cx: &Cx) -> BackendResult<()>;
241
242    /// Persist a new Delta table row and return it resolved.
243    ///
244    /// When [`spec.adopt_staging`](CreateTableSpec::adopt_staging) is `Some`, this
245    /// call **atomically consumes that staging reservation and creates the table**
246    /// adopting its id (a transactional backend does both in one transaction,
247    /// closing the orphaned-reservation race; a non-transactional backend does the
248    /// two steps back-to-back with the same window as before).
249    async fn create_table_row(
250        &self,
251        spec: CreateTableSpec,
252        cx: &Cx,
253    ) -> BackendResult<ResolvedTable>;
254
255    /// Persist metadata changes to an existing table and return it resolved.
256    ///
257    /// When [`spec.expected_etag`](UpdateTableSpec::expected_etag) is `Some`, the
258    /// write is a **compare-and-swap** against that etag (see the field docs):
259    /// return [`DeltaBackendError::UpdateRequirementConflict`] if the table's
260    /// current etag no longer matches. Returns the refreshed table so the handler
261    /// need not re-resolve it.
262    ///
263    /// [`DeltaBackendError::UpdateRequirementConflict`]: crate::error::DeltaBackendError::UpdateRequirementConflict
264    async fn update_table_row(
265        &self,
266        spec: UpdateTableSpec,
267        cx: &Cx,
268    ) -> BackendResult<ResolvedTable>;
269
270    /// Delete a table.
271    async fn delete_table(&self, table: &TableRef, cx: &Cx) -> BackendResult<()>;
272
273    /// Rename a table within the same catalog + schema. Backends without rename
274    /// support return [`DeltaBackendError::NotImplemented`].
275    async fn rename_table(&self, from: &TableRef, to_name: &str, cx: &Cx) -> BackendResult<()>;
276
277    /// Allocate a staging reservation (uuid + managed location) under a schema.
278    async fn allocate_staging(
279        &self,
280        at: &SchemaRef,
281        name: &str,
282        cx: &Cx,
283    ) -> BackendResult<StagingReservation>;
284
285    /// Resolve a staging reservation by its managed location.
286    async fn resolve_staging_by_location(
287        &self,
288        location: &str,
289        cx: &Cx,
290    ) -> BackendResult<StagingReservation>;
291
292    /// Resolve a staging reservation by its uuid.
293    async fn resolve_staging_by_id(
294        &self,
295        table_id: &str,
296        cx: &Cx,
297    ) -> BackendResult<StagingReservation>;
298
299    /// Vend a credential for a table's location at the given access level.
300    async fn vend_table_credential(
301        &self,
302        table_id: &str,
303        access: CredentialAccess,
304        cx: &Cx,
305    ) -> BackendResult<VendedCredential>;
306
307    /// Vend a credential for an arbitrary path at the given access level.
308    async fn vend_path_credential(
309        &self,
310        location: &str,
311        access: CredentialAccess,
312        cx: &Cx,
313    ) -> BackendResult<VendedCredential>;
314
315    /// The commit coordinator backing this backend's Delta commit log.
316    fn commit_coordinator(&self) -> &dyn CommitCoordinator;
317}