diff --git a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index b672f46d..92f225b7 100644 --- a/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -33,7 +33,7 @@ "DATETIME": types.DATETIME, "FLOAT64": types.Float, "INT64": types.BIGINT, - "NUMERIC": types.DECIMAL, + "NUMERIC": types.NUMERIC(precision=38, scale=9), "STRING": types.String, "TIME": types.TIME, "TIMESTAMP": types.TIMESTAMP, @@ -176,6 +176,9 @@ def visit_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) @@ -314,12 +317,17 @@ def get_columns(self, connection, table_name, schema=None, **kw): columns = snap.execute_sql(sql) for col in columns: - type_ = "STRING" if col[1].startswith("STRING") else col[1] + if col[1].startswith("STRING"): + end = col[1].index(")") + size = int(col[1][7:end]) + type_ = _type_map["STRING"](length=size) + else: + type_ = _type_map[col[1]] cols_desc.append( { "name": col[0], - "type": _type_map[type_], + "type": type_, "nullable": col[2] == "YES", "default": None, } diff --git a/test/test_suite.py b/test/test_suite.py index da5f3cfa..7d50af1d 100644 --- a/test/test_suite.py +++ b/test/test_suite.py @@ -16,6 +16,7 @@ import operator import pytest +import pytz import sqlalchemy from sqlalchemy import inspect @@ -41,6 +42,7 @@ from sqlalchemy import Boolean from sqlalchemy import String from sqlalchemy.types import Integer +from sqlalchemy.types import Numeric from sqlalchemy.testing import requires from google.api_core.datetime_helpers import DatetimeWithNanoseconds @@ -50,6 +52,7 @@ from sqlalchemy.testing.suite.test_cte import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_ddl import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_dialect import * # noqa: F401, F403 +from sqlalchemy.testing.suite.test_results import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_cte import CTETest as _CTETest @@ -64,6 +67,7 @@ from sqlalchemy.testing.suite.test_reflection import ( ComponentReflectionTest as _ComponentReflectionTest, ) +from sqlalchemy.testing.suite.test_results import RowFetchTest as _RowFetchTest from sqlalchemy.testing.suite.test_select import ExistsTest as _ExistsTest from sqlalchemy.testing.suite.test_types import BooleanTest as _BooleanTest from sqlalchemy.testing.suite.test_types import IntegerTest as _IntegerTest @@ -739,6 +743,48 @@ def test_get_temp_table_indexes(self): def test_get_temp_table_unique_constraints(self): pass + @testing.requires.table_reflection + def test_numeric_reflection(self): + """ + SPANNER OVERRIDE: + + Spanner defines NUMERIC type with the constant precision=38 + and scale=9. Overriding the test to check if the NUMERIC + column is successfully created and has dimensions + correct for Cloud Spanner. + """ + for typ in self._type_round_trip(Numeric(18, 5)): + assert isinstance(typ, Numeric) + eq_(typ.precision, 38) + eq_(typ.scale, 9) + + +class RowFetchTest(_RowFetchTest): + def test_row_w_scalar_select(self): + """ + SPANNER OVERRIDE: + + Cloud Spanner returns a DatetimeWithNanoseconds() for date + data types. Overriding the test to use a DatetimeWithNanoseconds + type value as an expected result. + -------------- + + test that a scalar select as a column is returned as such + and that type conversion works OK. + + (this is half a SQLAlchemy Core test and half to catch database + backends that may have unusual behavior with scalar selects.) + """ + datetable = self.tables.has_dates + s = select([datetable.alias("x").c.today]).as_scalar() + s2 = select([datetable.c.id, s.label("somelabel")]) + row = config.db.execute(s2).first() + + eq_( + row["somelabel"], + DatetimeWithNanoseconds(2006, 5, 12, 12, 0, 0, tzinfo=pytz.UTC), + ) + class InsertBehaviorTest(_InsertBehaviorTest): @pytest.mark.skip("Spanner doesn't support empty inserts")