diff --git a/aiochclient/_types.pyx b/aiochclient/_types.pyx index a5b2856..1a1f9ae 100644 --- a/aiochclient/_types.pyx +++ b/aiochclient/_types.pyx @@ -65,7 +65,7 @@ RE_NULLABLE = re.compile(r"^Nullable\((.*)\)$") RE_LOW_CARDINALITY = re.compile(r"^LowCardinality\((.*)\)$") RE_MAP = re.compile(r"^Map\((.*)\)$") RE_REPLACE_QUOTE = re.compile(r"(? IPv4Address: return self.p_type(value.decode()) @staticmethod - def unconvert(value: UUID) -> bytes: + def unconvert(value: IPv4Address) -> bytes: return b"%a" % str(value) @@ -323,14 +323,27 @@ def __init__(self, name: str, **kwargs): self.value_type = what_py_type(tps[comma_index + 1 :], container=True) def p_type(self, string: str) -> dict: - key, value = string[1:-1].split(':', 1) - return { - self.key_type.p_type(key): self.value_type.p_type(value) - - } + """Parse a TSV-encoded Map string into a dictionary.""" + # Remove surrounding brackets or quotes if present + string = string.strip("[]'\"") + if not string: + return {} + + # Split by tabs (TSV format for Map in ClickHouse) + parts = string.split('\t') + if len(parts) % 2 != 0: + raise ChClientError(f"Invalid Map TSV format: {string}") + + # Convert pairs into a dictionary + result = {} + for i in range(0, len(parts), 2): + key = self.key_type.p_type(parts[i]) + value = self.value_type.p_type(parts[i + 1]) + result[key] = value + return result def convert(self, value: bytes) -> dict: - return self.p_type(value.decode()) + return self.p_type(self.decode(value)) @staticmethod def unconvert(value) -> bytes: @@ -349,10 +362,7 @@ def __init__(self, name: str, **kwargs): self.type = what_py_type(RE_ARRAY.findall(name)[0], container=True) def p_type(self, string: str) -> list: - return [ - self.type.p_type(val) - for val in self.seq_parser(string[1:-1]) - ] + return [self.type.p_type(val) for val in self.seq_parser(string[1:-1])] def convert(self, value: bytes) -> list: return self.p_type(value.decode()) diff --git a/tests/test_map_fix.py b/tests/test_map_fix.py new file mode 100644 index 0000000..a915e15 --- /dev/null +++ b/tests/test_map_fix.py @@ -0,0 +1,59 @@ +import pytest +from aiohttp import ClientSession +from aiochclient import ChClient + + +@pytest.mark.asyncio +async def test_map_decoding_multiple_pairs(): + async with ClientSession() as session: + client = ChClient(session, url="http://localhost:8123", user="default", password="") + result = await client.fetch( + "SELECT map('a', '1', 'b', '2', 'c', '3') AS data", + decode=True + ) + assert len(result) == 1 + assert result[0]['data'] == {'a': '1', 'b': '2', 'c': '3'}, "Multi-pair map decoding failed" + + +@pytest.mark.asyncio +async def test_map_decoding_single_pair(): + async with ClientSession() as session: + client = ChClient(session, url="http://localhost:8123") + result = await client.fetch( + "SELECT map('x', 'y') AS data", + decode=True + ) + assert len(result) == 1 + assert result[0]['data'] == {'x': 'y'}, "Single-pair map decoding failed" + + +@pytest.mark.asyncio +async def test_map_decoding_empty(): + async with ClientSession() as session: + client = ChClient(session, url="http://localhost:8123") + result = await client.fetch( + "SELECT map() AS data", + decode=True + ) + assert len(result) == 1 + assert result[0]['data'] == {}, "Empty map decoding failed" + + +@pytest.mark.asyncio +async def test_map_table_insert_and_fetch(): + async with ClientSession() as session: + client = ChClient(session, url="http://localhost:8123") + await client.execute( + "CREATE TABLE IF NOT EXISTS test_map (id UInt8, data Map(String, String)) ENGINE = Memory" + ) + await client.execute( + "INSERT INTO test_map VALUES", + (1, {'a': '1', 'b': '2', 'c': '3'}) + ) + result = await client.fetch( + "SELECT data FROM test_map WHERE id = 1", + decode=True + ) + assert len(result) == 1 + assert result[0]['data'] == {'a': '1', 'b': '2', 'c': '3'}, "Table map decoding failed" + await client.execute("DROP TABLE test_map") \ No newline at end of file