|
18 | 18 | import os |
19 | 19 | import uuid |
20 | 20 | from pathlib import PosixPath |
| 21 | +from typing import Any |
21 | 22 | from unittest import mock |
22 | 23 | from unittest.mock import MagicMock |
23 | 24 |
|
|
28 | 29 | from pyiceberg import __version__ |
29 | 30 | from pyiceberg.catalog.memory import InMemoryCatalog |
30 | 31 | from pyiceberg.cli.console import run |
| 32 | +from pyiceberg.exceptions import NoSuchTableError, NoSuchViewError |
31 | 33 | from pyiceberg.io import WAREHOUSE |
32 | 34 | from pyiceberg.partitioning import PartitionField, PartitionSpec |
33 | 35 | from pyiceberg.schema import Schema |
34 | 36 | from pyiceberg.transforms import IdentityTransform |
35 | 37 | from pyiceberg.typedef import Properties |
36 | 38 | from pyiceberg.types import LongType, NestedField |
37 | 39 | from pyiceberg.utils.config import Config |
| 40 | +from pyiceberg.view import View |
| 41 | +from pyiceberg.view.metadata import ViewMetadata |
38 | 42 |
|
39 | 43 |
|
40 | 44 | def test_missing_uri(mocker: MockFixture, empty_home_dir_path: str) -> None: |
@@ -220,7 +224,7 @@ def test_describe_table_does_not_exists(catalog: InMemoryCatalog) -> None: |
220 | 224 | runner = CliRunner() |
221 | 225 | result = runner.invoke(run, ["describe", "default.doesnotexist"]) |
222 | 226 | assert result.exit_code == 1 |
223 | | - assert result.output == "Table or namespace does not exist: default.doesnotexist\n" |
| 227 | + assert result.output == "Table, view, or namespace does not exist: default.doesnotexist\n" |
224 | 228 |
|
225 | 229 |
|
226 | 230 | @pytest.mark.parametrize("entity_args", [[], ["--entity", "table"]], ids=["any", "table"]) |
@@ -737,7 +741,7 @@ def test_json_describe_table_does_not_exists(catalog: InMemoryCatalog) -> None: |
737 | 741 | assert result.exit_code == 1 |
738 | 742 | assert ( |
739 | 743 | result.output |
740 | | - == """{"type": "NoSuchTableError", "message": "Table or namespace does not exist: default.doesnotexist"}\n""" |
| 744 | + == """{"type": "NoSuchTableError", "message": "Table, view, or namespace does not exist: default.doesnotexist"}\n""" |
741 | 745 | ) |
742 | 746 |
|
743 | 747 |
|
@@ -1171,3 +1175,136 @@ def test_warehouse_cli_option_forwarded_to_catalog(mocker: MockFixture) -> None: |
1171 | 1175 | assert result.exit_code == 0 |
1172 | 1176 | mock_basicConfig.assert_called_once() |
1173 | 1177 | mock_load_catalog.assert_called_once_with("rest", uri="https://catalog.service", warehouse="example-warehouse") |
| 1178 | + |
| 1179 | + |
| 1180 | +TEST_VIEW_IDENTIFIER = ("default", "my_view") |
| 1181 | +TEST_VIEW_METADATA: dict[str, Any] = { |
| 1182 | + "view-uuid": "b30125c8-7284-442c-9aea-15fee620737c", |
| 1183 | + "format-version": 1, |
| 1184 | + "location": "s3://warehouse/default/my_view", |
| 1185 | + "current-version-id": 1, |
| 1186 | + "versions": [ |
| 1187 | + { |
| 1188 | + "version-id": 1, |
| 1189 | + "timestamp-ms": 1602638573874, |
| 1190 | + "schema-id": 1, |
| 1191 | + "summary": {}, |
| 1192 | + "representations": [{"type": "sql", "sql": "SELECT * FROM my_table", "dialect": "spark"}], |
| 1193 | + "default-namespace": ["default"], |
| 1194 | + } |
| 1195 | + ], |
| 1196 | + "schemas": [ |
| 1197 | + { |
| 1198 | + "type": "struct", |
| 1199 | + "schema-id": 1, |
| 1200 | + "fields": [ |
| 1201 | + {"id": 1, "name": "x", "required": True, "type": "long"}, |
| 1202 | + ], |
| 1203 | + } |
| 1204 | + ], |
| 1205 | + "version-log": [{"timestamp-ms": 1602638573874, "version-id": 1}], |
| 1206 | + "properties": {}, |
| 1207 | +} |
| 1208 | + |
| 1209 | + |
| 1210 | +@pytest.fixture(name="catalog_with_view") |
| 1211 | +def fixture_catalog_with_view(mocker: MockFixture, catalog: InMemoryCatalog) -> tuple[InMemoryCatalog, View]: |
| 1212 | + view = View(TEST_VIEW_IDENTIFIER, ViewMetadata.model_validate(TEST_VIEW_METADATA)) |
| 1213 | + catalog.list_views = MagicMock(return_value=[TEST_VIEW_IDENTIFIER]) # type: ignore |
| 1214 | + catalog.load_view = MagicMock(return_value=view) # type: ignore |
| 1215 | + catalog.drop_view = MagicMock() # type: ignore |
| 1216 | + return catalog, view |
| 1217 | + |
| 1218 | + |
| 1219 | +def test_list_views(catalog_with_view: tuple[InMemoryCatalog, View]) -> None: |
| 1220 | + catalog, _ = catalog_with_view |
| 1221 | + |
| 1222 | + runner = CliRunner() |
| 1223 | + result = runner.invoke(run, ["list-views", "default"]) |
| 1224 | + assert result.exit_code == 0 |
| 1225 | + assert "default.my_view" in result.output |
| 1226 | + |
| 1227 | + |
| 1228 | +def test_list_views_does_not_exist(catalog: InMemoryCatalog) -> None: |
| 1229 | + catalog.list_views = MagicMock(side_effect=NoSuchViewError("Namespace does not exist: doesnotexist")) # type: ignore |
| 1230 | + |
| 1231 | + runner = CliRunner() |
| 1232 | + result = runner.invoke(run, ["list-views", "doesnotexist"]) |
| 1233 | + assert result.exit_code == 1 |
| 1234 | + assert "Namespace does not exist: doesnotexist" in result.output |
| 1235 | + |
| 1236 | + |
| 1237 | +def test_describe_view(catalog_with_view: tuple[InMemoryCatalog, View]) -> None: |
| 1238 | + runner = CliRunner() |
| 1239 | + result = runner.invoke(run, ["describe", "--entity=view", "default.my_view"]) |
| 1240 | + assert result.exit_code == 0 |
| 1241 | + assert "b30125c8-7284-442c-9aea-15fee620737c" in result.output |
| 1242 | + assert "spark: SELECT * FROM my_table" in result.output |
| 1243 | + |
| 1244 | + |
| 1245 | +def test_describe_view_does_not_exist(catalog: InMemoryCatalog) -> None: |
| 1246 | + catalog.load_view = MagicMock(side_effect=NoSuchViewError("View does not exist: default.doesnotexist")) # type: ignore |
| 1247 | + |
| 1248 | + runner = CliRunner() |
| 1249 | + result = runner.invoke(run, ["describe", "--entity=view", "default.doesnotexist"]) |
| 1250 | + assert result.exit_code == 1 |
| 1251 | + assert "View does not exist: default.doesnotexist" in result.output |
| 1252 | + |
| 1253 | + |
| 1254 | +def test_describe_any_falls_through_to_view(catalog_with_view: tuple[InMemoryCatalog, View]) -> None: |
| 1255 | + catalog, _ = catalog_with_view |
| 1256 | + catalog.load_table = MagicMock(side_effect=NoSuchTableError("Table does not exist: default.my_view")) # type: ignore |
| 1257 | + |
| 1258 | + runner = CliRunner() |
| 1259 | + result = runner.invoke(run, ["describe", "default.my_view"]) |
| 1260 | + assert result.exit_code == 0 |
| 1261 | + assert "b30125c8-7284-442c-9aea-15fee620737c" in result.output |
| 1262 | + |
| 1263 | + |
| 1264 | +def test_drop_view(catalog_with_view: tuple[InMemoryCatalog, View]) -> None: |
| 1265 | + catalog, _ = catalog_with_view |
| 1266 | + |
| 1267 | + runner = CliRunner() |
| 1268 | + result = runner.invoke(run, ["drop", "view", "default.my_view"]) |
| 1269 | + assert result.exit_code == 0 |
| 1270 | + assert result.output == "Dropped view: default.my_view\n" |
| 1271 | + catalog.drop_view.assert_called_once_with("default.my_view") # type: ignore |
| 1272 | + |
| 1273 | + |
| 1274 | +def test_drop_view_does_not_exist(catalog: InMemoryCatalog) -> None: |
| 1275 | + catalog.drop_view = MagicMock(side_effect=NoSuchViewError("View does not exist: default.doesnotexist")) # type: ignore |
| 1276 | + |
| 1277 | + runner = CliRunner() |
| 1278 | + result = runner.invoke(run, ["drop", "view", "default.doesnotexist"]) |
| 1279 | + assert result.exit_code == 1 |
| 1280 | + assert "View does not exist: default.doesnotexist" in result.output |
| 1281 | + |
| 1282 | + |
| 1283 | +def test_json_list_views(catalog_with_view: tuple[InMemoryCatalog, View]) -> None: |
| 1284 | + runner = CliRunner() |
| 1285 | + result = runner.invoke(run, ["--output=json", "list-views", "default"]) |
| 1286 | + assert result.exit_code == 0 |
| 1287 | + assert result.output == '["default.my_view"]\n' |
| 1288 | + |
| 1289 | + |
| 1290 | +def test_json_describe_view(catalog_with_view: tuple[InMemoryCatalog, View]) -> None: |
| 1291 | + runner = CliRunner() |
| 1292 | + result = runner.invoke(run, ["--output=json", "describe", "--entity=view", "default.my_view"]) |
| 1293 | + assert result.exit_code == 0 |
| 1294 | + assert "b30125c8-7284-442c-9aea-15fee620737c" in result.output |
| 1295 | + |
| 1296 | + |
| 1297 | +def test_json_drop_view(catalog_with_view: tuple[InMemoryCatalog, View]) -> None: |
| 1298 | + runner = CliRunner() |
| 1299 | + result = runner.invoke(run, ["--output=json", "drop", "view", "default.my_view"]) |
| 1300 | + assert result.exit_code == 0 |
| 1301 | + assert result.output == '"Dropped view: default.my_view"\n' |
| 1302 | + |
| 1303 | + |
| 1304 | +def test_json_drop_view_does_not_exist(catalog: InMemoryCatalog) -> None: |
| 1305 | + catalog.drop_view = MagicMock(side_effect=NoSuchViewError("View does not exist: default.doesnotexist")) # type: ignore |
| 1306 | + |
| 1307 | + runner = CliRunner() |
| 1308 | + result = runner.invoke(run, ["--output=json", "drop", "view", "default.doesnotexist"]) |
| 1309 | + assert result.exit_code == 1 |
| 1310 | + assert result.output == '{"type": "NoSuchViewError", "message": "View does not exist: default.doesnotexist"}\n' |
0 commit comments