Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c22f70e
fix: Add deltalake dependency and refactor schema extraction logic
pkontek Apr 30, 2026
c0dd71b
fix: Refactor `fab tables schema` to utilize `deltalake` library for …
pkontek Apr 30, 2026
f0327bc
fix: Correct typo in error constant for invalid Delta table
pkontek Apr 30, 2026
746e8f6
Remove unused import
pkontek Apr 30, 2026
ca07e17
fix: Replace TableNotFoundError with DeltaError for schema extraction…
pkontek May 7, 2026
0d18818
Merge branch 'tables_schema_fix' of https://github.com/SoletPL/fabric…
pkontek May 7, 2026
baaba5d
fix: Update schema extraction to use json.loads for DeltaTable schema
pkontek May 7, 2026
8221723
Potential fix for pull request finding
pkontek May 7, 2026
1012572
fix: Add unit tests for table schema command
pkontek May 7, 2026
3963efa
fix: Add deltalake dependency to development requirements
pkontek May 7, 2026
34c7141
rename test_tables to test_tables_schema
pkontek May 15, 2026
bce23dc
extract shared mock fixtures in test_tables_schema
pkontek May 15, 2026
a4c4ba7
combine delta error tests into parametrized test case
pkontek May 15, 2026
55c3515
Potential fix for pull request finding
pkontek May 15, 2026
f206e12
feat: add integration tests for table schema command and mock API res…
pkontek May 15, 2026
4cda700
fix: improve error handling in table schema extraction
pkontek May 15, 2026
c954a2a
fix: ensure access token is validated before schema extraction
pkontek May 17, 2026
0ba9da9
fix: correct URL formatting in do_request function
pkontek Jun 17, 2026
ed43c39
Merge branch 'main' into tables_schema_fix
pkontek Jun 17, 2026
c015864
fix: redact internal exception details from table schema error message
pkontek Jun 18, 2026
b6da0e7
refactor: extract DeltaTable construction into fab_delta_client
pkontek Jun 18, 2026
7996c2e
fix: use context.local_path instead of reconstructing table path in s…
pkontek Jun 18, 2026
6d4ac61
fix: strip .Shortcut suffix from table_local_path in add_table_props_…
pkontek Jun 18, 2026
ed998af
fix: reject item types without Delta-compatible Tables/ in schema com…
pkontek Jun 18, 2026
18df707
test: lock delta-rs schema serialisation contract for complex types
pkontek Jun 18, 2026
ceba9fc
fix: use ErrorMessages.Auth.access_token_failed() in fab_delta_client
pkontek Jun 18, 2026
ee79ee5
test: add checkpoint regression test for issue #228
pkontek Jun 18, 2026
d0a7e23
refactor: split test_tables_schema by taxonomy per test.instructions.md
pkontek Jun 18, 2026
82c732b
refactor: replace VCR cassette with pure dispatch test for table schema
pkontek Jun 18, 2026
b7d0831
test: add missing auth token None test for fab_delta_client
pkontek Jun 18, 2026
3761717
hore: tighten deltalake dependency to >=1.0.0,<2.0.0
pkontek Jun 18, 2026
df0360b
fix: strip .Shortcut from args.table_name in add_table_props_to_args
pkontek Jun 18, 2026
eebb6f2
style: apply black formatting to changed files
pkontek Jun 18, 2026
12bfa45
Fix deltalake version in requirements
pkontek Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/fixed-20260430-130558.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"requests",
"cryptography",
"fabric-cicd>=0.3.1",
"deltalake>=1.0.0,<2.0.0",
]

[project.scripts]
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ psutil==7.0.0
requests
cryptography
fabric-cicd>=0.3.1
deltalake>=1.0.0,<2.0.0

# Testing and Building Requirements
tox>=4.20.0
Expand Down
74 changes: 74 additions & 0 deletions src/fabric_cli/client/fab_delta_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import json
from argparse import Namespace

from deltalake import DeltaTable
from deltalake.exceptions import DeltaError

from fabric_cli.core import fab_constant
from fabric_cli.core.fab_auth import FabAuth
from fabric_cli.core.fab_exceptions import FabricCLIError
from fabric_cli.errors import ErrorMessages

# Item types whose OneLake Tables/ folder contains standard Delta tables.
# SemanticModel is explicitly excluded: its Tables/ entries are columnar
# semantic-model representations, not Delta-compatible parquet.
_DELTA_SUPPORTED_ITEM_TYPES: frozenset[str] = frozenset(
{
"Lakehouse",
"Warehouse",
"KQLDatabase",
"MirroredDatabase",
"SQLDatabase",
}
)


