Skip to main content

unitycatalog_delta_api/
models.rs

1//! Hand-written serde models for the UC Delta REST API (`/delta/v1/...`).
2//!
3//! These mirror the component schemas in `openapi/delta.yaml` (vendored from the
4//! Unity Catalog Java reference, `api/delta.yaml`). The wire format is kebab-case
5//! JSON, so every struct uses `#[serde(rename_all = "kebab-case")]`; individual
6//! fields whose JSON key is not a straight kebab-case of the Rust identifier
7//! carry an explicit `#[serde(rename = "...")]`.
8//!
9//! This is a standalone REST protocol (not resource-CRUD), so — unlike most of
10//! the workspace — it is not generated from proto. The pure wire DTOs live here
11//! so every server implementation of the Delta API and every client shares a
12//! single definition; the runtime error envelope (`DeltaApiError` +
13//! `IntoResponse`) lives in the sibling [`crate::error`] module.
14
15use std::collections::BTreeMap;
16
17use serde::{Deserialize, Serialize};
18
19// ===================================================================
20// Enums
21// ===================================================================
22
23/// The type of a Delta table.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
26pub enum DeltaTableType {
27    Managed,
28    External,
29}
30
31/// The data source format of a table, as carried on `DeltaCreateTableRequest`.
32///
33/// Mirrors the canonical Unity Catalog `DataSourceFormat` set. Serializes to the
34/// uppercase UC format strings (e.g. `"DELTA"`), matching what the Java OSS
35/// server's createTable handler expects. The Delta REST API only operates on
36/// Delta tables, so in practice this is always [`Delta`](Self::Delta); the wider
37/// set is kept so the field round-trips any value the catalog might report.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
40pub enum DeltaDataSourceFormat {
41    Delta,
42    Iceberg,
43    Hudi,
44    Parquet,
45    Csv,
46    Json,
47    Orc,
48    Avro,
49    Text,
50    UnityCatalog,
51    Deltasharing,
52}
53
54/// The permission level for a storage credential.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
57pub enum DeltaCredentialOperation {
58    Read,
59    ReadWrite,
60}
61
62// ===================================================================
63// Shared credential types
64// ===================================================================
65
66/// Cloud provider-specific credential configuration. Only keys for the relevant
67/// cloud provider are populated; the others are omitted (`None`).
68#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct DeltaStorageCredentialConfig {
70    #[serde(rename = "s3.access-key-id", skip_serializing_if = "Option::is_none")]
71    pub s3_access_key_id: Option<String>,
72    #[serde(
73        rename = "s3.secret-access-key",
74        skip_serializing_if = "Option::is_none"
75    )]
76    pub s3_secret_access_key: Option<String>,
77    #[serde(rename = "s3.session-token", skip_serializing_if = "Option::is_none")]
78    pub s3_session_token: Option<String>,
79    #[serde(rename = "azure.sas-token", skip_serializing_if = "Option::is_none")]
80    pub azure_sas_token: Option<String>,
81    #[serde(rename = "gcs.oauth-token", skip_serializing_if = "Option::is_none")]
82    pub gcs_oauth_token: Option<String>,
83}
84
85/// Temporary storage credential with prefix and config.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "kebab-case")]
88pub struct DeltaStorageCredential {
89    /// Storage path prefix this credential applies to.
90    pub prefix: String,
91    pub operation: DeltaCredentialOperation,
92    pub config: DeltaStorageCredentialConfig,
93    /// Credential expiration time in epoch milliseconds.
94    pub expiration_time_ms: i64,
95}
96
97/// Response carrying temporary cloud storage credentials.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "kebab-case")]
100pub struct DeltaCredentialsResponse {
101    pub storage_credentials: Vec<DeltaStorageCredential>,
102}
103
104// ===================================================================
105// Configuration
106// ===================================================================
107
108/// Catalog configuration and supported endpoints.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "kebab-case")]
111pub struct DeltaCatalogConfig {
112    /// List of supported endpoints.
113    pub endpoints: Vec<String>,
114    /// The negotiated protocol version (e.g., "1.0").
115    pub protocol_version: String,
116}
117
118// ===================================================================
119// Delta data types (schema)
120// ===================================================================
121
122/// A Delta column data type.
123///
124/// On the wire, primitive types are bare JSON strings (e.g. `"long"`, `"string"`,
125/// `"decimal(10,2)"`) while complex types are JSON objects carrying a `type`
126/// discriminator (`"array"`, `"map"`, `"struct"`, `"decimal"`). serde's `untagged`
127/// representation drives the split: a JSON string matches [`DeltaDataType::Primitive`];
128/// a JSON object matches one of the object variants. Each object variant carries its
129/// own `type` constant so it both round-trips and self-identifies.
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[serde(untagged)]
132pub enum DeltaDataType {
133    /// Bare-string primitive type, e.g. `"long"`, `"string"`, `"decimal(10,2)"`.
134    Primitive(String),
135    Array(Box<DeltaArrayType>),
136    Map(Box<DeltaMapType>),
137    Struct(Box<DeltaStructType>),
138    /// Object form of decimal (`{"type":"decimal","precision":..,"scale":..}`).
139    /// The string form `"decimal(p,s)"` is carried by [`DeltaDataType::Primitive`].
140    Decimal(DeltaDecimalType),
141}
142
143/// The `type` discriminator constant for an array data type.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
145#[serde(rename_all = "lowercase")]
146pub enum ArrayTypeTag {
147    #[default]
148    Array,
149}
150
151/// The `type` discriminator constant for a map data type.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
153#[serde(rename_all = "lowercase")]
154pub enum MapTypeTag {
155    #[default]
156    Map,
157}
158
159/// The `type` discriminator constant for a decimal data type.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
161#[serde(rename_all = "lowercase")]
162pub enum DecimalTypeTag {
163    #[default]
164    Decimal,
165}
166
167/// Object form of an array type (`{"type":"array","element-type":..,"contains-null":..}`).
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169#[serde(rename_all = "kebab-case")]
170pub struct DeltaArrayType {
171    #[serde(rename = "type", default)]
172    pub type_tag: ArrayTypeTag,
173    pub element_type: DeltaDataType,
174    pub contains_null: bool,
175}
176
177/// Object form of a map type (`{"type":"map","key-type":..,"value-type":..}`).
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(rename_all = "kebab-case")]
180pub struct DeltaMapType {
181    #[serde(rename = "type", default)]
182    pub type_tag: MapTypeTag,
183    pub key_type: DeltaDataType,
184    pub value_type: DeltaDataType,
185    pub value_contains_null: bool,
186}
187
188/// Object form of a decimal type (`{"type":"decimal","precision":..,"scale":..}`).
189///
190/// The compact string form `"decimal(p,s)"` is carried by
191/// [`DeltaDataType::Primitive`] instead.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct DeltaDecimalType {
194    #[serde(rename = "type", default)]
195    pub type_tag: DecimalTypeTag,
196    pub precision: i32,
197    pub scale: i32,
198}
199
200/// A field in a [`DeltaStructType`]: name, type, nullability, and metadata.
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202pub struct DeltaStructField {
203    pub name: String,
204    #[serde(rename = "type")]
205    pub data_type: DeltaDataType,
206    pub nullable: bool,
207    /// Arbitrary column metadata (e.g. `comment`, `delta.columnMapping.id`).
208    /// Values can be strings, numbers, booleans, or nested objects.
209    pub metadata: BTreeMap<String, serde_json::Value>,
210}
211
212/// Struct type containing named fields. Carries the `type: "struct"`
213/// discriminator on the wire (it is the schema container for tables), so it is
214/// emitted/accepted explicitly even when used as a bare `columns` value.
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216pub struct DeltaStructType {
217    #[serde(rename = "type", default = "struct_type_tag")]
218    pub type_tag: StructTypeTag,
219    pub fields: Vec<DeltaStructField>,
220}
221
222fn struct_type_tag() -> StructTypeTag {
223    StructTypeTag::Struct
224}
225
226/// The constant `"struct"` discriminator for [`DeltaStructType`].
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
228#[serde(rename_all = "lowercase")]
229pub enum StructTypeTag {
230    #[default]
231    Struct,
232}
233
234// ===================================================================
235// Protocol
236// ===================================================================
237
238/// Delta table protocol specification.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(rename_all = "kebab-case")]
241pub struct DeltaProtocol {
242    pub min_reader_version: i32,
243    pub min_writer_version: i32,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub reader_features: Option<Vec<String>>,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub writer_features: Option<Vec<String>>,
248}
249
250/// Suggested protocol advertised in the staging response (no version fields,
251/// only feature hints).
252#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "kebab-case")]
254pub struct DeltaSuggestedProtocol {
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub reader_features: Option<Vec<String>>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub writer_features: Option<Vec<String>>,
259}
260
261// ===================================================================
262// Domain metadata
263// ===================================================================
264
265/// Configuration for Clustered Tables.
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct DeltaClusteringDomainMetadata {
268    /// Clustering column paths. Each inner array is a path from root schema to a
269    /// column.
270    #[serde(rename = "clusteringColumns")]
271    pub clustering_columns: Vec<Vec<String>>,
272}
273
274/// Configuration for Row Tracking.
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct DeltaRowTrackingDomainMetadata {
277    /// The highest fresh Row ID assigned so far.
278    #[serde(rename = "rowIdHighWaterMark")]
279    pub row_id_high_water_mark: i64,
280}
281
282/// Domain metadata entries keyed by domain name.
283#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
284pub struct DeltaDomainMetadataUpdates {
285    #[serde(rename = "delta.clustering", skip_serializing_if = "Option::is_none")]
286    pub delta_clustering: Option<DeltaClusteringDomainMetadata>,
287    #[serde(rename = "delta.rowTracking", skip_serializing_if = "Option::is_none")]
288    pub delta_row_tracking: Option<DeltaRowTrackingDomainMetadata>,
289}
290
291// ===================================================================
292// Uniform / Iceberg
293// ===================================================================
294
295/// Iceberg-specific conversion metadata.
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(rename_all = "kebab-case")]
298pub struct DeltaIcebergMetadata {
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub metadata_location: Option<String>,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub converted_delta_version: Option<i64>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub converted_delta_timestamp: Option<i64>,
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub base_converted_delta_version: Option<i64>,
307}
308
309/// UniForm conversion metadata. Present only for Uniform (Delta + Iceberg) tables.
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct DeltaUniformMetadata {
312    pub iceberg: DeltaIcebergMetadata,
313}
314
315// ===================================================================
316// Commit
317// ===================================================================
318
319/// Delta commit information for CCv2.
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "kebab-case")]
322pub struct DeltaCommit {
323    pub version: i64,
324    /// In-commit timestamp, in epoch milliseconds.
325    pub timestamp: i64,
326    /// UUID-based commit file name.
327    pub file_name: String,
328    pub file_size: i64,
329    /// File modification timestamp, in epoch milliseconds.
330    pub file_modification_timestamp: i64,
331}
332
333// ===================================================================
334// Table lifecycle
335// ===================================================================
336
337/// Request to create a staging table.
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339pub struct DeltaCreateStagingTableRequest {
340    pub name: String,
341}
342
343/// Response from creating a staging table. Advertises the UC catalog-managed
344/// contract (required / suggested protocol + properties) plus READ_WRITE
345/// credentials for writing the initial commit.
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347#[serde(rename_all = "kebab-case")]
348pub struct DeltaStagingTableResponse {
349    pub table_id: String,
350    pub table_type: DeltaTableType,
351    pub location: String,
352    pub storage_credentials: Vec<DeltaStorageCredential>,
353    pub required_protocol: DeltaProtocol,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub suggested_protocol: Option<DeltaSuggestedProtocol>,
356    /// Properties that must be set; null values mean any valid value is allowed.
357    pub required_properties: BTreeMap<String, Option<String>>,
358    /// Properties that should be set whenever possible; null values mean the
359    /// client generates the value.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub suggested_properties: Option<BTreeMap<String, Option<String>>>,
362}
363
364/// Request to create a Delta table (managed or external).
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
366#[serde(rename_all = "kebab-case")]
367pub struct DeltaCreateTableRequest {
368    pub name: String,
369    pub location: String,
370    pub table_type: DeltaTableType,
371    /// Data source format — `"DELTA"` for Delta tables. Required by the running
372    /// Java UC OSS server (`ghcr.io/roeap/unitycatalog:v0.0.0-dev-3`), whose
373    /// createTable handler reads it from the request body. (Newer server code
374    /// hardcodes `DELTA` in `DeltaCreateTableMapper` and ignores this field, and
375    /// the `delta.yaml` schema omits it — a spec/impl drift; sending it satisfies
376    /// both.)
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub data_source_format: Option<DeltaDataSourceFormat>,
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub comment: Option<String>,
381    pub columns: DeltaStructType,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub partition_columns: Option<Vec<String>>,
384    pub protocol: DeltaProtocol,
385    pub properties: BTreeMap<String, String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub domain_metadata: Option<DeltaDomainMetadataUpdates>,
388    /// Timestamp of version 0 (the commit the client wrote before calling this
389    /// endpoint), in epoch milliseconds.
390    pub last_commit_timestamp_ms: i64,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub uniform: Option<DeltaUniformMetadata>,
393}
394
395/// Complete table metadata.
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397#[serde(rename_all = "kebab-case")]
398pub struct DeltaTableMetadata {
399    /// Entity tag for optimistic concurrency control.
400    pub etag: String,
401    pub table_type: DeltaTableType,
402    pub table_uuid: String,
403    pub location: String,
404    pub created_time: i64,
405    pub updated_time: i64,
406    pub columns: DeltaStructType,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub partition_columns: Option<Vec<String>>,
409    pub properties: BTreeMap<String, String>,
410    /// The version of the last commit that changed table metadata
411    /// (`delta.lastUpdateVersion`).
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub last_commit_version: Option<i64>,
414    /// Timestamp of the last commit that changed table metadata, in epoch
415    /// milliseconds (`delta.lastCommitTimestamp`).
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub last_commit_timestamp_ms: Option<i64>,
418}
419
420/// Response from `loadTable` / `createTable` / `updateTable`.
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422#[serde(rename_all = "kebab-case")]
423pub struct DeltaLoadTableResponse {
424    pub metadata: DeltaTableMetadata,
425    /// All unbackfilled CCv2 commits.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub commits: Option<Vec<DeltaCommit>>,
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub uniform: Option<DeltaUniformMetadata>,
430    /// The latest ratified table version tracked by the server, including
431    /// data-only commits.
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub latest_table_version: Option<i64>,
434}
435
436/// Request to rename a table within the same catalog and schema.
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438#[serde(rename_all = "kebab-case")]
439pub struct DeltaRenameTableRequest {
440    pub new_name: String,
441}
442
443// ===================================================================
444// Update table (requirements + actions)
445// ===================================================================
446
447/// A pre-condition that must hold for the update to apply.
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(tag = "type", rename_all = "kebab-case")]
450pub enum DeltaTableRequirement {
451    /// `assert-table-uuid`: the table UUID must match `uuid`.
452    AssertTableUuid { uuid: String },
453    /// `assert-etag`: the table etag must match `etag`.
454    AssertEtag { etag: String },
455}
456
457/// A single update action. Tagged by the `action` discriminator, matching the
458/// `propertyName: action` + `const` mapping in `delta.yaml`.
459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
460#[serde(tag = "action", rename_all = "kebab-case")]
461pub enum DeltaTableUpdate {
462    SetProperties {
463        updates: BTreeMap<String, String>,
464    },
465    RemoveProperties {
466        removals: Vec<String>,
467    },
468    SetColumns {
469        columns: DeltaStructType,
470    },
471    SetTableComment {
472        comment: String,
473    },
474    AddCommit {
475        commit: DeltaCommit,
476        #[serde(skip_serializing_if = "Option::is_none")]
477        uniform: Option<DeltaUniformMetadata>,
478    },
479    SetLatestBackfilledVersion {
480        #[serde(rename = "latest-published-version")]
481        latest_published_version: i64,
482    },
483    SetProtocol {
484        protocol: DeltaProtocol,
485    },
486    SetDomainMetadata {
487        updates: DeltaDomainMetadataUpdates,
488    },
489    RemoveDomainMetadata {
490        domains: Vec<String>,
491    },
492    SetPartitionColumns {
493        #[serde(rename = "partition-columns")]
494        partition_columns: Vec<String>,
495    },
496    UpdateMetadataSnapshotVersion {
497        #[serde(rename = "last-commit-version")]
498        last_commit_version: i64,
499        #[serde(rename = "last-commit-timestamp-ms")]
500        last_commit_timestamp_ms: i64,
501    },
502}
503
504/// Request to update a table with requirements and updates.
505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
506pub struct DeltaUpdateTableRequest {
507    pub requirements: Vec<DeltaTableRequirement>,
508    pub updates: Vec<DeltaTableUpdate>,
509}
510
511// ===================================================================
512// Metrics
513// ===================================================================
514
515/// Histogram tracking file counts and total bytes across size ranges.
516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
517#[serde(rename_all = "kebab-case")]
518pub struct DeltaFileSizeHistogram {
519    pub sorted_bin_boundaries: Vec<i64>,
520    pub file_counts: Vec<i64>,
521    pub total_bytes: Vec<i64>,
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub commit_version: Option<i64>,
524}
525
526/// Commit report metrics.
527#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(rename_all = "kebab-case")]
529pub struct DeltaCommitReport {
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub num_files_added: Option<i64>,
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub num_bytes_added: Option<i64>,
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub num_files_removed: Option<i64>,
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub num_bytes_removed: Option<i64>,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub num_rows_inserted: Option<i64>,
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub num_rows_removed: Option<i64>,
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub num_rows_updated: Option<i64>,
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub file_size_histogram: Option<DeltaFileSizeHistogram>,
546}
547
548/// The metrics being reported.
549#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(rename_all = "kebab-case")]
551pub struct DeltaReport {
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub commit_report: Option<DeltaCommitReport>,
554}
555
556/// Request to report commit metrics (telemetry) for a table.
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
558#[serde(rename_all = "kebab-case")]
559pub struct DeltaReportMetricsRequest {
560    /// Table UUID. Must match the table identified by the path.
561    pub table_id: String,
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub report: Option<DeltaReport>,
564}
565
566// ===================================================================
567// Errors (the /delta/v1 envelope)
568// ===================================================================
569
570/// Error type identifier returned in Delta API error responses. Mirrors the
571/// `DeltaErrorType` enum in `delta.yaml`.
572#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
573pub enum DeltaErrorType {
574    BadRequestException,
575    InvalidParameterValueException,
576    UnsupportedTableFormatException,
577    NotAuthorizedException,
578    PermissionDeniedException,
579    NotFoundException,
580    NoSuchCatalogException,
581    NoSuchSchemaException,
582    NoSuchTableException,
583    AlreadyExistsException,
584    CommitVersionConflictException,
585    UpdateRequirementConflictException,
586    ResourceExhaustedException,
587    TooManyRequestsException,
588    CommitStateUnknownException,
589    InternalServerErrorException,
590    NotImplementedException,
591}
592
593impl DeltaErrorType {
594    /// Whether this error type denotes a missing resource (404 family).
595    pub fn is_not_found(&self) -> bool {
596        matches!(
597            self,
598            DeltaErrorType::NotFoundException
599                | DeltaErrorType::NoSuchCatalogException
600                | DeltaErrorType::NoSuchSchemaException
601                | DeltaErrorType::NoSuchTableException
602        )
603    }
604
605    /// Whether this error type denotes a name collision (`AlreadyExistsException`).
606    pub fn is_already_exists(&self) -> bool {
607        matches!(self, DeltaErrorType::AlreadyExistsException)
608    }
609
610    /// Whether this error type denotes a commit-version conflict that the client
611    /// should resolve by rebuilding its snapshot and retrying.
612    pub fn is_commit_conflict(&self) -> bool {
613        matches!(self, DeltaErrorType::CommitVersionConflictException)
614    }
615
616    /// Whether this error type denotes an unmet `assert-etag`/`assert-table-uuid`
617    /// requirement; the client should reload the table and retry.
618    pub fn is_update_requirement_conflict(&self) -> bool {
619        matches!(self, DeltaErrorType::UpdateRequirementConflictException)
620    }
621
622    /// Whether the request was throttled or hit a resource limit (429 family).
623    /// The client should back off (and, for `ResourceExhaustedException`,
624    /// backfill pending commits) before retrying.
625    pub fn is_resource_exhausted(&self) -> bool {
626        matches!(
627            self,
628            DeltaErrorType::ResourceExhaustedException | DeltaErrorType::TooManyRequestsException
629        )
630    }
631
632    /// Whether the commit outcome is unknown; the client must check table state
633    /// before retrying to avoid duplicate commits.
634    pub fn is_commit_state_unknown(&self) -> bool {
635        matches!(self, DeltaErrorType::CommitStateUnknownException)
636    }
637
638    /// Whether the table is not Delta, or is a Delta table this `/delta/v1`
639    /// endpoint does not support (`UnsupportedTableFormatException`, a 400). Per
640    /// the spec the client should fall back to the legacy UC table API.
641    pub fn is_unsupported_table_format(&self) -> bool {
642        matches!(self, DeltaErrorType::UnsupportedTableFormatException)
643    }
644
645    /// Whether the requested `/delta/v1` functionality is not supported by the
646    /// server (`NotImplementedException`, a 501). The client should fall back to
647    /// the legacy UC table API.
648    pub fn is_not_implemented(&self) -> bool {
649        matches!(self, DeltaErrorType::NotImplementedException)
650    }
651}
652
653/// The JSON error payload (`DeltaErrorModel` in `delta.yaml`).
654#[derive(Debug, Clone, Serialize, Deserialize)]
655pub struct DeltaErrorModel {
656    pub message: String,
657    #[serde(rename = "type")]
658    pub error_type: DeltaErrorType,
659    /// HTTP response code.
660    pub code: u16,
661    /// Stack trace, only populated when the server runs in debug mode.
662    #[serde(skip_serializing_if = "Option::is_none")]
663    pub stack: Option<Vec<String>>,
664}
665
666/// The JSON wrapper for all Delta API error responses (`DeltaErrorResponse`).
667#[derive(Debug, Clone, Serialize, Deserialize)]
668pub struct DeltaErrorResponse {
669    pub error: DeltaErrorModel,
670}
671
672#[cfg(test)]
673mod tests {
674    //! Round-trip tests over the example payloads in `openapi/delta.yaml`.
675    //!
676    //! Each test deserializes a spec example into our hand-written model and
677    //! re-serializes it, asserting the JSON survives a round-trip with the
678    //! kebab-case keys, enum casing, and tagged-union discriminators intact.
679    //! This is the Phase-1 parity gate: it pins our models to the vendored spec.
680
681    use super::*;
682    use serde_json::{Value, json};
683
684    /// Deserialize `json` into `T`, re-serialize, and assert structural equality
685    /// with the original JSON value (key order independent).
686    fn round_trip<T>(value: Value)
687    where
688        T: Serialize + for<'de> Deserialize<'de>,
689    {
690        let parsed: T = serde_json::from_value(value.clone())
691            .unwrap_or_else(|e| panic!("deserialize failed: {e}\njson: {value:#}"));
692        let reserialized = serde_json::to_value(&parsed).expect("serialize");
693        assert_eq!(
694            reserialized, value,
695            "round-trip mismatch\n  expected: {value:#}\n  got:      {reserialized:#}"
696        );
697    }
698
699    #[test]
700    fn staging_table_response() {
701        round_trip::<DeltaStagingTableResponse>(json!({
702            "table-id": "123e4567-e89b-12d3-a456-426614174000",
703            "table-type": "MANAGED",
704            "location": "s3://bucket/warehouse/catalog/schema/table",
705            "storage-credentials": [{
706                "prefix": "s3://bucket/warehouse/catalog/schema/table/",
707                "operation": "READ_WRITE",
708                "config": {
709                    "s3.access-key-id": "AK...example",
710                    "s3.secret-access-key": "ExampleKey",
711                    "s3.session-token": "token"
712                },
713                "expiration-time-ms": 1234567890000_i64
714            }],
715            "required-protocol": {
716                "min-reader-version": 3,
717                "min-writer-version": 7,
718                "reader-features": ["deletionVectors", "vacuumProtocolCheck"],
719                "writer-features": ["catalogManaged", "deletionVectors"]
720            },
721            "suggested-protocol": {
722                "reader-features": ["typeWidening"],
723                "writer-features": ["domainMetadata", "rowTracking"]
724            },
725            "required-properties": { "delta.checkpointPolicy": "v2" },
726            "suggested-properties": {
727                "delta.rowTracking.materializedRowIdColumnName": null,
728                "delta.rowTracking.materializedRowCommitVersionColumnName": null
729            }
730        }));
731    }
732
733    #[test]
734    fn create_table_request_with_decimal_and_partition() {
735        round_trip::<DeltaCreateTableRequest>(json!({
736            "name": "sales",
737            "location": "s3://bucket/warehouse/catalog/schema/sales",
738            "table-type": "MANAGED",
739            "columns": {
740                "type": "struct",
741                "fields": [
742                    { "name": "id", "type": "long", "nullable": false, "metadata": {} },
743                    {
744                        "name": "amount",
745                        "type": { "type": "decimal", "precision": 10, "scale": 2 },
746                        "nullable": true,
747                        "metadata": {}
748                    }
749                ]
750            },
751            "partition-columns": ["id"],
752            "protocol": {
753                "min-reader-version": 3,
754                "min-writer-version": 7,
755                "reader-features": ["deletionVectors"],
756                "writer-features": ["deletionVectors", "invariants"]
757            },
758            "properties": { "delta.enableDeletionVectors": "true" },
759            "last-commit-timestamp-ms": 1704067400000_i64
760        }));
761    }
762
763    #[test]
764    fn struct_type_carries_type_tag() {
765        // A bare `columns` value round-trips with its `type: "struct"` tag.
766        round_trip::<DeltaStructType>(json!({
767            "type": "struct",
768            "fields": [
769                { "name": "id", "type": "long", "nullable": false, "metadata": {} },
770                { "name": "name", "type": "string", "nullable": true, "metadata": {} }
771            ]
772        }));
773    }
774
775    #[test]
776    fn nested_array_and_map_types() {
777        round_trip::<DeltaStructField>(json!({
778            "name": "tags",
779            "type": {
780                "type": "array",
781                "element-type": "string",
782                "contains-null": true
783            },
784            "nullable": true,
785            "metadata": {}
786        }));
787        round_trip::<DeltaStructField>(json!({
788            "name": "props",
789            "type": {
790                "type": "map",
791                "key-type": "string",
792                "value-type": "long",
793                "value-contains-null": false
794            },
795            "nullable": true,
796            "metadata": {}
797        }));
798    }
799
800    #[test]
801    fn load_table_response_with_commits() {
802        round_trip::<DeltaLoadTableResponse>(json!({
803            "metadata": {
804                "etag": "etag-1",
805                "table-type": "MANAGED",
806                "table-uuid": "123e4567-e89b-12d3-a456-426614174000",
807                "location": "s3://bucket/warehouse/catalog/schema/table",
808                "created-time": 1705600000000_i64,
809                "updated-time": 1705600000000_i64,
810                "columns": { "type": "struct", "fields": [] },
811                "properties": { "delta.checkpointPolicy": "v2" },
812                "last-commit-version": 0,
813                "last-commit-timestamp-ms": 1704067400000_i64
814            },
815            "commits": [{
816                "version": 1,
817                "timestamp": 1704067200000_i64,
818                "file-name": "00000000-0000-0000-0000-00000000002a.json",
819                "file-size": 2048,
820                "file-modification-timestamp": 1704067200000_i64
821            }],
822            "latest-table-version": 1
823        }));
824    }
825
826    #[test]
827    fn update_table_request_add_commit() {
828        round_trip::<DeltaUpdateTableRequest>(json!({
829            "requirements": [
830                { "type": "assert-table-uuid", "uuid": "123e4567-e89b-12d3-a456-426614174000" },
831                { "type": "assert-etag", "etag": "etag-1" }
832            ],
833            "updates": [
834                {
835                    "action": "add-commit",
836                    "commit": {
837                        "version": 1,
838                        "timestamp": 1704067200000_i64,
839                        "file-name": "v.uuid1.json",
840                        "file-size": 2048,
841                        "file-modification-timestamp": 1704067200000_i64
842                    }
843                },
844                { "action": "set-latest-backfilled-version", "latest-published-version": 0 }
845            ]
846        }));
847    }
848
849    #[test]
850    fn update_table_request_metadata_actions() {
851        round_trip::<DeltaUpdateTableRequest>(json!({
852            "requirements": [],
853            "updates": [
854                { "action": "set-properties", "updates": { "k": "v" } },
855                { "action": "remove-properties", "removals": ["old"] },
856                { "action": "set-table-comment", "comment": "hello" },
857                {
858                    "action": "set-protocol",
859                    "protocol": { "min-reader-version": 3, "min-writer-version": 7 }
860                },
861                { "action": "set-partition-columns", "partition-columns": ["id"] },
862                {
863                    "action": "update-metadata-snapshot-version",
864                    "last-commit-version": 5,
865                    "last-commit-timestamp-ms": 1704067400000_i64
866                }
867            ]
868        }));
869    }
870
871    #[test]
872    fn update_table_domain_metadata_actions() {
873        round_trip::<DeltaUpdateTableRequest>(json!({
874            "requirements": [],
875            "updates": [
876                {
877                    "action": "set-domain-metadata",
878                    "updates": {
879                        "delta.clustering": { "clusteringColumns": [["id"], ["address", "city"]] },
880                        "delta.rowTracking": { "rowIdHighWaterMark": 42 }
881                    }
882                },
883                { "action": "remove-domain-metadata", "domains": ["delta.clustering"] }
884            ]
885        }));
886    }
887
888    #[test]
889    fn credentials_response() {
890        round_trip::<DeltaCredentialsResponse>(json!({
891            "storage-credentials": [{
892                "prefix": "s3://bucket/path/",
893                "operation": "READ",
894                "config": { "s3.access-key-id": "AK", "s3.secret-access-key": "SK" },
895                "expiration-time-ms": 1234567890000_i64
896            }]
897        }));
898    }
899
900    #[test]
901    fn catalog_config() {
902        round_trip::<DeltaCatalogConfig>(json!({
903            "endpoints": ["GET /v1/temporary-path-credentials"],
904            "protocol-version": "1.0"
905        }));
906    }
907
908    #[test]
909    fn report_metrics_request() {
910        round_trip::<DeltaReportMetricsRequest>(json!({
911            "table-id": "123e4567-e89b-12d3-a456-426614174000",
912            "report": {
913                "commit-report": {
914                    "num-files-added": 10,
915                    "num-bytes-added": 104857600_i64,
916                    "file-size-histogram": {
917                        "sorted-bin-boundaries": [0, 1024, 2048],
918                        "file-counts": [100, 40, 5],
919                        "total-bytes": [104857600_i64, 167772160_i64, 83886080_i64],
920                        "commit-version": 6
921                    }
922                }
923            }
924        }));
925    }
926
927    #[test]
928    fn error_response_round_trips() {
929        round_trip::<DeltaErrorResponse>(json!({
930            "error": {
931                "message": "table not found",
932                "type": "NoSuchTableException",
933                "code": 404
934            }
935        }));
936    }
937
938    #[test]
939    fn error_response_with_stack_round_trips() {
940        round_trip::<DeltaErrorResponse>(json!({
941            "error": {
942                "message": "boom",
943                "type": "InternalServerErrorException",
944                "code": 500,
945                "stack": ["frame one", "frame two"]
946            }
947        }));
948    }
949
950    #[test]
951    fn uniform_metadata_round_trips() {
952        round_trip::<DeltaUniformMetadata>(json!({
953            "iceberg": {
954                "metadata-location": "s3://bucket/metadata/v1.json",
955                "converted-delta-version": 42,
956                "converted-delta-timestamp": 1704067200000_i64
957            }
958        }));
959    }
960}