Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add support for json data type #593

Merged
merged 9 commits into from Oct 4, 2021
2 changes: 2 additions & 0 deletions google/cloud/spanner_dbapi/parse_utils.py
Expand Up @@ -21,6 +21,7 @@

import sqlparse
from google.cloud import spanner_v1 as spanner
from google.cloud.spanner_v1.types.data_types import JsonObject

from .exceptions import Error, ProgrammingError
from .parser import parse_values
Expand All @@ -38,6 +39,7 @@
DateStr: spanner.param_types.DATE,
TimestampStr: spanner.param_types.TIMESTAMP,
decimal.Decimal: spanner.param_types.NUMERIC,
JsonObject: spanner.param_types.JSON,
}

SPANNER_RESERVED_KEYWORDS = {
Expand Down
7 changes: 6 additions & 1 deletion google/cloud/spanner_v1/_helpers.py
Expand Up @@ -17,6 +17,7 @@
import datetime
import decimal
import math
import json

import six

Expand All @@ -28,7 +29,7 @@
from google.cloud._helpers import _datetime_to_rfc3339
from google.cloud.spanner_v1 import TypeCode
from google.cloud.spanner_v1 import ExecuteSqlRequest

from google.cloud.spanner_v1.types.data_types import JsonObject

# Validation error messages
NUMERIC_MAX_SCALE_ERR_MSG = (
Expand Down Expand Up @@ -166,6 +167,10 @@ def _make_value_pb(value):
if isinstance(value, decimal.Decimal):
_assert_numeric_precision_and_scale(value)
return Value(string_value=str(value))
if isinstance(value, JsonObject):
return Value(
string_value=json.dumps(value, sort_keys=True, separators=(",", ":"),)
)
raise ValueError("Unknown type: %s" % (value,))


Expand Down
2 changes: 2 additions & 0 deletions google/cloud/spanner_v1/types/__init__.py
Expand Up @@ -62,6 +62,7 @@
Type,
TypeCode,
)
from .data_types import JsonObject

__all__ = (
"CommitResponse",
Expand Down Expand Up @@ -101,4 +102,5 @@
"StructType",
"Type",
"TypeCode",
"JsonObject",
)
25 changes: 25 additions & 0 deletions google/cloud/spanner_v1/types/data_types.py
@@ -0,0 +1,25 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Custom data types for spanner."""


class JsonObject(dict):
"""
JsonObject type help format Django jsonfield to compatible Cloud Spanner's
vi3k6i5 marked this conversation as resolved.
Show resolved Hide resolved
JSON type. Before making queries, it'll help differentiate between
normal parameters and JSON parameters.
"""

pass
49 changes: 48 additions & 1 deletion tests/system/test_dbapi.py
Expand Up @@ -15,11 +15,11 @@
import hashlib
import pickle
import pkg_resources

import pytest

from google.cloud import spanner_v1
from google.cloud.spanner_dbapi.connection import connect, Connection
from google.cloud.spanner_v1.types.data_types import JsonObject
from . import _helpers

DATABASE_NAME = "dbapi-txn"
Expand Down Expand Up @@ -328,6 +328,53 @@ def test_DDL_autocommit(shared_instance, dbapi_database):
conn.commit()


@pytest.mark.skipif(_helpers.USE_EMULATOR, reason="Emulator does not support json.")
def test_autocommit_with_json_data(shared_instance, dbapi_database):
"""Check that DDLs in autocommit mode are immediately executed for
json fields."""
# Create table
conn = Connection(shared_instance, dbapi_database)
conn.autocommit = True

cur = conn.cursor()
cur.execute(
"""
CREATE TABLE JsonDetails (
DataId INT64 NOT NULL,
Details JSON,
) PRIMARY KEY (DataId)
"""
)
conn.close()
vi3k6i5 marked this conversation as resolved.
Show resolved Hide resolved

# Insert data to table
conn = Connection(shared_instance, dbapi_database)
conn.autocommit = True
cur = conn.cursor()
cur.execute(
sql="INSERT INTO JsonDetails (DataId, Details) VALUES (%s, %s)",
args=(123, JsonObject({"name": "Jakob", "age": "26"})),
)

# Read back the data.
conn = Connection(shared_instance, dbapi_database)
cur = conn.cursor()
cur.execute("""select * from JsonDetails;""")
got_rows = cur.fetchall()
conn.close()

# Assert the response
assert len(got_rows) == 1
assert got_rows[0][0] == 123
assert got_rows[0][1] == '{"age":"26","name":"Jakob"}'

# Drop the table
conn = Connection(shared_instance, dbapi_database)
cur = conn.cursor()
cur.execute("DROP TABLE JsonDetails")
conn.commit()


def test_DDL_commit(shared_instance, dbapi_database):
"""Check that DDLs in commit mode are executed on calling `commit()`."""
conn = Connection(shared_instance, dbapi_database)
Expand Down
11 changes: 8 additions & 3 deletions tests/unit/spanner_dbapi/test_parse_utils.py
Expand Up @@ -16,6 +16,7 @@
import unittest

from google.cloud.spanner_v1 import param_types
from google.cloud.spanner_v1.types.data_types import JsonObject


class TestParseUtils(unittest.TestCase):
Expand Down Expand Up @@ -333,9 +334,11 @@ def test_get_param_types(self):
import datetime
import decimal

from google.cloud.spanner_dbapi.parse_utils import DateStr
from google.cloud.spanner_dbapi.parse_utils import TimestampStr
from google.cloud.spanner_dbapi.parse_utils import get_param_types
from google.cloud.spanner_dbapi.parse_utils import (
DateStr,
TimestampStr,
get_param_types,
)

params = {
"a1": 10,
Expand All @@ -349,6 +352,7 @@ def test_get_param_types(self):
"i1": b"bytes",
"j1": None,
"k1": decimal.Decimal("3.194387483193242e+19"),
"l1": JsonObject({"key": "value"}),
}
want_types = {
"a1": param_types.INT64,
Expand All @@ -361,6 +365,7 @@ def test_get_param_types(self):
"h1": param_types.DATE,
"i1": param_types.BYTES,
"k1": param_types.NUMERIC,
"l1": param_types.JSON,
}
got_types = get_param_types(params)
self.assertEqual(got_types, want_types)
Expand Down