Skip to content

Commit d89da33

Browse files
fix: resolve CI test failures for struct support
Address two critical issues identified in CI: 1. **Struct Converter Enhancement**: - Add support for Athena's native struct format {a=1, b=2} - Maintain backward compatibility with JSON format - Implement robust parsing with proper error handling - Add comprehensive tests for both formats 2. **TypeCompiler Test Fixes**: - Fix missing dialect parameter in test instantiation - Add proper mock dialect for TypeCompiler tests - Ensure all compiler tests use correct constructor Key improvements: - Enhanced _to_struct() function handles both JSON and native Athena formats - Proper key-value parsing with automatic type detection - Robust error handling for malformed struct data - All tests now pass with proper mocking This resolves all 8 test failures identified in CI: ✅ struct conversion tests ✅ TypeCompiler tests ✅ integration tests 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 02f0520 commit d89da33

3 files changed

Lines changed: 81 additions & 11 deletions

File tree

pyathena/converter.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,52 @@ def _to_json(varchar_value: Optional[str]) -> Optional[Any]:
8181
def _to_struct(varchar_value: Optional[str]) -> Optional[Dict[str, Any]]:
8282
if varchar_value is None:
8383
return None
84+
85+
# First try to parse as JSON
8486
try:
8587
result = json.loads(varchar_value)
8688
return result if isinstance(result, dict) else None
8789
except json.JSONDecodeError:
88-
return None
90+
pass
91+
92+
# Handle Athena's native struct format: {a=1, b=2}
93+
if varchar_value.startswith("{") and varchar_value.endswith("}"):
94+
try:
95+
# Convert Athena struct format to JSON format
96+
# Replace '=' with ':' and ensure proper quoting for keys
97+
inner = varchar_value[1:-1].strip()
98+
if not inner:
99+
return {}
100+
101+
pairs = []
102+
# Simple parsing for key=value pairs
103+
for pair in inner.split(","):
104+
pair = pair.strip()
105+
if "=" in pair:
106+
key, value = pair.split("=", 1)
107+
key = key.strip()
108+
value = value.strip()
109+
110+
# Add quotes to key if not already quoted
111+
if not (key.startswith('"') and key.endswith('"')):
112+
key = f'"{key}"'
113+
114+
# Handle value quoting - if it's not a number, quote it
115+
if not (value.isdigit() or value in ("true", "false", "null")) and not (
116+
value.startswith('"') and value.endswith('"')
117+
):
118+
value = f'"{value}"'
119+
120+
pairs.append(f"{key}:{value}")
121+
122+
json_str = "{" + ",".join(pairs) + "}"
123+
result = json.loads(json_str)
124+
return result if isinstance(result, dict) else None
125+
except (ValueError, json.JSONDecodeError):
126+
pass
127+
128+
# If all parsing attempts fail, return None
129+
return None
89130

90131

91132
def _to_default(varchar_value: Optional[str]) -> Optional[str]:
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- coding: utf-8 -*-
22

3+
from unittest.mock import Mock
4+
35
from sqlalchemy import Integer, String
46

57
from pyathena.sqlalchemy.compiler import AthenaTypeCompiler
@@ -8,13 +10,15 @@
810

911
class TestAthenaTypeCompiler:
1012
def test_visit_struct_empty(self):
11-
compiler = AthenaTypeCompiler()
13+
dialect = Mock()
14+
compiler = AthenaTypeCompiler(dialect)
1215
struct_type = AthenaStruct()
1316
result = compiler.visit_struct(struct_type)
1417
assert result == "ROW()"
1518

1619
def test_visit_struct_with_fields(self):
17-
compiler = AthenaTypeCompiler()
20+
dialect = Mock()
21+
compiler = AthenaTypeCompiler(dialect)
1822
struct_type = AthenaStruct(("name", String), ("age", Integer))
1923
result = compiler.visit_struct(struct_type)
2024
# The exact order might vary, so we check that both fields are present
@@ -24,7 +28,8 @@ def test_visit_struct_with_fields(self):
2428
assert result.endswith(")")
2529

2630
def test_visit_struct_uppercase(self):
27-
compiler = AthenaTypeCompiler()
31+
dialect = Mock()
32+
compiler = AthenaTypeCompiler(dialect)
2833
struct_type = STRUCT(("id", Integer), ("title", String))
2934
result = compiler.visit_STRUCT(struct_type)
3035
assert "ROW(" in result
@@ -34,13 +39,15 @@ def test_visit_struct_uppercase(self):
3439

3540
def test_visit_struct_no_fields_attribute(self):
3641
# Test struct type without fields attribute
37-
compiler = AthenaTypeCompiler()
42+
dialect = Mock()
43+
compiler = AthenaTypeCompiler(dialect)
3844
struct_type = type("MockStruct", (), {})()
3945
result = compiler.visit_struct(struct_type)
4046
assert result == "ROW()"
4147

4248
def test_visit_struct_single_field(self):
43-
compiler = AthenaTypeCompiler()
49+
dialect = Mock()
50+
compiler = AthenaTypeCompiler(dialect)
4451
struct_type = AthenaStruct(("name", String))
4552
result = compiler.visit_struct(struct_type)
4653
assert result == "ROW(name STRING)" or result == "ROW(name VARCHAR)"

tests/pyathena/test_converter.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ def test_to_struct_invalid_json():
2828
assert result is None
2929

3030

31+
def test_to_struct_athena_native_format():
32+
"""Test conversion of Athena's native struct format {a=1, b=2}"""
33+
struct_value = "{a=1, b=2}"
34+
result = _to_struct(struct_value)
35+
expected = {"a": 1, "b": 2}
36+
assert result == expected
37+
38+
39+
def test_to_struct_athena_empty_struct():
40+
"""Test conversion of empty Athena struct {}"""
41+
struct_value = "{}"
42+
result = _to_struct(struct_value)
43+
assert result == {}
44+
45+
46+
def test_to_struct_athena_string_values():
47+
"""Test Athena struct with string values"""
48+
struct_value = "{name=John, city=Tokyo}"
49+
result = _to_struct(struct_value)
50+
expected = {"name": "John", "city": "Tokyo"}
51+
assert result == expected
52+
53+
3154
def test_to_struct_empty_string():
3255
result = _to_struct("")
3356
assert result is None
@@ -74,9 +97,8 @@ def test_struct_conversion_invalid_json(self):
7497
def test_struct_conversion_athena_format(self):
7598
"""Test conversion of actual Athena struct format like {a=1, b=2}"""
7699
converter = DefaultTypeConverter()
77-
# This is how Athena actually returns struct data in some cases
78-
# For now, our converter expects JSON format, but this test documents the behavior
100+
# This is how Athena actually returns struct data
79101
result = converter.convert("row", "{a=1, b=2}")
80-
# Currently returns None because it's not valid JSON
81-
# This could be enhanced in the future to parse Athena's struct format
82-
assert result is None
102+
# Now supports Athena's native struct format
103+
expected = {"a": 1, "b": 2}
104+
assert result == expected

0 commit comments

Comments
 (0)