diff --git a/.changes/unreleased/Bug Fix-20251205-114146.yaml b/.changes/unreleased/Bug Fix-20251205-114146.yaml new file mode 100644 index 00000000..7ed9595b --- /dev/null +++ b/.changes/unreleased/Bug Fix-20251205-114146.yaml @@ -0,0 +1,3 @@ +kind: Bug Fix +body: Fix JSON serialization error when querying metrics that return Decimal values from the dbt Semantic Layer +time: 2025-12-05T11:41:46.988675841Z diff --git a/src/dbt_mcp/semantic_layer/client.py b/src/dbt_mcp/semantic_layer/client.py index 859d6b75..13c77324 100644 --- a/src/dbt_mcp/semantic_layer/client.py +++ b/src/dbt_mcp/semantic_layer/client.py @@ -3,6 +3,7 @@ from collections.abc import Callable from contextlib import AbstractContextManager from datetime import date, datetime +from decimal import Decimal from typing import Any, Protocol import pyarrow as pa @@ -44,15 +45,17 @@ def DEFAULT_RESULT_FORMATTER(table: pa.Table) -> str: # Convert PyArrow table to list of dictionaries records = table.to_pylist() - # Custom JSON encoder to handle date/datetime objects - class DateTimeEncoder(json.JSONEncoder): + # Custom JSON encoder to handle date/datetime and Decimal objects + class ExtendedJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime | date): return obj.isoformat() + if isinstance(obj, Decimal): + return float(obj) return super().default(obj) # Return JSON with records format and proper indentation - return json.dumps(records, indent=2, cls=DateTimeEncoder) + return json.dumps(records, indent=2, cls=ExtendedJSONEncoder) class SemanticLayerClientProtocol(Protocol): diff --git a/tests/unit/semantic_layer/test_client.py b/tests/unit/semantic_layer/test_client.py index ecd1a379..3208d737 100644 --- a/tests/unit/semantic_layer/test_client.py +++ b/tests/unit/semantic_layer/test_client.py @@ -1,5 +1,6 @@ import datetime as dt import json +from decimal import Decimal import pyarrow as pa @@ -140,3 +141,31 @@ def test_default_result_formatter_various_numeric_types() -> None: assert parsed[0]["int_col"] == 1 assert abs(parsed[0]["float_col"] - 1.1) < 0.0001 assert abs(parsed[0]["decimal_col"] - 100.50) < 0.0001 + + +def test_default_result_formatter_with_python_decimal() -> None: + """Test handling of Python Decimal objects from PyArrow decimal128 columns. + + This tests the fix for Decimal JSON serialization where PyArrow decimal128 + columns return Python Decimal objects that need special handling in JSON encoding. + """ + # Create a PyArrow table with decimal128 type (which returns Python Decimal objects) + decimal_array = pa.array( + [Decimal("123.45"), Decimal("678.90"), Decimal("0.01")], + type=pa.decimal128(10, 2), + ) + table = pa.table( + { + "amount": decimal_array, + "name": pa.array(["a", "b", "c"]), + } + ) + + # This should not raise "Object of type Decimal is not JSON serializable" + output = DEFAULT_RESULT_FORMATTER(table) + parsed = json.loads(output) + + # Verify Decimal values are correctly converted to floats + assert abs(parsed[0]["amount"] - 123.45) < 0.0001 + assert abs(parsed[1]["amount"] - 678.90) < 0.0001 + assert abs(parsed[2]["amount"] - 0.01) < 0.0001