Skip to content

feat(django-spanner)!: support Django 5.2 and deprecate Django 3.2/4.2#16624

Open
sinhasubham wants to merge 4 commits intomainfrom
django_upgrade_5_2
Open

feat(django-spanner)!: support Django 5.2 and deprecate Django 3.2/4.2#16624
sinhasubham wants to merge 4 commits intomainfrom
django_upgrade_5_2

Conversation

@sinhasubham
Copy link
Copy Markdown
Contributor

@sinhasubham sinhasubham commented Apr 13, 2026

This PR migrates the django-google-spanner package to support Django 5.2 and deprecates support for older Django versions (3.2 and 4.2). Due to the deprecation of older versions, this is a breaking change and requires a major version release.
💥 BREAKING CHANGES:

  • Dropped support for Django 3.2 and 4.2.
  • Minimum supported Python version is now 3.10 (as required by Django 5.2).

@sinhasubham sinhasubham changed the title Django upgrade 5 2 feat: migrate Django 5.2 upgrade and 3.2/4.2 deprecations Apr 13, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Spanner database backend to support Django 5.2 and Python 3.10+, while removing legacy support for Django 3.2 and 4.2. Key changes include the implementation of a global client cache to prevent initialization crashes, support for JSONArray, GeneratedField, and db_default, and a retry mechanism for flushing tables with foreign key constraints. Feedback highlights several critical issues: the use of a 32-bit mask for primary key generation significantly reduces the key space, and making GOOGLE_CLOUD_PROJECT mandatory is a breaking change for local development. Additionally, the package version was incorrectly decremented, and a minor version bump was recommended for this release. Finally, using WHERE 1=1 was suggested as a more idiomatic alternative to WHERE true for flush operations.

Comment thread packages/django-google-spanner/django_spanner/__init__.py Outdated
Comment thread packages/django-google-spanner/django_spanner/base.py Outdated
Comment thread packages/django-google-spanner/django_spanner/base.py Outdated
Comment thread packages/django-google-spanner/django_spanner/version.py Outdated
Comment thread packages/django-google-spanner/django_spanner/operations.py Outdated
@sinhasubham sinhasubham force-pushed the django_upgrade_5_2 branch 4 times, most recently from 2b71c2a to f6098f1 Compare April 16, 2026 09:17
@sinhasubham sinhasubham marked this pull request as ready for review April 20, 2026 05:57
@sinhasubham sinhasubham requested review from a team as code owners April 20, 2026 05:57

# Global cache for Spanner client to prevent multiple initializations
# which can cause OpenTelemetry 'MeterProvider override' crashes.
_SPANNER_CLIENT_CACHE = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont understand why do we need this, ideally our Spanner client library should handle the override crash ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this global cache to fix a crash observed during testing: OpenTelemetry MeterProvider override crashes. In Django, DatabaseWrapper instances are created frequently (e.g., per thread or request), leading to multiple spanner.Client initializations. While it would be ideal for the client library to handle this, this cache effectively prevents the crash and resource exhaustion in the meantime.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Please create a bug in Python Spanner Client and add a todo here to clean this up once the bug is fixed

Comment on lines +161 to +164
"project": os.environ.get("GOOGLE_CLOUD_PROJECT")
or self.settings_dict.get("project")
or self.settings_dict.get("PROJECT")
or "test-project",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is being done in multiple places, better to extract in "init" method once and use it everywhere.

Also why do we support self.settings_dict.get("project") and self.settings_dict.get("PROJECT") now ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. I made these changes for some debugging purpose earlier, missed to revert in the final commit.

