diff --git a/samples/noxfile.py b/samples/noxfile.py index cd28a3f0..29709cd1 100644 --- a/samples/noxfile.py +++ b/samples/noxfile.py @@ -97,6 +97,11 @@ def insertmany(session): _sample(session) +@nox.session() +def parse_json(session): + _sample(session) + + @nox.session() def _all_samples(session): _sample(session) diff --git a/samples/parse_json_sample.py b/samples/parse_json_sample.py new file mode 100644 index 00000000..b0868ea8 --- /dev/null +++ b/samples/parse_json_sample.py @@ -0,0 +1,50 @@ +# Copyright 2025 Google LLC All rights reserved. +# +# 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. + +from sqlalchemy import create_engine, func, text +from sqlalchemy.orm import Session + +from sample_helper import run_sample +from model import Venue + +# Shows how to use the PARSE_JSON function in Spanner using SQLAlchemy. +def parse_json_sample(): + engine = create_engine( + "spanner:///projects/sample-project/" + "instances/sample-instance/" + "databases/sample-database", + echo=True, + ) + with Session(engine) as session: + venue = Venue( + code="LCH", + active=True, + name="Large Concert Hall", + # The SQLAlchemy func function is very lenient and allows you to call any + # database function that Spanner supports. Use a text instance to add a + # specific SQL fragment to the function call. + description=func.parse_json( + '{"type": "Stadium", "size": 13.7391432}', + text("wide_number_mode=>'round'"), + ), + ) + session.add(venue) + session.commit() + + venue = session.query(Venue).filter_by(code="LCH").one() + print(venue.description) + + +if __name__ == "__main__": + run_sample(parse_json_sample) diff --git a/test/mockserver_tests/test_json.py b/test/mockserver_tests/test_json.py index 2d37a335..244e5d62 100644 --- a/test/mockserver_tests/test_json.py +++ b/test/mockserver_tests/test_json.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import select +from sqlalchemy import func, select, text from sqlalchemy.orm import Session from sqlalchemy.testing import eq_, is_instance_of from google.cloud.spanner_v1 import ( @@ -73,7 +73,24 @@ def test_insert_array(self): '[{"size":"Great","type":"Stadium"}]', ) - def _test_insert_json(self, description, expected): + def test_insert_fn(self): + add_update_count( + "INSERT INTO venues (id, name, description) " + "VALUES (@a0, @a1, parse_json(@a2, wide_number_mode=>'round'))", + 1, + ) + self._test_insert_json( + func.parse_json( + '{"type": "Stadium", "size": "Great"}', + text("wide_number_mode=>'round'"), + ), + '{"type": "Stadium", "size": "Great"}', + expected_type_code=TypeCode.STRING, + ) + + def _test_insert_json( + self, description, expected, expected_type_code=TypeCode.JSON + ): from test.mockserver_tests.json_model import Venue add_update_count( @@ -100,7 +117,7 @@ def _test_insert_json(self, description, expected): eq_(expected, request.params["a2"]) eq_(TypeCode.INT64, request.param_types["a0"].code) eq_(TypeCode.STRING, request.param_types["a1"].code) - eq_(TypeCode.JSON, request.param_types["a2"].code) + eq_(expected_type_code, request.param_types["a2"].code) def test_select_dict(self): self._test_select_json(