diff --git a/google/cloud/sqlalchemy_spanner/requirements.py b/google/cloud/sqlalchemy_spanner/requirements.py index 3ec9f7a0..1b929847 100644 --- a/google/cloud/sqlalchemy_spanner/requirements.py +++ b/google/cloud/sqlalchemy_spanner/requirements.py @@ -68,3 +68,9 @@ def get_isolation_levels(self, _): dict: isolation levels description. """ return {"default": "SERIALIZABLE", "supported": ["SERIALIZABLE", "AUTOCOMMIT"]} + + @property + def precision_numerics_enotation_large(self): + """target backend supports Decimal() objects using E notation + to represent very large values.""" + return exclusions.open() diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index 42d66a8a..cd4a1572 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -288,9 +288,6 @@ def visit_large_binary(self, type_, **kw): def visit_DECIMAL(self, type_, **kw): return "NUMERIC" - def visit_NUMERIC(self, type_, **kw): - return "NUMERIC" - def visit_VARCHAR(self, type_, **kw): return "STRING({})".format(type_.length or "MAX") @@ -303,6 +300,9 @@ def visit_BOOLEAN(self, type_, **kw): def visit_DATETIME(self, type_, **kw): return "TIMESTAMP" + def visit_NUMERIC(self, type_, **kw): + return "NUMERIC" + def visit_BIGINT(self, type_, **kw): return "INT64" @@ -329,6 +329,7 @@ class SpannerDialect(DefaultDialect): supports_sequences = True supports_native_enum = True supports_native_boolean = True + supports_native_decimal = True ddl_compiler = SpannerDDLCompiler preparer = SpannerIdentifierPreparer diff --git a/test/test_suite.py b/test/test_suite.py index 49f789d9..3904f159 100644 --- a/test/test_suite.py +++ b/test/test_suite.py @@ -16,6 +16,7 @@ import operator import pytest +import decimal import pytz import sqlalchemy @@ -28,8 +29,8 @@ from sqlalchemy.testing import engines from sqlalchemy.testing import db from sqlalchemy.testing import eq_ +from sqlalchemy.testing import provide_metadata, emits_warning from sqlalchemy.testing import fixtures -from sqlalchemy.testing import provide_metadata from sqlalchemy.testing.provision import temp_table_keyword_args from sqlalchemy.testing.schema import Column from sqlalchemy.testing.schema import Table @@ -41,8 +42,9 @@ from sqlalchemy import util from sqlalchemy import event from sqlalchemy import exists -from sqlalchemy import LargeBinary from sqlalchemy import Boolean +from sqlalchemy import Float +from sqlalchemy import LargeBinary from sqlalchemy import String from sqlalchemy.types import Integer from sqlalchemy.types import Numeric @@ -94,6 +96,7 @@ DateTimeTest as _DateTimeTest, IntegerTest as _IntegerTest, _LiteralRoundTripFixture, + NumericTest as _NumericTest, StringTest as _StringTest, TextTest as _TextTest, TimeTest as _TimeTest, @@ -514,19 +517,18 @@ def _literal_round_trip(self, type_, input_, output, filter_=None): """ SPANNER OVERRIDE: - Spanner DBAPI does not execute DDL statements unless followed by a - non DDL statement, which is preventing correct table clean up. - The table already exists after related tests finish, so it doesn't - create a new table and when running tests for other data types - insertions will fail with `400 Duplicate name in schema: t`. - Overriding the tests to create and drop a new table to prevent - database existence errors. - """ + Spanner is not able cleanup data and drop the table correctly, + table was already exists after related tests finished, so it doesn't + create a new table and when started tests for other data type following + insertions will fail with `400 Duplicate name in schema: t. + Overriding the tests to create a new table for test and drop table manually + before it creates a new table to avoid the same failures.""" # for literal, we test the literal render in an INSERT # into a typed column. we can then SELECT it back as its # official type; ideally we'd be able to use CAST here # but MySQL in particular can't CAST fully + t = Table("int_t", self.metadata, Column("x", type_)) t.create() @@ -1020,3 +1022,320 @@ def test_text_roundtrip(self): config.db.execute(text_table.insert(), {"id": 1, "text_data": "some text"}) row = config.db.execute(select([text_table.c.text_data])).first() eq_(row, ("some text",)) + + +class NumericTest(_NumericTest): + @emits_warning(r".*does \*not\* support Decimal objects natively") + def test_render_literal_numeric(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + """ + self._literal_round_trip( + Numeric(precision=8, scale=4), [15.7563], [decimal.Decimal("15.7563")], + ) + self._literal_round_trip( + Numeric(precision=8, scale=4), + [decimal.Decimal("15.7563")], + [decimal.Decimal("15.7563")], + ) + + @emits_warning(r".*does \*not\* support Decimal objects natively") + def test_render_literal_numeric_asfloat(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + """ + self._literal_round_trip( + Numeric(precision=8, scale=4, asdecimal=False), [15.7563], [15.7563], + ) + self._literal_round_trip( + Numeric(precision=8, scale=4, asdecimal=False), + [decimal.Decimal("15.7563")], + [15.7563], + ) + + def test_render_literal_float(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + """ + self._literal_round_trip( + Float(4), + [decimal.Decimal("15.7563")], + [15.7563], + filter_=lambda n: n is not None and round(n, 5) or None, + ) + + self._literal_round_trip( + Float(4), + [decimal.Decimal("15.7563")], + [15.7563], + filter_=lambda n: n is not None and round(n, 5) or None, + ) + + @requires.precision_generic_float_type + def test_float_custom_scale(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + """ + self._do_test( + Float(None, decimal_return_scale=7, asdecimal=True), + [15.7563827], + [decimal.Decimal("15.7563827")], + check_scale=True, + ) + + self._do_test( + Float(None, decimal_return_scale=7, asdecimal=True), + [15.7563827], + [decimal.Decimal("15.7563827")], + check_scale=True, + ) + + def test_numeric_as_decimal(self): + """ + SPANNER OVERRIDE: + + Spanner throws an error 400 Value has type FLOAT64 which cannot be + inserted into column x, which has type NUMERIC for value 15.7563. + Overriding the test to remove the failure case. + """ + self._do_test( + Numeric(precision=8, scale=4), + [decimal.Decimal("15.7563")], + [decimal.Decimal("15.7563")], + ) + + def test_numeric_as_float(self): + """ + SPANNER OVERRIDE: + + Spanner throws an error 400 Value has type FLOAT64 which cannot be + inserted into column x, which has type NUMERIC for value 15.7563. + Overriding the test to remove the failure case. + """ + + self._do_test( + Numeric(precision=8, scale=4, asdecimal=False), + [decimal.Decimal("15.7563")], + [15.7563], + ) + + @requires.floats_to_four_decimals + def test_float_as_decimal(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + """ + self._do_test( + Float(precision=8, asdecimal=True), [15.7563], [decimal.Decimal("15.7563")], + ) + + self._do_test( + Float(precision=8, asdecimal=True), + [decimal.Decimal("15.7563")], + [decimal.Decimal("15.7563")], + ) + + def test_float_as_float(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + """ + self._do_test( + Float(precision=8), + [15.7563], + [15.7563], + filter_=lambda n: n is not None and round(n, 5) or None, + ) + + self._do_test( + Float(precision=8), + [decimal.Decimal("15.7563")], + [15.7563], + filter_=lambda n: n is not None and round(n, 5) or None, + ) + + @requires.precision_numerics_general + def test_precision_decimal(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + + Remove an extra digits after decimal point as cloud spanner is + capable of representing an exact numeric value with a precision + of 38 and scale of 9. + """ + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("54.234246451")], + [decimal.Decimal("54.234246451")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("0.004354")], + [decimal.Decimal("0.004354")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("900.0")], + [decimal.Decimal("900.0")], + ) + + @testing.requires.precision_numerics_enotation_large + def test_enotation_decimal_large(self): + """test exceedingly large decimals. + + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + """ + + self._do_test( + Numeric(precision=25, scale=2), + [decimal.Decimal("4E+8")], + [decimal.Decimal("4E+8")], + ) + + self._do_test( + Numeric(precision=25, scale=2), + [decimal.Decimal("5748E+15")], + [decimal.Decimal("5748E+15")], + ) + + self._do_test( + Numeric(precision=25, scale=2), + [decimal.Decimal("1.521E+15")], + [decimal.Decimal("1.521E+15")], + ) + + self._do_test( + Numeric(precision=25, scale=2), + [decimal.Decimal("00000000000000.1E+12")], + [decimal.Decimal("00000000000000.1E+12")], + ) + + @testing.requires.precision_numerics_enotation_large + def test_enotation_decimal(self): + """test exceedingly small decimals. + + Decimal reports values with E notation when the exponent + is greater than 6. + + SPANNER OVERRIDE: + + Cloud Spanner supports tables with an empty primary key, but + only a single row can be inserted into such a table - + following insertions will fail with `Row [] already exists". + Overriding the test to avoid the same failure. + + Remove extra digits after decimal point as cloud spanner is + capable of representing an exact numeric value with a precision + of 38 and scale of 9. + """ + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("1E-2")], + [decimal.Decimal("1E-2")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("1E-3")], + [decimal.Decimal("1E-3")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("1E-4")], + [decimal.Decimal("1E-4")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("1E-5")], + [decimal.Decimal("1E-5")], + ) + + self._do_test( + Numeric(precision=18, scale=14), + [decimal.Decimal("1E-6")], + [decimal.Decimal("1E-6")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("1E-7")], + [decimal.Decimal("1E-7")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("1E-8")], + [decimal.Decimal("1E-8")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("0.010000059")], + [decimal.Decimal("0.010000059")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("0.000000059")], + [decimal.Decimal("0.000000059")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("0.000000696")], + [decimal.Decimal("0.000000696")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("0.700000696")], + [decimal.Decimal("0.700000696")], + ) + + self._do_test( + Numeric(precision=18, scale=9), + [decimal.Decimal("696E-9")], + [decimal.Decimal("696E-9")], + )