FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: add decimal validation for numeric precision and scale supported by Spanner by vi3k6i5 · Pull Request #340 · googleapis/python-spanner · GitHub

This repository was archived by the owner on Jun 8, 2026. It is now read-only.
/ python-spanner Public archive
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .py  (3) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
33 changes: 33 additions & 0 deletions google/cloud/spanner_v1/_helpers.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@
from google.cloud.spanner_v1 import ExecuteSqlRequest


# Validation error messages
NUMERIC_MAX_SCALE_ERR_MSG = (
"Max scale for a numeric is 9. The requested numeric has scale {}"
)
NUMERIC_MAX_PRECISION_ERR_MSG = (
"Max precision for the whole component of a numeric is 29. The requested "
+ "numeric has a whole component with precision {}"
)


def _try_to_coerce_bytes(bytestring):
"""Try to coerce a byte string into the right thing based on Python
version and whether or not it is base64 encoded.
Expand Down Expand Up @@ -87,6 +97,28 @@ def _merge_query_options(base, merge):
return combined


def _assert_numeric_precision_and_scale(value):
"""
Asserts that input numeric field is within Spanner supported range.

Spanner supports fixed 38 digits of precision and 9 digits of scale.
This number can be optionally prefixed with a plus or minus sign.
Read more here: https://cloud.google.com/spanner/docs/data-types#numeric_type

:type value: decimal.Decimal
:param value: The value to check for Cloud Spanner compatibility.

:raises NotSupportedError: If value is not within supported precision or scale of Spanner.
"""
scale = value.as_tuple().exponent
precision = len(value.as_tuple().digits)

if scale < -9:
raise ValueError(NUMERIC_MAX_SCALE_ERR_MSG.format(abs(scale)))
if precision + scale > 29:
raise ValueError(NUMERIC_MAX_PRECISION_ERR_MSG.format(precision + scale))


# pylint: disable=too-many-return-statements,too-many-branches
def _make_value_pb(value):
"""Helper for :func:`_make_list_value_pbs`.
Expand Down Expand Up @@ -129,6 +161,7 @@ def _make_value_pb(value):
if isinstance(value, ListValue):
return Value(list_value=value)
if isinstance(value, decimal.Decimal):
_assert_numeric_precision_and_scale(value)
return Value(string_value=str(value))
raise ValueError("Unknown type: %s" % (value,))

Expand Down
12 changes: 0 additions & 12 deletions tests/unit/spanner_dbapi/test_parse_utils.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,6 @@ def test_rows_for_insert_or_update(self):

@unittest.skipIf(skip_condition, skip_message)
def test_sql_pyformat_args_to_spanner(self):
import decimal

from google.cloud.spanner_dbapi.parse_utils import sql_pyformat_args_to_spanner

cases = [
Expand Down Expand Up @@ -300,16 +298,6 @@ def test_sql_pyformat_args_to_spanner(self):
("SELECT * from t WHERE id=10", {"f1": "app", "f2": "name"}),
("SELECT * from t WHERE id=10", {"f1": "app", "f2": "name"}),
),
(
(
"SELECT (an.p + %s) AS np FROM an WHERE (an.p + %s) = %s",
(1, 1.0, decimal.Decimal("31")),
),
(
"SELECT (an.p + @a0) AS np FROM an WHERE (an.p + @a1) = @a2",
{"a0": 1, "a1": 1.0, "a2": decimal.Decimal("31")},
),
),
]
for ((sql_in, params), sql_want) in cases:
with self.subTest(sql=sql_in):
Expand Down
59 changes: 59 additions & 0 deletions tests/unit/test__helpers.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,65 @@ def test_w_unknown_type(self):
with self.assertRaises(ValueError):
self._callFUT(object())

def test_w_numeric_precision_and_scale_valid(self):
import decimal
from google.protobuf.struct_pb2 import Value

cases = [
decimal.Decimal("42"),
decimal.Decimal("9.9999999999999999999999999999999999999E+28"),
decimal.Decimal("-9.9999999999999999999999999999999999999E+28"),
decimal.Decimal("99999999999999999999999999999.999999999"),
decimal.Decimal("1E+28"),
decimal.Decimal("1E-9"),
]
for value in cases:
with self.subTest(value=value):
value_pb = self._callFUT(value)
self.assertIsInstance(value_pb, Value)
self.assertEqual(value_pb.string_value, str(value))

def test_w_numeric_precision_and_scale_invalid(self):
import decimal
from google.cloud.spanner_v1._helpers import (
NUMERIC_MAX_SCALE_ERR_MSG,
NUMERIC_MAX_PRECISION_ERR_MSG,
)

max_precision_error_msg = NUMERIC_MAX_PRECISION_ERR_MSG.format("30")
max_scale_error_msg = NUMERIC_MAX_SCALE_ERR_MSG.format("10")

cases = [
(
decimal.Decimal("9.9999999999999999999999999999999999999E+29"),
max_precision_error_msg,
),
(
decimal.Decimal("-9.9999999999999999999999999999999999999E+29"),
max_precision_error_msg,
),
(
decimal.Decimal("999999999999999999999999999999.99999999"),
max_precision_error_msg,
),
(
decimal.Decimal("-999999999999999999999999999999.99999999"),
max_precision_error_msg,
),
(
decimal.Decimal("999999999999999999999999999999"),
max_precision_error_msg,
),
(decimal.Decimal("1E+29"), max_precision_error_msg),
(decimal.Decimal("1E-10"), max_scale_error_msg),
]

for value, err_msg in cases:
with self.subTest(value=value, err_msg=err_msg):
self.assertRaisesRegex(
ValueError, err_msg, lambda: self._callFUT(value),
)


class Test_make_list_value_pb(unittest.TestCase):
def _callFUT(self, *args, **kw):
Expand Down

Back | FazBrowse Home | New Git URL