From c22f70e3f8854a5fe6baac3e6ef7f98472d204cf Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Thu, 30 Apr 2026 13:45:49 +0200 Subject: [PATCH 01/16] fix: Add deltalake dependency and refactor schema extraction logic --- pyproject.toml | 1 + .../commands/tables/fab_tables_schema.py | 91 ++++++------------- 2 files changed, 30 insertions(+), 62 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f2c52dc5d..9995a06c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "requests", "cryptography", "fabric-cicd>=0.3.1", + "deltalake>=0.18.0", ] [project.scripts] diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index 74f3b39dd..fbe2c4dd1 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -3,78 +3,45 @@ import json from argparse import Namespace -from typing import Optional -from fabric_cli.client import fab_api_onelake as onelake_api +from deltalake import DeltaTable +from deltalake.exceptions import TableNotFoundError + from fabric_cli.core import fab_constant -from fabric_cli.core import fab_handle_context as handle_context +from fabric_cli.core.fab_auth import FabAuth from fabric_cli.core.fab_exceptions import FabricCLIError -from fabric_cli.core.hiearchy.fab_hiearchy import OneLakeItem from fabric_cli.utils import fab_ui -from fabric_cli.utils import fab_util as utils def exec_command(args: Namespace) -> None: - schema = _extract_schema_from_commit_logs(args) - if schema: - fab_ui.print_grey("Schema extracted successfully") - _schema = json.loads(schema)["fields"] - fab_ui.print_output_format(args, data=_schema, show_headers=True) + schema_fields = _get_table_schema(args) + fab_ui.print_grey("Schema extracted successfully") + fab_ui.print_output_format(args, data=schema_fields, show_headers=True) + +def _get_table_schema(args: Namespace) -> list[dict]: + token = FabAuth().get_access_token(fab_constant.SCOPE_ONELAKE_DEFAULT) + if args.schema: + local_path = f"Tables/{args.schema}/{args.table_name}" else: + local_path = f"Tables/{args.table_name}" + + table_uri = ( + f"abfss://{args.ws_id}@{fab_constant.API_ENDPOINT_ONELAKE}" + f"/{args.lakehouse_id}/{local_path}" + ) + + try: + table = DeltaTable( + table_uri, + storage_options={ + "bearer_token": token, + "use_fabric_endpoint": "true", + }, + ) + return table.schema().json()["fields"] + except TableNotFoundError: raise FabricCLIError( "Failed to extract the table schema. Please ensure the path points to a valid Delta table", fab_constant.ERROR_INVALID_DETLA_TABLE, ) - - -def _get_commit_logs(args: Namespace) -> Optional[list[str]]: - _delta_log_path = args.path - _delta_log_path[-1] = _delta_log_path[-1] + "/_delta_log" - - _context = handle_context.get_command_context(_delta_log_path, raise_error=True) - assert isinstance(_context, OneLakeItem) - onelake: OneLakeItem = _context - workspace_id = onelake.workspace.id - item_id = onelake.item.id - local_path = onelake.local_path - - local_path = utils.remove_dot_suffix(local_path) - args.directory = f"{workspace_id}/?recursive=false&resource=filesystem&directory={item_id}/{local_path}&getShortcutMetadata=true" - response = onelake_api.list_tables_files_recursive(args) - - if response.status_code in {200, 201}: - file_names = [f["name"] for f in response.json().get("paths", [])] - json_files = [ - f"{workspace_id}/{item_id}/{f.split('/', 1)[1]}" - for f in file_names - if f.endswith(".json") and f != "_temporary" - ] - json_files.sort(reverse=True) - return json_files - return None - - -def _extract_schema_from_commit_logs(args: Namespace) -> Optional[str]: - commit_logs = _get_commit_logs(args) - - if not commit_logs: - return None - - for log in commit_logs: - args.from_path = log - args.wait = True - response = onelake_api.read(args) - - if response.status_code in {200, 201}: - json_string = response.text - json_objects = json_string.strip().split("\n") - - for obj in json_objects: - commit_data = json.loads(obj) - if "metaData" in commit_data: - metadata = commit_data["metaData"] - schema = metadata["schemaString"] - return schema - - return None From c0dd71bba830a5b8d7ba588ddcb33ad4f54c0476 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Thu, 30 Apr 2026 15:08:21 +0200 Subject: [PATCH 02/16] fix: Refactor `fab tables schema` to utilize `deltalake` library for schema extraction via ABFSS URI --- .changes/unreleased/fixed-20260430-130558.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/fixed-20260430-130558.yaml diff --git a/.changes/unreleased/fixed-20260430-130558.yaml b/.changes/unreleased/fixed-20260430-130558.yaml new file mode 100644 index 000000000..16dd7fabb --- /dev/null +++ b/.changes/unreleased/fixed-20260430-130558.yaml @@ -0,0 +1,6 @@ +kind: fixed +body: Refactor `fab tables schema` to use the `deltalake` Python library for schema extraction via ABFSS URI instead of manually parsing Delta log commit files +time: 2026-04-30T13:05:58.364670843Z +custom: + Author: pkontek + AuthorLink: https://github.com/pkontek From f0327bc075eeed1b249bbef3896a7e75dee41d8e Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Thu, 30 Apr 2026 15:16:20 +0200 Subject: [PATCH 03/16] fix: Correct typo in error constant for invalid Delta table --- src/fabric_cli/commands/tables/fab_tables_schema.py | 2 +- src/fabric_cli/core/fab_constant.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index fbe2c4dd1..5d2ecad27 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -43,5 +43,5 @@ def _get_table_schema(args: Namespace) -> list[dict]: except TableNotFoundError: raise FabricCLIError( "Failed to extract the table schema. Please ensure the path points to a valid Delta table", - fab_constant.ERROR_INVALID_DETLA_TABLE, + fab_constant.ERROR_INVALID_DELTA_TABLE, ) diff --git a/src/fabric_cli/core/fab_constant.py b/src/fabric_cli/core/fab_constant.py index 4fd00e7b9..424299aaa 100644 --- a/src/fabric_cli/core/fab_constant.py +++ b/src/fabric_cli/core/fab_constant.py @@ -19,8 +19,7 @@ ) API_ENDPOINT_POWER_BI = ( - validate_and_get_env_variable( - "FAB_API_ENDPOINT_POWER_BI", "api.powerbi.com") + validate_and_get_env_variable("FAB_API_ENDPOINT_POWER_BI", "api.powerbi.com") + "/v1.0/myorg" ) @@ -264,7 +263,7 @@ ERROR_INVALID_OPERATION = "InvalidOperation" ERROR_INVALID_PATH = "InvalidPath" ERROR_INVALID_PROPERTY = "InvalidProperty" -ERROR_INVALID_DETLA_TABLE = "InvalidDeltaTable" +ERROR_INVALID_DELTA_TABLE = "InvalidDeltaTable" ERROR_INVALID_QUERY_FIELDS = "InvalidQueryFields" ERROR_INVALID_WORKSPACE_TYPE = "InvalidWorkspaceType" ERROR_INVALID_QUERY = "InvalidQuery" @@ -351,4 +350,3 @@ # Invalid query parameters for set command across all fabric resources SET_COMMAND_INVALID_QUERIES = ["id", "type", "workspaceId", "folderId"] - From 746e8f6e7b76cde7058554ad930ff5bf5149a995 Mon Sep 17 00:00:00 2001 From: pkontek Date: Thu, 30 Apr 2026 15:26:53 +0200 Subject: [PATCH 04/16] Remove unused import Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/fabric_cli/commands/tables/fab_tables_schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index 5d2ecad27..a9aae2cf2 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import json from argparse import Namespace from deltalake import DeltaTable From ca07e175fad689014b232f2c84677b58c2bd1180 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Thu, 7 May 2026 09:56:10 +0200 Subject: [PATCH 05/16] fix: Replace TableNotFoundError with DeltaError for schema extraction failure handling --- src/fabric_cli/commands/tables/fab_tables_schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index 5d2ecad27..6ba672823 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -5,7 +5,7 @@ from argparse import Namespace from deltalake import DeltaTable -from deltalake.exceptions import TableNotFoundError +from deltalake.exceptions import DeltaError from fabric_cli.core import fab_constant from fabric_cli.core.fab_auth import FabAuth @@ -40,7 +40,7 @@ def _get_table_schema(args: Namespace) -> list[dict]: }, ) return table.schema().json()["fields"] - except TableNotFoundError: + except DeltaError: raise FabricCLIError( "Failed to extract the table schema. Please ensure the path points to a valid Delta table", fab_constant.ERROR_INVALID_DELTA_TABLE, From baaba5dd0d84ad57f7dabaddd68e038b2a6c70d0 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Thu, 7 May 2026 10:24:15 +0200 Subject: [PATCH 06/16] fix: Update schema extraction to use json.loads for DeltaTable schema --- src/fabric_cli/commands/tables/fab_tables_schema.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index 07d9d3548..90e5d499f 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +import json from argparse import Namespace from deltalake import DeltaTable @@ -38,7 +39,8 @@ def _get_table_schema(args: Namespace) -> list[dict]: "use_fabric_endpoint": "true", }, ) - return table.schema().json()["fields"] + schema_dict = json.loads(table.schema().to_json()) + return schema_dict["fields"] except DeltaError: raise FabricCLIError( "Failed to extract the table schema. Please ensure the path points to a valid Delta table", From 8221723ad331c11c283dec10938a06c710073bb7 Mon Sep 17 00:00:00 2001 From: pkontek Date: Thu, 7 May 2026 10:31:48 +0200 Subject: [PATCH 07/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/fabric_cli/commands/tables/fab_tables_schema.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index 90e5d499f..c015fc3b9 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -39,9 +39,13 @@ def _get_table_schema(args: Namespace) -> list[dict]: "use_fabric_endpoint": "true", }, ) - schema_dict = json.loads(table.schema().to_json()) - return schema_dict["fields"] - except DeltaError: + schema_json = table.schema().to_json() + schema_dict = json.loads(schema_json) + schema_fields = schema_dict.get("fields") + if not isinstance(schema_fields, list): + raise ValueError("Delta table schema JSON does not contain a valid 'fields' list.") + return schema_fields + except (DeltaError, json.JSONDecodeError, TypeError, KeyError, ValueError): raise FabricCLIError( "Failed to extract the table schema. Please ensure the path points to a valid Delta table", fab_constant.ERROR_INVALID_DELTA_TABLE, From 101257290e596e8197dd2f992b531467af727376 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Thu, 7 May 2026 11:04:28 +0200 Subject: [PATCH 08/16] fix: Add unit tests for table schema command --- tests/test_commands/commands_parser.py | 11 +- tests/test_commands/test_tables.py | 342 +++++++++++++++++++++++++ 2 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 tests/test_commands/test_tables.py diff --git a/tests/test_commands/commands_parser.py b/tests/test_commands/commands_parser.py index db66f60b1..34695af74 100644 --- a/tests/test_commands/commands_parser.py +++ b/tests/test_commands/commands_parser.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import platform + from prompt_toolkit.input import DummyInput from prompt_toolkit.output import DummyOutput @@ -12,6 +13,9 @@ from fabric_cli.parsers.fab_config_parser import ( register_parser as register_config_parser, ) +from fabric_cli.parsers.fab_find_parser import ( + register_parser as register_find_parser, +) from fabric_cli.parsers.fab_fs_parser import ( register_assign_parser, register_cd_parser, @@ -32,13 +36,13 @@ register_stop_parser, register_unassign_parser, ) -from fabric_cli.parsers.fab_find_parser import ( - register_parser as register_find_parser, -) from fabric_cli.parsers.fab_jobs_parser import register_parser as register_jobs_parser from fabric_cli.parsers.fab_labels_parser import ( register_parser as register_labels_parser, ) +from fabric_cli.parsers.fab_tables_parser import ( + register_parser as register_tables_parser, +) parserHandlers = [ register_labels_parser, @@ -65,6 +69,7 @@ register_rm_parser, register_mkdir_parser, register_jobs_parser, + register_tables_parser, ] diff --git a/tests/test_commands/test_tables.py b/tests/test_commands/test_tables.py new file mode 100644 index 000000000..71e99fa9f --- /dev/null +++ b/tests/test_commands/test_tables.py @@ -0,0 +1,342 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import json +from argparse import Namespace +from unittest.mock import MagicMock, patch + +import pytest +from deltalake.exceptions import DeltaError, TableNotFoundError + +from fabric_cli.commands.tables import fab_tables_schema +from fabric_cli.core import fab_constant +from fabric_cli.core.fab_exceptions import FabricCLIError + + +class TestTablesSchemaUnit: + """Unit tests for table schema command - direct function calls without VCR.""" + + def test_get_table_schema_success(self): + """Test successful schema extraction.""" + # Setup + args = Namespace( + ws_id="test-ws-id", + lakehouse_id="test-lakehouse-id", + table_name="test_table", + schema=None, + ) + + mock_schema = { + "fields": [ + {"name": "id", "type": "integer", "nullable": False, "metadata": {}}, + {"name": "name", "type": "string", "nullable": True, "metadata": {}}, + ] + } + + # Mock DeltaTable + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + # Configure mocks + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "mock_token" + mock_auth.return_value = mock_auth_instance + + mock_table_instance = MagicMock() + mock_arrow_schema = MagicMock() + mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) + mock_table_instance.schema.return_value = mock_arrow_schema + mock_delta_table.return_value = mock_table_instance + + # Call function + result = fab_tables_schema._get_table_schema(args) + + # Assert + assert isinstance(result, list) + assert len(result) == 2 + assert result[0]["name"] == "id" + assert result[0]["type"] == "integer" + assert result[1]["name"] == "name" + assert result[1]["type"] == "string" + + # Verify DeltaTable was called correctly + mock_delta_table.assert_called_once() + call_args = mock_delta_table.call_args + assert "test-lakehouse-id" in call_args[0][0] + assert "Tables/test_table" in call_args[0][0] + assert call_args[1]["storage_options"]["bearer_token"] == "mock_token" + assert call_args[1]["storage_options"]["use_fabric_endpoint"] == "true" + + def test_get_table_schema_with_explicit_schema_success(self): + """Test schema extraction with explicit schema name (e.g., dbo).""" + # Setup + args = Namespace( + ws_id="test-ws-id", + lakehouse_id="test-lakehouse-id", + table_name="test_table", + schema="dbo", + ) + + mock_schema = { + "fields": [ + {"name": "col1", "type": "long", "nullable": True, "metadata": {}}, + ] + } + + # Mock DeltaTable + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + # Configure mocks + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "mock_token" + mock_auth.return_value = mock_auth_instance + + mock_table_instance = MagicMock() + mock_arrow_schema = MagicMock() + mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) + mock_table_instance.schema.return_value = mock_arrow_schema + mock_delta_table.return_value = mock_table_instance + + # Call function + result = fab_tables_schema._get_table_schema(args) + + # Verify table URI includes schema path + call_args = mock_delta_table.call_args + assert "Tables/dbo/test_table" in call_args[0][0] + + # Assert result + assert isinstance(result, list) + assert len(result) == 1 + assert result[0]["name"] == "col1" + + def test_get_table_schema_table_not_found_error(self): + """Test TableNotFoundError is mapped to FabricCLIError.""" + # Setup + args = Namespace( + ws_id="test-ws-id", + lakehouse_id="test-lakehouse-id", + table_name="nonexistent", + schema=None, + ) + + # Mock DeltaTable to raise TableNotFoundError + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "mock_token" + mock_auth.return_value = mock_auth_instance + + mock_delta_table.side_effect = TableNotFoundError("Table not found") + + # Call function and expect error + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) + + # Assert error details + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message + + def test_get_table_schema_delta_error(self): + """Test generic DeltaError is mapped to FabricCLIError.""" + # Setup + args = Namespace( + ws_id="test-ws-id", + lakehouse_id="test-lakehouse-id", + table_name="test_table", + schema=None, + ) + + # Mock DeltaTable to raise DeltaError + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "mock_token" + mock_auth.return_value = mock_auth_instance + + mock_delta_table.side_effect = DeltaError("Generic delta error") + + # Call function and expect error + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) + + # Assert error details + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message + + def test_get_table_schema_invalid_json_error(self): + """Test invalid JSON in schema is handled.""" + # Setup + args = Namespace( + ws_id="test-ws-id", + lakehouse_id="test-lakehouse-id", + table_name="test_table", + schema=None, + ) + + # Mock DeltaTable to return invalid JSON + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "mock_token" + mock_auth.return_value = mock_auth_instance + + mock_table_instance = MagicMock() + mock_arrow_schema = MagicMock() + mock_arrow_schema.to_json.return_value = "invalid json {" + mock_table_instance.schema.return_value = mock_arrow_schema + mock_delta_table.return_value = mock_table_instance + + # Call function and expect error + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) + + # Assert error details + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message + + def test_get_table_schema_missing_fields_key(self): + """Test schema JSON without 'fields' key is handled.""" + # Setup + args = Namespace( + ws_id="test-ws-id", + lakehouse_id="test-lakehouse-id", + table_name="test_table", + schema=None, + ) + + mock_schema = {"some_other_key": "value"} + + # Mock DeltaTable + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "mock_token" + mock_auth.return_value = mock_auth_instance + + mock_table_instance = MagicMock() + mock_arrow_schema = MagicMock() + mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) + mock_table_instance.schema.return_value = mock_arrow_schema + mock_delta_table.return_value = mock_table_instance + + # Call function and expect error + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) + + # Assert error details + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message + + def test_get_table_schema_fields_not_list(self): + """Test schema JSON with 'fields' not being a list is handled.""" + # Setup + args = Namespace( + ws_id="test-ws-id", + lakehouse_id="test-lakehouse-id", + table_name="test_table", + schema=None, + ) + + mock_schema = {"fields": "not a list"} + + # Mock DeltaTable + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "mock_token" + mock_auth.return_value = mock_auth_instance + + mock_table_instance = MagicMock() + mock_arrow_schema = MagicMock() + mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) + mock_table_instance.schema.return_value = mock_arrow_schema + mock_delta_table.return_value = mock_table_instance + + # Call function and expect error + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) + + # Assert error details + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message + + def test_get_table_schema_verifies_abfss_uri_format(self): + """Test that table URI is correctly formatted with ABFSS protocol.""" + # Setup + args = Namespace( + ws_id="workspace-guid-123", + lakehouse_id="lakehouse-guid-456", + table_name="my_table", + schema=None, + ) + + mock_schema = { + "fields": [ + {"name": "col1", "type": "string", "nullable": True, "metadata": {}} + ] + } + + # Mock DeltaTable + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_delta_table, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + + mock_auth_instance = MagicMock() + mock_auth_instance.get_access_token.return_value = "test_token_123" + mock_auth.return_value = mock_auth_instance + + mock_table_instance = MagicMock() + mock_arrow_schema = MagicMock() + mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) + mock_table_instance.schema.return_value = mock_arrow_schema + mock_delta_table.return_value = mock_table_instance + + # Call function + result = fab_tables_schema._get_table_schema(args) + + # Verify DeltaTable was called with correct URI format + call_args = mock_delta_table.call_args + table_uri = call_args[0][0] + + # Verify ABFSS format + assert table_uri.startswith("abfss://workspace-guid-123@") + assert "lakehouse-guid-456" in table_uri + assert "Tables/my_table" in table_uri + + # Verify storage options + storage_options = call_args[1]["storage_options"] + assert storage_options["bearer_token"] == "test_token_123" + assert storage_options["use_fabric_endpoint"] == "true" + + # Verify result + assert isinstance(result, list) + assert len(result) == 1 From 3963efa9f2316acaa456074fd697453519076d4c Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Thu, 7 May 2026 11:12:07 +0200 Subject: [PATCH 09/16] fix: Add deltalake dependency to development requirements --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1b831e57e..ca684beb1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,6 +12,7 @@ psutil==7.0.0 requests cryptography fabric-cicd>=0.3.1 +deltalake>=0.18.0 # Testing and Building Requirements tox>=4.20.0 From 34c71416049ef8a9cfc688d77af21211b93d4944 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Fri, 15 May 2026 23:22:14 +0200 Subject: [PATCH 10/16] rename test_tables to test_tables_schema --- .vscode/settings.json | 11 ++++++++++- .../{test_tables.py => test_tables_schema.py} | 0 2 files changed, 10 insertions(+), 1 deletion(-) rename tests/test_commands/{test_tables.py => test_tables_schema.py} (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index e4470e31e..7133826c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,14 @@ "--lines-between-types", "0", "--remove-redundant-aliases" - ] + ], + "chat.agentSkillsLocations": { + ".agents/skills": true, + ".github/skills": true, + ".claude/skills": true, + "~/.agents/skills": true, + "~/.copilot/skills": true, + "~/.claude/skills": true, + "~/.vscode/extensions/synapsevscode.synapse-1.23.0/copilot/skills": true + } } \ No newline at end of file diff --git a/tests/test_commands/test_tables.py b/tests/test_commands/test_tables_schema.py similarity index 100% rename from tests/test_commands/test_tables.py rename to tests/test_commands/test_tables_schema.py From bce23dc25f812854441cca8baefd8ec2ad1078e4 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Fri, 15 May 2026 23:51:47 +0200 Subject: [PATCH 11/16] extract shared mock fixtures in test_tables_schema --- tests/test_commands/test_tables_schema.py | 322 +++++++--------------- 1 file changed, 96 insertions(+), 226 deletions(-) diff --git a/tests/test_commands/test_tables_schema.py b/tests/test_commands/test_tables_schema.py index 71e99fa9f..f99274430 100644 --- a/tests/test_commands/test_tables_schema.py +++ b/tests/test_commands/test_tables_schema.py @@ -16,9 +16,28 @@ class TestTablesSchemaUnit: """Unit tests for table schema command - direct function calls without VCR.""" - def test_get_table_schema_success(self): + @pytest.fixture + def mock_auth(self): + with patch("fabric_cli.commands.tables.fab_tables_schema.FabAuth") as mock: + instance = MagicMock() + instance.get_access_token.return_value = "mock_token" + mock.return_value = instance + yield mock + + @pytest.fixture + def mock_delta_table(self): + with patch("fabric_cli.commands.tables.fab_tables_schema.DeltaTable") as mock: + yield mock + + def _make_delta_table_mock(self, mock_delta_table, schema_json): + mock_arrow_schema = MagicMock() + mock_arrow_schema.to_json.return_value = schema_json + mock_table_instance = MagicMock() + mock_table_instance.schema.return_value = mock_arrow_schema + mock_delta_table.return_value = mock_table_instance + + def test_get_table_schema_success(self, mock_auth, mock_delta_table): """Test successful schema extraction.""" - # Setup args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -32,47 +51,26 @@ def test_get_table_schema_success(self): {"name": "name", "type": "string", "nullable": True, "metadata": {}}, ] } + self._make_delta_table_mock(mock_delta_table, json.dumps(mock_schema)) - # Mock DeltaTable - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: - - # Configure mocks - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "mock_token" - mock_auth.return_value = mock_auth_instance - - mock_table_instance = MagicMock() - mock_arrow_schema = MagicMock() - mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) - mock_table_instance.schema.return_value = mock_arrow_schema - mock_delta_table.return_value = mock_table_instance - - # Call function - result = fab_tables_schema._get_table_schema(args) - - # Assert - assert isinstance(result, list) - assert len(result) == 2 - assert result[0]["name"] == "id" - assert result[0]["type"] == "integer" - assert result[1]["name"] == "name" - assert result[1]["type"] == "string" - - # Verify DeltaTable was called correctly - mock_delta_table.assert_called_once() - call_args = mock_delta_table.call_args - assert "test-lakehouse-id" in call_args[0][0] - assert "Tables/test_table" in call_args[0][0] - assert call_args[1]["storage_options"]["bearer_token"] == "mock_token" - assert call_args[1]["storage_options"]["use_fabric_endpoint"] == "true" - - def test_get_table_schema_with_explicit_schema_success(self): + result = fab_tables_schema._get_table_schema(args) + + assert isinstance(result, list) + assert len(result) == 2 + assert result[0]["name"] == "id" + assert result[0]["type"] == "integer" + assert result[1]["name"] == "name" + assert result[1]["type"] == "string" + + mock_delta_table.assert_called_once() + call_args = mock_delta_table.call_args + assert "test-lakehouse-id" in call_args[0][0] + assert "Tables/test_table" in call_args[0][0] + assert call_args[1]["storage_options"]["bearer_token"] == "mock_token" + assert call_args[1]["storage_options"]["use_fabric_endpoint"] == "true" + + def test_get_table_schema_with_explicit_schema_success(self, mock_auth, mock_delta_table): """Test schema extraction with explicit schema name (e.g., dbo).""" - # Setup args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -85,40 +83,19 @@ def test_get_table_schema_with_explicit_schema_success(self): {"name": "col1", "type": "long", "nullable": True, "metadata": {}}, ] } + self._make_delta_table_mock(mock_delta_table, json.dumps(mock_schema)) + + result = fab_tables_schema._get_table_schema(args) + + call_args = mock_delta_table.call_args + assert "Tables/dbo/test_table" in call_args[0][0] + + assert isinstance(result, list) + assert len(result) == 1 + assert result[0]["name"] == "col1" - # Mock DeltaTable - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: - - # Configure mocks - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "mock_token" - mock_auth.return_value = mock_auth_instance - - mock_table_instance = MagicMock() - mock_arrow_schema = MagicMock() - mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) - mock_table_instance.schema.return_value = mock_arrow_schema - mock_delta_table.return_value = mock_table_instance - - # Call function - result = fab_tables_schema._get_table_schema(args) - - # Verify table URI includes schema path - call_args = mock_delta_table.call_args - assert "Tables/dbo/test_table" in call_args[0][0] - - # Assert result - assert isinstance(result, list) - assert len(result) == 1 - assert result[0]["name"] == "col1" - - def test_get_table_schema_table_not_found_error(self): + def test_get_table_schema_table_not_found_error(self, mock_auth, mock_delta_table): """Test TableNotFoundError is mapped to FabricCLIError.""" - # Setup args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -126,30 +103,16 @@ def test_get_table_schema_table_not_found_error(self): schema=None, ) - # Mock DeltaTable to raise TableNotFoundError - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: + mock_delta_table.side_effect = TableNotFoundError("Table not found") - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "mock_token" - mock_auth.return_value = mock_auth_instance + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) - mock_delta_table.side_effect = TableNotFoundError("Table not found") + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message - # Call function and expect error - with pytest.raises(FabricCLIError) as exc_info: - fab_tables_schema._get_table_schema(args) - - # Assert error details - assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE - assert "Failed to extract the table schema" in exc_info.value.message - - def test_get_table_schema_delta_error(self): + def test_get_table_schema_delta_error(self, mock_auth, mock_delta_table): """Test generic DeltaError is mapped to FabricCLIError.""" - # Setup args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -157,30 +120,16 @@ def test_get_table_schema_delta_error(self): schema=None, ) - # Mock DeltaTable to raise DeltaError - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: - - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "mock_token" - mock_auth.return_value = mock_auth_instance + mock_delta_table.side_effect = DeltaError("Generic delta error") - mock_delta_table.side_effect = DeltaError("Generic delta error") + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) - # Call function and expect error - with pytest.raises(FabricCLIError) as exc_info: - fab_tables_schema._get_table_schema(args) + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message - # Assert error details - assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE - assert "Failed to extract the table schema" in exc_info.value.message - - def test_get_table_schema_invalid_json_error(self): + def test_get_table_schema_invalid_json_error(self, mock_auth, mock_delta_table): """Test invalid JSON in schema is handled.""" - # Setup args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -188,34 +137,16 @@ def test_get_table_schema_invalid_json_error(self): schema=None, ) - # Mock DeltaTable to return invalid JSON - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: - - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "mock_token" - mock_auth.return_value = mock_auth_instance + self._make_delta_table_mock(mock_delta_table, "invalid json {") - mock_table_instance = MagicMock() - mock_arrow_schema = MagicMock() - mock_arrow_schema.to_json.return_value = "invalid json {" - mock_table_instance.schema.return_value = mock_arrow_schema - mock_delta_table.return_value = mock_table_instance + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) - # Call function and expect error - with pytest.raises(FabricCLIError) as exc_info: - fab_tables_schema._get_table_schema(args) + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message - # Assert error details - assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE - assert "Failed to extract the table schema" in exc_info.value.message - - def test_get_table_schema_missing_fields_key(self): + def test_get_table_schema_missing_fields_key(self, mock_auth, mock_delta_table): """Test schema JSON without 'fields' key is handled.""" - # Setup args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -223,36 +154,16 @@ def test_get_table_schema_missing_fields_key(self): schema=None, ) - mock_schema = {"some_other_key": "value"} - - # Mock DeltaTable - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: - - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "mock_token" - mock_auth.return_value = mock_auth_instance + self._make_delta_table_mock(mock_delta_table, json.dumps({"some_other_key": "value"})) - mock_table_instance = MagicMock() - mock_arrow_schema = MagicMock() - mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) - mock_table_instance.schema.return_value = mock_arrow_schema - mock_delta_table.return_value = mock_table_instance + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) - # Call function and expect error - with pytest.raises(FabricCLIError) as exc_info: - fab_tables_schema._get_table_schema(args) + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message - # Assert error details - assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE - assert "Failed to extract the table schema" in exc_info.value.message - - def test_get_table_schema_fields_not_list(self): + def test_get_table_schema_fields_not_list(self, mock_auth, mock_delta_table): """Test schema JSON with 'fields' not being a list is handled.""" - # Setup args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -260,36 +171,16 @@ def test_get_table_schema_fields_not_list(self): schema=None, ) - mock_schema = {"fields": "not a list"} - - # Mock DeltaTable - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: - - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "mock_token" - mock_auth.return_value = mock_auth_instance + self._make_delta_table_mock(mock_delta_table, json.dumps({"fields": "not a list"})) - mock_table_instance = MagicMock() - mock_arrow_schema = MagicMock() - mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) - mock_table_instance.schema.return_value = mock_arrow_schema - mock_delta_table.return_value = mock_table_instance + with pytest.raises(FabricCLIError) as exc_info: + fab_tables_schema._get_table_schema(args) - # Call function and expect error - with pytest.raises(FabricCLIError) as exc_info: - fab_tables_schema._get_table_schema(args) + assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE + assert "Failed to extract the table schema" in exc_info.value.message - # Assert error details - assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE - assert "Failed to extract the table schema" in exc_info.value.message - - def test_get_table_schema_verifies_abfss_uri_format(self): + def test_get_table_schema_verifies_abfss_uri_format(self, mock_auth, mock_delta_table): """Test that table URI is correctly formatted with ABFSS protocol.""" - # Setup args = Namespace( ws_id="workspace-guid-123", lakehouse_id="lakehouse-guid-456", @@ -302,41 +193,20 @@ def test_get_table_schema_verifies_abfss_uri_format(self): {"name": "col1", "type": "string", "nullable": True, "metadata": {}} ] } + self._make_delta_table_mock(mock_delta_table, json.dumps(mock_schema)) + + result = fab_tables_schema._get_table_schema(args) + + call_args = mock_delta_table.call_args + table_uri = call_args[0][0] + + assert table_uri.startswith("abfss://workspace-guid-123@") + assert "lakehouse-guid-456" in table_uri + assert "Tables/my_table" in table_uri + + storage_options = call_args[1]["storage_options"] + assert storage_options["bearer_token"] == "mock_token" + assert storage_options["use_fabric_endpoint"] == "true" - # Mock DeltaTable - with patch( - "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" - ) as mock_delta_table, patch( - "fabric_cli.commands.tables.fab_tables_schema.FabAuth" - ) as mock_auth: - - mock_auth_instance = MagicMock() - mock_auth_instance.get_access_token.return_value = "test_token_123" - mock_auth.return_value = mock_auth_instance - - mock_table_instance = MagicMock() - mock_arrow_schema = MagicMock() - mock_arrow_schema.to_json.return_value = json.dumps(mock_schema) - mock_table_instance.schema.return_value = mock_arrow_schema - mock_delta_table.return_value = mock_table_instance - - # Call function - result = fab_tables_schema._get_table_schema(args) - - # Verify DeltaTable was called with correct URI format - call_args = mock_delta_table.call_args - table_uri = call_args[0][0] - - # Verify ABFSS format - assert table_uri.startswith("abfss://workspace-guid-123@") - assert "lakehouse-guid-456" in table_uri - assert "Tables/my_table" in table_uri - - # Verify storage options - storage_options = call_args[1]["storage_options"] - assert storage_options["bearer_token"] == "test_token_123" - assert storage_options["use_fabric_endpoint"] == "true" - - # Verify result - assert isinstance(result, list) - assert len(result) == 1 + assert isinstance(result, list) + assert len(result) == 1 From a4c4ba7cce27201c529935d7c91a80fb2f6c3b25 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Fri, 15 May 2026 23:55:57 +0200 Subject: [PATCH 12/16] combine delta error tests into parametrized test case --- tests/test_commands/test_tables_schema.py | 24 ++++------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/tests/test_commands/test_tables_schema.py b/tests/test_commands/test_tables_schema.py index f99274430..8fb735e3f 100644 --- a/tests/test_commands/test_tables_schema.py +++ b/tests/test_commands/test_tables_schema.py @@ -94,25 +94,9 @@ def test_get_table_schema_with_explicit_schema_success(self, mock_auth, mock_del assert len(result) == 1 assert result[0]["name"] == "col1" - def test_get_table_schema_table_not_found_error(self, mock_auth, mock_delta_table): - """Test TableNotFoundError is mapped to FabricCLIError.""" - args = Namespace( - ws_id="test-ws-id", - lakehouse_id="test-lakehouse-id", - table_name="nonexistent", - schema=None, - ) - - mock_delta_table.side_effect = TableNotFoundError("Table not found") - - with pytest.raises(FabricCLIError) as exc_info: - fab_tables_schema._get_table_schema(args) - - assert exc_info.value.status_code == fab_constant.ERROR_INVALID_DELTA_TABLE - assert "Failed to extract the table schema" in exc_info.value.message - - def test_get_table_schema_delta_error(self, mock_auth, mock_delta_table): - """Test generic DeltaError is mapped to FabricCLIError.""" + @pytest.mark.parametrize("error_cls", [TableNotFoundError, DeltaError]) + def test_get_table_schema_delta_exceptions(self, mock_auth, mock_delta_table, error_cls): + """Test that DeltaTable errors are mapped to FabricCLIError.""" args = Namespace( ws_id="test-ws-id", lakehouse_id="test-lakehouse-id", @@ -120,7 +104,7 @@ def test_get_table_schema_delta_error(self, mock_auth, mock_delta_table): schema=None, ) - mock_delta_table.side_effect = DeltaError("Generic delta error") + mock_delta_table.side_effect = error_cls("error") with pytest.raises(FabricCLIError) as exc_info: fab_tables_schema._get_table_schema(args) From 55c35156ffc4a733bb692b3d9bff2edfe8fc2aef Mon Sep 17 00:00:00 2001 From: pkontek Date: Sat, 16 May 2026 00:00:59 +0200 Subject: [PATCH 13/16] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .vscode/settings.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7133826c8..e4470e31e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,14 +21,5 @@ "--lines-between-types", "0", "--remove-redundant-aliases" - ], - "chat.agentSkillsLocations": { - ".agents/skills": true, - ".github/skills": true, - ".claude/skills": true, - "~/.agents/skills": true, - "~/.copilot/skills": true, - "~/.claude/skills": true, - "~/.vscode/extensions/synapsevscode.synapse-1.23.0/copilot/skills": true - } + ] } \ No newline at end of file From f206e12232a16033e0c4f9365648da09397dae78 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Sat, 16 May 2026 00:07:40 +0200 Subject: [PATCH 14/16] feat: add integration tests for table schema command and mock API responses --- .../test_tables_schema/class_setup.yaml | 180 ++++++++++++ .../test_table_schema_success.yaml | 258 ++++++++++++++++++ tests/test_commands/test_tables_schema.py | 36 +++ 3 files changed, 474 insertions(+) create mode 100644 tests/test_commands/recordings/test_commands/test_tables_schema/class_setup.yaml create mode 100644 tests/test_commands/recordings/test_commands/test_tables_schema/test_table_schema_success.yaml diff --git a/tests/test_commands/recordings/test_commands/test_tables_schema/class_setup.yaml b/tests/test_commands/recordings/test_commands/test_tables_schema/class_setup.yaml new file mode 100644 index 000000000..a6d2e8bd5 --- /dev/null +++ b/tests/test_commands/recordings/test_commands/test_tables_schema/class_setup.yaml @@ -0,0 +1,180 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "94da8ea5-0bd6-4a9e-b717-5fdb482f4c71", "displayName": + "My workspace", "description": "", "type": "Personal"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "94da8ea5-0bd6-4a9e-b717-5fdb482f4c71", "displayName": + "My workspace", "description": "", "type": "Personal"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/capacities + response: + body: + string: '{"value": [{"id": "00000000-0000-0000-0000-000000000004", "displayName": + "mocked_fabriccli_capacity_name", "sku": "F2", "region": "West Europe", "state": + "Active"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: '{"displayName": "fabriccli_WorkspacePerTestclass_000001", "capacityId": "00000000-0000-0000-0000-000000000004"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: POST + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"id": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0", "displayName": "fabriccli_WorkspacePerTestclass_000001", + "type": "Workspace", "capacityId": "00000000-0000-0000-0000-000000000004"}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "94da8ea5-0bd6-4a9e-b717-5fdb482f4c71", "displayName": + "My workspace", "description": "", "type": "Personal"}, {"id": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0", + "displayName": "fabriccli_WorkspacePerTestclass_000001", "type": "Workspace", "capacityId": + "00000000-0000-0000-0000-000000000004"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/items + response: + body: + string: '{"value": []}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: DELETE + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0 + response: + body: + string: '' + headers: + Content-Type: + - application/octet-stream + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_commands/recordings/test_commands/test_tables_schema/test_table_schema_success.yaml b/tests/test_commands/recordings/test_commands/test_tables_schema/test_table_schema_success.yaml new file mode 100644 index 000000000..67735e36a --- /dev/null +++ b/tests/test_commands/recordings/test_commands/test_tables_schema/test_table_schema_success.yaml @@ -0,0 +1,258 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "94da8ea5-0bd6-4a9e-b717-5fdb482f4c71", "displayName": + "My workspace", "description": "", "type": "Personal"}, {"id": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0", + "displayName": "fabriccli_WorkspacePerTestclass_000001", "type": "Workspace", "capacityId": + "00000000-0000-0000-0000-000000000004"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/items + response: + body: + string: '{"value": []}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/items + response: + body: + string: '{"value": []}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: '{"displayName": "fabcli000001", "type": "Lakehouse", "folderId": null}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: POST + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/lakehouses + response: + body: + string: '{"id": "e6f7a8b9-c0d1-e2f3-a4b5-c6d7e8f9a0b1", "type": "Lakehouse", + "displayName": "fabcli000001", "workspaceId": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0"}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "94da8ea5-0bd6-4a9e-b717-5fdb482f4c71", "displayName": + "My workspace", "description": "", "type": "Personal"}, {"id": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0", + "displayName": "fabriccli_WorkspacePerTestclass_000001", "type": "Workspace", "capacityId": + "00000000-0000-0000-0000-000000000004"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/items + response: + body: + string: '{"value": [{"id": "e6f7a8b9-c0d1-e2f3-a4b5-c6d7e8f9a0b1", "type": "Lakehouse", + "displayName": "fabcli000001", "workspaceId": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: HEAD + uri: https://onelake.dfs.fabric.microsoft.com/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/e6f7a8b9-c0d1-e2f3-a4b5-c6d7e8f9a0b1/Tables/my_table + response: + body: + string: '' + headers: + Content-Type: + - application/octet-stream + x-ms-resource-type: + - directory + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "94da8ea5-0bd6-4a9e-b717-5fdb482f4c71", "displayName": + "My workspace", "description": "", "type": "Personal"}, {"id": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0", + "displayName": "fabriccli_WorkspacePerTestclass_000001", "type": "Workspace", "capacityId": + "00000000-0000-0000-0000-000000000004"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/items + response: + body: + string: '{"value": [{"id": "e6f7a8b9-c0d1-e2f3-a4b5-c6d7e8f9a0b1", "type": "Lakehouse", + "displayName": "fabcli000001", "workspaceId": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0"}]}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.0.0 + method: DELETE + uri: https://api.fabric.microsoft.com/v1/workspaces/d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0/items/e6f7a8b9-c0d1-e2f3-a4b5-c6d7e8f9a0b1 + response: + body: + string: '' + headers: + Content-Type: + - application/octet-stream + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_commands/test_tables_schema.py b/tests/test_commands/test_tables_schema.py index 8fb735e3f..3356d5baf 100644 --- a/tests/test_commands/test_tables_schema.py +++ b/tests/test_commands/test_tables_schema.py @@ -11,6 +11,9 @@ from fabric_cli.commands.tables import fab_tables_schema from fabric_cli.core import fab_constant from fabric_cli.core.fab_exceptions import FabricCLIError +from fabric_cli.core.fab_types import ItemType +from tests.conftest import mock_questionary_print # noqa: F401 +from tests.test_commands.commands_parser import CLIExecutor class TestTablesSchemaUnit: @@ -194,3 +197,36 @@ def test_get_table_schema_verifies_abfss_uri_format(self, mock_auth, mock_delta_ assert isinstance(result, list) assert len(result) == 1 + + +class TestTablesSchemaIntegration: + """Integration tests for table schema command - validates full dispatch stack.""" + + def test_table_schema_success( + self, + item_factory, + cli_executor: CLIExecutor, + mock_questionary_print, + ): + lakehouse = item_factory(ItemType.LAKEHOUSE) + + mock_questionary_print.reset_mock() + + with patch( + "fabric_cli.commands.tables.fab_tables_schema.DeltaTable" + ) as mock_dt, patch( + "fabric_cli.commands.tables.fab_tables_schema.FabAuth" + ) as mock_auth: + mock_auth.return_value.get_access_token.return_value = "mock_token" + mock_table = MagicMock() + mock_table.schema.return_value.to_json.return_value = json.dumps({ + "fields": [{"name": "id", "type": "integer", "nullable": False, "metadata": {}}] + }) + mock_dt.return_value = mock_table + + cli_executor.exec_command( + f"table schema {lakehouse.full_path}/Tables/my_table" + ) + + calls = mock_questionary_print.call_args_list + assert any("Schema extracted successfully" in str(c) for c in calls) From 4cda700ddf6022747c78f5de33faa807337c57de Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Sat, 16 May 2026 00:12:41 +0200 Subject: [PATCH 15/16] fix: improve error handling in table schema extraction --- src/fabric_cli/commands/tables/fab_tables_schema.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index c015fc3b9..7acc91705 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -45,8 +45,8 @@ def _get_table_schema(args: Namespace) -> list[dict]: if not isinstance(schema_fields, list): raise ValueError("Delta table schema JSON does not contain a valid 'fields' list.") return schema_fields - except (DeltaError, json.JSONDecodeError, TypeError, KeyError, ValueError): + except (DeltaError, json.JSONDecodeError, ValueError) as exc: raise FabricCLIError( - "Failed to extract the table schema. Please ensure the path points to a valid Delta table", + f"Failed to extract the table schema. Please ensure the path points to a valid Delta table: {exc}", fab_constant.ERROR_INVALID_DELTA_TABLE, - ) + ) from exc From c954a2a6f02fc0f5bb7255f58a7f41b6596aa1c0 Mon Sep 17 00:00:00 2001 From: Piotr Kontek Date: Sun, 17 May 2026 13:23:44 +0200 Subject: [PATCH 16/16] fix: ensure access token is validated before schema extraction --- src/fabric_cli/commands/tables/fab_tables_schema.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fabric_cli/commands/tables/fab_tables_schema.py b/src/fabric_cli/commands/tables/fab_tables_schema.py index 7acc91705..bda99d66e 100644 --- a/src/fabric_cli/commands/tables/fab_tables_schema.py +++ b/src/fabric_cli/commands/tables/fab_tables_schema.py @@ -21,6 +21,11 @@ def exec_command(args: Namespace) -> None: def _get_table_schema(args: Namespace) -> list[dict]: token = FabAuth().get_access_token(fab_constant.SCOPE_ONELAKE_DEFAULT) + if token is None: + raise FabricCLIError( + "Failed to obtain access token.", + fab_constant.ERROR_AUTHENTICATION_FAILED, + ) if args.schema: local_path = f"Tables/{args.schema}/{args.table_name}" else: