perf(spanner): optimize query result decoding#17375
Conversation
There was a problem hiding this comment.
Code Review
This pull request optimizes performance across Spanner streaming and helper modules by caching list append methods, pre-compiling regular expressions, and replacing helper functions with direct attribute getters or lambdas. However, several critical issues were identified in _helpers.py: missing imports for datetime and base64 will cause runtime errors, BYTES values are incorrectly encoded to UTF-8 instead of being base64-decoded, and the new timestamp parsing logic ignores timezone offsets for non-UTC inputs, resulting in incorrect times.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces several performance optimizations to the Spanner client library, such as caching local variables and methods during row merging, using lambdas and operator.attrgetter for type decoding, pre-compiling regex patterns, and optimizing timestamp parsing. The reviewer provided valuable feedback to further boost performance, including checking the more common number_value field first for float decoding, using string slicing instead of regular expressions for timestamp parsing, and removing the unused re import.
rahul2393
left a comment
There was a problem hiding this comment.
LGTM - with minor comments/questions and test assert fix
| if val[19] == ".": | ||
| if val.endswith("Z"): | ||
| offset = "Z" | ||
| fraction = val[20:-1] |
There was a problem hiding this comment.
Spanner Timestamp seems to be always UTC Z at the API/wire level. Non-Z offset only come from CAST(ts as String), which is a STRING column and routes through the STRING decoder.
+/-HH:MM handling plus the astimezone call won't ever be executed on real TIMESTAMP value? Can we drop it to shrink the risky code, or comment marking it deliberately defensive. The Z fast-path is the legitimate win and short-circuits before this anyway?
There was a problem hiding this comment.
I agree that Spanner (currently) always sends timestamps in Zulu time. BUT: It is not strictly part of the API contract, meaning that future feature changes in Spanner could (in theory) trigger timestamps to be sent with an offset. Also, the previous implementation also supported offsets, so I included this in order not to cause any potential regressions from the previous implementation.
I added an additional test for parsing timestamps with an offset (in this case one without a sub-second fraction).
|
|
||
| parsed = self._callFUT(value_pb, field_type, field_name) | ||
| self.assertIsInstance(parsed, datetime_helpers.DatetimeWithNanoseconds) | ||
| self.assertEqual(parsed, expected) |
There was a problem hiding this comment.
DatetimeWithNanoseconds.eq is inherited from datetime and compares only to microsecond - it ignores .nanosecond. So this passes even if sub-us digits are wrong.
There was a problem hiding this comment.
Oh, great catch, thanks! I updated the various checks in all the tests in this file, and also added a test that would otherwise have reported a false positive.
| index = 0 | ||
| else: | ||
| for value in values: | ||
| if value.HasField("null_value"): |
There was a problem hiding this comment.
value.HasField("null_value") duplicates _parse_nullables's semantics(still used in lazy/array/single-value paths). Should we add comment here to keep in sync so duplication doesn't silently drift?
There was a problem hiding this comment.
Added a comment to explain both why it is there, and that it needs to be kept in sync.
| return operator.attrgetter("bool_value") | ||
| elif type_code == TypeCode.INT64: | ||
| return _parse_int64 | ||
| return lambda value_pb: int(value_pb.string_value) |
There was a problem hiding this comment.
Maybe a comment on why only STRING/BOOL use attrgetter(faster) while transform trypes use lambdas?
There was a problem hiding this comment.
Added a comment to explain.
PR created by the Librarian CLI to initialize a release. Merging this PR will auto trigger a release. Librarian Version: v0.19.0 Language Image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:234b9d1f2ddb057ed7ac6a38db0bf8163d839c65c6cf88ade52530cddebce59e <details><summary>gapic-generator: v1.35.0</summary> ## [v1.35.0](gapic-generator-v1.34.1...gapic-generator-v1.35.0) (2026-06-11) ### Features * setup.py matches prerelease versions (#17370) ([25b857e](25b857e1)) ### Bug Fixes * require protobuf 6.33.5 to address CVE-2026-0994 (#17349) ([6642263](66422636)) </details> <details><summary>google-auth: v2.54.0</summary> ## [v2.54.0](google-auth-v2.53.0...google-auth-v2.54.0) (2026-06-11) ### Features * implement regional access boundary support for standalone JWT and async service accounts (#17025) ([35af616](35af6168)) ### Bug Fixes * configure mTLS for impersonated credentials (#17404) ([57269d5](57269d56)) * fail-fast on missing ECP config file to avoid 30s hang (#17377) ([e096127](e0961270)) * Rename the 'seed' argument for setting an initial regional access boundary for clarity (#17186) ([e5c8cf9](e5c8cf92)) * update incorrect urls in setup.py to point at monorepo vs splitrepo (#17237) ([eaed04b](eaed04ba)) </details> <details><summary>google-cloud-alloydb: v0.11.0</summary> ## [v0.11.0](google-cloud-alloydb-v0.10.0...google-cloud-alloydb-v0.11.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-biglake: v0.5.0</summary> ## [v0.5.0](google-cloud-biglake-v0.4.0...google-cloud-biglake-v0.5.0) (2026-06-11) ### Features * update API sources and regenerate (#17431) ([2e75c78](2e75c78c)) </details> <details><summary>google-cloud-ces: v0.7.0</summary> ## [v0.7.0](google-cloud-ces-v0.6.0...google-cloud-ces-v0.7.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-confidentialcomputing: v0.11.0</summary> ## [v0.11.0](google-cloud-confidentialcomputing-v0.10.0...google-cloud-confidentialcomputing-v0.11.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-modelarmor: v0.7.0</summary> ## [v0.7.0](google-cloud-modelarmor-v0.6.0...google-cloud-modelarmor-v0.7.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-network-services: v0.10.0</summary> ## [v0.10.0](google-cloud-network-services-v0.9.0...google-cloud-network-services-v0.10.0) (2026-06-11) ### Features * update API sources and regenerate (#17431) ([2e75c78](2e75c78c)) </details> <details><summary>google-cloud-oracledatabase: v0.6.0</summary> ## [v0.6.0](google-cloud-oracledatabase-v0.5.0...google-cloud-oracledatabase-v0.6.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-spanner: v3.68.0</summary> ## [v3.68.0](google-cloud-spanner-v3.67.0...google-cloud-spanner-v3.68.0) (2026-06-11) ### Features * add asynchronous code snippets and minor cleanup changes (#17337) ([d6aaf61](d6aaf610)) ### Performance Improvements * optimize query result decoding (#17375) ([3f70b2f](3f70b2ff)) </details> <details><summary>google-cloud-storage: v3.12.0</summary> ## [v3.12.0](google-cloud-storage-v3.11.0...google-cloud-storage-v3.12.0) (2026-06-11) ### Features * full object checksum: implement rolling checksum and verification in reads resumption strategy (#17262) ([2361ba6](2361ba6e)) * Enable full object checksum PR 1/3 : parse finalize_time and server crc32c in async object stream (#17261) ([72c7a27](72c7a272)) * full object checksum: integrate full-object checksum in AsyncMultiRangeDownloader (#17263) ([b6a85e4](b6a85e49)) </details> <details><summary>google-developer-knowledge: v0.1.0</summary> ## [v0.1.0](google-developer-knowledge-v0.0.0...google-developer-knowledge-v0.1.0) (2026-06-11) ### Features * add google-developer-knowledge (#17417) ([ca02afc](ca02afce)) </details>

Work in progress.
Optimizes the decoding and reading of (large) result sets for Spanner.