def get_table_schema(args: Namespace, local_path: str) -> list[dict]:
"""Return schema fields for a Delta table on OneLake.

Single seam for DeltaTable construction: owns item-type validation,
URI building, storage_options assembly, token acquisition, and
exception mapping.
"""
item_type = getattr(args, "item_type", None)
if item_type is not None and item_type not in _DELTA_SUPPORTED_ITEM_TYPES:
raise FabricCLIError(
ErrorMessages.Table.unsupported_item_type_for_delta(item_type),
fab_constant.ERROR_INVALID_ITEM_TYPE,
)

token = FabAuth().get_access_token(fab_constant.SCOPE_ONELAKE_DEFAULT)
if token is None:
raise FabricCLIError(
ErrorMessages.Auth.access_token_failed(),
fab_constant.ERROR_AUTHENTICATION_FAILED,
)

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",
},
)
schema_dict = json.loads(table.schema().to_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, ValueError) as exc:
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 exc
75 changes: 6 additions & 69 deletions src/fabric_cli/commands/tables/fab_tables_schema.py
Comment thread
pkontek marked this conversation as resolved.
Comment thread
pkontek marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,80 +1,17 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import json
from argparse import Namespace
from typing import Optional

from fabric_cli.client import fab_api_onelake as onelake_api
from fabric_cli.core import fab_constant
from fabric_cli.core import fab_handle_context as handle_context
from fabric_cli.core.fab_exceptions import FabricCLIError
from fabric_cli.core.hiearchy.fab_hiearchy import OneLakeItem
from fabric_cli.client import fab_delta_client as delta_client
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)

Comment thread
pkontek marked this conversation as resolved.
else:
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"]
Comment thread
pkontek marked this conversation as resolved.
return schema

return None
def _get_table_schema(args: Namespace) -> list[dict]:
return delta_client.get_table_schema(args, args.table_local_path)
6 changes: 2 additions & 4 deletions src/fabric_cli/core/fab_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is fab_constant considered a stable/public interface that external tooling imports directly? If so, a deprecated alias makes sense. If it's internal-only, the sweep was clean and no alias is needed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ayeshurun, what do you think about that?

Comment thread
pkontek marked this conversation as resolved.
ERROR_INVALID_QUERY_FIELDS = "InvalidQueryFields"
ERROR_INVALID_WORKSPACE_TYPE = "InvalidWorkspaceType"
ERROR_INVALID_QUERY = "InvalidQuery"
Expand Down Expand Up @@ -351,4 +350,3 @@

# Invalid query parameters for set command across all fabric resources
SET_COMMAND_INVALID_QUERIES = ["id", "type", "workspaceId", "folderId"]

9 changes: 8 additions & 1 deletion src/fabric_cli/errors/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ def invalid_format_argument(part: str) -> str:

@staticmethod
def invalid_key(key: str, allowed_keys: str) -> str:
return f"Invalid key: '{key}'. Allowed keys are: {allowed_keys}"
return f"Invalid key: '{key}'. Allowed keys are: {allowed_keys}"

@staticmethod
def unsupported_item_type_for_delta(item_type: str) -> str:
return (
f"'{item_type}' does not expose Delta tables under Tables/. "
"Delta table schema is supported for: Lakehouse, Warehouse, KQLDatabase, MirroredDatabase, SQLDatabase."
)
4 changes: 3 additions & 1 deletion src/fabric_cli/utils/fab_cmd_table_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from fabric_cli.core.fab_exceptions import FabricCLIError
from fabric_cli.core.hiearchy.fab_onelake_element import OneLakeItem
from fabric_cli.errors import ErrorMessages
from fabric_cli.utils import fab_util as utils


def add_table_props_to_args(args: Any, context: OneLakeItem) -> None:
Expand All @@ -21,8 +22,9 @@ def add_table_props_to_args(args: Any, context: OneLakeItem) -> None:
args.lakehouse_path = context.item.path

table_path = context.local_path.split("/")
args.table_name = table_path[-1]
args.table_name = utils.remove_dot_suffix(table_path[-1])
args.schema = table_path[-2] if len(table_path) == 3 else None
args.table_local_path = utils.remove_dot_suffix(context.local_path)


def convert_hours_to_dhhmmss(hours: int) -> str:
Expand Down
11 changes: 8 additions & 3 deletions tests/test_commands/commands_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Licensed under the MIT License.

import platform

from prompt_toolkit.input import DummyInput
from prompt_toolkit.output import DummyOutput

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -65,6 +69,7 @@
register_rm_parser,
register_mkdir_parser,
register_jobs_parser,
register_tables_parser,
]


Expand Down
Loading