Skip to content

Commit e3eeb9e

Browse files
authored
feat(storage): add analytics (Iceberg) bucket CRUD (#1588)
## Summary Adds support for analytics (Apache Iceberg) buckets to `storage_client`, covering the bucket lifecycle: create, list, and delete. These map to the storage server's Iceberg bucket endpoints under `/iceberg/bucket`. New public API on the storage bucket client: - `createAnalyticsBucket(String id)` → `AnalyticsBucket` — `POST /iceberg/bucket` - `listAnalyticsBuckets([ListBucketsOptions options])` → `List<AnalyticsBucket>` — `GET /iceberg/bucket` - `deleteAnalyticsBucket(String id)` → `String` — `DELETE /iceberg/bucket/:name` Listing reuses the existing `ListBucketsOptions` (limit/offset/search/sortColumn/sortOrder), matching the file bucket listing API. A new `AnalyticsBucket` type models the response (`id`, `name`, `createdAt`, `updatedAt`). ## Scope This PR covers the analytics **bucket** capabilities only. The Iceberg REST Catalog operations (namespaces and tables) are a much larger surface (a standalone `iceberg-js`-equivalent client) and are tracked as a follow-up in SDK-1309. ## Outcome `implemented` for: - `storage.analytics.create_analytics_bucket` - `storage.analytics.list_analytics_buckets` - `storage.analytics.delete_analytics_bucket` ## Tests Added unit tests in `basic_test.dart` (mock HTTP client) verifying request method/path/body and response parsing for all three methods. Full `storage_client` mock test suite passes. ## Reference Supabase Storage server Iceberg bucket routes (`src/http/routes/iceberg/bucket.ts`). In supabase-js the analytics bucket API is not yet in the storage client; the Iceberg catalog lives in the standalone `supabase/iceberg-js` package. ## Compliance matrix `sdk-compliance.yaml`: the three capabilities above → `implemented`, with new public symbols registered. Part of SDK-1308.
1 parent b994dab commit e3eeb9e

12 files changed

Lines changed: 3000 additions & 5 deletions
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/// Error thrown by [IcebergRestCatalog] operations when the Iceberg REST
2+
/// Catalog API returns an error response or a request fails at the network
3+
/// level.
4+
///
5+
/// This is a sealed hierarchy: match on the concrete subtype to handle a
6+
/// specific failure, for example
7+
///
8+
/// ```dart
9+
/// try {
10+
/// await catalog.loadTable(id);
11+
/// } on IcebergNotFoundException {
12+
/// // the table does not exist
13+
/// } on IcebergException catch (error) {
14+
/// // any other Iceberg failure
15+
/// }
16+
/// ```
17+
sealed class IcebergException implements Exception {
18+
/// Human readable error message.
19+
final String message;
20+
21+
/// The HTTP status code of the response. `0` indicates a network level
22+
/// failure before a response was received.
23+
final int statusCode;
24+
25+
/// The Iceberg error type reported by the server, for example
26+
/// `NoSuchTableException`.
27+
final String? type;
28+
29+
/// The Iceberg error code reported by the server.
30+
final int? code;
31+
32+
/// The raw error payload, when available.
33+
final Object? details;
34+
35+
const IcebergException(
36+
this.message, {
37+
required this.statusCode,
38+
this.type,
39+
this.code,
40+
this.details,
41+
});
42+
43+
/// Builds the appropriate [IcebergException] subtype from an error response.
44+
factory IcebergException.fromResponse(int statusCode, Object? body) {
45+
var message = 'Request failed with status $statusCode';
46+
String? type;
47+
int? code;
48+
if (body is Map<String, dynamic> && body['error'] is Map) {
49+
final error = body['error'] as Map<String, dynamic>;
50+
message = (error['message'] as String?) ?? message;
51+
type = error['type'] as String?;
52+
code = error['code'] as int?;
53+
}
54+
55+
if (type == 'CommitStateUnknownException') {
56+
return IcebergCommitStateUnknownException(
57+
message,
58+
statusCode: statusCode,
59+
code: code,
60+
details: body,
61+
);
62+
}
63+
64+
return switch (statusCode) {
65+
404 => IcebergNotFoundException(
66+
message,
67+
type: type,
68+
code: code,
69+
details: body,
70+
),
71+
409 => IcebergConflictException(
72+
message,
73+
type: type,
74+
code: code,
75+
details: body,
76+
),
77+
419 => IcebergAuthenticationTimeoutException(
78+
message,
79+
type: type,
80+
code: code,
81+
details: body,
82+
),
83+
>= 500 => IcebergServerException(
84+
message,
85+
statusCode: statusCode,
86+
type: type,
87+
code: code,
88+
details: body,
89+
),
90+
_ => IcebergUnknownException(
91+
message,
92+
statusCode: statusCode,
93+
type: type,
94+
code: code,
95+
details: body,
96+
),
97+
};
98+
}
99+
100+
@override
101+
String toString() =>
102+
'$runtimeType(message: $message, statusCode: $statusCode, '
103+
'type: $type, code: $code)';
104+
}
105+
106+
/// A request failed at the network level, before any response was received.
107+
final class IcebergNetworkException extends IcebergException {
108+
const IcebergNetworkException(super.message, {super.details})
109+
: super(statusCode: 0);
110+
}
111+
112+
/// The requested namespace or table does not exist (HTTP 404).
113+
final class IcebergNotFoundException extends IcebergException {
114+
const IcebergNotFoundException(
115+
super.message, {
116+
super.type,
117+
super.code,
118+
super.details,
119+
}) : super(statusCode: 404);
120+
}
121+
122+
/// The request conflicts with the current state, for example the resource
123+
/// already exists or a commit lost a race (HTTP 409).
124+
final class IcebergConflictException extends IcebergException {
125+
const IcebergConflictException(
126+
super.message, {
127+
super.type,
128+
super.code,
129+
super.details,
130+
}) : super(statusCode: 409);
131+
}
132+
133+
/// Authentication timed out and the request should be retried with fresh
134+
/// credentials (HTTP 419).
135+
final class IcebergAuthenticationTimeoutException extends IcebergException {
136+
const IcebergAuthenticationTimeoutException(
137+
super.message, {
138+
super.type,
139+
super.code,
140+
super.details,
141+
}) : super(statusCode: 419);
142+
}
143+
144+
/// A table commit was sent but its outcome is unknown, so retrying it could
145+
/// duplicate data.
146+
final class IcebergCommitStateUnknownException extends IcebergException {
147+
const IcebergCommitStateUnknownException(
148+
super.message, {
149+
required super.statusCode,
150+
super.code,
151+
super.details,
152+
}) : super(type: 'CommitStateUnknownException');
153+
}
154+
155+
/// The server failed to handle the request (HTTP 5xx).
156+
final class IcebergServerException extends IcebergException {
157+
const IcebergServerException(
158+
super.message, {
159+
required super.statusCode,
160+
super.type,
161+
super.code,
162+
super.details,
163+
});
164+
}
165+
166+
/// Any Iceberg failure that does not fit a more specific subtype.
167+
final class IcebergUnknownException extends IcebergException {
168+
const IcebergUnknownException(
169+
super.message, {
170+
required super.statusCode,
171+
super.type,
172+
super.code,
173+
super.details,
174+
});
175+
}

0 commit comments

Comments
 (0)