mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 07:57:01 +00:00
feat(table-catalog): add refs and views semantics (#3502)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -718,6 +718,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
|||||||
GET_TABLE_METADATA,
|
GET_TABLE_METADATA,
|
||||||
RouteRiskLevel::Sensitive,
|
RouteRiskLevel::Sensitive,
|
||||||
),
|
),
|
||||||
|
admin(
|
||||||
|
HttpMethod::Head,
|
||||||
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
|
GET_TABLE,
|
||||||
|
RouteRiskLevel::Sensitive,
|
||||||
|
),
|
||||||
admin(
|
admin(
|
||||||
HttpMethod::Post,
|
HttpMethod::Post,
|
||||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
@@ -736,6 +742,18 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
|||||||
GET_TABLE_METADATA,
|
GET_TABLE_METADATA,
|
||||||
RouteRiskLevel::Sensitive,
|
RouteRiskLevel::Sensitive,
|
||||||
),
|
),
|
||||||
|
admin(
|
||||||
|
HttpMethod::Put,
|
||||||
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
COMMIT_TABLE,
|
||||||
|
RouteRiskLevel::High,
|
||||||
|
),
|
||||||
|
admin(
|
||||||
|
HttpMethod::Delete,
|
||||||
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
COMMIT_TABLE,
|
||||||
|
RouteRiskLevel::High,
|
||||||
|
),
|
||||||
admin(
|
admin(
|
||||||
HttpMethod::Get,
|
HttpMethod::Get,
|
||||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/metadata-location",
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/metadata-location",
|
||||||
@@ -929,6 +947,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
|||||||
GET_TABLE_METADATA,
|
GET_TABLE_METADATA,
|
||||||
RouteRiskLevel::Sensitive,
|
RouteRiskLevel::Sensitive,
|
||||||
),
|
),
|
||||||
|
admin(
|
||||||
|
HttpMethod::Head,
|
||||||
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
|
GET_TABLE,
|
||||||
|
RouteRiskLevel::Sensitive,
|
||||||
|
),
|
||||||
admin(
|
admin(
|
||||||
HttpMethod::Post,
|
HttpMethod::Post,
|
||||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
@@ -947,6 +971,18 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
|||||||
GET_TABLE_METADATA,
|
GET_TABLE_METADATA,
|
||||||
RouteRiskLevel::Sensitive,
|
RouteRiskLevel::Sensitive,
|
||||||
),
|
),
|
||||||
|
admin(
|
||||||
|
HttpMethod::Put,
|
||||||
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
COMMIT_TABLE,
|
||||||
|
RouteRiskLevel::High,
|
||||||
|
),
|
||||||
|
admin(
|
||||||
|
HttpMethod::Delete,
|
||||||
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
COMMIT_TABLE,
|
||||||
|
RouteRiskLevel::High,
|
||||||
|
),
|
||||||
admin(
|
admin(
|
||||||
HttpMethod::Get,
|
HttpMethod::Get,
|
||||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/metadata-location",
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/metadata-location",
|
||||||
@@ -1214,7 +1250,7 @@ mod tests {
|
|||||||
let table_specs = ADMIN_ROUTE_POLICY_SPECS
|
let table_specs = ADMIN_ROUTE_POLICY_SPECS
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|spec| spec.path().starts_with("/iceberg/v1") || spec.path().starts_with("/_iceberg/v1"));
|
.filter(|spec| spec.path().starts_with("/iceberg/v1") || spec.path().starts_with("/_iceberg/v1"));
|
||||||
assert_eq!(table_specs.count(), 72);
|
assert_eq!(table_specs.count(), 78);
|
||||||
assert_action(HttpMethod::Put, "/iceberg/v1/buckets/{warehouse}", SET_TABLE_BUCKET);
|
assert_action(HttpMethod::Put, "/iceberg/v1/buckets/{warehouse}", SET_TABLE_BUCKET);
|
||||||
assert_action(HttpMethod::Get, "/_iceberg/v1/buckets/{warehouse}", GET_TABLE_BUCKET);
|
assert_action(HttpMethod::Get, "/_iceberg/v1/buckets/{warehouse}", GET_TABLE_BUCKET);
|
||||||
assert_action(HttpMethod::Get, "/iceberg/v1/{warehouse}/namespaces", GET_TABLE_NAMESPACE);
|
assert_action(HttpMethod::Get, "/iceberg/v1/{warehouse}/namespaces", GET_TABLE_NAMESPACE);
|
||||||
@@ -1244,6 +1280,11 @@ mod tests {
|
|||||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
GET_TABLE_METADATA,
|
GET_TABLE_METADATA,
|
||||||
);
|
);
|
||||||
|
assert_action(
|
||||||
|
HttpMethod::Head,
|
||||||
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
|
GET_TABLE,
|
||||||
|
);
|
||||||
assert_action(
|
assert_action(
|
||||||
HttpMethod::Post,
|
HttpMethod::Post,
|
||||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
@@ -1259,6 +1300,16 @@ mod tests {
|
|||||||
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs",
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs",
|
||||||
GET_TABLE_METADATA,
|
GET_TABLE_METADATA,
|
||||||
);
|
);
|
||||||
|
assert_action(
|
||||||
|
HttpMethod::Put,
|
||||||
|
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
COMMIT_TABLE,
|
||||||
|
);
|
||||||
|
assert_action(
|
||||||
|
HttpMethod::Delete,
|
||||||
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
COMMIT_TABLE,
|
||||||
|
);
|
||||||
assert_action(
|
assert_action(
|
||||||
HttpMethod::Get,
|
HttpMethod::Get,
|
||||||
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
"/_iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
|
||||||
|
|||||||
@@ -352,6 +352,11 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
|||||||
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
"/analytics/namespaces/sales/views/recent_orders",
|
"/analytics/namespaces/sales/views/recent_orders",
|
||||||
),
|
),
|
||||||
|
table_route_sample(
|
||||||
|
Method::HEAD,
|
||||||
|
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
|
"/analytics/namespaces/sales/views/recent_orders",
|
||||||
|
),
|
||||||
table_route_sample(
|
table_route_sample(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
@@ -367,6 +372,16 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
|||||||
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs",
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs",
|
||||||
"/analytics/namespaces/sales/tables/orders/refs",
|
"/analytics/namespaces/sales/tables/orders/refs",
|
||||||
),
|
),
|
||||||
|
table_route_sample(
|
||||||
|
Method::PUT,
|
||||||
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
"/analytics/namespaces/sales/tables/orders/refs/audit",
|
||||||
|
),
|
||||||
|
table_route_sample(
|
||||||
|
Method::DELETE,
|
||||||
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
"/analytics/namespaces/sales/tables/orders/refs/audit",
|
||||||
|
),
|
||||||
table_route_sample(
|
table_route_sample(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"/{warehouse}/namespaces/{namespace}/tables/{table}/maintenance/metadata",
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/maintenance/metadata",
|
||||||
@@ -500,6 +515,11 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
|||||||
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
"/analytics/namespaces/sales/views/recent_orders",
|
"/analytics/namespaces/sales/views/recent_orders",
|
||||||
),
|
),
|
||||||
|
compat_table_route_sample(
|
||||||
|
Method::HEAD,
|
||||||
|
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
|
"/analytics/namespaces/sales/views/recent_orders",
|
||||||
|
),
|
||||||
compat_table_route_sample(
|
compat_table_route_sample(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
"/{warehouse}/namespaces/{namespace}/views/{view}",
|
||||||
@@ -515,6 +535,16 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
|||||||
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs",
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs",
|
||||||
"/analytics/namespaces/sales/tables/orders/refs",
|
"/analytics/namespaces/sales/tables/orders/refs",
|
||||||
),
|
),
|
||||||
|
compat_table_route_sample(
|
||||||
|
Method::PUT,
|
||||||
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
"/analytics/namespaces/sales/tables/orders/refs/audit",
|
||||||
|
),
|
||||||
|
compat_table_route_sample(
|
||||||
|
Method::DELETE,
|
||||||
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/refs/{ref}",
|
||||||
|
"/analytics/namespaces/sales/tables/orders/refs/audit",
|
||||||
|
),
|
||||||
compat_table_route_sample(
|
compat_table_route_sample(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"/{warehouse}/namespaces/{namespace}/tables/{table}/maintenance/metadata",
|
"/{warehouse}/namespaces/{namespace}/tables/{table}/maintenance/metadata",
|
||||||
@@ -719,6 +749,11 @@ fn test_register_routes_cover_representative_admin_paths() {
|
|||||||
Method::GET,
|
Method::GET,
|
||||||
&table_catalog_path("/analytics/namespaces/sales/views/recent_orders"),
|
&table_catalog_path("/analytics/namespaces/sales/views/recent_orders"),
|
||||||
);
|
);
|
||||||
|
assert_route(
|
||||||
|
&router,
|
||||||
|
Method::HEAD,
|
||||||
|
&table_catalog_path("/analytics/namespaces/sales/views/recent_orders"),
|
||||||
|
);
|
||||||
assert_route(
|
assert_route(
|
||||||
&router,
|
&router,
|
||||||
Method::POST,
|
Method::POST,
|
||||||
@@ -734,6 +769,16 @@ fn test_register_routes_cover_representative_admin_paths() {
|
|||||||
Method::GET,
|
Method::GET,
|
||||||
&table_catalog_path("/analytics/namespaces/sales/tables/orders/refs"),
|
&table_catalog_path("/analytics/namespaces/sales/tables/orders/refs"),
|
||||||
);
|
);
|
||||||
|
assert_route(
|
||||||
|
&router,
|
||||||
|
Method::PUT,
|
||||||
|
&table_catalog_path("/analytics/namespaces/sales/tables/orders/refs/audit"),
|
||||||
|
);
|
||||||
|
assert_route(
|
||||||
|
&router,
|
||||||
|
Method::DELETE,
|
||||||
|
&table_catalog_path("/analytics/namespaces/sales/tables/orders/refs/audit"),
|
||||||
|
);
|
||||||
assert_route(
|
assert_route(
|
||||||
&router,
|
&router,
|
||||||
Method::POST,
|
Method::POST,
|
||||||
@@ -841,6 +886,11 @@ fn test_register_routes_cover_representative_admin_paths() {
|
|||||||
Method::GET,
|
Method::GET,
|
||||||
&compat_table_catalog_path("/analytics/namespaces/sales/views/recent_orders"),
|
&compat_table_catalog_path("/analytics/namespaces/sales/views/recent_orders"),
|
||||||
);
|
);
|
||||||
|
assert_route(
|
||||||
|
&router,
|
||||||
|
Method::HEAD,
|
||||||
|
&compat_table_catalog_path("/analytics/namespaces/sales/views/recent_orders"),
|
||||||
|
);
|
||||||
assert_route(
|
assert_route(
|
||||||
&router,
|
&router,
|
||||||
Method::POST,
|
Method::POST,
|
||||||
@@ -856,6 +906,16 @@ fn test_register_routes_cover_representative_admin_paths() {
|
|||||||
Method::GET,
|
Method::GET,
|
||||||
&compat_table_catalog_path("/analytics/namespaces/sales/tables/orders/refs"),
|
&compat_table_catalog_path("/analytics/namespaces/sales/tables/orders/refs"),
|
||||||
);
|
);
|
||||||
|
assert_route(
|
||||||
|
&router,
|
||||||
|
Method::PUT,
|
||||||
|
&compat_table_catalog_path("/analytics/namespaces/sales/tables/orders/refs/audit"),
|
||||||
|
);
|
||||||
|
assert_route(
|
||||||
|
&router,
|
||||||
|
Method::DELETE,
|
||||||
|
&compat_table_catalog_path("/analytics/namespaces/sales/tables/orders/refs/audit"),
|
||||||
|
);
|
||||||
assert_route(
|
assert_route(
|
||||||
&router,
|
&router,
|
||||||
Method::POST,
|
Method::POST,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ pub const TABLE_RESERVED_PREFIX: &str = BUCKET_TABLE_RESERVED_PREFIX;
|
|||||||
const WAREHOUSE_ROOT: &str = "warehouses";
|
const WAREHOUSE_ROOT: &str = "warehouses";
|
||||||
const NAMESPACE_ROOT: &str = "namespaces";
|
const NAMESPACE_ROOT: &str = "namespaces";
|
||||||
const TABLE_ROOT: &str = "tables";
|
const TABLE_ROOT: &str = "tables";
|
||||||
|
const VIEW_ROOT: &str = "views";
|
||||||
const NAMESPACE_MARKER_FILE: &str = "namespace.json";
|
const NAMESPACE_MARKER_FILE: &str = "namespace.json";
|
||||||
const TABLE_MARKER_FILE: &str = "table.json";
|
const TABLE_MARKER_FILE: &str = "table.json";
|
||||||
const CURRENT_POINTER_FILE: &str = "current.json";
|
const CURRENT_POINTER_FILE: &str = "current.json";
|
||||||
@@ -77,6 +78,7 @@ const DELETE_DIR: &str = "delete";
|
|||||||
const TABLE_BUCKET_ENTRY_FILE: &str = "table-bucket.json";
|
const TABLE_BUCKET_ENTRY_FILE: &str = "table-bucket.json";
|
||||||
const NAMESPACE_ENTRY_FILE: &str = "namespace-entry.json";
|
const NAMESPACE_ENTRY_FILE: &str = "namespace-entry.json";
|
||||||
const TABLE_ENTRY_FILE: &str = "table-entry.json";
|
const TABLE_ENTRY_FILE: &str = "table-entry.json";
|
||||||
|
const VIEW_ENTRY_FILE: &str = "view-entry.json";
|
||||||
const INTERNAL_CATALOG_ROOT: &str = BUCKET_TABLE_CATALOG_META_PREFIX;
|
const INTERNAL_CATALOG_ROOT: &str = BUCKET_TABLE_CATALOG_META_PREFIX;
|
||||||
const TABLE_BUCKET_ROOT: &str = BUCKET_TABLE_CATALOG_TABLE_BUCKETS_PREFIX;
|
const TABLE_BUCKET_ROOT: &str = BUCKET_TABLE_CATALOG_TABLE_BUCKETS_PREFIX;
|
||||||
const COMMIT_LOG_ROOT: &str = "commits";
|
const COMMIT_LOG_ROOT: &str = "commits";
|
||||||
@@ -221,6 +223,28 @@ pub(crate) struct TableEntry {
|
|||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub(crate) struct ViewEntry {
|
||||||
|
pub version: u16,
|
||||||
|
pub table_bucket: String,
|
||||||
|
pub namespace: String,
|
||||||
|
pub view: String,
|
||||||
|
pub view_id: String,
|
||||||
|
pub view_uuid: String,
|
||||||
|
pub format: String,
|
||||||
|
pub format_version: u16,
|
||||||
|
pub warehouse_location: String,
|
||||||
|
pub metadata_location: String,
|
||||||
|
pub version_token: String,
|
||||||
|
pub generation: u64,
|
||||||
|
pub state: TableCatalogEntryState,
|
||||||
|
#[serde(default)]
|
||||||
|
pub properties: BTreeMap<String, String>,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
pub(crate) enum CommitLogStatus {
|
pub(crate) enum CommitLogStatus {
|
||||||
@@ -273,6 +297,23 @@ pub(crate) struct TableCommitResult {
|
|||||||
pub commit_log: CommitLogEntry,
|
pub commit_log: CommitLogEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub(crate) struct ViewCommitRequest {
|
||||||
|
pub table_bucket: String,
|
||||||
|
pub namespace: String,
|
||||||
|
pub view: String,
|
||||||
|
pub expected_version_token: String,
|
||||||
|
pub expected_metadata_location: String,
|
||||||
|
pub new_metadata_location: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub(crate) struct ViewCommitResult {
|
||||||
|
pub view: ViewEntry,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub(crate) struct TableMaintenanceConfig {
|
pub(crate) struct TableMaintenanceConfig {
|
||||||
@@ -984,6 +1025,16 @@ pub(crate) trait TableCatalogStore: Send + Sync {
|
|||||||
|
|
||||||
async fn drop_table(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()>;
|
async fn drop_table(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()>;
|
||||||
|
|
||||||
|
async fn create_view(&self, entry: ViewEntry) -> TableCatalogStoreResult<()>;
|
||||||
|
|
||||||
|
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>>;
|
||||||
|
|
||||||
|
async fn load_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<Option<ViewEntry>>;
|
||||||
|
|
||||||
|
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult>;
|
||||||
|
|
||||||
|
async fn drop_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<()>;
|
||||||
|
|
||||||
async fn get_commit_by_id(
|
async fn get_commit_by_id(
|
||||||
&self,
|
&self,
|
||||||
table_bucket: &str,
|
table_bucket: &str,
|
||||||
@@ -1104,6 +1155,19 @@ impl TableCatalogObjectPaths {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn view_entries_prefix(&self, table_bucket: &str, namespace: &Namespace) -> String {
|
||||||
|
format!("{}{}/{}/", self.namespace_entries_prefix(table_bucket), namespace.storage_id(), VIEW_ROOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_entry_path(&self, table_bucket: &str, namespace: &Namespace, view: &IdentifierSegment) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}/{}",
|
||||||
|
self.view_entries_prefix(table_bucket, namespace),
|
||||||
|
view.as_str(),
|
||||||
|
VIEW_ENTRY_FILE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn table_maintenance_config_path(
|
pub fn table_maintenance_config_path(
|
||||||
&self,
|
&self,
|
||||||
table_bucket: &str,
|
table_bucket: &str,
|
||||||
@@ -1338,6 +1402,25 @@ where
|
|||||||
Ok(Some((entry, etag)))
|
Ok(Some((entry, etag)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_view_with_etag_unlocked(
|
||||||
|
&self,
|
||||||
|
table_bucket: &str,
|
||||||
|
namespace: &Namespace,
|
||||||
|
view: &IdentifierSegment,
|
||||||
|
) -> TableCatalogStoreResult<Option<(ViewEntry, String)>> {
|
||||||
|
let view_path = self.paths.view_entry_path(table_bucket, namespace, view);
|
||||||
|
let Some((entry, etag)) = self
|
||||||
|
.read_entry_unlocked::<ViewEntry>(self.catalog_bucket(), &view_path)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(etag) = etag else {
|
||||||
|
return Err(TableCatalogStoreError::Internal(format!("catalog view entry has no etag: {view_path}")));
|
||||||
|
};
|
||||||
|
Ok(Some((entry, etag)))
|
||||||
|
}
|
||||||
|
|
||||||
async fn write_table_entry(
|
async fn write_table_entry(
|
||||||
&self,
|
&self,
|
||||||
entry: TableEntry,
|
entry: TableEntry,
|
||||||
@@ -1358,6 +1441,22 @@ where
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn write_view_entry(&self, entry: ViewEntry, precondition: TableCatalogPutPrecondition) -> TableCatalogStoreResult<()> {
|
||||||
|
validate_catalog_entry_version("view", entry.version)?;
|
||||||
|
self.require_table_bucket(&entry.table_bucket).await?;
|
||||||
|
let namespace = parse_namespace_for_store(&entry.namespace)?;
|
||||||
|
let view = parse_table_for_store(&entry.view)?;
|
||||||
|
if self.get_namespace(&entry.table_bucket, &entry.namespace).await?.is_none() {
|
||||||
|
return Err(TableCatalogStoreError::NotFound(format!(
|
||||||
|
"namespace {}/{}",
|
||||||
|
entry.table_bucket, entry.namespace
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let view_path = self.paths.view_entry_path(&entry.table_bucket, &namespace, &view);
|
||||||
|
self.write_entry(self.catalog_bucket(), &view_path, &entry, precondition)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn read_commit_by_path(&self, object: &str) -> TableCatalogStoreResult<Option<CommitLogEntry>> {
|
async fn read_commit_by_path(&self, object: &str) -> TableCatalogStoreResult<Option<CommitLogEntry>> {
|
||||||
self.read_entry::<CommitLogEntry>(self.catalog_bucket(), object)
|
self.read_entry::<CommitLogEntry>(self.catalog_bucket(), object)
|
||||||
.await
|
.await
|
||||||
@@ -2912,6 +3011,13 @@ where
|
|||||||
namespace.public_name()
|
namespace.public_name()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
if !self.list_views(table_bucket, &namespace.public_name()).await?.is_empty() {
|
||||||
|
return Err(TableCatalogStoreError::Conflict(format!(
|
||||||
|
"namespace {}/{} is not empty",
|
||||||
|
table_bucket,
|
||||||
|
namespace.public_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
self.backend
|
self.backend
|
||||||
.delete_object(self.catalog_bucket(), &self.paths.namespace_entry_path(table_bucket, &namespace))
|
.delete_object(self.catalog_bucket(), &self.paths.namespace_entry_path(table_bucket, &namespace))
|
||||||
.await
|
.await
|
||||||
@@ -3234,6 +3340,115 @@ where
|
|||||||
self.backend.delete_object(self.catalog_bucket(), &object).await
|
self.backend.delete_object(self.catalog_bucket(), &object).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_view(&self, entry: ViewEntry) -> TableCatalogStoreResult<()> {
|
||||||
|
self.write_view_entry(entry, TableCatalogPutPrecondition::IfAbsent).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>> {
|
||||||
|
let namespace = parse_namespace_for_store(namespace)?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for object in self
|
||||||
|
.backend
|
||||||
|
.list_objects(self.catalog_bucket(), &self.paths.view_entries_prefix(table_bucket, &namespace))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if !object.ends_with(VIEW_ENTRY_FILE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some((entry, _)) = self.read_entry::<ViewEntry>(self.catalog_bucket(), &object).await? {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.sort_by(|left, right| left.view.cmp(&right.view));
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<Option<ViewEntry>> {
|
||||||
|
let namespace = parse_namespace_for_store(namespace)?;
|
||||||
|
let view = parse_table_for_store(view)?;
|
||||||
|
self.read_entry::<ViewEntry>(self.catalog_bucket(), &self.paths.view_entry_path(table_bucket, &namespace, &view))
|
||||||
|
.await
|
||||||
|
.map(|entry| entry.map(|(view, _)| view))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
|
let namespace = parse_namespace_for_store(&request.namespace)?;
|
||||||
|
let view = parse_table_for_store(&request.view)?;
|
||||||
|
let view_path = self.paths.view_entry_path(&request.table_bucket, &namespace, &view);
|
||||||
|
let _guard = self.backend.acquire_write_lock(self.catalog_bucket(), &view_path).await?;
|
||||||
|
let Some((current, current_etag)) = self
|
||||||
|
.read_view_with_etag_unlocked(&request.table_bucket, &namespace, &view)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Err(TableCatalogStoreError::NotFound(format!(
|
||||||
|
"view {}/{}/{}",
|
||||||
|
request.table_bucket, request.namespace, request.view
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
if current.version_token != request.expected_version_token {
|
||||||
|
return Err(TableCatalogStoreError::Conflict(
|
||||||
|
"current view version token does not match expected token".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if current.metadata_location != request.expected_metadata_location {
|
||||||
|
return Err(TableCatalogStoreError::Conflict(
|
||||||
|
"current view metadata location does not match expected location".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !is_valid_view_metadata_location(&namespace, &view, &request.new_metadata_location) {
|
||||||
|
return Err(TableCatalogStoreError::Invalid(
|
||||||
|
"new metadata location must be inside the view metadata directory".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let Some(new_metadata_object) = self
|
||||||
|
.backend
|
||||||
|
.read_object(&request.table_bucket, &request.new_metadata_location)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Err(TableCatalogStoreError::NotFound(format!(
|
||||||
|
"new view metadata object {}",
|
||||||
|
request.new_metadata_location
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
let next_warehouse_location =
|
||||||
|
metadata_warehouse_location(&request.table_bucket, &request.new_metadata_location, &new_metadata_object)?;
|
||||||
|
|
||||||
|
let mut next = current;
|
||||||
|
next.metadata_location = request.new_metadata_location;
|
||||||
|
if let Some(warehouse_location) = next_warehouse_location {
|
||||||
|
next.warehouse_location = warehouse_location;
|
||||||
|
}
|
||||||
|
next.version_token = format!("token-{}", Uuid::new_v4());
|
||||||
|
next.generation = next.generation.saturating_add(1);
|
||||||
|
self.write_entry_unlocked(
|
||||||
|
self.catalog_bucket(),
|
||||||
|
&view_path,
|
||||||
|
&next,
|
||||||
|
TableCatalogPutPrecondition::IfMatch(current_etag),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(ViewCommitResult { view: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn drop_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<()> {
|
||||||
|
let namespace = parse_namespace_for_store(namespace)?;
|
||||||
|
let view = parse_table_for_store(view)?;
|
||||||
|
let object = self.paths.view_entry_path(table_bucket, &namespace, &view);
|
||||||
|
if self
|
||||||
|
.load_view(table_bucket, &namespace.public_name(), view.as_str())
|
||||||
|
.await?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Err(TableCatalogStoreError::NotFound(format!(
|
||||||
|
"view {}/{}/{}",
|
||||||
|
table_bucket,
|
||||||
|
namespace.public_name(),
|
||||||
|
view.as_str()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
self.backend.delete_object(self.catalog_bucket(), &object).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_commit_by_id(
|
async fn get_commit_by_id(
|
||||||
&self,
|
&self,
|
||||||
table_bucket: &str,
|
table_bucket: &str,
|
||||||
@@ -6163,6 +6378,22 @@ pub(crate) fn default_table_delete_dir_path(namespace: &Namespace, table: &Ident
|
|||||||
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), DELETE_DIR)
|
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), DELETE_DIR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_view_root_prefix(namespace: &Namespace) -> String {
|
||||||
|
format!("{}{}/{}/", default_namespace_root_prefix(), namespace.storage_id(), VIEW_ROOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_view_metadata_dir_path(namespace: &Namespace, view: &IdentifierSegment) -> String {
|
||||||
|
format!("{}{}/{}", default_view_root_prefix(namespace), view.as_str(), METADATA_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_view_metadata_file_path(
|
||||||
|
namespace: &Namespace,
|
||||||
|
view: &IdentifierSegment,
|
||||||
|
metadata_file_name: &str,
|
||||||
|
) -> String {
|
||||||
|
format!("{}/{}", default_view_metadata_dir_path(namespace, view), metadata_file_name)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_table_metadata_file_path(
|
pub(crate) fn default_table_metadata_file_path(
|
||||||
namespace: &Namespace,
|
namespace: &Namespace,
|
||||||
table: &IdentifierSegment,
|
table: &IdentifierSegment,
|
||||||
@@ -6229,6 +6460,17 @@ pub(crate) fn is_valid_table_metadata_location(
|
|||||||
.is_some_and(is_valid_table_metadata_file_name)
|
.is_some_and(is_valid_table_metadata_file_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_valid_view_metadata_location(namespace: &Namespace, view: &IdentifierSegment, metadata_location: &str) -> bool {
|
||||||
|
if metadata_location.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata_prefix = format!("{}/", default_view_metadata_dir_path(namespace, view));
|
||||||
|
metadata_location
|
||||||
|
.strip_prefix(&metadata_prefix)
|
||||||
|
.is_some_and(is_valid_table_metadata_file_name)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn is_valid_table_metadata_file_name(metadata_file_name: &str) -> bool {
|
pub(crate) fn is_valid_table_metadata_file_name(metadata_file_name: &str) -> bool {
|
||||||
if metadata_file_name.is_empty()
|
if metadata_file_name.is_empty()
|
||||||
|| metadata_file_name.len() > TABLE_METADATA_FILE_NAME_MAX_LEN
|
|| metadata_file_name.len() > TABLE_METADATA_FILE_NAME_MAX_LEN
|
||||||
@@ -6554,6 +6796,50 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_view(&self, _entry: ViewEntry) -> TableCatalogStoreResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_views(&self, _table_bucket: &str, _namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_view(
|
||||||
|
&self,
|
||||||
|
_table_bucket: &str,
|
||||||
|
_namespace: &str,
|
||||||
|
_view: &str,
|
||||||
|
) -> TableCatalogStoreResult<Option<ViewEntry>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
|
Ok(ViewCommitResult {
|
||||||
|
view: ViewEntry {
|
||||||
|
version: TABLE_CATALOG_ENTRY_VERSION,
|
||||||
|
table_bucket: request.table_bucket,
|
||||||
|
namespace: request.namespace,
|
||||||
|
view: request.view,
|
||||||
|
view_id: "view-id".to_string(),
|
||||||
|
view_uuid: "view-uuid".to_string(),
|
||||||
|
format: "ICEBERG_VIEW".to_string(),
|
||||||
|
format_version: 1,
|
||||||
|
warehouse_location: "s3://analytics/views/view-id".to_string(),
|
||||||
|
metadata_location: request.new_metadata_location,
|
||||||
|
version_token: "token-v2".to_string(),
|
||||||
|
generation: 2,
|
||||||
|
state: TableCatalogEntryState::Active,
|
||||||
|
properties: BTreeMap::new(),
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn drop_view(&self, _table_bucket: &str, _namespace: &str, _view: &str) -> TableCatalogStoreResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_commit_by_id(
|
async fn get_commit_by_id(
|
||||||
&self,
|
&self,
|
||||||
_table_bucket: &str,
|
_table_bucket: &str,
|
||||||
@@ -6636,6 +6922,10 @@ mod tests {
|
|||||||
paths.table_entry_path(bucket, &namespace, &table),
|
paths.table_entry_path(bucket, &namespace, &table),
|
||||||
format!("{bucket_root}namespaces/analytics/daily_events/tables/events/table-entry.json")
|
format!("{bucket_root}namespaces/analytics/daily_events/tables/events/table-entry.json")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
paths.view_entry_path(bucket, &namespace, &table),
|
||||||
|
format!("{bucket_root}namespaces/analytics/daily_events/views/events/view-entry.json")
|
||||||
|
);
|
||||||
|
|
||||||
let commit_path = paths.commit_log_entry_path("table/../bucket", "table/../id", "commit/%2f\nid");
|
let commit_path = paths.commit_log_entry_path("table/../bucket", "table/../id", "commit/%2f\nid");
|
||||||
let idempotency_path = paths.commit_idempotency_entry_path("table/../bucket", "table/../id", "client/%2f\nrequest");
|
let idempotency_path = paths.commit_idempotency_entry_path("table/../bucket", "table/../id", "client/%2f\nrequest");
|
||||||
@@ -7045,6 +7335,27 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_view_entry(bucket: &str, namespace: &Namespace, view: &IdentifierSegment, metadata_location: String) -> ViewEntry {
|
||||||
|
ViewEntry {
|
||||||
|
version: TABLE_CATALOG_ENTRY_VERSION,
|
||||||
|
table_bucket: bucket.to_string(),
|
||||||
|
namespace: namespace.public_name(),
|
||||||
|
view: view.as_str().to_string(),
|
||||||
|
view_id: "view-id".to_string(),
|
||||||
|
view_uuid: "view-uuid".to_string(),
|
||||||
|
format: "ICEBERG_VIEW".to_string(),
|
||||||
|
format_version: 1,
|
||||||
|
warehouse_location: format!("s3://{bucket}/views/view-id"),
|
||||||
|
metadata_location,
|
||||||
|
version_token: "token-v1".to_string(),
|
||||||
|
generation: 1,
|
||||||
|
state: TableCatalogEntryState::Active,
|
||||||
|
properties: BTreeMap::new(),
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn seed_table_for_metadata_maintenance(
|
async fn seed_table_for_metadata_maintenance(
|
||||||
store: &ObjectTableCatalogStore<TestCatalogObjectBackend>,
|
store: &ObjectTableCatalogStore<TestCatalogObjectBackend>,
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
@@ -7078,6 +7389,75 @@ mod tests {
|
|||||||
assert_eq!(object_buckets, BTreeSet::from([rustfs_ecstore::disk::RUSTFS_META_BUCKET]));
|
assert_eq!(object_buckets, BTreeSet::from([rustfs_ecstore::disk::RUSTFS_META_BUCKET]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn object_table_catalog_store_persists_view_entries_and_blocks_non_empty_namespace_drop() {
|
||||||
|
let backend = TestCatalogObjectBackend::default();
|
||||||
|
let store = ObjectTableCatalogStore::new(backend.clone());
|
||||||
|
let bucket = "analytics";
|
||||||
|
let namespace = Namespace::parse("sales").unwrap();
|
||||||
|
let view = IdentifierSegment::parse("recent_orders").unwrap();
|
||||||
|
let current_metadata = default_view_metadata_file_path(&namespace, &view, "00001.metadata.json");
|
||||||
|
let next_metadata = default_view_metadata_file_path(&namespace, &view, "00002.metadata.json");
|
||||||
|
|
||||||
|
store.put_table_bucket(test_bucket_entry(bucket)).await.unwrap();
|
||||||
|
store
|
||||||
|
.create_namespace(test_namespace_entry(bucket, &namespace))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.create_view(test_view_entry(bucket, &namespace, &view, current_metadata.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(store.list_views(bucket, &namespace.public_name()).await.unwrap()[0].view, "recent_orders");
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.load_view(bucket, &namespace.public_name(), view.as_str())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
store.drop_namespace(bucket, &namespace.public_name()).await,
|
||||||
|
Err(TableCatalogStoreError::Conflict(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
backend
|
||||||
|
.seed_object(
|
||||||
|
bucket,
|
||||||
|
&next_metadata,
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"format-version": 1,
|
||||||
|
"view-uuid": "view-uuid",
|
||||||
|
"location": format!("s3://{bucket}/views/view-id")
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let result = store
|
||||||
|
.replace_view(ViewCommitRequest {
|
||||||
|
table_bucket: bucket.to_string(),
|
||||||
|
namespace: namespace.public_name(),
|
||||||
|
view: view.as_str().to_string(),
|
||||||
|
expected_version_token: "token-v1".to_string(),
|
||||||
|
expected_metadata_location: current_metadata,
|
||||||
|
new_metadata_location: next_metadata.clone(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.view.metadata_location, next_metadata);
|
||||||
|
assert_eq!(result.view.generation, 2);
|
||||||
|
assert_ne!(result.view.version_token, "token-v1");
|
||||||
|
|
||||||
|
store
|
||||||
|
.drop_view(bucket, &namespace.public_name(), view.as_str())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(store.list_views(bucket, &namespace.public_name()).await.unwrap().is_empty());
|
||||||
|
store.drop_namespace(bucket, &namespace.public_name()).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn maintenance_dry_run_keeps_current_metadata() {
|
async fn maintenance_dry_run_keeps_current_metadata() {
|
||||||
let backend = TestCatalogObjectBackend::default();
|
let backend = TestCatalogObjectBackend::default();
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ work items. They are intentionally conservative: only PyIceberg is automated by
|
|||||||
this script today; other engines are documented until a repeatable harness is
|
this script today; other engines are documented until a repeatable harness is
|
||||||
added.
|
added.
|
||||||
|
|
||||||
|
RustFS also exposes catalog-backed advanced Iceberg surfaces that are not part
|
||||||
|
of the PyIceberg append smoke path yet:
|
||||||
|
|
||||||
|
- table refs can be listed, created or replaced, and deleted through catalog
|
||||||
|
commits; refs with explicit retention policy require a forced delete, and
|
||||||
|
`main` cannot be deleted
|
||||||
|
- Iceberg views support basic create, list, load, replace, existence check, and
|
||||||
|
drop routes with persisted view metadata and view-scoped authorization
|
||||||
|
|
||||||
## Client Matrix
|
## Client Matrix
|
||||||
|
|
||||||
| Client | Current status | Claim |
|
| Client | Current status | Claim |
|
||||||
@@ -154,7 +163,6 @@ current unsupported inventory is:
|
|||||||
- snapshot expiration dry-run planning and manual catalog commit: supported through metadata maintenance reports
|
- snapshot expiration dry-run planning and manual catalog commit: supported through metadata maintenance reports
|
||||||
- automatic maintenance scheduling: external scheduler hook supported through the worker run endpoint; built-in periodic scheduling is not claimed
|
- automatic maintenance scheduling: external scheduler hook supported through the worker run endpoint; built-in periodic scheduling is not claimed
|
||||||
- compaction rewrite: controlled run-once support for unpartitioned Parquet binpack through metadata maintenance; built-in periodic scheduling, sort compaction, delete-file rewrite, and row-level compaction are not claimed
|
- compaction rewrite: controlled run-once support for unpartitioned Parquet binpack through metadata maintenance; built-in periodic scheduling, sort compaction, delete-file rewrite, and row-level compaction are not claimed
|
||||||
- Iceberg views: stable unsupported routes are registered and return explicit unsupported JSON
|
|
||||||
- external catalog bridges: metadata import/register is supported, but Polaris/Glue/DLF/Hive synchronization is unsupported
|
- external catalog bridges: metadata import/register is supported, but Polaris/Glue/DLF/Hive synchronization is unsupported
|
||||||
- multi-table transactions: not a short-term production claim
|
- multi-table transactions: not a short-term production claim
|
||||||
|
|
||||||
|
|||||||
@@ -156,12 +156,6 @@ UNSUPPORTED_INVENTORY: list[dict[str, str]] = [
|
|||||||
"roadmap_area": "snapshot-maintenance",
|
"roadmap_area": "snapshot-maintenance",
|
||||||
"expected_behavior": "metadata maintenance can plan binpack candidates and commit a safe unpartitioned Parquet rewrite through the catalog; built-in periodic scheduling, sort compaction, delete-file rewrite, and row-level compaction are not claimed",
|
"expected_behavior": "metadata maintenance can plan binpack candidates and commit a safe unpartitioned Parquet rewrite through the catalog; built-in periodic scheduling, sort compaction, delete-file rewrite, and row-level compaction are not claimed",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"capability": "iceberg-views",
|
|
||||||
"status": "stable-unsupported-routes",
|
|
||||||
"roadmap_area": "view-api",
|
|
||||||
"expected_behavior": "view routes are registered and should return a stable unsupported JSON response until implemented",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"capability": "external-catalog-bridge",
|
"capability": "external-catalog-bridge",
|
||||||
"status": "metadata-import-only",
|
"status": "metadata-import-only",
|
||||||
|
|||||||
Reference in New Issue
Block a user