Skip to content

Commit 1c1b601

Browse files
vi3k6i5asthamohtabusunkim96gcf-owl-bot[bot]release-please[bot]
authored
feat: add support for json type (#714)
* performance files * test_benchmark * performance testing changes * changes in benchmark performance for prod * changes to number of runs * adding comments * linting changes * 3.2 changes * adding version change * lint changes and resmoving performance changes * version changes * chore: fix release build (#659) * chore: fix release build Fix release build by migrating to secret manager secrets and use templated kokoro configs for docs/ and release/ * fix: fix config names * chore: add populate secrets script * docs: fix license * chore: preserve original year * chore: revert years in manually committed files * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/master/packages/owl-bot/README.md * chore: update lockfile * chore: add .kokoro/docker directory Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com> * feat: Added support for check constraint (#679) * feat: Added support for check constraint * fix: change decimal out of scale ProgramingError to ValueError * fix: skip check_constraints tests when running on emmulator * fix: remove check constraint for emulator * docs: update docs to show decimal field support and check constraints but no support for unsigned data type (#683) * test: Performance Testing (#675) * performance files * test_benchmark * performance testing changes * changes in benchmark performance for prod * changes to number of runs * adding comments * linting changes * changes for 3.2 * Revert "changes for 3.2" This reverts commit 488035c. * adding licence * chore: release 2.2.1b2 (#685) Release-As: 2.2.1b2 * chore: release 2.2.1b2 (#687) * chore: release 2.2.1b2 * Updated CHANGELOG.md Corrected the change log msg. Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Vikash Singh <3116482+vi3k6i5@users.noreply.github.com> * fix: Bump version number after 2.2.1b2 release (#688) * chor: Update repo to say beta release instead of alpha (#691) * fix: Bump version number after 2.2.1b2 release * Update setup.py Current release is beta so updating the same in setup.py * chore: release 2.2.1b3 (#693) Release-As: 2.2.1b3 * chore: release 2.2.1b3 (#694) * chore: release 2.2.1b3 * Updated CHANGELOG.md Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Vikash Singh <3116482+vi3k6i5@users.noreply.github.com> * fix: Bump version number after 2.2.1b3 release (#696) * chore: release 2.2.1b3 Release-As: 2.2.1b3 * fix: Bump version number after 2.2.1b3 release * fix: add test samples script and related kokoro files * fix: correct repo name from python-spanner to python-spanner-django * fix: correct license from Apache to BSD style * docs: lint fix for samples (#697) * Docs: fix changelog link and sample examples. (#700) * docs: update docs to show decimal field support and check constraints but no support for unsigned data type * docs: linked changelog correctly * docs: fix doc links for sample examples * fix: skip test cursor_executemany_with_empty_params_list as spanner support is not there * docs: update dbapi location in overview asset file (#702) * chore: migrate to main branch (#706) * chore: migrate to main branch * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * use latest post processor image * remove obsolete replacements in owlbot.py * update post processor image Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Anthonios Partheniou <partheniou@google.com> * fix: added fixes for latest feature changes in django 3.2 * fix: fixes for running tests for django3.2 * fix: change django repo path * test: test fixes for order by nulls first and last * docs: fix readme link target * test: set default auto field type in test settings * fix: update features to skip tests that are not support by spanner * fix: remove choices module from django3.2 as it has been removed from django 3.2 * feat: add json support * fix: correct JsonObject import path * fix: table_type is not supported in emulator * !fix: update dependency for json support * fix: views are not supported by spanner Co-authored-by: Astha Mohta <asthamohta@gmail.com> Co-authored-by: Astha Mohta <35952883+asthamohta@users.noreply.github.com> Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> Co-authored-by: Anthonios Partheniou <partheniou@google.com>
1 parent b163ed2 commit 1c1b601

File tree

9 files changed

+113
-23
lines changed

9 files changed

+113
-23
lines changed

django_spanner/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
from uuid import uuid4
1313

1414
import pkg_resources
15+
from google.cloud.spanner_v1 import JsonObject
1516
from django.db.models.fields import (
1617
AutoField,
1718
SmallAutoField,
1819
BigAutoField,
1920
Field,
2021
)
22+
from django.db.models import JSONField
2123

2224
# Monkey-patch google.DatetimeWithNanoseconds's __eq__ compare against
2325
# datetime.datetime.
@@ -59,6 +61,17 @@ def autofield_init(self, *args, **kwargs):
5961
SmallAutoField.validators = []
6062
BigAutoField.validators = []
6163

64+
65+
def get_prep_value(self, value):
66+
# Json encoding and decoding for spanner is done in python-spanner.
67+
if not isinstance(value, JsonObject) and isinstance(value, dict):
68+
return JsonObject(value)
69+
70+
return value
71+
72+
73+
JSONField.get_prep_value = get_prep_value
74+
6275
old_datetimewithnanoseconds_eq = getattr(
6376
DatetimeWithNanoseconds, "__eq__", None
6477
)

django_spanner/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
3434
"DateField": "DATE",
3535
"DateTimeField": "TIMESTAMP",
3636
"DecimalField": "NUMERIC",
37+
"JSONField": "JSON",
3738
"DurationField": "INT64",
3839
"EmailField": "STRING(%(max_length)s)",
3940
"FileField": "STRING(%(max_length)s)",

django_spanner/features.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from django.db.backends.base.features import BaseDatabaseFeatures
1010
from django.db.utils import InterfaceError
11+
from django_spanner import USE_EMULATOR
1112

1213

1314
class DatabaseFeatures(BaseDatabaseFeatures):
@@ -34,8 +35,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3435
supports_column_check_constraints = True
3536
supports_table_check_constraints = True
3637
supports_order_by_nulls_modifier = False
37-
# Spanner does not support json
38-
supports_json_field = False
38+
if USE_EMULATOR:
39+
# Emulator does not support json.
40+
supports_json_field = False
41+
else:
42+
supports_json_field = True
3943
supports_primitives_in_json_field = False
4044
# Spanner does not support SELECTing an arbitrary expression that also
4145
# appears in the GROUP BY clause.
@@ -67,7 +71,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
6771
"model_fields.test_autofield.SmallAutoFieldTests.test_redundant_backend_range_validators",
6872
# Spanner does not support deferred unique constraints
6973
"migrations.test_operations.OperationTests.test_create_model_with_deferred_unique_constraint",
70-
# Spanner does not support JSON objects
74+
# Spanner does not support JSON object query on fields.
7175
"db_functions.comparison.test_json_object.JSONObjectTests.test_empty",
7276
"db_functions.comparison.test_json_object.JSONObjectTests.test_basic",
7377
"db_functions.comparison.test_json_object.JSONObjectTests.test_expressions",
@@ -268,17 +272,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
268272
"timezones.tests.NewDatabaseTests.test_query_datetimes",
269273
# using NULL with + crashes: https://github.com/googleapis/python-spanner-django/issues/201
270274
"annotations.tests.NonAggregateAnnotationTestCase.test_combined_annotation_commutative",
271-
# Spanner loses DecimalField precision due to conversion to float:
272-
# https://github.com/googleapis/python-spanner-django/pull/133#pullrequestreview-328482925
273-
"aggregation.tests.AggregateTestCase.test_decimal_max_digits_has_no_effect",
274-
"aggregation.tests.AggregateTestCase.test_related_aggregate",
275+
# Spanner does not support custom precision on DecimalField
275276
"db_functions.comparison.test_cast.CastTests.test_cast_to_decimal_field",
276277
"model_fields.test_decimalfield.DecimalFieldTests.test_fetch_from_db_without_float_rounding",
277278
"model_fields.test_decimalfield.DecimalFieldTests.test_roundtrip_with_trailing_zeros",
278-
# Spanner does not support unsigned integer field.
279-
"model_fields.test_integerfield.PositiveIntegerFieldTests.test_negative_values",
280-
# Spanner doesn't support the variance the standard deviation database
281-
# functions:
279+
# Spanner doesn't support the variance the standard deviation database functions on full population.
282280
"aggregation.test_filter_argument.FilteredAggregateTests.test_filtered_numerical_aggregates",
283281
"aggregation_regress.tests.AggregationTests.test_stddev",
284282
# SELECT list expression references <column> which is neither grouped
@@ -358,12 +356,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
358356
"transaction_hooks.tests.TestConnectionOnCommit.test_discards_hooks_from_rolled_back_savepoint",
359357
"transaction_hooks.tests.TestConnectionOnCommit.test_inner_savepoint_rolled_back_with_outer",
360358
"transaction_hooks.tests.TestConnectionOnCommit.test_inner_savepoint_does_not_affect_outer",
361-
# Spanner doesn't support views.
362-
"inspectdb.tests.InspectDBTransactionalTests.test_include_views",
363-
"introspection.tests.IntrospectionTests.test_table_names_with_views",
364-
# Fields: JSON, GenericIPAddressField are mapped to String in Spanner
359+
# Field: GenericIPAddressField is mapped to String in Spanner
365360
"inspectdb.tests.InspectDBTestCase.test_field_types",
366-
"inspectdb.tests.InspectDBTestCase.test_json_field",
367361
# BigIntegerField is mapped to IntegerField in Spanner
368362
"inspectdb.tests.InspectDBTestCase.test_number_field_types",
369363
# No sequence for AutoField in Spanner.
@@ -479,6 +473,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
479473
if os.environ.get("SPANNER_EMULATOR_HOST", None):
480474
# Some code isn't yet supported by the Spanner emulator.
481475
skip_tests += (
476+
# Views are not supported by emulator
477+
"inspectdb.tests.InspectDBTransactionalTests.test_include_views", # noqa
478+
"introspection.tests.IntrospectionTests.test_table_names_with_views", # noqa
482479
# Untyped parameters are not supported:
483480
# https://github.com/GoogleCloudPlatform/cloud-spanner-emulator#features-and-limitations
484481
"auth_tests.test_views.PasswordResetTest.test_confirm_custom_reset_url_token_link_redirects_to_set_password_page", # noqa
@@ -1588,7 +1585,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
15881585
"queries.tests.Queries1Tests.test_ticket2306", # noqa
15891586
"queries.tests.Queries1Tests.test_ticket2400", # noqa
15901587
"queries.tests.Queries1Tests.test_ticket2496", # noqa
1591-
# "queries.tests.Queries1Tests.test_ticket2902", # noqa
15921588
"queries.tests.Queries1Tests.test_ticket3037", # noqa
15931589
"queries.tests.Queries1Tests.test_ticket3141", # noqa
15941590
"queries.tests.Queries1Tests.test_ticket4358", # noqa
@@ -1812,7 +1808,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
18121808
"sitemaps_tests.test_http.HTTPSitemapTests.test_paged_sitemap", # noqa
18131809
"sitemaps_tests.test_http.HTTPSitemapTests.test_requestsite_sitemap", # noqa
18141810
"sitemaps_tests.test_http.HTTPSitemapTests.test_simple_custom_sitemap", # noqa
1815-
# "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_i18nsitemap_index", # noqa
18161811
"sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_index", # noqa
18171812
"sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_limited", # noqa
18181813
"sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_xdefault", # noqa

django_spanner/introspection.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
from django.db.models import Index
1313
from google.cloud.spanner_v1 import TypeCode
14+
from django_spanner import USE_EMULATOR
1415

1516

1617
class DatabaseIntrospection(BaseDatabaseIntrospection):
@@ -25,7 +26,28 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
2526
TypeCode.STRING: "CharField",
2627
TypeCode.TIMESTAMP: "DateTimeField",
2728
TypeCode.NUMERIC: "DecimalField",
29+
TypeCode.JSON: "JSONField",
2830
}
31+
if USE_EMULATOR:
32+
# Emulator does not support table_type yet.
33+
# https://github.com/GoogleCloudPlatform/cloud-spanner-emulator/issues/43
34+
LIST_TABLE_SQL = """
35+
SELECT
36+
t.table_name, t.table_name
37+
FROM
38+
information_schema.tables AS t
39+
WHERE
40+
t.table_catalog = '' and t.table_schema = ''
41+
"""
42+
else:
43+
LIST_TABLE_SQL = """
44+
SELECT
45+
t.table_name, t.table_type
46+
FROM
47+
information_schema.tables AS t
48+
WHERE
49+
t.table_catalog = '' and t.table_schema = ''
50+
"""
2951

3052
def get_field_type(self, data_type, description):
3153
"""A hook for a Spanner database to use the cursor description to
@@ -53,8 +75,15 @@ def get_table_list(self, cursor):
5375
:rtype: list
5476
:returns: A list of table and view names in the current database.
5577
"""
78+
results = cursor.run_sql_in_snapshot(self.LIST_TABLE_SQL)
79+
tables = []
5680
# The second TableInfo field is 't' for table or 'v' for view.
57-
return [TableInfo(row[0], "t") for row in cursor.list_tables()]
81+
for row in results:
82+
table_type = "t"
83+
if row[1] == "VIEW":
84+
table_type = "v"
85+
tables.append(TableInfo(row[0], table_type))
86+
return tables
5887

5988
def get_table_description(self, cursor, table_name):
6089
"""Return a description of the table with the DB-API cursor.description

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# 'Development Status :: 4 - Beta'
1818
# 'Development Status :: 5 - Production/Stable'
1919
release_status = "Development Status :: 4 - Beta"
20-
dependencies = ["sqlparse >= 0.3.0", "google-cloud-spanner >= 3.0.0"]
20+
dependencies = ["sqlparse >= 0.3.0", "google-cloud-spanner >= 3.11.1"]
2121
extras = {
2222
"tracing": [
2323
"opentelemetry-api >= 1.1.0",

testing/constraints-3.6.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev",
77
# Then this file should have foo==1.14.0
88
sqlparse==0.3.0
9-
google-cloud-spanner==3.0.0
9+
google-cloud-spanner==3.11.1
1010
opentelemetry-api==1.1.0
1111
opentelemetry-sdk==1.1.0
1212
opentelemetry-instrumentation==0.20b0

tests/system/django_spanner/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ class Meta:
3434
name="check_start_date",
3535
),
3636
]
37+
38+
39+
class Detail(models.Model):
40+
value = models.JSONField()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
from .models import Detail
8+
import unittest
9+
from django.test import TransactionTestCase
10+
from django.db import connection
11+
from django_spanner import USE_EMULATOR
12+
from tests.system.django_spanner.utils import (
13+
setup_instance,
14+
teardown_instance,
15+
setup_database,
16+
teardown_database,
17+
)
18+
19+
20+
@unittest.skipIf(USE_EMULATOR, "Jsonfield is not implemented in emulator.")
21+
class TestJsonField(TransactionTestCase):
22+
@classmethod
23+
def setUpClass(cls):
24+
setup_instance()
25+
setup_database()
26+
with connection.schema_editor() as editor:
27+
# Create the tables
28+
editor.create_model(Detail)
29+
30+
@classmethod
31+
def tearDownClass(cls):
32+
with connection.schema_editor() as editor:
33+
# delete the table
34+
editor.delete_model(Detail)
35+
teardown_database()
36+
teardown_instance()
37+
38+
def test_insert_and_fetch_value(self):
39+
"""
40+
Tests model object creation with Detail model.
41+
Inserting json data into the model and retrieving it.
42+
"""
43+
json_data = Detail(value={"name": "Jakob", "age": "26"})
44+
json_data.save()
45+
qs1 = Detail.objects.all()
46+
self.assertEqual(qs1[0].value, {"name": "Jakob", "age": "26"})
47+
# Delete data from Detail table.
48+
Detail.objects.all().delete()

tests/unit/django_spanner/test_introspection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ def test_get_table_list(self):
4949
cursor = mock.MagicMock()
5050

5151
def list_tables(*args, **kwargs):
52-
return [["Table_1"], ["Table_2"]]
52+
return [["Table_1", "t"], ["Table_2", "t"]]
5353

54-
cursor.list_tables = list_tables
54+
cursor.run_sql_in_snapshot = list_tables
5555
table_list = db_introspection.get_table_list(cursor=cursor)
5656
self.assertEqual(
5757
table_list,

0 commit comments

Comments
 (0)