Comment on lines +185 to +187
conn_params.pop("instance", None)
conn_params.pop("instance_id", None)
conn_params.pop("client", None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change is required ?
We are not using instance_id or client anywhere. We are only using instance.instance_id or instance._client

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because get_connection_params puts instance_id into the conn_params dictionary by default, passing **conn_params will try to pass instance_id again. Python rejects this because it is receiving the same argument twice.
Popping instance_id and client from conn_params is required to support the global client cache fix. Removing this crashes the adapter on connection.

Comment on lines +51 to +52
# This method copies the complete code of this overridden method from
# Django core and modify it for Spanner by adding one line
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicate comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

Comment thread packages/django-google-spanner/django_spanner/creation.py
# https://developers.google.com/open-source/licenses/bsd

__version__ = "4.0.3"
__version__ = "4.1.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we modifying this version manually ? shouldn't this be updated automatically once we do the release ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, addressed

self.assertEqual(
sql_compiled,
"SELECT tests_report.name FROM tests_report ORDER BY "
"SELECT tests_report.name AS name FROM tests_report ORDER BY "
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my knowledge why this change is required ?


UNIT_TEST_PYTHON_VERSIONS = [
"3.8",
"3.9",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we received 3.9 support as well, as we removed 3.9 integration test suite file

Copy link
Copy Markdown
Contributor Author

@sinhasubham sinhasubham Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit 3.9 workflow step fails without version 3.9 in this list. And we cannot remove 3.9 from github workflows as it is a common workflow file for monorepo now. I will check on this with owning team how we are planning to handle this

Comment on lines +126 to +127
if session.python == "3.9":
session.skip("Python 3.9 is not supported for Django 5.2 tests")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets remove 3.9 from the list then ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since unittest.yml is a shared github workflow for all packages in the monorepo, I cannot remove Python 3.9 from it without affecting other libraries.

# https://developers.google.com/open-source/licenses/bsd

__version__ = "4.0.3"
__version__ = "4.1.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this should go as a Major version release, since we are deprecating older vesion of Django.

PR title should be feat! to trigger a major version

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will update the PR title

from django.db import DEFAULT_DB_ALIAS
from django.db.models.fields import (

from django.db import DEFAULT_DB_ALIAS # noqa: E402
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you suppressing linting errors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed


def gen_rand_int64():
# Credit to https://stackoverflow.com/a/3530326.
# Use 32-bit integer for Emulator compatibility (High-bit issues observed).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a valid comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had removed the 32-bit integer changes here but missed to remove this comment earlier. Removed now

method_name,
skip("unsupported by Spanner")(method),
)
try:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make you sure you clean up the method. Not suppress the error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we need to support a range of Django versions, a test that is non-existent in 5.2 might still need to be skipped in 4.2. Suppressing the AttributeError is the best way to handle this scenario in long term according to me.

"constraint": self.sql_check_constraint % {"check": check},
}

def _unique_sql(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we removing it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method was initially removed in an attempt to rely on the Django 5.2 base class implementation. But i agree, the custom implementation is required to prevent Django from generating inline UNIQUE constraints in CREATE TABLE statements, which base class implementation does not support in the same way. I have restored the method and updated it to support Django 5.2's nulls_distinct feature.

@@ -28,7 +28,6 @@
DEFAULT_PYTHON_VERSION = "3.14"

UNIT_TEST_PYTHON_VERSIONS = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove support for 3.8 and 3.9

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit 3.9 workflow step fails without version 3.9 in this list. And we cannot remove 3.9 from github workflows as it is a common workflow file for monorepo now. I will check on this with owning team how we are planning to handle this

else:
return []

def execute_sql_flush(self, sql_list):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks so bruteforce. is there any other better approach?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier, we were relying on Django's default execute_sql_flush, which fails on Spanner when foreign key constraints are present. This retry logic was introduced in this PR specifically to handle Spanner's inability to disable foreign key checks during test cleanup.
I agree that the retry loop in execute_sql_flush is a brute-force approach. But to avoid the complexity of implementing a full topological sort of the tables based on their foreign key dependencies to determine the exact deletion order (child tables first, then parents), i used this multi-pass approach.

# Spanner does not support expression indexes
# example: CREATE INDEX index_name ON table (LOWER(column_name))
supports_expression_indexes = False
supports_stored_generated_columns = True

# Django tests that aren't supported by Spanner.
skip_tests = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to group them by cause. Otherwise we will not know if we are doing it correctly or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

try:
cursor.execute(sql)
progress = True
except Exception as e:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

catch more specific exception ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spanner might raise various errors (such as Aborted, FailedPrecondition, or IntegrityError) depending on which constraint is violated first. I felt adding generic exception catching was better in this case instead of maintaining list of exceptions for same block.

@sinhasubham sinhasubham changed the title feat: migrate Django 5.2 upgrade and 3.2/4.2 deprecations feat!: migrate Django 5.2 upgrade and 3.2/4.2 deprecations Apr 23, 2026
@sinhasubham sinhasubham changed the title feat!: migrate Django 5.2 upgrade and 3.2/4.2 deprecations feat(django-spanner)!: support Django 5.2 and deprecate Django 3.2/4.2 Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants