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}