|
83 | 83 | from pyiceberg.table.metadata import TableMetadata |
84 | 84 | from pyiceberg.table.sorting import UNSORTED_SORT_ORDER, SortOrder, assign_fresh_sort_order_ids |
85 | 85 | from pyiceberg.table.update import ( |
| 86 | + AddSchemaUpdate, |
| 87 | + AddViewVersionUpdate, |
| 88 | + AssertViewUUID, |
| 89 | + SetCurrentViewVersionUpdate, |
| 90 | + SetLocationUpdate, |
| 91 | + SetPropertiesUpdate, |
86 | 92 | TableRequirement, |
87 | 93 | TableUpdate, |
| 94 | + ViewRequirement, |
| 95 | + ViewUpdate, |
88 | 96 | ) |
89 | 97 | from pyiceberg.typedef import EMPTY_DICT, UTF8, IcebergBaseModel, Identifier, Properties |
90 | 98 | from pyiceberg.types import transform_dict_value_to_str |
@@ -155,6 +163,7 @@ class Endpoints: |
155 | 163 | list_views: str = "namespaces/{namespace}/views" |
156 | 164 | load_view: str = "namespaces/{namespace}/views/{view}" |
157 | 165 | create_view: str = "namespaces/{namespace}/views" |
| 166 | + update_view: str = "namespaces/{namespace}/views/{view}" |
158 | 167 | register_view: str = "namespaces/{namespace}/register-view" |
159 | 168 | drop_view: str = "namespaces/{namespace}/views/{view}" |
160 | 169 | view_exists: str = "namespaces/{namespace}/views/{view}" |
@@ -185,6 +194,7 @@ class Capability: |
185 | 194 | V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}") |
186 | 195 | V1_LOAD_VIEW = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_view}") |
187 | 196 | V1_VIEW_EXISTS = Endpoint(http_method=HttpMethod.HEAD, path=f"{API_PREFIX}/{Endpoints.view_exists}") |
| 197 | + V1_UPDATE_VIEW = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.update_view}") |
188 | 198 | V1_REGISTER_VIEW = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_view}") |
189 | 199 | V1_DELETE_VIEW = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_view}") |
190 | 200 | V1_SUBMIT_TABLE_SCAN_PLAN = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.plan_table_scan}") |
@@ -215,6 +225,7 @@ class Capability: |
215 | 225 | ( |
216 | 226 | Capability.V1_LIST_VIEWS, |
217 | 227 | Capability.V1_LOAD_VIEW, |
| 228 | + Capability.V1_UPDATE_VIEW, |
218 | 229 | Capability.V1_DELETE_VIEW, |
219 | 230 | ) |
220 | 231 | ) |
@@ -337,6 +348,12 @@ class RegisterViewRequest(IcebergBaseModel): |
337 | 348 | metadata_location: str = Field(..., alias="metadata-location") |
338 | 349 |
|
339 | 350 |
|
| 351 | +class CommitViewRequest(IcebergBaseModel): |
| 352 | + identifier: TableIdentifier = Field() |
| 353 | + requirements: tuple[ViewRequirement, ...] = Field(default_factory=tuple) |
| 354 | + updates: tuple[ViewUpdate, ...] = Field(default_factory=tuple) |
| 355 | + |
| 356 | + |
340 | 357 | class ConfigResponse(IcebergBaseModel): |
341 | 358 | defaults: Properties | None = Field(default_factory=dict) |
342 | 359 | overrides: Properties | None = Field(default_factory=dict) |
@@ -1000,6 +1017,75 @@ def create_view( |
1000 | 1017 | view_response = ViewResponse.model_validate_json(response.text) |
1001 | 1018 | return self._response_to_view(self.identifier_to_tuple(identifier), view_response) |
1002 | 1019 |
|
| 1020 | + @override |
| 1021 | + @retry(**_RETRY_ARGS) |
| 1022 | + def replace_view( |
| 1023 | + self, |
| 1024 | + identifier: str | Identifier, |
| 1025 | + schema: Schema | pa.Schema, |
| 1026 | + view_version: ViewVersion, |
| 1027 | + location: str | None = None, |
| 1028 | + properties: Properties = EMPTY_DICT, |
| 1029 | + ) -> View: |
| 1030 | + self._check_endpoint(Capability.V1_UPDATE_VIEW) |
| 1031 | + iceberg_schema = self._convert_schema_if_needed(schema) |
| 1032 | + |
| 1033 | + namespace_and_view = self._split_identifier_for_path(identifier, IdentifierKind.VIEW) |
| 1034 | + if self.table_exists(identifier): |
| 1035 | + raise TableAlreadyExistsError(f"Table with same name already exists: {identifier}") |
| 1036 | + if not self.view_exists(identifier): |
| 1037 | + raise NoSuchViewError(f"View does not exist: {identifier}") |
| 1038 | + |
| 1039 | + current_view = self.load_view(identifier) |
| 1040 | + |
| 1041 | + if location: |
| 1042 | + location = location.rstrip("/") |
| 1043 | + |
| 1044 | + # Check if schema already exists in view metadata by comparing structure |
| 1045 | + schema_id = None |
| 1046 | + for existing_schema in current_view.metadata.schemas: |
| 1047 | + if existing_schema.as_struct() == iceberg_schema.as_struct(): |
| 1048 | + schema_id = existing_schema.schema_id |
| 1049 | + break |
| 1050 | + |
| 1051 | + updates: list[ViewUpdate] = [] |
| 1052 | + if schema_id is None: |
| 1053 | + # Schema not found, add new schema with next schema_id |
| 1054 | + next_schema_id = max((s.schema_id for s in current_view.metadata.schemas), default=0) + 1 |
| 1055 | + schema_to_add = iceberg_schema.model_copy(update={"schema_id": next_schema_id}) |
| 1056 | + updates.append(AddSchemaUpdate(schema_=schema_to_add)) |
| 1057 | + schema_id = next_schema_id |
| 1058 | + |
| 1059 | + fresh_view_version = view_version.model_copy(update={"schema_id": schema_id}) |
| 1060 | + updates.append(AddViewVersionUpdate(view_version=fresh_view_version)) |
| 1061 | + updates.append(SetCurrentViewVersionUpdate(view_version_id=fresh_view_version.version_id)) |
| 1062 | + |
| 1063 | + updates_tuple: tuple[ViewUpdate, ...] = tuple(updates) |
| 1064 | + if location: |
| 1065 | + updates_tuple = updates_tuple + (SetLocationUpdate(location=location),) |
| 1066 | + if properties: |
| 1067 | + updates_tuple = updates_tuple + (SetPropertiesUpdate(updates=properties),) |
| 1068 | + |
| 1069 | + requirements: tuple[ViewRequirement, ...] = (AssertViewUUID(uuid=current_view.metadata.view_uuid),) |
| 1070 | + |
| 1071 | + identifier = current_view.name() |
| 1072 | + view_identifier = TableIdentifier(namespace=identifier[:-1], name=identifier[-1]) |
| 1073 | + request = CommitViewRequest(identifier=view_identifier, requirements=requirements, updates=updates_tuple) |
| 1074 | + |
| 1075 | + serialized_json = request.model_dump_json().encode(UTF8) |
| 1076 | + response = self._session.post( |
| 1077 | + self.url(Endpoints.update_view, **namespace_and_view), |
| 1078 | + data=serialized_json, |
| 1079 | + ) |
| 1080 | + |
| 1081 | + try: |
| 1082 | + response.raise_for_status() |
| 1083 | + except HTTPError as exc: |
| 1084 | + _handle_non_200_response(exc, {409: CommitFailedException, 404: NoSuchViewError}) |
| 1085 | + |
| 1086 | + view_response = ViewResponse.model_validate_json(response.text) |
| 1087 | + return self._response_to_view(self.identifier_to_tuple(identifier), view_response) |
| 1088 | + |
1003 | 1089 | @retry(**_RETRY_ARGS) |
1004 | 1090 | @override |
1005 | 1091 | def register_table(self, identifier: str | Identifier, metadata_location: str, overwrite: bool = False) -> Table: |
|
0 commit comments