Skip to content

Commit 6f3f74e

Browse files
authored
fix(NoTicket): better decimal parsing and testing (#429)
1 parent abe0d56 commit 6f3f74e

File tree

13 files changed

+249
-57
lines changed

13 files changed

+249
-57
lines changed

src/firebolt/common/_types.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,8 @@ def parse_value(
341341
raise DataError(f"Invalid bytea value {value}: str expected")
342342
return _parse_bytea(value)
343343
if isinstance(ctype, DECIMAL):
344-
if not isinstance(value, (str, int, float)):
344+
if not isinstance(value, (str, int)):
345345
raise DataError(f"Invalid decimal value {value}: str or int expected")
346-
if isinstance(value, float):
347-
# Decimal constructor doesn't support float
348-
# so we need to convert it to string first
349-
value = str(value)
350346
return Decimal(value)
351347
if isinstance(ctype, ARRAY):
352348
if not isinstance(value, list):

src/firebolt/common/row_set/streaming_common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ def _next_json_lines_record_from_line(
9494
return None
9595

9696
try:
97-
record = json.loads(next_line)
97+
# Skip parsing floats to properly parse them later
98+
record = json.loads(next_line, parse_float=str)
9899
except json.JSONDecodeError as err:
99100
raise OperationalError(
100101
f"Invalid JSON line response format: {next_line}"

src/firebolt/common/row_set/synchronous/in_memory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ def append_response_stream(self, stream: Iterator[bytes]) -> None:
4343
self.append_empty_response()
4444
else:
4545
try:
46-
query_data = json.loads(content)
46+
# Skip parsing floats to properly parse them later
47+
query_data = json.loads(content, parse_float=str)
4748

4849
if "errors" in query_data and len(query_data["errors"]) > 0:
4950
raise FireboltStructuredError(query_data)

tests/integration/dbapi/async/V1/conftest.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
from decimal import Decimal
2-
from typing import List
3-
41
from pytest import fixture
52

63
from firebolt.async_db import Connection, connect
74
from firebolt.client.auth.base import Auth
8-
from firebolt.common._types import ColType
95

106

117
@fixture
@@ -78,14 +74,3 @@ async def connection_no_engine(
7874
api_endpoint=api_endpoint,
7975
) as connection:
8076
yield connection
81-
82-
83-
@fixture
84-
def all_types_query_response_v1(
85-
all_types_query_response: List[List[ColType]],
86-
) -> List[List[ColType]]:
87-
"""
88-
V1 still returns decimals as floats, despite overflow. That's why it's not fully accurate.
89-
"""
90-
all_types_query_response[0][18] = Decimal("1231232.1234599999152123928070068359375")
91-
return all_types_query_response

tests/integration/dbapi/async/V1/test_queries_async.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,15 @@ async def test_connect_engine_name(
7878
connection_engine_name: Connection,
7979
all_types_query: str,
8080
all_types_query_description: List[Column],
81-
all_types_query_response_v1: List[ColType],
81+
all_types_query_response: List[ColType],
8282
timezone_name: str,
8383
) -> None:
8484
"""Connecting with engine name is handled properly."""
8585
await test_select(
8686
connection_engine_name,
8787
all_types_query,
8888
all_types_query_description,
89-
all_types_query_response_v1,
89+
all_types_query_response,
9090
timezone_name,
9191
)
9292

@@ -95,15 +95,15 @@ async def test_connect_no_engine(
9595
connection_no_engine: Connection,
9696
all_types_query: str,
9797
all_types_query_description: List[Column],
98-
all_types_query_response_v1: List[ColType],
98+
all_types_query_response: List[ColType],
9999
timezone_name: str,
100100
) -> None:
101101
"""Connecting with engine name is handled properly."""
102102
await test_select(
103103
connection_no_engine,
104104
all_types_query,
105105
all_types_query_description,
106-
all_types_query_response_v1,
106+
all_types_query_response,
107107
timezone_name,
108108
)
109109

@@ -112,7 +112,7 @@ async def test_select(
112112
connection: Connection,
113113
all_types_query: str,
114114
all_types_query_description: List[Column],
115-
all_types_query_response_v1: List[ColType],
115+
all_types_query_response: List[ColType],
116116
timezone_name: str,
117117
) -> None:
118118
"""Select handles all data types properly."""
@@ -130,15 +130,15 @@ async def test_select(
130130
assert c.rowcount == 1, "Invalid rowcount value"
131131
data = await c.fetchall()
132132
assert len(data) == c.rowcount, "Invalid data length"
133-
assert_deep_eq(data, all_types_query_response_v1, "Invalid data")
133+
assert_deep_eq(data, all_types_query_response, "Invalid data")
134134
assert c.description == all_types_query_description, "Invalid description value"
135135
assert len(data[0]) == len(c.description), "Invalid description length"
136136
assert len(await c.fetchall()) == 0, "Redundant data returned by fetchall"
137137

138138
# Different fetch types
139139
await c.execute(all_types_query)
140140
assert (
141-
await c.fetchone() == all_types_query_response_v1[0]
141+
await c.fetchone() == all_types_query_response[0]
142142
), "Invalid fetchone data"
143143
assert await c.fetchone() is None, "Redundant data returned by fetchone"
144144

@@ -147,7 +147,7 @@ async def test_select(
147147
data = await c.fetchmany()
148148
assert len(data) == 1, "Invalid data size returned by fetchmany"
149149
assert_deep_eq(
150-
data, all_types_query_response_v1, "Invalid data returned by fetchmany"
150+
data, all_types_query_response, "Invalid data returned by fetchmany"
151151
)
152152

153153

@@ -328,7 +328,7 @@ async def test_empty_query(c: Cursor, query: str, params: tuple) -> None:
328328
datetime(2022, 1, 1, 1, 1, 1),
329329
True,
330330
[1, 2, 3],
331-
Decimal(123.456),
331+
Decimal("123.456"),
332332
]
333333

334334
await test_empty_query(

tests/integration/dbapi/sync/V1/conftest.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
from decimal import Decimal
2-
from typing import List
3-
41
from pytest import fixture
52

63
from firebolt.client.auth.base import Auth
7-
from firebolt.common._types import ColType
84
from firebolt.db import Connection, connect
95

106

@@ -97,14 +93,3 @@ def connection_system_engine(
9793
)
9894
yield connection
9995
connection.close()
100-
101-
102-
@fixture
103-
def all_types_query_response_v1(
104-
all_types_query_response: List[List[ColType]],
105-
) -> List[List[ColType]]:
106-
"""
107-
V1 still returns decimals as floats, despite overflow. That's why it's not fully accurate.
108-
"""
109-
all_types_query_response[0][18] = Decimal("1231232.1234599999152123928070068359375")
110-
return all_types_query_response

tests/integration/dbapi/sync/V1/test_queries.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ def test_connect_engine_name(
3030
connection_engine_name: Connection,
3131
all_types_query: str,
3232
all_types_query_description: List[Column],
33-
all_types_query_response_v1: List[ColType],
33+
all_types_query_response: List[ColType],
3434
timezone_name: str,
3535
) -> None:
3636
"""Connecting with engine name is handled properly."""
3737
test_select(
3838
connection_engine_name,
3939
all_types_query,
4040
all_types_query_description,
41-
all_types_query_response_v1,
41+
all_types_query_response,
4242
timezone_name,
4343
)
4444

@@ -47,15 +47,15 @@ def test_connect_no_engine(
4747
connection_no_engine: Connection,
4848
all_types_query: str,
4949
all_types_query_description: List[Column],
50-
all_types_query_response_v1: List[ColType],
50+
all_types_query_response: List[ColType],
5151
timezone_name: str,
5252
) -> None:
5353
"""Connecting with engine name is handled properly."""
5454
test_select(
5555
connection_no_engine,
5656
all_types_query,
5757
all_types_query_description,
58-
all_types_query_response_v1,
58+
all_types_query_response,
5959
timezone_name,
6060
)
6161

@@ -64,7 +64,7 @@ def test_select(
6464
connection: Connection,
6565
all_types_query: str,
6666
all_types_query_description: List[Column],
67-
all_types_query_response_v1: List[ColType],
67+
all_types_query_response: List[ColType],
6868
timezone_name: str,
6969
) -> None:
7070
"""Select handles all data types properly."""
@@ -82,22 +82,22 @@ def test_select(
8282
assert c.rowcount == 1, "Invalid rowcount value"
8383
data = c.fetchall()
8484
assert len(data) == c.rowcount, "Invalid data length"
85-
assert_deep_eq(data, all_types_query_response_v1, "Invalid data")
85+
assert_deep_eq(data, all_types_query_response, "Invalid data")
8686
assert c.description == all_types_query_description, "Invalid description value"
8787
assert len(data[0]) == len(c.description), "Invalid description length"
8888
assert len(c.fetchall()) == 0, "Redundant data returned by fetchall"
8989

9090
# Different fetch types
9191
c.execute(all_types_query)
92-
assert c.fetchone() == all_types_query_response_v1[0], "Invalid fetchone data"
92+
assert c.fetchone() == all_types_query_response[0], "Invalid fetchone data"
9393
assert c.fetchone() is None, "Redundant data returned by fetchone"
9494

9595
c.execute(all_types_query)
9696
assert len(c.fetchmany(0)) == 0, "Invalid data size returned by fetchmany"
9797
data = c.fetchmany()
9898
assert len(data) == 1, "Invalid data size returned by fetchmany"
9999
assert_deep_eq(
100-
data, all_types_query_response_v1, "Invalid data returned by fetchmany"
100+
data, all_types_query_response, "Invalid data returned by fetchmany"
101101
)
102102

103103

@@ -273,7 +273,7 @@ def test_empty_query(c: Cursor, query: str, params: tuple) -> None:
273273
datetime(2022, 1, 1, 1, 1, 1),
274274
True,
275275
[1, 2, 3],
276-
Decimal(123.456),
276+
Decimal("123.456"),
277277
]
278278

279279
test_empty_query(

tests/unit/common/row_set/asynchronous/test_in_memory.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from decimal import Decimal
23
from unittest.mock import MagicMock, patch
34

45
import pytest
@@ -173,6 +174,7 @@ def test_append_empty_response(self, in_memory_rowset):
173174

174175
async def test_append_response(self, in_memory_rowset, mock_response):
175176
"""Test appending a response with data."""
177+
176178
# Create a proper aclose method
177179
async def mock_aclose():
178180
mock_response.is_closed = True
@@ -207,6 +209,7 @@ async def test_append_response_empty_content(
207209
self, in_memory_rowset, mock_empty_response
208210
):
209211
"""Test appending a response with empty content."""
212+
210213
# Create a proper aclose method
211214
async def mock_aclose():
212215
mock_empty_response.is_closed = True
@@ -226,6 +229,7 @@ async def test_append_response_invalid_json(
226229
self, in_memory_rowset, mock_invalid_json_response
227230
):
228231
"""Test appending a response with invalid JSON."""
232+
229233
# Create a proper aclose method
230234
async def mock_aclose():
231235
mock_invalid_json_response.is_closed = True
@@ -245,6 +249,7 @@ async def test_append_response_missing_meta(
245249
self, in_memory_rowset, mock_missing_meta_response
246250
):
247251
"""Test appending a response with missing meta field."""
252+
248253
# Create a proper aclose method
249254
async def mock_aclose():
250255
mock_missing_meta_response.is_closed = True
@@ -264,6 +269,7 @@ async def test_append_response_missing_data(
264269
self, in_memory_rowset, mock_missing_data_response
265270
):
266271
"""Test appending a response with missing data field."""
272+
267273
# Create a proper aclose method
268274
async def mock_aclose():
269275
mock_missing_data_response.is_closed = True
@@ -281,6 +287,7 @@ async def mock_aclose():
281287

282288
async def test_nextset_no_more_sets(self, in_memory_rowset, mock_response):
283289
"""Test nextset when there are no more result sets."""
290+
284291
# Create a proper aclose method
285292
async def mock_aclose():
286293
pass
@@ -296,6 +303,7 @@ async def test_nextset_with_more_sets(self, in_memory_rowset, mock_response):
296303
The implementation seems to add rowsets correctly, but behaves differently
297304
than expected when accessing them via nextset.
298305
"""
306+
299307
# Create a proper aclose method
300308
async def mock_aclose():
301309
pass
@@ -322,6 +330,7 @@ async def mock_aclose():
322330

323331
async def test_iteration(self, in_memory_rowset, mock_response):
324332
"""Test row iteration."""
333+
325334
# Create a proper aclose method
326335
async def mock_aclose():
327336
pass
@@ -347,6 +356,7 @@ async def test_iteration_after_nextset(self, in_memory_rowset, mock_response):
347356
This test is tricky because in the mock setup, the second row set
348357
is actually empty despite us adding the same mock response.
349358
"""
359+
350360
# Create a proper aclose method
351361
async def mock_aclose():
352362
pass
@@ -410,6 +420,7 @@ async def test_empty_rowset_iteration(self, in_memory_rowset):
410420

411421
async def test_aclose(self, in_memory_rowset, mock_response):
412422
"""Test aclose method."""
423+
413424
# Create a proper aclose method
414425
async def mock_aclose():
415426
pass
@@ -423,3 +434,23 @@ async def mock_aclose():
423434

424435
# Verify sync close was called
425436
mock_close.assert_called_once()
437+
438+
async def test_append_response_with_decimals(
439+
self, in_memory_rowset: InMemoryAsyncRowSet, mock_decimal_bytes_stream: Response
440+
):
441+
442+
await in_memory_rowset.append_response(mock_decimal_bytes_stream)
443+
444+
# Verify basic properties
445+
assert in_memory_rowset.row_count == 2
446+
assert len(in_memory_rowset.columns) == 3
447+
448+
# Get the row values and check decimal values are equal
449+
rows = [row async for row in in_memory_rowset]
450+
451+
# Verify the decimal value is correctly parsed
452+
for row in rows:
453+
assert isinstance(row[2], Decimal), "Expected Decimal type"
454+
assert (
455+
str(row[2]) == "1231232.123459999990457054844258706536"
456+
), "Decimal value mismatch"

0 commit comments

Comments
 (0)