Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/arcadedb/type_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import json
import math
import re
import sys
from datetime import date, datetime, time, timedelta, timezone
from decimal import Decimal
Expand Down Expand Up @@ -746,18 +747,24 @@ def _has_subnormal(value: Any) -> bool:
return False


# the exponent json.dumps writes for a subnormal double (5e-324 up to 2.2250738585072009e-308): e-308 to e-329 after a digit
_SUBNORMAL_EXPONENT = re.compile(r"[0-9]e-3(?:0[89]|[12][0-9])")


def _dumps_checked(rows: Any, **kwargs: Any) -> str:
"""json.dumps for the bulk paths, refusing a subnormal double.

The engine's JSON parser reads the text of a subnormal (``5e-324``) as a
BigDecimal, so it was stored as a DECIMAL and read back as ``Decimal``
where every other write path stores a double. ValueError sends the row down
the per-row path, which stores the double. Every subnormal is written with
an exponent of -308 or below, so the text is searched first (C speed) and
only a hit pays for the walk over the rows.
an exponent of -308 or below, so the text is searched first (C speed) for
such an exponent after a digit, and only a hit pays for the walk over the
rows. A bare "e-3" would also hit ordinary text (hex ids, UUIDs, "name-3")
and send every such load through the walk.
"""
text = json.dumps(rows, allow_nan=False, **kwargs)
if "e-3" in text and any(_has_subnormal(row) for row in rows):
if _SUBNORMAL_EXPONENT.search(text) and any(_has_subnormal(row) for row in rows):
raise ValueError("a subnormal double cannot go through the JSON bulk path")
return text

Expand Down
17 changes: 17 additions & 0 deletions tests/test_bulk_json_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,20 @@ def test_ordinary_rows_stay_on_the_bulk_path(temp_db_path):
)
def test_json_bulk_scalar_ok(value, ok):
assert type_conversion.json_bulk_scalar_ok(value) is ok


def test_text_that_only_looks_like_an_exponent_skips_the_subnormal_walk(monkeypatch):
# "e-3" is common in ordinary text (hex ids, UUIDs, "name-3"): only an exponent a subnormal can have may send a chunk
# through the per-row Python walk, so a load of such text keeps the fast check
calls = []
real = type_conversion._has_subnormal
monkeypatch.setattr(
type_conversion, "_has_subnormal", lambda row: calls.append(row) or real(row)
)
rows = [{"id": f"4fe-3a{i}", "tag": "name-3", "x": 1e-30} for i in range(3)]
assert type_conversion.json_bulk_dumps(rows) == json.dumps(rows)
assert calls == []
with pytest.raises(ValueError):
type_conversion.json_bulk_dumps([{"x": 5e-324}])
with pytest.raises(ValueError):
type_conversion.json_bulk_dumps([{"x": 2.2250738585072009e-308}])
Loading