Sitelet https://github.com/googleapis/google-cloud-python/pull/19067
Skip to content

feat(django-google-spanner): add batch_ddl database option - #19067

Open
sakthivelmanii wants to merge 5 commits into
mainfrom
chore/django-spanner-test-db-ddl-batching
Open

sakthivelmanii wants to merge 5 commits into
mainfrom
chore/django-spanner-test-db-ddl-batching

Conversation

@sakthivelmanii

@sakthivelmanii sakthivelmanii commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add OPTIONS["batch_ddl"] (default False) to batch DDL statements generated within a DatabaseSchemaEditor context (manage.py migrate, test database setup, and schema_editor() blocks) into batched UpdateDatabaseDdl requests by toggling connection autocommit.
  • Strip batch_ddl in DatabaseWrapper.get_new_connection() before calling spanner_dbapi.connect().
  • Require google-cloud-spanner >= 3.72.0 so buffered DDL statements are flushed on set_autocommit(True) and cleared on rollback().
  • Enable 'OPTIONS': {'batch_ddl': True} in the Django 5.2, Django 6.0, and foreign-key test scripts and document the option in README.rst and docs/.

Scope autocommit-disabled DDL batching in DatabaseSchemaEditor to DatabaseCreation.create_test_db() so test database schema setup sends queued CREATE TABLE, index, and foreign key statements in batched UpdateDatabaseDdl requests while keeping autocommit enabled during test execution.
@sakthivelmanii
sakthivelmanii requested a review from a team as a code owner November 13, 2026 08:04

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

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.

Code Review

This pull request introduces DDL batching during test database creation for the Django Google Spanner backend. It sets a _batch_ddl_in_schema_editor flag on the connection during create_test_db and overrides __enter__ and __exit__ in DatabaseSchemaEditor to temporarily disable autocommit, batching DDL statements together and restoring autocommit on exit. Comprehensive unit and mock server tests are added to verify this behavior. A critical issue was identified in DatabaseSchemaEditor.__exit__ where an exception raised during super().__exit__ (such as during deferred DDL execution) would not trigger a rollback because exc_type is initially None. A code suggestion is provided to track exceptions occurring during the parent exit call to ensure proper rollback.

Comment on lines +73 to +82
def __exit__(self, exc_type, exc_value, traceback):
try:
super().__exit__(exc_type, exc_value, traceback)
finally:
if getattr(self, "_restore_autocommit", False):
try:
if exc_type is not None:
self.connection.rollback()
finally:
self.connection.set_autocommit(True)

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.

high

If an exception occurs during super().__exit__ (for example, when executing deferred DDL statements), exc_type passed to __exit__ is initially None. As a result, exc_type is not None evaluates to False, and self.connection.rollback() is not called, leaving the connection in an inconsistent state. We should track whether an exception occurred either inside the with block or during super().__exit__ to ensure we roll back correctly.

    def __exit__(self, exc_type, exc_value, traceback):
        has_exception = exc_type is not None
        try:
            return super().__exit__(exc_type, exc_value, traceback)
        except Exception:
            has_exception = True
            raise
        finally:
            if getattr(self, "_restore_autocommit", False):
                try:
                    if has_exception:
                        self.connection.rollback()
                finally:
                    self.connection.set_autocommit(True)

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.

Updated __exit__ to track clean exit across super().__exit__() and separate explicit commit()/rollback() from set_autocommit(True) so autocommit is always restored even when deferred or server-side DDL execution fails.

if exc_type is not None:
self.connection.rollback()
finally:
self.connection.set_autocommit(True)

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.

set_autocommit(True) only triggers a flush of the buffered DDL statements in google-cloud-spanner >= 3.72.0. So we should probably also update the minimum version required for Django.

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.

Updated google-cloud-spanner minimum version to >= 3.72.0 in setup.py and noxfile.py.

if os.environ.get("RUNNING_SPANNER_BACKEND_TESTS") == "1":
self.mark_skips()
super().create_test_db(*args, **kwargs)
self.connection._batch_ddl_in_schema_editor = True

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 a bit of a code smell. It introduces a private flag that is currently only used by tests, but that someone who inspects the code could find and set, and then it will also be used in production. Can we try to make a design that makes a clear choice between one of the following:

  1. A test-only fix that makes sure that our tests use DDL batching
  2. A feature that makes DDL batching a public and supported feature for Spanner Django

A test-only solution could look like this:

# In django_spanner/creation.py
from django_spanner.schema import DatabaseSchemaEditor


class BatchingDatabaseSchemaEditor(DatabaseSchemaEditor):
    """Schema editor that batches DDL statements by toggling autocommit."""

    def __enter__(self):
        super().__enter__()
        self._restore_autocommit = False
        if (
            not self.collect_sql
            and not self.connection.in_atomic_block
            and self.connection.get_autocommit()
        ):
            self.connection.set_autocommit(False)
            self._restore_autocommit = True
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        has_exception = exc_type is not None
        try:
            super().__exit__(exc_type, exc_value, traceback)
        except Exception:
            has_exception = True
            raise
        finally:
            if getattr(self, "_restore_autocommit", False):
                try:
                    if has_exception:
                        self.connection.rollback()
                finally:
                    self.connection.set_autocommit(True)


class DatabaseCreation(BaseDatabaseCreation):
    def create_test_db(self, *args, **kwargs):
        if os.environ.get("RUNNING_SPANNER_BACKEND_TESTS") == "1":
            self.mark_skips()
        
        # Temporarily use the batching schema editor during test database creation
        original_schema_editor_class = self.connection.SchemaEditorClass
        self.connection.SchemaEditorClass = BatchingDatabaseSchemaEditor
        try:
            return super().create_test_db(*args, **kwargs)
        finally:
            self.connection.SchemaEditorClass = original_schema_editor_class

Or if we want to support this publicly for both tests and applications, then we could add an option to DATABASES:

# In settings.py
DATABASES = {
    "default": {
        "ENGINE": "django_spanner",
        "OPTIONS": {
            "batch_ddl": True,  # Defaults to True or False depending on what we find the safest
        },
    }
}

# In DatabaseSchemaEditor.__enter__
batch_ddl = self.connection.settings_dict.get("OPTIONS", {}).get("batch_ddl", True)
if batch_ddl and not self.collect_sql and not self.connection.in_atomic_block and self.connection.get_autocommit():
    ...

I would personally lean towards making it a public option, and maybe disable it by default in the first release, and consider making it the default in a follow-up 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.

Replaced the private flag with the public OPTIONS["batch_ddl"] option (defaulting to False), stripped batch_ddl in get_new_connection(), enabled it in the test suite scripts, and documented it in README.rst.

@sakthivelmanii sakthivelmanii changed the title chore(django-google-spanner): batch DDL statements during create_test_db feat(django-google-spanner): add batch_ddl database option Nov 13, 2026

This branch has not been deployed

No deployments
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.

2 participants