-
Notifications
You must be signed in to change notification settings - Fork 109
Fall back to the original string for complex MAP/STRUCT/ARRAY values instead of returning None #763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8a75aea
f04125b
cd3bd74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,7 +89,7 @@ def _to_json(varchar_value: str | None) -> Any | None: | |
| return json.loads(varchar_value) | ||
|
|
||
|
|
||
| def _to_array(varchar_value: str | None) -> list[Any] | None: | ||
| def _to_array(varchar_value: str | None) -> list[Any] | str | None: | ||
| """Convert array data to Python list. | ||
|
|
||
| Supports two formats: | ||
|
|
@@ -102,7 +102,9 @@ def _to_array(varchar_value: str | None) -> list[Any] | None: | |
| varchar_value: String representation of array data | ||
|
|
||
| Returns: | ||
| List representation of array, or None if parsing fails | ||
| List representation of array, the original string if the value is | ||
| too complex to parse (so the data is preserved), or None if the | ||
| input is None or does not look like an array | ||
| """ | ||
| if varchar_value is None: | ||
| return None | ||
|
|
@@ -127,15 +129,18 @@ def _to_array(varchar_value: str | None) -> list[Any] | None: | |
| try: | ||
| # For nested arrays, too complex for basic parsing | ||
| if "[" in inner: | ||
| # Contains nested arrays - too complex for basic parsing | ||
| return None | ||
| # Try native parsing (including struct arrays) | ||
| return _parse_array_native(inner) | ||
| # Contains nested arrays - keep the original string | ||
| # instead of silently dropping the value | ||
| return varchar_value | ||
| # Try native parsing (including struct arrays); if the helper had to | ||
| # give up (or skip any item), keep the original string | ||
| result = _parse_array_native(inner) | ||
| return result if result is not None else varchar_value | ||
| except Exception: | ||
| return None | ||
| return varchar_value | ||
|
|
||
|
|
||
| def _to_map(varchar_value: str | None) -> dict[str, Any] | None: | ||
| def _to_map(varchar_value: str | None) -> dict[str, Any] | str | None: | ||
| """Convert map data to Python dictionary. | ||
|
|
||
| Supports two formats: | ||
|
|
@@ -148,7 +153,9 @@ def _to_map(varchar_value: str | None) -> dict[str, Any] | None: | |
| varchar_value: String representation of map data | ||
|
|
||
| Returns: | ||
| Dictionary representation of map, or None if parsing fails | ||
| Dictionary representation of map, the original string if the value | ||
| is too complex to parse (so the data is preserved), or None if the | ||
| input is None or does not look like a map | ||
| """ | ||
| if varchar_value is None: | ||
| return None | ||
|
|
@@ -177,16 +184,20 @@ def _to_map(varchar_value: str | None) -> dict[str, Any] | None: | |
|
|
||
| try: | ||
| # MAP format is always key=value pairs | ||
| # But for complex structures, return None to keep as string | ||
| # But for complex structures, keep the original string | ||
| if any(char in inner for char in "()[]"): | ||
| # Contains complex structures (arrays, structs), skip parsing | ||
| return None | ||
| return _parse_map_native(inner) | ||
| # and keep the original string instead of silently dropping it | ||
| return varchar_value | ||
| # If the helper had to give up (or skip any pair, e.g. nested | ||
| # braces-only values like '{a={b=1}}'), keep the original string | ||
| result = _parse_map_native(inner) | ||
| return result if result is not None else varchar_value | ||
| except Exception: | ||
| return None | ||
| return varchar_value | ||
|
|
||
|
|
||
| def _to_struct(varchar_value: str | None) -> dict[str, Any] | None: | ||
| def _to_struct(varchar_value: str | None) -> dict[str, Any] | str | None: | ||
| """Convert struct data to Python dictionary. | ||
|
|
||
| Supports two formats: | ||
|
|
@@ -199,7 +210,9 @@ def _to_struct(varchar_value: str | None) -> dict[str, Any] | None: | |
| varchar_value: String representation of struct data | ||
|
|
||
| Returns: | ||
| Dictionary representation of struct, or None if parsing fails | ||
| Dictionary representation of struct, the original string if the | ||
| value is too complex to parse (so the data is preserved), or None | ||
| if the input is None or does not look like a struct | ||
| """ | ||
| if varchar_value is None: | ||
| return None | ||
|
|
@@ -228,12 +241,15 @@ def _to_struct(varchar_value: str | None) -> dict[str, Any] | None: | |
|
|
||
| try: | ||
| if "=" in inner: | ||
| # Named struct: {a=1, b=2} | ||
| return _parse_named_struct(inner) | ||
| # Named struct: {a=1, b=2}; if the helper had to give up | ||
| # (or skip any pair), keep the original string | ||
| result = _parse_named_struct(inner) | ||
| return result if result is not None else varchar_value | ||
| # Unnamed struct: {Alice, 25} | ||
| return _parse_unnamed_struct(inner) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
_to_struct('{"a"=1}') # None — the key containing a quote is skippedWorth applying the same
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f04125b — |
||
| except Exception: | ||
| return None | ||
| # Keep the original string instead of silently dropping the value | ||
| return varchar_value | ||
|
|
||
|
|
||
| def _parse_array_native(inner: str) -> list[Any] | None: | ||
|
|
@@ -243,7 +259,10 @@ def _parse_array_native(inner: str) -> list[Any] | None: | |
| inner: Interior content of array without brackets. | ||
|
|
||
| Returns: | ||
| List with parsed values, or None if no valid values found. | ||
| List with parsed values, or None if no valid values were found or | ||
| any item had to be skipped. Returning None on a skipped item lets | ||
| callers fall back to the original string, so the result is always | ||
| either fully parsed or the intact raw value - never a partial list. | ||
| """ | ||
| result = [] | ||
|
|
||
|
|
@@ -258,13 +277,15 @@ def _parse_array_native(inner: str) -> list[Any] | None: | |
| if item.strip().startswith("{") and item.strip().endswith("}"): | ||
| # This is a struct value - parse it as a struct | ||
| struct_value = _to_struct(item.strip()) | ||
| if struct_value is not None: | ||
| result.append(struct_value) | ||
| if struct_value is None: | ||
| return None | ||
| result.append(struct_value) | ||
| continue | ||
|
|
||
| # Skip items with nested arrays or complex quoting (safety check) | ||
| # Items with nested arrays or complex quoting (safety check): | ||
| # give up entirely instead of silently dropping the item | ||
| if any(char in item for char in '[]="'): | ||
| continue | ||
| return None | ||
|
|
||
| # Convert item to appropriate type | ||
| converted_item = _convert_value(item) | ||
|
|
@@ -280,24 +301,34 @@ def _parse_map_native(inner: str) -> dict[str, Any] | None: | |
| inner: Interior content of map without braces. | ||
|
|
||
| Returns: | ||
| Dictionary with parsed key-value pairs, or None if no valid pairs found. | ||
| Dictionary with parsed key-value pairs, or None if no valid pairs | ||
| were found or any pair had to be skipped. Returning None on a | ||
| skipped pair lets callers fall back to the original string, so the | ||
| result is always either fully parsed or the intact raw value - | ||
| never a partial dict. | ||
| """ | ||
| result = {} | ||
|
|
||
| # Simple split by comma for basic cases | ||
| pairs = [pair.strip() for pair in inner.split(",")] | ||
|
|
||
| for pair in pairs: | ||
| if "=" not in pair: | ||
| if not pair: | ||
| continue | ||
|
|
||
| # A chunk without '=' is not a parseable pair: give up entirely | ||
| # instead of silently dropping it | ||
| if "=" not in pair: | ||
| return None | ||
|
|
||
| key, value = pair.split("=", 1) | ||
| key = key.strip() | ||
| value = value.strip() | ||
|
|
||
| # Skip pairs with special characters (safety check) | ||
| # Pairs with special characters (safety check): give up entirely | ||
| # instead of silently dropping the pair | ||
| if any(char in key for char in '{}="') or any(char in value for char in '{}="'): | ||
| continue | ||
| return None | ||
|
|
||
| # Convert both key and value to appropriate types | ||
| converted_key = _convert_value(key) | ||
|
|
@@ -317,24 +348,34 @@ def _parse_named_struct(inner: str) -> dict[str, Any] | None: | |
| inner: Interior content of struct without braces. | ||
|
|
||
| Returns: | ||
| Dictionary with parsed key-value pairs, or None if no valid pairs found. | ||
| Dictionary with parsed key-value pairs, or None if no valid pairs | ||
| were found or any pair had to be skipped. Returning None on a | ||
| skipped pair lets callers fall back to the original string, so the | ||
| result is always either fully parsed or the intact raw value - | ||
| never a partial dict. | ||
| """ | ||
| result = {} | ||
|
|
||
| # Use smart split to handle nested structures | ||
| pairs = _split_array_items(inner) | ||
|
|
||
| for pair in pairs: | ||
| if "=" not in pair: | ||
| if not pair: | ||
| continue | ||
|
|
||
| # A chunk without '=' is not a parseable pair: give up entirely | ||
| # instead of silently dropping it | ||
| if "=" not in pair: | ||
| return None | ||
|
|
||
| key, value = pair.split("=", 1) | ||
| key = key.strip() | ||
| value = value.strip() | ||
|
|
||
| # Skip if key contains special characters (safety check) | ||
| # Keys with special characters (safety check): give up entirely | ||
| # instead of silently dropping the pair | ||
| if any(char in key for char in '{}="'): | ||
| continue | ||
| return None | ||
|
|
||
| # Handle nested struct values | ||
| if value.startswith("{") and value.endswith("}"): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more case in the same class of silent data loss (pre-existing, but I'd like it folded into this PR since it directly undermines the "never silently lose data" goal):
_parse_map_nativesilently drops individual pairs whose key/value contains{}=", so a partially parseable map returns a partial dict — which is arguably worse thanNonewas, because the loss is invisible:_parse_array_nativehas the same issue for skipped items (_to_array('[a, b=1]')→['a']).A clean way to handle this together with the other comments: make the
_parse_*_nativehelpers returnNonewhenever they had to skip anything, and let the outer functions fall back to the original string. That way the result is always either fully parsed or the intact raw string, never a partial value.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that a partial dict/list is worse than
Nonewas — implemented exactly as you suggested in f04125b:_parse_map_native/_parse_array_native/_parse_named_structnow returnNonewhenever any pair/item has to be skipped, and the outer functions fall back to the original string. The result is always either fully parsed or the intact raw string, never partial. Added tests for{a="x", b=1},{a, b=1}, and[a, b=1](previously{'b': '1'}/['a']). All 137 unit tests pass, ruff/mypy clean.