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}