Skip to content

Commit bc5ddc3

Browse files
authored
fix: add support for json data type (#593)
* fix: add support for json data type * fix: skip json test for emulator * refactor: move JsonObject data type to spanner_v1/types/datatypes.py * refactor: remove duplicate import * refactor: remove extra connection creation in test * refactor: move data_types.py file to google/cloud/spanner_v1/ * fix: increased db version time to current time, to give db backup more time * fix: undo database_version_time method definition.
1 parent 97b2d6b commit bc5ddc3

File tree

7 files changed

+89
-5
lines changed

7 files changed

+89
-5
lines changed

google/cloud/spanner_dbapi/parse_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import sqlparse
2323
from google.cloud import spanner_v1 as spanner
24+
from google.cloud.spanner_v1 import JsonObject
2425

2526
from .exceptions import Error, ProgrammingError
2627
from .parser import parse_values
@@ -38,6 +39,7 @@
3839
DateStr: spanner.param_types.DATE,
3940
TimestampStr: spanner.param_types.TIMESTAMP,
4041
decimal.Decimal: spanner.param_types.NUMERIC,
42+
JsonObject: spanner.param_types.JSON,
4143
}
4244

4345
SPANNER_RESERVED_KEYWORDS = {

google/cloud/spanner_v1/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from .types.type import StructType
5959
from .types.type import Type
6060
from .types.type import TypeCode
61+
from .data_types import JsonObject
6162

6263
from google.cloud.spanner_v1 import param_types
6364
from google.cloud.spanner_v1.client import Client
@@ -132,6 +133,8 @@
132133
"TransactionSelector",
133134
"Type",
134135
"TypeCode",
136+
# Custom spanner related data types
137+
"JsonObject",
135138
# google.cloud.spanner_v1.services
136139
"SpannerClient",
137140
)

google/cloud/spanner_v1/_helpers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import datetime
1818
import decimal
1919
import math
20+
import json
2021

2122
import six
2223

@@ -28,7 +29,7 @@
2829
from google.cloud._helpers import _datetime_to_rfc3339
2930
from google.cloud.spanner_v1 import TypeCode
3031
from google.cloud.spanner_v1 import ExecuteSqlRequest
31-
32+
from google.cloud.spanner_v1 import JsonObject
3233

3334
# Validation error messages
3435
NUMERIC_MAX_SCALE_ERR_MSG = (
@@ -166,6 +167,10 @@ def _make_value_pb(value):
166167
if isinstance(value, decimal.Decimal):
167168
_assert_numeric_precision_and_scale(value)
168169
return Value(string_value=str(value))
170+
if isinstance(value, JsonObject):
171+
return Value(
172+
string_value=json.dumps(value, sort_keys=True, separators=(",", ":"),)
173+
)
169174
raise ValueError("Unknown type: %s" % (value,))
170175

171176

google/cloud/spanner_v1/data_types.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Custom data types for spanner."""
16+
17+
18+
class JsonObject(dict):
19+
"""
20+
JsonObject type help format Django JSONField to compatible Cloud Spanner's
21+
JSON type. Before making queries, it'll help differentiate between
22+
normal parameters and JSON parameters.
23+
"""
24+
25+
pass

tests/system/test_backup_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,11 @@ def test_instance_list_backups(
400400
)
401401
expire_time_1_stamp = expire_time_1.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
402402

403+
# Backup tests are failing because of timeout. As a temporary fix
404+
# we are increasing db version time to current time.
405+
# Read more: https://github.com/googleapis/python-spanner/issues/496
406+
database_version_time = datetime.datetime.now(datetime.timezone.utc)
407+
403408
backup1 = shared_instance.backup(
404409
backup_id_1,
405410
database=shared_database,

tests/system/test_dbapi.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
import hashlib
1616
import pickle
1717
import pkg_resources
18-
1918
import pytest
2019

2120
from google.cloud import spanner_v1
2221
from google.cloud.spanner_dbapi.connection import connect, Connection
22+
from google.cloud.spanner_v1 import JsonObject
2323
from . import _helpers
2424

2525
DATABASE_NAME = "dbapi-txn"
@@ -328,6 +328,45 @@ def test_DDL_autocommit(shared_instance, dbapi_database):
328328
conn.commit()
329329

330330

331+
@pytest.mark.skipif(_helpers.USE_EMULATOR, reason="Emulator does not support json.")
332+
def test_autocommit_with_json_data(shared_instance, dbapi_database):
333+
"""Check that DDLs in autocommit mode are immediately executed for
334+
json fields."""
335+
# Create table
336+
conn = Connection(shared_instance, dbapi_database)
337+
conn.autocommit = True
338+
339+
cur = conn.cursor()
340+
cur.execute(
341+
"""
342+
CREATE TABLE JsonDetails (
343+
DataId INT64 NOT NULL,
344+
Details JSON,
345+
) PRIMARY KEY (DataId)
346+
"""
347+
)
348+
349+
# Insert data to table
350+
cur.execute(
351+
sql="INSERT INTO JsonDetails (DataId, Details) VALUES (%s, %s)",
352+
args=(123, JsonObject({"name": "Jakob", "age": "26"})),
353+
)
354+
355+
# Read back the data.
356+
cur.execute("""select * from JsonDetails;""")
357+
got_rows = cur.fetchall()
358+
359+
# Assert the response
360+
assert len(got_rows) == 1
361+
assert got_rows[0][0] == 123
362+
assert got_rows[0][1] == '{"age":"26","name":"Jakob"}'
363+
364+
# Drop the table
365+
cur.execute("DROP TABLE JsonDetails")
366+
conn.commit()
367+
conn.close()
368+
369+
331370
def test_DDL_commit(shared_instance, dbapi_database):
332371
"""Check that DDLs in commit mode are executed on calling `commit()`."""
333372
conn = Connection(shared_instance, dbapi_database)

tests/unit/spanner_dbapi/test_parse_utils.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import unittest
1717

1818
from google.cloud.spanner_v1 import param_types
19+
from google.cloud.spanner_v1 import JsonObject
1920

2021

2122
class TestParseUtils(unittest.TestCase):
@@ -333,9 +334,11 @@ def test_get_param_types(self):
333334
import datetime
334335
import decimal
335336

336-
from google.cloud.spanner_dbapi.parse_utils import DateStr
337-
from google.cloud.spanner_dbapi.parse_utils import TimestampStr
338-
from google.cloud.spanner_dbapi.parse_utils import get_param_types
337+
from google.cloud.spanner_dbapi.parse_utils import (
338+
DateStr,
339+
TimestampStr,
340+
get_param_types,
341+
)
339342

340343
params = {
341344
"a1": 10,
@@ -349,6 +352,7 @@ def test_get_param_types(self):
349352
"i1": b"bytes",
350353
"j1": None,
351354
"k1": decimal.Decimal("3.194387483193242e+19"),
355+
"l1": JsonObject({"key": "value"}),
352356
}
353357
want_types = {
354358
"a1": param_types.INT64,
@@ -361,6 +365,7 @@ def test_get_param_types(self):
361365
"h1": param_types.DATE,
362366
"i1": param_types.BYTES,
363367
"k1": param_types.NUMERIC,
368+
"l1": param_types.JSON,
364369
}
365370
got_types = get_param_types(params)
366371
self.assertEqual(got_types, want_types)

0 commit comments

Comments
 (0)