From 75cd9bc25bed283f9e22cf2b4b3a9791f5fc5059 Mon Sep 17 00:00:00 2001 From: Ankit Agarwal <146331865+ankiaga@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:05:46 +0530 Subject: [PATCH 1/2] feat!: Support Django 4.2 (#865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Changes for Django 4.2 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Adding allow_transactions_in_auto_commit property * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Comments incorporated * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Update README.rst Co-authored-by: Knut Olav Løite * Update README.rst Co-authored-by: Knut Olav Løite * Update README.rst Co-authored-by: Knut Olav Løite * Update django_spanner/base.py Co-authored-by: Knut Olav Løite * Comments incorporated --------- Co-authored-by: Owl Bot Co-authored-by: Knut Olav Løite --- ... => django4.2_tests_against_emulator0.yml} | 6 +- ... => django4.2_tests_against_emulator1.yml} | 4 +- ... => django4.2_tests_against_emulator2.yml} | 4 +- ... => django4.2_tests_against_emulator3.yml} | 4 +- ... => django4.2_tests_against_emulator4.yml} | 4 +- ... => django4.2_tests_against_emulator5.yml} | 4 +- ... => django4.2_tests_against_emulator6.yml} | 4 +- ... => django4.2_tests_against_emulator7.yml} | 4 +- ... => django4.2_tests_against_emulator8.yml} | 4 +- ... => django4.2_tests_against_emulator9.yml} | 4 +- ...tegration-tests-against-emulator-3.10.yml} | 8 +- ...integration-tests-against-emulator-3.8.yml | 2 - ...integration-tests-against-emulator-3.9.yml | 2 - README.rst | 18 + django_spanner/__init__.py | 48 +- django_spanner/base.py | 25 +- django_spanner/compiler.py | 239 +++++-- django_spanner/expressions.py | 33 - django_spanner/features.py | 611 ++++++++++-------- django_spanner/introspection.py | 39 +- django_spanner/operations.py | 481 ++++++++++---- django_spanner/schema.py | 48 +- ..._test_suite.sh => django_test_suite_4.2.sh | 3 +- noxfile.py | 26 +- run_testing_worker.py | 2 +- setup.py | 6 +- tests/unit/django_spanner/test_compiler.py | 181 ++++-- .../unit/django_spanner/test_introspection.py | 81 +-- tests/unit/django_spanner/test_operations.py | 177 +++-- 29 files changed, 1299 insertions(+), 773 deletions(-) rename .github/workflows/{django_tests_against_emulator0.yml => django4.2_tests_against_emulator0.yml} (63%) rename .github/workflows/{django_tests_against_emulator1.yml => django4.2_tests_against_emulator1.yml} (94%) rename .github/workflows/{django_tests_against_emulator2.yml => django4.2_tests_against_emulator2.yml} (93%) rename .github/workflows/{django_tests_against_emulator3.yml => django4.2_tests_against_emulator3.yml} (93%) rename .github/workflows/{django_tests_against_emulator4.yml => django4.2_tests_against_emulator4.yml} (94%) rename .github/workflows/{django_tests_against_emulator5.yml => django4.2_tests_against_emulator5.yml} (93%) rename .github/workflows/{django_tests_against_emulator6.yml => django4.2_tests_against_emulator6.yml} (92%) rename .github/workflows/{django_tests_against_emulator7.yml => django4.2_tests_against_emulator7.yml} (92%) rename .github/workflows/{django_tests_against_emulator8.yml => django4.2_tests_against_emulator8.yml} (93%) rename .github/workflows/{django_tests_against_emulator9.yml => django4.2_tests_against_emulator9.yml} (92%) rename .github/workflows/{integration-tests-against-emulator-3.7.yml => integration-tests-against-emulator-3.10.yml} (79%) delete mode 100644 django_spanner/expressions.py rename django_test_suite.sh => django_test_suite_4.2.sh (96%) diff --git a/.github/workflows/django_tests_against_emulator0.yml b/.github/workflows/django4.2_tests_against_emulator0.yml similarity index 63% rename from .github/workflows/django_tests_against_emulator0.yml rename to .github/workflows/django4.2_tests_against_emulator0.yml index c834ebf67e..10dcf1ba3b 100644 --- a/.github/workflows/django_tests_against_emulator0.yml +++ b/.github/workflows/django4.2_tests_against_emulator0.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests0 +name: django4.2-tests0 jobs: system-tests: runs-on: ubuntu-latest @@ -22,11 +22,11 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project GOOGLE_CLOUD_TESTS_CREATE_SPANNER_INSTANCE: true RUNNING_SPANNER_BACKEND_TESTS: 1 SPANNER_TEST_INSTANCE: google-cloud-django-backend-tests - DJANGO_TEST_APPS: admin_changelist admin_ordering aggregation choices distinct_on_fields expressions_window fixtures_model_package datetimes custom_methods generic_inline_admin field_defaults datatypes empty m2o_recursive many_to_one_null migration_test_data_persistence admin_docs invalid_models_tests migrate_signals model_forms.test_uuid model_forms.test_modelchoicefield syndication_tests view_tests update test_utils select_related_onetoone sessions_tests + DJANGO_TEST_APPS: admin_changelist admin_ordering aggregation distinct_on_fields expressions_window fixtures_model_package datetimes custom_methods generic_inline_admin field_defaults datatypes empty m2o_recursive many_to_one_null migration_test_data_persistence admin_docs invalid_models_tests migrate_signals model_forms.test_uuid model_forms.test_modelchoicefield syndication_tests view_tests update test_utils select_related_onetoone sessions_tests diff --git a/.github/workflows/django_tests_against_emulator1.yml b/.github/workflows/django4.2_tests_against_emulator1.yml similarity index 94% rename from .github/workflows/django_tests_against_emulator1.yml rename to .github/workflows/django4.2_tests_against_emulator1.yml index 06b4ef4595..7f44ce5f4c 100644 --- a/.github/workflows/django_tests_against_emulator1.yml +++ b/.github/workflows/django4.2_tests_against_emulator1.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests1 +name: django4.2-tests1 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator2.yml b/.github/workflows/django4.2_tests_against_emulator2.yml similarity index 93% rename from .github/workflows/django_tests_against_emulator2.yml rename to .github/workflows/django4.2_tests_against_emulator2.yml index bcdc0d179a..9f86bb01cd 100644 --- a/.github/workflows/django_tests_against_emulator2.yml +++ b/.github/workflows/django4.2_tests_against_emulator2.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests2 +name: django4.2-tests2 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator3.yml b/.github/workflows/django4.2_tests_against_emulator3.yml similarity index 93% rename from .github/workflows/django_tests_against_emulator3.yml rename to .github/workflows/django4.2_tests_against_emulator3.yml index 4e8a92bd00..c666f065fb 100644 --- a/.github/workflows/django_tests_against_emulator3.yml +++ b/.github/workflows/django4.2_tests_against_emulator3.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests3 +name: django4.2-tests3 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator4.yml b/.github/workflows/django4.2_tests_against_emulator4.yml similarity index 94% rename from .github/workflows/django_tests_against_emulator4.yml rename to .github/workflows/django4.2_tests_against_emulator4.yml index acff31c3a6..30645fb684 100644 --- a/.github/workflows/django_tests_against_emulator4.yml +++ b/.github/workflows/django4.2_tests_against_emulator4.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests4 +name: django4.2-tests4 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator5.yml b/.github/workflows/django4.2_tests_against_emulator5.yml similarity index 93% rename from .github/workflows/django_tests_against_emulator5.yml rename to .github/workflows/django4.2_tests_against_emulator5.yml index 60f54b9add..ff7d22ed31 100644 --- a/.github/workflows/django_tests_against_emulator5.yml +++ b/.github/workflows/django4.2_tests_against_emulator5.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests5 +name: django4.2-tests5 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator6.yml b/.github/workflows/django4.2_tests_against_emulator6.yml similarity index 92% rename from .github/workflows/django_tests_against_emulator6.yml rename to .github/workflows/django4.2_tests_against_emulator6.yml index 539353f11a..9e70c967cc 100644 --- a/.github/workflows/django_tests_against_emulator6.yml +++ b/.github/workflows/django4.2_tests_against_emulator6.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests6 +name: django4.2-tests6 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator7.yml b/.github/workflows/django4.2_tests_against_emulator7.yml similarity index 92% rename from .github/workflows/django_tests_against_emulator7.yml rename to .github/workflows/django4.2_tests_against_emulator7.yml index 6805359abb..48828fca77 100644 --- a/.github/workflows/django_tests_against_emulator7.yml +++ b/.github/workflows/django4.2_tests_against_emulator7.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests7 +name: django4.2-tests7 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator8.yml b/.github/workflows/django4.2_tests_against_emulator8.yml similarity index 93% rename from .github/workflows/django_tests_against_emulator8.yml rename to .github/workflows/django4.2_tests_against_emulator8.yml index 873eb0763d..5f2c8f0c24 100644 --- a/.github/workflows/django_tests_against_emulator8.yml +++ b/.github/workflows/django4.2_tests_against_emulator8.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests8 +name: django4.2-tests8 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/django_tests_against_emulator9.yml b/.github/workflows/django4.2_tests_against_emulator9.yml similarity index 92% rename from .github/workflows/django_tests_against_emulator9.yml rename to .github/workflows/django4.2_tests_against_emulator9.yml index da75b76902..3a898a5460 100644 --- a/.github/workflows/django_tests_against_emulator9.yml +++ b/.github/workflows/django4.2_tests_against_emulator9.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: django-tests9 +name: django4.2-tests9 jobs: system-tests: runs-on: ubuntu-latest @@ -22,7 +22,7 @@ jobs: with: python-version: 3.8 - name: Run Django tests - run: sh django_test_suite.sh + run: sh django_test_suite_4.2.sh env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/integration-tests-against-emulator-3.7.yml b/.github/workflows/integration-tests-against-emulator-3.10.yml similarity index 79% rename from .github/workflows/integration-tests-against-emulator-3.7.yml rename to .github/workflows/integration-tests-against-emulator-3.10.yml index ac7c042798..e64a784e78 100644 --- a/.github/workflows/integration-tests-against-emulator-3.7.yml +++ b/.github/workflows/integration-tests-against-emulator-3.10.yml @@ -3,7 +3,7 @@ on: branches: - main pull_request: -name: Run Django Spanner integration tests against emulator 3.7 +name: Run Django Spanner integration tests against emulator 3.10 jobs: system-tests: runs-on: ubuntu-latest @@ -18,14 +18,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python 3.7 + - name: Set up Python 3.20 uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: "3.10" - name: Install nox run: python -m pip install nox - name: Run nox - run: nox -s unit-3.7 + run: nox -s unit-3.10 env: SPANNER_EMULATOR_HOST: localhost:9010 GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.github/workflows/integration-tests-against-emulator-3.8.yml b/.github/workflows/integration-tests-against-emulator-3.8.yml index 39334d1a12..b479d1a16e 100644 --- a/.github/workflows/integration-tests-against-emulator-3.8.yml +++ b/.github/workflows/integration-tests-against-emulator-3.8.yml @@ -24,8 +24,6 @@ jobs: python-version: 3.8 - name: Install nox run: python -m pip install nox - with: - python-version: 3.8 - name: Run nox run: nox -s unit-3.8 env: diff --git a/.github/workflows/integration-tests-against-emulator-3.9.yml b/.github/workflows/integration-tests-against-emulator-3.9.yml index 2a36652ce4..371f071afa 100644 --- a/.github/workflows/integration-tests-against-emulator-3.9.yml +++ b/.github/workflows/integration-tests-against-emulator-3.9.yml @@ -24,8 +24,6 @@ jobs: python-version: 3.9 - name: Install nox run: python -m pip install nox - with: - python-version: 3.9 - name: Run nox run: nox -s unit-3.9 env: diff --git a/README.rst b/README.rst index c4d7096328..a9aa48084f 100644 --- a/README.rst +++ b/README.rst @@ -140,6 +140,24 @@ configured: } } + Transaction support in autocommit mode + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Django version 4.2 and higher by default supports transactions in autocommit mode. + A transaction is automatically started if you define an + [atomic block](https://docs.djangoproject.com/en/4.2/topics/db/transactions/#controlling-transactions-explicitly). + + Django version 3.2 and earlier did not support transactions in autocommit mode with Spanner. + You can enable transactions in autocommit mode with Spanner with the + `ALLOW_TRANSACTIONS_IN_AUTO_COMMIT` configuration option. + + - To enable transactions in autocommit mode in V3.2, set + the flag "ALLOW_TRANSACTIONS_IN_AUTO_COMMIT" to True in your + settings.py file. + - To disable transactions in autocommit mode in V4.2, set + the flag "ALLOW_TRANSACTIONS_IN_AUTO_COMMIT" to False in your + settings.py file. + Set credentials and project environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/django_spanner/__init__.py b/django_spanner/__init__.py index ad4bc4e38d..171f8c4d31 100644 --- a/django_spanner/__init__.py +++ b/django_spanner/__init__.py @@ -20,7 +20,6 @@ Field, ) -from .expressions import register_expressions from .functions import register_functions from .lookups import register_lookups from .utils import check_django_compatability @@ -34,22 +33,24 @@ if django.VERSION[:2] == (3, 2): USING_DJANGO_3 = True -if USING_DJANGO_3: - from django.db.models.fields import ( - SmallAutoField, - BigAutoField, - ) - from django.db.models import JSONField +USING_DJANGO_4 = False +if django.VERSION[:2] == (4, 2): + USING_DJANGO_4 = True + +from django.db.models.fields import ( + SmallAutoField, + BigAutoField, +) +from django.db.models import JSONField __version__ = pkg_resources.get_distribution("django-google-spanner").version USE_EMULATOR = os.getenv("SPANNER_EMULATOR_HOST") is not None -# Only active LTS django versions (2.2.*, 3.2.*) are supported by this library right now. -SUPPORTED_DJANGO_VERSIONS = [(2, 2), (3, 2)] +# Only active LTS django versions (3.2.*, 4.2.*) are supported by this library right now. +SUPPORTED_DJANGO_VERSIONS = [(3, 2), (4, 2)] check_django_compatability(SUPPORTED_DJANGO_VERSIONS) -register_expressions(USING_DJANGO_3) register_functions() register_lookups() @@ -73,23 +74,24 @@ def autofield_init(self, *args, **kwargs): AutoField.__init__ = autofield_init AutoField.db_returning = False AutoField.validators = [] -if USING_DJANGO_3: - SmallAutoField.__init__ = autofield_init - BigAutoField.__init__ = autofield_init - SmallAutoField.db_returning = False - BigAutoField.db_returning = False - SmallAutoField.validators = [] - BigAutoField.validators = [] - def get_prep_value(self, value): - # Json encoding and decoding for spanner is done in python-spanner. - if not isinstance(value, JsonObject) and isinstance(value, dict): - return JsonObject(value) +SmallAutoField.__init__ = autofield_init +BigAutoField.__init__ = autofield_init +SmallAutoField.db_returning = False +BigAutoField.db_returning = False +SmallAutoField.validators = [] +BigAutoField.validators = [] + + +def get_prep_value(self, value): + # Json encoding and decoding for spanner is done in python-spanner. + if not isinstance(value, JsonObject) and isinstance(value, dict): + return JsonObject(value) - return value + return value - JSONField.get_prep_value = get_prep_value +JSONField.get_prep_value = get_prep_value old_datetimewithnanoseconds_eq = getattr( DatetimeWithNanoseconds, "__eq__", None diff --git a/django_spanner/base.py b/django_spanner/base.py index 25c42416a5..bdbdbc4de1 100644 --- a/django_spanner/base.py +++ b/django_spanner/base.py @@ -17,6 +17,7 @@ from .introspection import DatabaseIntrospection from .operations import DatabaseOperations from .schema import DatabaseSchemaEditor +from django_spanner import USING_DJANGO_3 class DatabaseWrapper(BaseDatabaseWrapper): @@ -123,6 +124,15 @@ def instance(self): project=os.environ["GOOGLE_CLOUD_PROJECT"] ).instance(self.settings_dict["INSTANCE"]) + @property + def allow_transactions_in_auto_commit(self): + if "ALLOW_TRANSACTIONS_IN_AUTO_COMMIT" in self.settings_dict: + return self.settings_dict["ALLOW_TRANSACTIONS_IN_AUTO_COMMIT"] + if USING_DJANGO_3: + return False + else: + return True + @property def _nodb_connection(self): raise NotImplementedError( @@ -205,15 +215,14 @@ def is_usable(self): return True - # The usual way to start a transaction is to turn autocommit off. - # Spanner DB API does not properly start a transaction when disabling - # autocommit. To avoid this buggy behavior and to actually enter a new - # transaction, an explicit SELECT 1 is required. def _start_transaction_under_autocommit(self): """ Start a transaction explicitly in autocommit mode. - - Staying in autocommit mode works around a bug that breaks - save points when autocommit is disabled by django. """ - self.connection.cursor().execute("SELECT 1") + if self.allow_transactions_in_auto_commit: + self.connection.cursor().execute("BEGIN") + else: + # This won't start a transaction and was a bug in Spanner Django 3.2 version. + # Set ALLOW_TRANSACTIONS_IN_AUTO_COMMIT = True in your settings.py file to enable + # transactions in autocommit mode for Django 3.2. + self.connection.cursor().execute("SELECT 1") diff --git a/django_spanner/compiler.py b/django_spanner/compiler.py index 4b584e8e5b..a2175113f7 100644 --- a/django_spanner/compiler.py +++ b/django_spanner/compiler.py @@ -13,6 +13,7 @@ SQLUpdateCompiler as BaseSQLUpdateCompiler, ) from django.db.utils import DatabaseError +from django_spanner import USING_DJANGO_3 class SQLCompiler(BaseSQLCompiler): @@ -38,76 +39,182 @@ def get_combinator_sql(self, combinator, all): :returns: A tuple containing SQL statement(s) with some additional parameters. """ - features = self.connection.features - compilers = [ - query.get_compiler(self.using, self.connection) - for query in self.query.combined_queries - if not query.is_empty() - ] - if not features.supports_slicing_ordering_in_compound: - for query, compiler in zip(self.query.combined_queries, compilers): - if query.low_mark or query.high_mark: - raise DatabaseError( - "LIMIT/OFFSET not allowed in subqueries of compound " - "statements." - ) - if compiler.get_order_by(): - raise DatabaseError( - "ORDER BY not allowed in subqueries of compound " - "statements." - ) - parts = () - for compiler in compilers: - try: - # If the columns list is limited, then all combined queries - # must have the same columns list. Set the selects defined on - # the query on all combined queries, if not already set. - if ( - not compiler.query.values_select - and self.query.values_select + # This method copies the complete code of this overridden method from + # Django core and modify it for Spanner by adding one line + if USING_DJANGO_3: + features = self.connection.features + compilers = [ + query.get_compiler(self.using, self.connection) + for query in self.query.combined_queries + if not query.is_empty() + ] + if not features.supports_slicing_ordering_in_compound: + for query, compiler in zip( + self.query.combined_queries, compilers ): - compiler.query.set_values( - ( - *self.query.extra_select, - *self.query.values_select, - *self.query.annotation_select, + if query.low_mark or query.high_mark: + raise DatabaseError( + "LIMIT/OFFSET not allowed in subqueries of compound " + "statements." + ) + if compiler.get_order_by(): + raise DatabaseError( + "ORDER BY not allowed in subqueries of compound " + "statements." + ) + parts = () + for compiler in compilers: + try: + # If the columns list is limited, then all combined queries + # must have the same columns list. Set the selects defined on + # the query on all combined queries, if not already set. + if ( + not compiler.query.values_select + and self.query.values_select + ): + compiler.query.set_values( + ( + *self.query.extra_select, + *self.query.values_select, + *self.query.annotation_select, + ) ) + part_sql, part_args = compiler.as_sql() + if compiler.query.combinator: + # Wrap in a subquery if wrapping in parentheses isn't + # supported. + if not features.supports_parentheses_in_compound: + part_sql = "SELECT * FROM ({})".format(part_sql) + # Add parentheses when combining with compound query if not + # already added for all compound queries. + elif ( + not features.supports_slicing_ordering_in_compound + ): + part_sql = "({})".format(part_sql) + parts += ((part_sql, part_args),) + except EmptyResultSet: + # Omit the empty queryset with UNION and with DIFFERENCE if the + # first queryset is nonempty. + if combinator == "union" or ( + combinator == "difference" and parts + ): + continue + raise + if not parts: + raise EmptyResultSet + combinator_sql = self.connection.ops.set_operators[combinator] + # This is the only line that is changed from the Django core + # implementation of this method + combinator_sql += " ALL" if all else " DISTINCT" + braces = ( + "({})" + if features.supports_slicing_ordering_in_compound + else "{}" + ) + sql_parts, args_parts = zip( + *((braces.format(sql), args) for sql, args in parts) + ) + result = [" {} ".format(combinator_sql).join(sql_parts)] + params = [] + for part in args_parts: + params.extend(part) + + return result, params + # As the code of this method has somewhat changed in Django 4.2 core + # version, so we are copying the complete code of this overridden method + # and modifying it for Spanner + else: + features = self.connection.features + compilers = [ + query.get_compiler( + self.using, self.connection, self.elide_empty + ) + for query in self.query.combined_queries + ] + if not features.supports_slicing_ordering_in_compound: + for compiler in compilers: + if compiler.query.is_sliced: + raise DatabaseError( + "LIMIT/OFFSET not allowed in subqueries of compound statements." + ) + if compiler.get_order_by(): + raise DatabaseError( + "ORDER BY not allowed in subqueries of compound statements." + ) + elif self.query.is_sliced and combinator == "union": + for compiler in compilers: + # A sliced union cannot have its parts elided as some of them + # might be sliced as well and in the event where only a single + # part produces a non-empty resultset it might be impossible to + # generate valid SQL. + compiler.elide_empty = False + parts = () + for compiler in compilers: + try: + # If the columns list is limited, then all combined queries + # must have the same columns list. Set the selects defined on + # the query on all combined queries, if not already set. + if ( + not compiler.query.values_select + and self.query.values_select + ): + compiler.query = compiler.query.clone() + compiler.query.set_values( + ( + *self.query.extra_select, + *self.query.values_select, + *self.query.annotation_select, + ) + ) + part_sql, part_args = compiler.as_sql( + with_col_aliases=True ) - part_sql, part_args = compiler.as_sql() - if compiler.query.combinator: - # Wrap in a subquery if wrapping in parentheses isn't - # supported. - if not features.supports_parentheses_in_compound: - part_sql = "SELECT * FROM ({})".format(part_sql) - # Add parentheses when combining with compound query if not - # already added for all compound queries. - elif not features.supports_slicing_ordering_in_compound: + if compiler.query.combinator: + # Wrap in a subquery if wrapping in parentheses isn't + # supported. + if not features.supports_parentheses_in_compound: + part_sql = "SELECT * FROM ({})".format(part_sql) + # Add parentheses when combining with compound query if not + # already added for all compound queries. + elif ( + self.query.subquery + or not features.supports_slicing_ordering_in_compound + ): + part_sql = "({})".format(part_sql) + elif ( + self.query.subquery + and features.supports_slicing_ordering_in_compound + ): part_sql = "({})".format(part_sql) - parts += ((part_sql, part_args),) - except EmptyResultSet: - # Omit the empty queryset with UNION and with DIFFERENCE if the - # first queryset is nonempty. - if combinator == "union" or ( - combinator == "difference" and parts - ): - continue - raise - if not parts: - raise EmptyResultSet - combinator_sql = self.connection.ops.set_operators[combinator] - combinator_sql += " ALL" if all else " DISTINCT" - braces = ( - "({})" if features.supports_slicing_ordering_in_compound else "{}" - ) - sql_parts, args_parts = zip( - *((braces.format(sql), args) for sql, args in parts) - ) - result = [" {} ".format(combinator_sql).join(sql_parts)] - params = [] - for part in args_parts: - params.extend(part) - - return result, params + parts += ((part_sql, part_args),) + except EmptyResultSet: + # Omit the empty queryset with UNION and with DIFFERENCE if the + # first queryset is nonempty. + if combinator == "union" or ( + combinator == "difference" and parts + ): + continue + raise + if not parts: + raise EmptyResultSet + combinator_sql = self.connection.ops.set_operators[combinator] + # This is the only line that is changed from the Django core + # implementation of this method + combinator_sql += " ALL" if all else " DISTINCT" + braces = "{}" + if ( + not self.query.subquery + and features.supports_slicing_ordering_in_compound + ): + braces = "({})" + sql_parts, args_parts = zip( + *((braces.format(sql), args) for sql, args in parts) + ) + result = [" {} ".format(combinator_sql).join(sql_parts)] + params = [] + for part in args_parts: + params.extend(part) + return result, params class SQLInsertCompiler(BaseSQLInsertCompiler, SQLCompiler): diff --git a/django_spanner/expressions.py b/django_spanner/expressions.py deleted file mode 100644 index 44a90f586d..0000000000 --- a/django_spanner/expressions.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2020 Google LLC -# -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file or at -# https://developers.google.com/open-source/licenses/bsd - -from django.db.models.expressions import OrderBy - - -def order_by(self, compiler, connection, **extra_context): - """ - Order expressions in the SQL query and generate a new query using - Spanner-specific templates. - - :rtype: str - :returns: A SQL query. - """ - template = None - if self.nulls_last: - template = "%(expression)s IS NULL, %(expression)s %(ordering)s" - elif self.nulls_first: - template = "%(expression)s IS NOT NULL, %(expression)s %(ordering)s" - return self.as_sql( - compiler, connection, template=template, **extra_context - ) - - -def register_expressions(using_django_3=False): - """Add Spanner-specific attribute to the Django OrderBy class for django 2.2.""" - # In Django >= 3.1, this can be replaced with - # DatabaseFeatures.supports_order_by_nulls_modifier = False. - if not using_django_3: - OrderBy.as_spanner = order_by diff --git a/django_spanner/features.py b/django_spanner/features.py index 4f7a81458c..1cdaccd4e3 100644 --- a/django_spanner/features.py +++ b/django_spanner/features.py @@ -8,7 +8,7 @@ from django.db.backends.base.features import BaseDatabaseFeatures from django.db.utils import InterfaceError -from django_spanner import USE_EMULATOR, USING_DJANGO_3 +from django_spanner import USE_EMULATOR, USING_DJANGO_3, USING_DJANGO_4 class DatabaseFeatures(BaseDatabaseFeatures): @@ -44,16 +44,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): else: supports_column_check_constraints = True supports_table_check_constraints = True - if USING_DJANGO_3: - supports_json_field = True - else: - # Since JsonField was introduced in django3.1 we don't support it for django 2.2 - supports_json_field = False + supports_json_field = True supports_primitives_in_json_field = False # Spanner does not support order by null modifiers. - # For Django 2.2 this feature is handled in code. - if USING_DJANGO_3: - supports_order_by_nulls_modifier = False + supports_order_by_nulls_modifier = False # Spanner does not support SELECTing an arbitrary expression that also # appears in the GROUP BY clause. supports_subqueries_in_group_by = False @@ -80,8 +74,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "generic_relations.tests.GenericRelationsTests.test_add_bulk_false", "generic_relations.tests.GenericRelationsTests.test_generic_update_or_create_when_updated", "generic_relations.tests.GenericRelationsTests.test_update_or_create_defaults", - "generic_relations.tests.GenericRelationsTests.test_unsaved_instance_on_generic_foreign_key", - "generic_relations_regress.tests.GenericRelationTests.test_target_model_is_unsaved", "m2m_through_regress.tests.ToFieldThroughTests.test_m2m_relations_unusable_on_null_pk_obj", "many_to_many.tests.ManyToManyTests.test_add", "many_to_one.tests.ManyToOneTests.test_fk_assignment_and_related_object_cache", @@ -160,7 +152,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "aggregation.tests.AggregateTestCase.test_filtering", "aggregation_regress.tests.AggregationTests.test_more_more", "aggregation_regress.tests.AggregationTests.test_more_more_more", - "aggregation_regress.tests.AggregationTests.test_ticket_11293", "defer_regress.tests.DeferRegressionTest.test_ticket_12163", "defer_regress.tests.DeferRegressionTest.test_ticket_23270", "distinct_on_fields.tests.DistinctOnTests.test_basic_distinct_on", @@ -378,121 +369,195 @@ class DatabaseFeatures(BaseDatabaseFeatures): "file_uploads.tests.DirectoryCreationTests.test_readonly_root", # Failing on kokoro but passes locally. Issue: Multiple queries executed expected 1. "contenttypes_tests.test_models.ContentTypesTests.test_cache_not_shared_between_managers", + # Spanner does not support UUID field natively + "model_fields.test_uuid.TestQuerying.test_iexact", + # Spanner does not support very long FK name: 400 Foreign Key name not valid + "backends.tests.FkConstraintsTests.test_check_constraints_sql_keywords", + # Spanner does not support setting a default value on columns. + "schema.tests.SchemaTests.test_alter_text_field_to_not_null_with_default_value", + # Direct SQL query test that do not follow spanner syntax. + "schema.tests.SchemaTests.test_alter_auto_field_quoted_db_column", + "schema.tests.SchemaTests.test_alter_primary_key_quoted_db_table", + # Insert sql with param variables using %(name)s parameter style is failing + # https://github.com/googleapis/python-spanner/issues/542 + "backends.tests.LastExecutedQueryTest.test_last_executed_query_dict", + # Spanner autofield is replaced with uuid4 so validation is disabled + "model_fields.test_autofield.AutoFieldTests.test_backend_range_validation", + "model_fields.test_autofield.AutoFieldTests.test_redundant_backend_range_validators", + "model_fields.test_autofield.AutoFieldTests.test_redundant_backend_range_validators", + "model_fields.test_autofield.BigAutoFieldTests.test_backend_range_validation", + "model_fields.test_autofield.BigAutoFieldTests.test_redundant_backend_range_validators", + "model_fields.test_autofield.BigAutoFieldTests.test_redundant_backend_range_validators", + "model_fields.test_autofield.SmallAutoFieldTests.test_backend_range_validation", + "model_fields.test_autofield.SmallAutoFieldTests.test_redundant_backend_range_validators", + "model_fields.test_autofield.SmallAutoFieldTests.test_redundant_backend_range_validators", + # Spanner does not support deferred unique constraints + "migrations.test_operations.OperationTests.test_create_model_with_deferred_unique_constraint", + # Spanner does not support JSON object query on fields. + "db_functions.comparison.test_json_object.JSONObjectTests.test_empty", + "db_functions.comparison.test_json_object.JSONObjectTests.test_basic", + "db_functions.comparison.test_json_object.JSONObjectTests.test_expressions", + "db_functions.comparison.test_json_object.JSONObjectTests.test_nested_empty_json_object", + "db_functions.comparison.test_json_object.JSONObjectTests.test_nested_json_object", + "db_functions.comparison.test_json_object.JSONObjectTests.test_textfield", + # Spanner does not support iso_week_day but week_day is supported. + "timezones.tests.LegacyDatabaseTests.test_query_datetime_lookups", + "timezones.tests.NewDatabaseTests.test_query_datetime_lookups", + "timezones.tests.NewDatabaseTests.test_query_datetime_lookups_in_other_timezone", + "db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_func", + "db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_iso_weekday_func", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func_with_timezone", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func_with_timezone", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_iso_weekday_func", + # Spanner gived SHA encryption output in bytes, django expects it in hex string format. + "db_functions.text.test_sha512.SHA512Tests.test_basic", + "db_functions.text.test_sha512.SHA512Tests.test_transform", + "db_functions.text.test_md5.MD5Tests.test_basic", + "db_functions.text.test_md5.MD5Tests.test_transform", + "db_functions.text.test_sha1.SHA1Tests.test_basic", + "db_functions.text.test_sha1.SHA1Tests.test_transform", + "db_functions.text.test_sha224.SHA224Tests.test_basic", + "db_functions.text.test_sha224.SHA224Tests.test_transform", + "db_functions.text.test_sha256.SHA256Tests.test_basic", + "db_functions.text.test_sha256.SHA256Tests.test_transform", + "db_functions.text.test_sha384.SHA384Tests.test_basic", + "db_functions.text.test_sha384.SHA384Tests.test_transform", + # Spanner does not support RANDOM number generation function + "db_functions.math.test_random.RandomTests.test", + # Spanner supports order by id, but it's does not work the same way as + # an auto increment field. + "model_forms.test_modelchoicefield.ModelChoiceFieldTests.test_choice_iterator_passes_model_to_widget", + "queries.test_qs_combinators.QuerySetSetOperationTests.test_union_with_values_list_and_order", + "ordering.tests.OrderingTests.test_order_by_self_referential_fk", + "fixtures.tests.ForwardReferenceTests.test_forward_reference_m2m_natural_key", + "fixtures.tests.ForwardReferenceTests.test_forward_reference_fk_natural_key", + # Spanner does not support empty list of DML statement. + "backends.tests.BackendTestCase.test_cursor_executemany_with_empty_params_list", + # Spanner does not support SELECTing an arbitrary expression that also + # appears in the GROUP BY clause. + "annotations.tests.NonAggregateAnnotationTestCase.test_grouping_by_q_expression_annotation", + # Tests that expect it to be empty untill saved in db. + "test_utils.test_testcase.TestDataTests.test_class_attribute_identity", + "model_fields.test_jsonfield.TestSerialization.test_dumping", + "model_fields.test_jsonfield.TestSerialization.test_dumping", + "model_fields.test_jsonfield.TestSerialization.test_dumping", + "model_fields.test_jsonfield.TestSerialization.test_xml_serialization", + "model_fields.test_jsonfield.TestSerialization.test_xml_serialization", + "model_fields.test_jsonfield.TestSerialization.test_xml_serialization", + "bulk_create.tests.BulkCreateTests.test_unsaved_parent", + # Tests that assume a serial pk. + "lookup.tests.LookupTests.test_exact_query_rhs_with_selected_columns", + "prefetch_related.tests.DirectPrefetchedObjectCacheReuseTests.test_detect_is_fetched", + "prefetch_related.tests.DirectPrefetchedObjectCacheReuseTests.test_detect_is_fetched_with_to_attr", + # datetimes retrieved from the database with the wrong hour when + # USE_TZ = True: https://github.com/googleapis/python-spanner-django/issues/193 + "timezones.tests.NewDatabaseTests.test_query_convert_timezones", + # Spanner doesn't support random ordering. + "aggregation.tests.AggregateTestCase.test_aggregation_random_ordering", + # Tests that require transactions. + "test_utils.tests.CaptureOnCommitCallbacksTests.test_execute", + "test_utils.tests.CaptureOnCommitCallbacksTests.test_no_arguments", + "test_utils.tests.CaptureOnCommitCallbacksTests.test_pre_callback", + "test_utils.tests.CaptureOnCommitCallbacksTests.test_using", + # Field: GenericIPAddressField is mapped to String in Spanner + "inspectdb.tests.InspectDBTestCase.test_field_types", + # BigIntegerField is mapped to IntegerField in Spanner + "inspectdb.tests.InspectDBTestCase.test_number_field_types", + # Spanner limitation: Cannot change type of column. + "schema.tests.SchemaTests.test_char_field_pk_to_auto_field", + "schema.tests.SchemaTests.test_ci_cs_db_collation", + # Spanner limitation: Cannot rename tables and columns. + "migrations.test_operations.OperationTests.test_rename_field_case", ) if USING_DJANGO_3: skip_tests += ( - # Spanner does not support UUID field natively - "model_fields.test_uuid.TestQuerying.test_iexact", - # Spanner does not support very long FK name: 400 Foreign Key name not valid - "backends.tests.FkConstraintsTests.test_check_constraints_sql_keywords", - # Spanner does not support setting a default value on columns. - "schema.tests.SchemaTests.test_alter_text_field_to_not_null_with_default_value", - # Direct SQL query test that do not follow spanner syntax. - "schema.tests.SchemaTests.test_alter_auto_field_quoted_db_column", - "schema.tests.SchemaTests.test_alter_primary_key_quoted_db_table", - # Insert sql with param variables using %(name)s parameter style is failing - # https://github.com/googleapis/python-spanner/issues/542 - "backends.tests.LastExecutedQueryTest.test_last_executed_query_dict", - # Spanner autofield is replaced with uuid4 so validation is disabled - "model_fields.test_autofield.AutoFieldTests.test_backend_range_validation", - "model_fields.test_autofield.AutoFieldTests.test_redundant_backend_range_validators", - "model_fields.test_autofield.AutoFieldTests.test_redundant_backend_range_validators", - "model_fields.test_autofield.BigAutoFieldTests.test_backend_range_validation", - "model_fields.test_autofield.BigAutoFieldTests.test_redundant_backend_range_validators", - "model_fields.test_autofield.BigAutoFieldTests.test_redundant_backend_range_validators", - "model_fields.test_autofield.SmallAutoFieldTests.test_backend_range_validation", - "model_fields.test_autofield.SmallAutoFieldTests.test_redundant_backend_range_validators", - "model_fields.test_autofield.SmallAutoFieldTests.test_redundant_backend_range_validators", - # Spanner does not support deferred unique constraints - "migrations.test_operations.OperationTests.test_create_model_with_deferred_unique_constraint", - # Spanner does not support JSON object query on fields. - "db_functions.comparison.test_json_object.JSONObjectTests.test_empty", - "db_functions.comparison.test_json_object.JSONObjectTests.test_basic", - "db_functions.comparison.test_json_object.JSONObjectTests.test_expressions", - "db_functions.comparison.test_json_object.JSONObjectTests.test_nested_empty_json_object", - "db_functions.comparison.test_json_object.JSONObjectTests.test_nested_json_object", - "db_functions.comparison.test_json_object.JSONObjectTests.test_textfield", - # Spanner does not support iso_week_day but week_day is supported. - "timezones.tests.LegacyDatabaseTests.test_query_datetime_lookups", - "timezones.tests.NewDatabaseTests.test_query_datetime_lookups", - "timezones.tests.NewDatabaseTests.test_query_datetime_lookups_in_other_timezone", - "db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_func", - "db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_iso_weekday_func", - "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func", - "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func_with_timezone", - "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func_with_timezone", - "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_iso_weekday_func", - # Spanner gived SHA encryption output in bytes, django expects it in hex string format. - "db_functions.text.test_sha512.SHA512Tests.test_basic", - "db_functions.text.test_sha512.SHA512Tests.test_transform", - "db_functions.text.test_md5.MD5Tests.test_basic", - "db_functions.text.test_md5.MD5Tests.test_transform", - "db_functions.text.test_sha1.SHA1Tests.test_basic", - "db_functions.text.test_sha1.SHA1Tests.test_transform", - "db_functions.text.test_sha224.SHA224Tests.test_basic", - "db_functions.text.test_sha224.SHA224Tests.test_transform", - "db_functions.text.test_sha256.SHA256Tests.test_basic", - "db_functions.text.test_sha256.SHA256Tests.test_transform", - "db_functions.text.test_sha384.SHA384Tests.test_basic", - "db_functions.text.test_sha384.SHA384Tests.test_transform", - # Spanner does not support RANDOM number generation function - "db_functions.math.test_random.RandomTests.test", - # Spanner supports order by id, but it's does not work the same way as - # an auto increment field. - "model_forms.test_modelchoicefield.ModelChoiceFieldTests.test_choice_iterator_passes_model_to_widget", - "queries.test_qs_combinators.QuerySetSetOperationTests.test_union_with_values_list_and_order", - "ordering.tests.OrderingTests.test_order_by_self_referential_fk", - "fixtures.tests.ForwardReferenceTests.test_forward_reference_m2m_natural_key", - "fixtures.tests.ForwardReferenceTests.test_forward_reference_fk_natural_key", - # Spanner does not support empty list of DML statement. - "backends.tests.BackendTestCase.test_cursor_executemany_with_empty_params_list", - # Spanner does not support SELECTing an arbitrary expression that also - # appears in the GROUP BY clause. - "annotations.tests.NonAggregateAnnotationTestCase.test_grouping_by_q_expression_annotation", # No Django transaction management in Spanner. "transactions.tests.DisableDurabiltityCheckTests.test_nested_both_durable", "transactions.tests.DisableDurabiltityCheckTests.test_nested_inner_durable", - # Tests that expect it to be empty untill saved in db. - "test_utils.test_testcase.TestDataTests.test_class_attribute_identity", - "model_fields.test_jsonfield.TestSerialization.test_dumping", - "model_fields.test_jsonfield.TestSerialization.test_dumping", - "model_fields.test_jsonfield.TestSerialization.test_dumping", - "model_fields.test_jsonfield.TestSerialization.test_xml_serialization", - "model_fields.test_jsonfield.TestSerialization.test_xml_serialization", - "model_fields.test_jsonfield.TestSerialization.test_xml_serialization", - "bulk_create.tests.BulkCreateTests.test_unsaved_parent", - # Tests that assume a serial pk. - "lookup.tests.LookupTests.test_exact_query_rhs_with_selected_columns", - "prefetch_related.tests.DirectPrefetchedObjectCacheReuseTests.test_detect_is_fetched", - "prefetch_related.tests.DirectPrefetchedObjectCacheReuseTests.test_detect_is_fetched_with_to_attr", - # datetimes retrieved from the database with the wrong hour when - # USE_TZ = True: https://github.com/googleapis/python-spanner-django/issues/193 - "timezones.tests.NewDatabaseTests.test_query_convert_timezones", - # Spanner doesn't support random ordering. - "aggregation.tests.AggregateTestCase.test_aggregation_random_ordering", - # Tests that require transactions. - "test_utils.tests.CaptureOnCommitCallbacksTests.test_execute", - "test_utils.tests.CaptureOnCommitCallbacksTests.test_no_arguments", - "test_utils.tests.CaptureOnCommitCallbacksTests.test_pre_callback", - "test_utils.tests.CaptureOnCommitCallbacksTests.test_using", - # Field: GenericIPAddressField is mapped to String in Spanner - "inspectdb.tests.InspectDBTestCase.test_field_types", - # BigIntegerField is mapped to IntegerField in Spanner - "inspectdb.tests.InspectDBTestCase.test_number_field_types", - # Spanner limitation: Cannot change type of column. - "schema.tests.SchemaTests.test_char_field_pk_to_auto_field", - "schema.tests.SchemaTests.test_ci_cs_db_collation", - # Spanner limitation: Cannot rename tables and columns. - "migrations.test_operations.OperationTests.test_rename_field_case", + "generic_relations.tests.GenericRelationsTests.test_unsaved_instance_on_generic_foreign_key", + "generic_relations_regress.tests.GenericRelationTests.test_target_model_is_unsaved", + "aggregation_regress.tests.AggregationTests.test_ticket_11293", # Warning is not raised, not related to spanner. "test_utils.test_testcase.TestDataTests.test_undeepcopyable_warning", ) - else: - # Tests specific to django 2.2 + if USING_DJANGO_4: skip_tests += ( - # Tests that assume a serial pk. - "prefetch_related.tests.DirectPrefechedObjectCacheReuseTests.test_detect_is_fetched", - "prefetch_related.tests.DirectPrefechedObjectCacheReuseTests.test_detect_is_fetched_with_to_attr", + "aggregation.tests.AggregateTestCase.test_aggregation_default_expression", + "aggregation.tests.AggregateTestCase.test_aggregation_default_integer", + "aggregation.tests.AggregateTestCase.test_aggregation_default_unset", + "aggregation.tests.AggregateTestCase.test_aggregation_default_using_duration_from_database", + "aggregation.tests.AggregateTestCase.test_aggregation_default_zero", + "aggregation.tests.AggregateTestCase.test_group_by_nested_expression_with_params", + "many_to_one_null.tests.ManyToOneNullTests.test_unsaved", + "model_formsets.tests.ModelFormsetTest.test_edit_only_object_outside_of_queryset", + "ordering.tests.OrderingTests.test_order_by_expression_ref", + "sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_language_for_item_i18n_sitemap", + "sitemaps_tests.test_http.HTTPSitemapTests.test_language_for_item_i18n_sitemap", + "null_queries.tests.NullQueriesTests.test_unsaved", + "prefetch_related.tests.GenericRelationTests.test_deleted_GFK", + "aggregation_regress.tests.AggregationTests.test_aggregate_and_annotate_duplicate_columns_proxy", + "aggregation_regress.tests.AggregationTests.test_annotation_disjunction", + "aggregation_regress.tests.AggregationTests.test_filter_aggregates_negated_and_connector", + "aggregation_regress.tests.AggregationTests.test_filter_aggregates_negated_xor_connector", + "aggregation_regress.tests.AggregationTests.test_filter_aggregates_or_connector", + "aggregation_regress.tests.AggregationTests.test_filter_aggregates_xor_connector", + "aggregation_regress.tests.AggregationTests.test_aggregate_and_annotate_duplicate_columns_unmanaged", + "queries.test_bulk_update.BulkUpdateTests.test_unsaved_parent", + "queries.test_q.QCheckTests.test_basic", + "queries.test_q.QCheckTests.test_boolean_expression", + "queries.test_q.QCheckTests.test_expression", + "queries.tests.ExcludeTests.test_exclude_unsaved_o2o_object", + "queries.tests.ExcludeTests.test_exclude_unsaved_object", + "queries.tests.Queries5Tests.test_filter_unsaved_object", + "queries.tests.QuerySetBitwiseOperationTests.test_xor_with_both_slice", + "queries.tests.QuerySetBitwiseOperationTests.test_xor_with_lhs_slice", + "queries.tests.QuerySetBitwiseOperationTests.test_xor_with_rhs_slice", + "queries.tests.QuerySetBitwiseOperationTests.test_xor_with_both_slice_and_ordering", + "queries.tests.Queries1Tests.test_filter_by_related_field_transform", + "known_related_objects.tests.ExistingRelatedInstancesTests.test_reverse_fk_select_related_multiple", + "known_related_objects.tests.ExistingRelatedInstancesTests.test_multilevel_reverse_fk_select_related", + "timezones.tests.NewDatabaseTests.test_aware_time_unsupported", + "contenttypes_tests.test_models.ContentTypesTests.test_app_labeled_name", + "db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_lookup_name_sql_injection", + "db_functions.datetime.test_extract_trunc.DateFunctionTests.test_trunc_lookup_name_sql_injection", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_lookup_name_sql_injection", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_lookup_name_sql_injection", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_ambiguous_and_invalid_times", + "custom_pk.tests.CustomPKTests.test_auto_field_subclass_create", + "constraints.tests.UniqueConstraintTests.test_validate_expression_condition", + "constraints.tests.CheckConstraintTests.test_validate", + "constraints.tests.CheckConstraintTests.test_validate_boolean_expressions", + "schema.tests.SchemaTests.test_add_auto_field", + "schema.tests.SchemaTests.test_alter_null_with_default_value_deferred_constraints", + "schema.tests.SchemaTests.test_autofield_to_o2o", + "backends.tests.BackendTestCase.test_queries_bare_where", + "expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor_right_null", + "expressions.tests.FTimeDeltaTests.test_durationfield_multiply_divide", + "inspectdb.tests.InspectDBTestCase.test_same_relations", + "migrations.test_operations.OperationTests.test_alter_field_pk_fk_char_to_int", + "migrations.test_operations.OperationTests.test_alter_field_with_func_unique_constraint", + "migrations.test_operations.OperationTests.test_alter_model_table_m2m_field", + "migrations.test_operations.OperationTests.test_remove_unique_together_on_unique_field", + "migrations.test_operations.OperationTests.test_rename_field_index_together", + "migrations.test_operations.OperationTests.test_rename_field_unique_together", + "migrations.test_operations.OperationTests.test_rename_model_with_db_table_rename_m2m", + "migrations.test_operations.OperationTests.test_rename_model_with_m2m_models_in_different_apps_with_same_name", + "delete.tests.DeletionTests.test_pk_none", + "db_functions.datetime.test_extract_trunc.DateFunctionTests.test_trunc_time_comparison", + "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_time_comparison", + "backends.tests.LastExecutedQueryTest.test_last_executed_query_dict_overlap_keys", + "backends.tests.LastExecutedQueryTest.test_last_executed_query_with_duplicate_params", + "backends.tests.BackendTestCase.test_queries_logger", + "generic_relations.tests.GenericRelationsTests.test_unsaved_generic_foreign_key_parent_bulk_create", + "generic_relations.tests.GenericRelationsTests.test_unsaved_generic_foreign_key_parent_save", + "schema.tests.SchemaTests.test_add_field_durationfield_with_default", + "delete.tests.DeletionTests.test_only_referenced_fields_selected", + "bulk_create.tests.BulkCreateTests.test_explicit_batch_size_efficiency", + "get_or_create.tests.UpdateOrCreateTests.test_update_only_defaults_and_pre_save_fields_when_local_fields", + "backends.base.test_base.DatabaseWrapperLoggingTests.test_commit_debug_log", + "backends.base.test_base.DatabaseWrapperLoggingTests.test_rollback_debug_log", + "backends.base.test_base.MultiDatabaseTests.test_multi_database_init_connection_state_called_once", ) if os.environ.get("SPANNER_EMULATOR_HOST", None): @@ -829,21 +894,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "auth_tests.test_forms.UserChangeFormTest.test_password_excluded", # noqa "auth_tests.test_forms.UserChangeFormTest.test_unusable_password", # noqa "auth_tests.test_forms.UserChangeFormTest.test_username_validity", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_both_passwords", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_custom_form", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_custom_form_hidden_username_field", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_custom_form_with_different_username_field", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_duplicate_normalized_unicode", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_invalid_data", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_normalize_username", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_password_help_text", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_password_verification", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_password_whitespace_not_stripped", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_success", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_unicode_username", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_user_already_exists", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_user_create_form_validates_password_with_all_data", # noqa - "auth_tests.test_forms.UserCreationFormTest.test_validates_password", # noqa "auth_tests.test_handlers.ModWsgiHandlerTestCase.test_check_password", # noqa "auth_tests.test_handlers.ModWsgiHandlerTestCase.test_check_password_custom_user", # noqa "auth_tests.test_handlers.ModWsgiHandlerTestCase.test_groups_for_user", # noqa @@ -1204,7 +1254,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "db_functions.text.test_trim.TrimTests.test_trim_transform", # noqa "db_functions.text.test_upper.UpperTests.test_basic", # noqa "db_functions.text.test_upper.UpperTests.test_transform", # noqa - "defer_regress.tests.DeferAnnotateSelectRelatedTest.test_defer_annotate_select_related", # noqa "delete_regress.tests.DeleteCascadeTransactionTests.test_inheritance", # noqa "delete_regress.tests.DeleteLockingTest.test_concurrent_delete", # noqa "expressions.test_queryset_values.ValuesExpressionsTests.test_chained_values_with_expression", # noqa @@ -1581,7 +1630,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "queries.tests.Queries1Tests.test_ticket6981", # noqa "queries.tests.Queries1Tests.test_ticket7076", # noqa "queries.tests.Queries1Tests.test_ticket7096", # noqa - "queries.tests.Queries1Tests.test_ticket7098", # noqa "queries.tests.Queries1Tests.test_ticket7155", # noqa "queries.tests.Queries1Tests.test_ticket7181", # noqa "queries.tests.Queries1Tests.test_ticket7235", # noqa @@ -1794,7 +1842,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "sitemaps_tests.test_http.HTTPSitemapTests.test_requestsite_sitemap", # noqa "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_custom_sitemap", # noqa "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_sitemap", # noqa - "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_sitemap_custom_index", # noqa "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_sitemap_index", # noqa "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_sitemap_section", # noqa "sitemaps_tests.test_http.HTTPSitemapTests.test_sitemap_get_urls_no_site_1", # noqa @@ -1973,165 +2020,163 @@ class DatabaseFeatures(BaseDatabaseFeatures): "validation.tests.GenericIPAddressFieldTests.test_empty_generic_ip_passes", # noqa "validation.tests.GenericIPAddressFieldTests.test_v4_unpack_uniqueness_detection", # noqa "validation.tests.GenericIPAddressFieldTests.test_v6_uniqueness_detection", # noqa + # Check constraints are not supported by Spanner emulator. + "constraints.tests.CheckConstraintTests.test_abstract_name", # noqa + "constraints.tests.CheckConstraintTests.test_database_constraint_unicode", # noqa + # Untyped parameters are not supported: + # https://github.com/GoogleCloudPlatform/cloud-spanner-emulator#features-and-limitations + "admin_changelist.test_date_hierarchy.DateHierarchyTests.test_bounded_params_with_dst_time_zone", # noqa + "admin_changelist.tests.ChangeListTests.test_changelist_search_form_validation", # noqa + "admin_changelist.tests.ChangeListTests.test_clear_all_filters_link", # noqa + "admin_changelist.tests.ChangeListTests.test_clear_all_filters_link_callable_filter", # noqa + "admin_changelist.tests.ChangeListTests.test_no_clear_all_filters_link", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_inherited_m2m_in_list_filter", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_m2m_in_list_filter", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_m2m_to_inherited_in_list_filter", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_many_to_many_at_second_level_in_search_fields", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_non_unique_related_object_in_list_filter", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_non_unique_related_object_in_search_fields", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_through_m2m_at_second_level_in_list_filter", # noqa + "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_through_m2m_in_list_filter", # noqa + "admin_changelist.tests.ChangeListTests.test_no_exists_for_m2m_in_list_filter_without_params", # noqa + "admin_changelist.tests.ChangeListTests.test_total_ordering_optimization_meta_constraints", # noqa + "admin_docs.test_middleware.XViewMiddlewareTest.test_no_auth_middleware", # noqa + "admin_docs.test_views.AdminDocViewDefaultEngineOnly.test_template_detail_path_traversal", # noqa + "admin_inlines.tests.TestInline.test_custom_form_tabular_inline_extra_field_label", # noqa + "admin_inlines.tests.TestInline.test_inlines_singular_heading_one_to_one", # noqa + "admin_inlines.tests.TestInline.test_non_editable_custom_form_tabular_inline_extra_field_label", # noqa + "admin_views.test_multidb.MultiDatabaseTests.test_delete_view", # noqa + "auth_tests.test_auth_backends.AuthenticateTests.test_authenticate_sensitive_variables", # noqa + "auth_tests.test_auth_backends.AuthenticateTests.test_clean_credentials_sensitive_variables", # noqa + "auth_tests.test_auth_backends.AuthenticateTests.test_skips_backends_with_decorated_method", # noqa + "auth_tests.test_auth_backends.BaseBackendTest.test_get_all_permissions", # noqa + "auth_tests.test_auth_backends.BaseBackendTest.test_get_group_permissions", # noqa + "auth_tests.test_auth_backends.BaseBackendTest.test_get_user_permissions", # noqa + "auth_tests.test_auth_backends.BaseBackendTest.test_has_perm", # noqa + "auth_tests.test_auth_backends.CustomPermissionsUserModelBackendTest.test_authentication_without_credentials", # noqa + "auth_tests.test_auth_backends.ExtensionUserModelBackendTest.test_authentication_without_credentials", # noqa + "auth_tests.test_auth_backends.ModelBackendTest.test_authentication_without_credentials", # noqa + "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa + "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa + "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa + "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa + "auth_tests.test_decorators.LoginRequiredTestCase.test_callable", # noqa + "auth_tests.test_decorators.LoginRequiredTestCase.test_login_required", # noqa + "auth_tests.test_decorators.LoginRequiredTestCase.test_login_required_next_url", # noqa + "auth_tests.test_decorators.LoginRequiredTestCase.test_view", # noqa + "auth_tests.test_forms.AdminPasswordChangeFormTest.test_html_autocomplete_attributes", # noqa + "auth_tests.test_forms.AuthenticationFormTest.test_html_autocomplete_attributes", # noqa + "auth_tests.test_forms.AuthenticationFormTest.test_username_field_autocapitalize_none", # noqa + "auth_tests.test_forms.PasswordChangeFormTest.test_html_autocomplete_attributes", # noqa + "auth_tests.test_forms.PasswordResetFormTest.test_html_autocomplete_attributes", # noqa + "auth_tests.test_forms.SetPasswordFormTest.test_html_autocomplete_attributes", # noqa + "auth_tests.test_forms.UserChangeFormTest.test_username_field_autocapitalize_none", # noqa + "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_environment_variable_non_interactive", # noqa + "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_fields_with_m2m", # noqa + "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_fields_with_m2m_interactive", # noqa + "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_fields_with_m2m_interactive_blank", # noqa + "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_ignore_environment_variable_interactive", # noqa + "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_ignore_environment_variable_non_interactive", # noqa + "auth_tests.test_management.GetDefaultUsernameTestCase.test_with_database", # noqa + "auth_tests.test_management.MultiDBCreatesuperuserTestCase.test_createsuperuser_command_suggested_username_with_database_option", # noqa + "auth_tests.test_middleware.TestAuthenticationMiddleware.test_no_session", # noqa + "auth_tests.test_models.UserManagerTestCase.test_runpython_manager_methods", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_backend_without_with_perm", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_basic", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_custom_backend", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_custom_backend_pass_obj", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_invalid_backend_type", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_invalid_permission_name", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_invalid_permission_type", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_multiple_backends", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_nonexistent_backend", # noqa + "auth_tests.test_models.UserWithPermTestCase.test_nonexistent_permission", # noqa + "auth_tests.test_remote_user.AllowAllUsersRemoteUserBackendTest.test_csrf_validation_passes_after_process_request_login", # noqa + "auth_tests.test_remote_user.CustomHeaderRemoteUserTest.test_csrf_validation_passes_after_process_request_login", # noqa + "auth_tests.test_remote_user.PersistentRemoteUserTest.test_csrf_validation_passes_after_process_request_login", # noqa + "auth_tests.test_remote_user.RemoteUserCustomTest.test_csrf_validation_passes_after_process_request_login", # noqa + "auth_tests.test_remote_user.RemoteUserTest.test_csrf_validation_passes_after_process_request_login", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_change_done_view", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_reset_change_view", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_reset_complete_view", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_reset_confirm_view_custom_username_hint", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_reset_confirm_view_invalid_token", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_reset_confirm_view_valid_token", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_reset_done_view", # noqa + "auth_tests.test_templates.AuthTemplateTests.test_password_reset_view", # noqa + "auth_tests.test_tokens.TokenGeneratorTest.test_token_with_different_email", # noqa + "auth_tests.test_views.PasswordResetTest.test_confirm_custom_reset_url_token", # noqa + "auth_tests.test_views.PasswordResetTest.test_confirm_custom_reset_url_token_link_redirects_to_set_password_page", # noqa + "datetimes.tests.DateTimesTests.test_datetimes_ambiguous_and_invalid_times", # noqa + "db_functions.comparison.test_cast.CastTests.test_cast_to_duration", # noqa + "fixtures.tests.TestCaseFixtureLoadingTests.test_class_fixtures", # noqa + "generic_inline_admin.tests.GenericInlineAdminParametersTest.test_max_num_param", # noqa + "queries.tests.Queries1Tests.test_excluded_intermediary_m2m_table_joined", # noqa + "queries.tests.Queries1Tests.test_field_with_filterable", # noqa + "queries.tests.Queries1Tests.test_negate_field", # noqa + "queries.tests.Queries1Tests.test_order_by_rawsql", # noqa + "queries.tests.Queries4Tests.test_combine_or_filter_reuse", # noqa + "queries.tests.Queries4Tests.test_filter_reverse_non_integer_pk", # noqa + "schema.tests.SchemaTests.test_alter_field_default_doesnt_perform_queries", # noqa + "sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_index", # noqa + "sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_limited", # noqa + "sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_xdefault", # noqa + "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_i18n_sitemap_index", # noqa + "test_client.tests.ClientTest.test_exc_info", # noqa + "test_client.tests.ClientTest.test_exc_info_none", # noqa + "test_client.tests.ClientTest.test_follow_307_and_308_get_head_query_string", # noqa + "test_client.tests.ClientTest.test_follow_307_and_308_preserves_query_string", # noqa ) - if USING_DJANGO_3: - # Some tests are different between django 3.2 and 2.2. skip_tests += ( - # Check constraints are not supported by Spanner emulator. - "constraints.tests.CheckConstraintTests.test_abstract_name", # noqa - "constraints.tests.CheckConstraintTests.test_database_constraint_expression", # noqa "constraints.tests.CheckConstraintTests.test_database_constraint_expressionwrapper", # noqa - "constraints.tests.CheckConstraintTests.test_database_constraint_unicode", # noqa - # Untyped parameters are not supported: - # https://github.com/GoogleCloudPlatform/cloud-spanner-emulator#features-and-limitations - "admin_changelist.test_date_hierarchy.DateHierarchyTests.test_bounded_params_with_dst_time_zone", # noqa - "admin_changelist.tests.ChangeListTests.test_changelist_search_form_validation", # noqa - "admin_changelist.tests.ChangeListTests.test_clear_all_filters_link", # noqa - "admin_changelist.tests.ChangeListTests.test_clear_all_filters_link_callable_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_clear_all_filters_link", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_inherited_m2m_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_m2m_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_m2m_to_inherited_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_many_to_many_at_second_level_in_search_fields", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_non_unique_related_object_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_non_unique_related_object_in_search_fields", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_through_m2m_at_second_level_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_duplicates_for_through_m2m_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_exists_for_m2m_in_list_filter_without_params", # noqa - "admin_changelist.tests.ChangeListTests.test_total_ordering_optimization_meta_constraints", # noqa - "admin_docs.test_middleware.XViewMiddlewareTest.test_no_auth_middleware", # noqa - "admin_docs.test_views.AdminDocViewDefaultEngineOnly.test_template_detail_path_traversal", # noqa - "admin_inlines.tests.TestInline.test_custom_form_tabular_inline_extra_field_label", # noqa - "admin_inlines.tests.TestInline.test_inlines_singular_heading_one_to_one", # noqa - "admin_inlines.tests.TestInline.test_non_editable_custom_form_tabular_inline_extra_field_label", # noqa - "admin_views.test_multidb.MultiDatabaseTests.test_delete_view", # noqa - "auth_tests.test_auth_backends.AuthenticateTests.test_authenticate_sensitive_variables", # noqa - "auth_tests.test_auth_backends.AuthenticateTests.test_clean_credentials_sensitive_variables", # noqa - "auth_tests.test_auth_backends.AuthenticateTests.test_skips_backends_with_decorated_method", # noqa - "auth_tests.test_auth_backends.BaseBackendTest.test_get_all_permissions", # noqa - "auth_tests.test_auth_backends.BaseBackendTest.test_get_group_permissions", # noqa - "auth_tests.test_auth_backends.BaseBackendTest.test_get_user_permissions", # noqa - "auth_tests.test_auth_backends.BaseBackendTest.test_has_perm", # noqa - "auth_tests.test_auth_backends.CustomPermissionsUserModelBackendTest.test_authentication_without_credentials", # noqa - "auth_tests.test_auth_backends.ExtensionUserModelBackendTest.test_authentication_without_credentials", # noqa - "auth_tests.test_auth_backends.ModelBackendTest.test_authentication_without_credentials", # noqa - "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa - "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa - "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa - "auth_tests.test_basic.BasicTestCase.test_superuser_no_email_or_password", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.test_callable", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.test_login_required", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.test_login_required_next_url", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.test_view", # noqa - "auth_tests.test_forms.AdminPasswordChangeFormTest.test_html_autocomplete_attributes", # noqa - "auth_tests.test_forms.AuthenticationFormTest.test_html_autocomplete_attributes", # noqa - "auth_tests.test_forms.AuthenticationFormTest.test_username_field_autocapitalize_none", # noqa - "auth_tests.test_forms.PasswordChangeFormTest.test_html_autocomplete_attributes", # noqa - "auth_tests.test_forms.PasswordResetFormTest.test_html_autocomplete_attributes", # noqa - "auth_tests.test_forms.SetPasswordFormTest.test_html_autocomplete_attributes", # noqa - "auth_tests.test_forms.UserChangeFormTest.test_username_field_autocapitalize_none", # noqa + "defer_regress.tests.DeferAnnotateSelectRelatedTest.test_defer_annotate_select_related", # noqa + "queries.tests.Queries1Tests.test_ticket7098", # noqa + "auth_tests.test_password_reset_timeout_days.DeprecationTests.test_timeout", # noqa + "constraints.tests.CheckConstraintTests.test_database_constraint_expression", # noqa + "queries.tests.Queries1Tests.test_order_by_raw_column_alias_warning", # noqa + "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_sitemap_custom_index", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_both_passwords", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_custom_form", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_custom_form_hidden_username_field", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_custom_form_with_different_username_field", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_duplicate_normalized_unicode", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_invalid_data", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_normalize_username", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_password_help_text", # noqa + "auth_tests.test_middleware.TestAuthenticationMiddleware.test_session_default_hashing_algorithm", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_password_verification", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_password_whitespace_not_stripped", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_success", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_unicode_username", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_user_already_exists", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_user_create_form_validates_password_with_all_data", # noqa + "auth_tests.test_forms.UserCreationFormTest.test_validates_password", # noqa "auth_tests.test_forms.UserCreationFormTest.test_html_autocomplete_attributes", # noqa "auth_tests.test_forms.UserCreationFormTest.test_username_field_autocapitalize_none", # noqa - "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_environment_variable_non_interactive", # noqa - "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_fields_with_m2m", # noqa - "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_fields_with_m2m_interactive", # noqa - "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_fields_with_m2m_interactive_blank", # noqa - "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_ignore_environment_variable_interactive", # noqa - "auth_tests.test_management.CreatesuperuserManagementCommandTestCase.test_ignore_environment_variable_non_interactive", # noqa - "auth_tests.test_management.GetDefaultUsernameTestCase.test_with_database", # noqa - "auth_tests.test_management.MultiDBCreatesuperuserTestCase.test_createsuperuser_command_suggested_username_with_database_option", # noqa "auth_tests.test_middleware.TestAuthenticationMiddleware.test_no_password_change_does_not_invalidate_legacy_session", # noqa - "auth_tests.test_middleware.TestAuthenticationMiddleware.test_no_session", # noqa - "auth_tests.test_middleware.TestAuthenticationMiddleware.test_session_default_hashing_algorithm", # noqa - "auth_tests.test_models.UserManagerTestCase.test_runpython_manager_methods", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_backend_without_with_perm", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_basic", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_custom_backend", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_custom_backend_pass_obj", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_invalid_backend_type", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_invalid_permission_name", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_invalid_permission_type", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_multiple_backends", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_nonexistent_backend", # noqa - "auth_tests.test_models.UserWithPermTestCase.test_nonexistent_permission", # noqa - "auth_tests.test_password_reset_timeout_days.DeprecationTests.test_timeout", # noqa - "auth_tests.test_remote_user.AllowAllUsersRemoteUserBackendTest.test_csrf_validation_passes_after_process_request_login", # noqa - "auth_tests.test_remote_user.CustomHeaderRemoteUserTest.test_csrf_validation_passes_after_process_request_login", # noqa - "auth_tests.test_remote_user.PersistentRemoteUserTest.test_csrf_validation_passes_after_process_request_login", # noqa - "auth_tests.test_remote_user.RemoteUserCustomTest.test_csrf_validation_passes_after_process_request_login", # noqa - "auth_tests.test_remote_user.RemoteUserTest.test_csrf_validation_passes_after_process_request_login", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_change_done_view", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_reset_change_view", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_reset_complete_view", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_reset_confirm_view_custom_username_hint", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_reset_confirm_view_invalid_token", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_reset_confirm_view_valid_token", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_reset_done_view", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_password_reset_view", # noqa "auth_tests.test_tokens.TokenGeneratorTest.test_legacy_days_timeout", # noqa "auth_tests.test_tokens.TokenGeneratorTest.test_legacy_token_validation", # noqa "auth_tests.test_tokens.TokenGeneratorTest.test_token_default_hashing_algorithm", # noqa - "auth_tests.test_tokens.TokenGeneratorTest.test_token_with_different_email", # noqa - "auth_tests.test_tokens.TokenGeneratorTest.test_token_with_different_email", # noqa - "auth_tests.test_tokens.TokenGeneratorTest.test_token_with_different_email", # noqa "auth_tests.test_views.LoginTest.test_legacy_session_key_flushed_on_login", # noqa - "auth_tests.test_views.PasswordResetTest.test_confirm_custom_reset_url_token", # noqa - "auth_tests.test_views.PasswordResetTest.test_confirm_custom_reset_url_token_link_redirects_to_set_password_page", # noqa - "datetimes.tests.DateTimesTests.test_datetimes_ambiguous_and_invalid_times", # noqa - "db_functions.comparison.test_cast.CastTests.test_cast_to_duration", # noqa - "fixtures.tests.TestCaseFixtureLoadingTests.test_class_fixtures", # noqa - "generic_inline_admin.tests.GenericInlineAdminParametersTest.test_max_num_param", # noqa - "queries.tests.Queries1Tests.test_excluded_intermediary_m2m_table_joined", # noqa - "queries.tests.Queries1Tests.test_field_with_filterable", # noqa - "queries.tests.Queries1Tests.test_negate_field", # noqa - "queries.tests.Queries1Tests.test_order_by_raw_column_alias_warning", # noqa - "queries.tests.Queries1Tests.test_order_by_rawsql", # noqa - "queries.tests.Queries4Tests.test_combine_or_filter_reuse", # noqa - "queries.tests.Queries4Tests.test_filter_reverse_non_integer_pk", # noqa - "schema.tests.SchemaTests.test_alter_field_default_doesnt_perform_queries", # noqa - "sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_index", # noqa - "sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_limited", # noqa - "sitemaps_tests.test_http.HTTPSitemapTests.test_alternate_i18n_sitemap_xdefault", # noqa - "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_i18n_sitemap_index", # noqa - "test_client.tests.ClientTest.test_exc_info", # noqa - "test_client.tests.ClientTest.test_exc_info_none", # noqa - "test_client.tests.ClientTest.test_follow_307_and_308_get_head_query_string", # noqa - "test_client.tests.ClientTest.test_follow_307_and_308_preserves_query_string", # noqa ) - else: + if USING_DJANGO_4: skip_tests += ( - # Untyped parameters are not supported: - # https://github.com/GoogleCloudPlatform/cloud-spanner-emulator#features-and-limitations - "queries.tests.Queries1Tests.test_ticket9411", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_inherited_m2m_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_m2m_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_m2m_to_inherited_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_many_to_many_at_second_level_in_search_fields", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_non_unique_related_object_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_non_unique_related_object_in_search_fields", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_through_m2m_at_second_level_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_distinct_for_through_m2m_in_list_filter", # noqa - "admin_changelist.tests.ChangeListTests.test_no_distinct_for_m2m_in_list_filter_without_params", # noqa - "aggregation.tests.AggregateTestCase.test_missing_output_field_raises_error", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.testCallable", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.testLoginRequired", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.testLoginRequiredNextUrl", # noqa - "auth_tests.test_decorators.LoginRequiredTestCase.testView", # noqa - "auth_tests.test_remote_user_deprecation.RemoteUserCustomTest.test_configure_user_deprecation_warning", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_PasswordChangeDoneView", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_PasswordResetChangeView", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_PasswordResetCompleteView", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_PasswordResetConfirmView_invalid_token", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_PasswordResetConfirmView_valid_token", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_PasswordResetDoneView", # noqa - "auth_tests.test_templates.AuthTemplateTests.test_PasswordResetView", # noqa - "fixtures.tests.TestCaseFixtureLoadingTests.testClassFixtures", # noqa - "fixtures_model_package.tests.SampleTestCase.testClassFixtures", # noqa - "generic_inline_admin.tests.GenericInlineAdminParametersTest.testMaxNumParam", # noqa - "migrations.test_operations.OperationTests.test_autofield_foreignfield_growth", # noqa - "ordering.tests.OrderingTests.test_deprecated_values_annotate", # noqa - "queries.tests.Queries1Tests.test_ticket2902", # noqa - "schema.tests.SchemaTests.test_alter_field_default_doesnt_perfom_queries", # noqa - "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_i18nsitemap_index", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_both_passwords", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_custom_form", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_custom_form_hidden_username_field", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_custom_form_with_different_username_field", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_duplicate_normalized_unicode", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_invalid_data", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_normalize_username", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_password_help_text", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_password_verification", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_password_whitespace_not_stripped", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_success", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_unicode_username", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_user_already_exists", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_user_create_form_validates_password_with_all_data", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_validates_password", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_html_autocomplete_attributes", # noqa + "auth_tests.test_forms.BaseUserCreationFormTest.test_username_field_autocapitalize_none", # noqa ) diff --git a/django_spanner/introspection.py b/django_spanner/introspection.py index c752ec303b..ddfa96c43e 100644 --- a/django_spanner/introspection.py +++ b/django_spanner/introspection.py @@ -12,7 +12,6 @@ from django.db.models import Index from google.cloud.spanner_v1 import TypeCode from django_spanner import USE_EMULATOR -from django_spanner import USING_DJANGO_3 class DatabaseIntrospection(BaseDatabaseIntrospection): @@ -107,33 +106,19 @@ def get_table_description(self, cursor, table_name): internal_size = int(internal_size) else: internal_size = None - if USING_DJANGO_3: - descriptions.append( - FieldInfo( - column_name, - type_code, - None, # display_size - internal_size, - None, # precision - None, # scale - details.null_ok, - None, # default - None, # collation - ) - ) - else: - descriptions.append( - FieldInfo( - column_name, - type_code, - None, # display_size - internal_size, - None, # precision - None, # scale - details.null_ok, - None, # default - ) + descriptions.append( + FieldInfo( + column_name, + type_code, + None, # display_size + internal_size, + None, # precision + None, # scale + details.null_ok, + None, # default + None, # collation ) + ) return descriptions diff --git a/django_spanner/operations.py b/django_spanner/operations.py index 5cc78b161c..04f0775baf 100644 --- a/django_spanner/operations.py +++ b/django_spanner/operations.py @@ -14,6 +14,7 @@ from django.db.backends.base.operations import BaseDatabaseOperations from django.db.utils import DatabaseError from django.utils import timezone +from django_spanner import USING_DJANGO_3 from django.utils.duration import duration_microseconds from google.cloud.spanner_dbapi.parse_utils import ( DateStr, @@ -346,7 +347,7 @@ def convert_uuidfield_value(self, value, expression, connection): value = UUID(value) return value - def date_extract_sql(self, lookup_type, field_name): + def date_extract_sql(self, lookup_type, field_name, params=None): """Extract date from the lookup. :type lookup_type: str @@ -355,37 +356,77 @@ def date_extract_sql(self, lookup_type, field_name): :type field_name: str :param field_name: The name of the field. + :type params: list(str) + :param params: list of query params. + :rtype: str :returns: A SQL statement for extracting. """ lookup_type = self.extract_names.get(lookup_type, lookup_type) - return "EXTRACT(%s FROM %s)" % (lookup_type, field_name) - - def datetime_extract_sql(self, lookup_type, field_name, tzname): - """Extract datetime from the lookup. - - :type lookup_type: str - :param lookup_type: A type of the lookup. - - :type field_name: str - :param field_name: The name of the field. + sql = "EXTRACT(%s FROM %s)" % (lookup_type, field_name) + if USING_DJANGO_3: + return sql + return sql, params + + if USING_DJANGO_3: + + def datetime_extract_sql(self, lookup_type, field_name, tzname): + """Extract datetime from the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type tzname: str + :param tzname: The time zone name. If using of time zone is not + allowed in settings default will be UTC. + + :rtype: str + :returns: A SQL statement for extracting. + """ + tzname = tzname if settings.USE_TZ and tzname else "UTC" + lookup_type = self.extract_names.get(lookup_type, lookup_type) + return 'EXTRACT(%s FROM %s AT TIME ZONE "%s")' % ( + lookup_type, + field_name, + tzname, + ) - :type tzname: str - :param tzname: The time zone name. If using of time zone is not - allowed in settings default will be UTC. + else: - :rtype: str - :returns: A SQL statement for extracting. - """ - tzname = tzname if settings.USE_TZ and tzname else "UTC" - lookup_type = self.extract_names.get(lookup_type, lookup_type) - return 'EXTRACT(%s FROM %s AT TIME ZONE "%s")' % ( - lookup_type, - field_name, - tzname, - ) + def datetime_extract_sql( + self, lookup_type, field_name, params, tzname + ): + """Extract datetime from the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type tzname: str + :param tzname: The time zone name. If using of time zone is not + allowed in settings default will be UTC. + + :rtype: str + :returns: A SQL statement for extracting. + """ + tzname = tzname if settings.USE_TZ and tzname else "UTC" + lookup_type = self.extract_names.get(lookup_type, lookup_type) + return ( + 'EXTRACT(%s FROM %s AT TIME ZONE "%s")' + % ( + lookup_type, + field_name, + tzname, + ), + params, + ) - def time_extract_sql(self, lookup_type, field_name): + def time_extract_sql(self, lookup_type, field_name, params=None): """Extract time from the lookup. :type lookup_type: str @@ -394,137 +435,325 @@ def time_extract_sql(self, lookup_type, field_name): :type field_name: str :param field_name: The name of the field. + :type params: list(str) + :param params: list of query params. + :rtype: str :returns: A SQL statement for extracting. """ # Time is stored as TIMESTAMP with UTC time zone. - return 'EXTRACT(%s FROM %s AT TIME ZONE "UTC")' % ( + sql = 'EXTRACT(%s FROM %s AT TIME ZONE "UTC")' % ( lookup_type, field_name, ) + if USING_DJANGO_3: + return sql + return sql, params + + if USING_DJANGO_3: + + def date_trunc_sql(self, lookup_type, field_name, tzname=None): + """Truncate date in the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type tzname: str + :param tzname: The name of the timezone. This is ignored because + Spanner does not support Timezone conversion in DATE_TRUNC function. + + :rtype: str + :returns: A SQL statement for truncating. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#date_trunc + if lookup_type == "week": + # Spanner truncates to Sunday but Django expects Monday. First, + # subtract a day so that a Sunday will be truncated to the previous + # week... + field_name = ( + "DATE_SUB(CAST(" + + field_name + + " AS DATE), INTERVAL 1 DAY)" + ) + sql = "DATE_TRUNC(CAST(%s AS DATE), %s)" % ( + field_name, + lookup_type, + ) + if lookup_type == "week": + # ...then add a day to get from Sunday to Monday. + sql = "DATE_ADD(CAST(" + sql + " AS DATE), INTERVAL 1 DAY)" + return sql + + else: + + def date_trunc_sql(self, lookup_type, field_name, params, tzname=None): + """Truncate date in the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type params: list(str) + :param params: list of query params. + + :type tzname: str + :param tzname: The name of the timezone. This is ignored because + Spanner does not support Timezone conversion in DATE_TRUNC function. + + :rtype: str + :returns: A SQL statement for truncating. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#date_trunc + if lookup_type == "week": + # Spanner truncates to Sunday but Django expects Monday. First, + # subtract a day so that a Sunday will be truncated to the previous + # week... + field_name = ( + "DATE_SUB(CAST(" + + field_name + + " AS DATE), INTERVAL 1 DAY)" + ) + sql = "DATE_TRUNC(CAST(%s AS DATE), %s)" % ( + field_name, + lookup_type, + ) + if lookup_type == "week": + # ...then add a day to get from Sunday to Monday. + sql = "DATE_ADD(CAST(" + sql + " AS DATE), INTERVAL 1 DAY)" + return sql, params + + if USING_DJANGO_3: + + def datetime_trunc_sql(self, lookup_type, field_name, tzname="UTC"): + """Truncate datetime in the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type tzname: str + :param tzname: The name of the timezone. + + :rtype: str + :returns: A SQL statement for truncating. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc + tzname = tzname if settings.USE_TZ and tzname else "UTC" + if lookup_type == "week": + # Spanner truncates to Sunday but Django expects Monday. First, + # subtract a day so that a Sunday will be truncated to the previous + # week... + field_name = ( + "TIMESTAMP_SUB(" + field_name + ", INTERVAL 1 DAY)" + ) + sql = 'TIMESTAMP_TRUNC(%s, %s, "%s")' % ( + field_name, + lookup_type, + tzname, + ) + if lookup_type == "week": + # ...then add a day to get from Sunday to Monday. + sql = "TIMESTAMP_ADD(" + sql + ", INTERVAL 1 DAY)" + return sql - def date_trunc_sql(self, lookup_type, field_name, tzname=None): - """Truncate date in the lookup. + else: - :type lookup_type: str - :param lookup_type: A type of the lookup. - - :type field_name: str - :param field_name: The name of the field. + def datetime_trunc_sql( + self, lookup_type, field_name, params, tzname="UTC" + ): + """Truncate datetime in the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type params: list(str) + :param params: list of query params. + + :type tzname: str + :param tzname: The name of the timezone. + + :rtype: str + :returns: A SQL statement for truncating. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc + tzname = tzname if settings.USE_TZ and tzname else "UTC" + if lookup_type == "week": + # Spanner truncates to Sunday but Django expects Monday. First, + # subtract a day so that a Sunday will be truncated to the previous + # week... + field_name = ( + "TIMESTAMP_SUB(" + field_name + ", INTERVAL 1 DAY)" + ) + sql = 'TIMESTAMP_TRUNC(%s, %s, "%s")' % ( + field_name, + lookup_type, + tzname, + ) + if lookup_type == "week": + # ...then add a day to get from Sunday to Monday. + sql = "TIMESTAMP_ADD(" + sql + ", INTERVAL 1 DAY)" + return sql, params + + if USING_DJANGO_3: + + def time_trunc_sql(self, lookup_type, field_name, tzname="UTC"): + """Truncate time in the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type tzname: str + :param tzname: The name of the timezone. Defaults to 'UTC' For backward compatability. + + :rtype: str + :returns: A SQL statement for truncating. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc + tzname = tzname if settings.USE_TZ and tzname else "UTC" + return 'TIMESTAMP_TRUNC(%s, %s, "%s")' % ( + field_name, + lookup_type, + tzname, + ) - :type tzname: str - :param tzname: The name of the timezone. This is ignored because - Spanner does not support Timezone conversion in DATE_TRUNC function. + else: - :rtype: str - :returns: A SQL statement for truncating. - """ - # https://cloud.google.com/spanner/docs/functions-and-operators#date_trunc - if lookup_type == "week": - # Spanner truncates to Sunday but Django expects Monday. First, - # subtract a day so that a Sunday will be truncated to the previous - # week... - field_name = ( - "DATE_SUB(CAST(" + field_name + " AS DATE), INTERVAL 1 DAY)" + def time_trunc_sql( + self, lookup_type, field_name, params, tzname="UTC" + ): + """Truncate time in the lookup. + + :type lookup_type: str + :param lookup_type: A type of the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type params: list(str) + :param params: list of query params. + + :type tzname: str + :param tzname: The name of the timezone. Defaults to 'UTC' For backward compatability. + + :rtype: str + :returns: A SQL statement for truncating. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc + tzname = tzname if settings.USE_TZ and tzname else "UTC" + return ( + 'TIMESTAMP_TRUNC(%s, %s, "%s")' + % ( + field_name, + lookup_type, + tzname, + ), + params, ) - sql = "DATE_TRUNC(CAST(%s AS DATE), %s)" % (field_name, lookup_type) - if lookup_type == "week": - # ...then add a day to get from Sunday to Monday. - sql = "DATE_ADD(CAST(" + sql + " AS DATE), INTERVAL 1 DAY)" - return sql - def datetime_trunc_sql(self, lookup_type, field_name, tzname="UTC"): - """Truncate datetime in the lookup. + if USING_DJANGO_3: - :type lookup_type: str - :param lookup_type: A type of the lookup. + def datetime_cast_date_sql(self, field_name, tzname): + """Cast date in the lookup. - :type field_name: str - :param field_name: The name of the field. + :type field_name: str + :param field_name: The name of the field. - :type tzname: str - :param tzname: The name of the timezone. + :type tzname: str + :param tzname: The time zone name. If using of time zone is not + allowed in settings default will be UTC. - :rtype: str - :returns: A SQL statement for truncating. - """ - # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc - tzname = tzname if settings.USE_TZ and tzname else "UTC" - if lookup_type == "week": - # Spanner truncates to Sunday but Django expects Monday. First, - # subtract a day so that a Sunday will be truncated to the previous - # week... - field_name = "TIMESTAMP_SUB(" + field_name + ", INTERVAL 1 DAY)" - sql = 'TIMESTAMP_TRUNC(%s, %s, "%s")' % ( - field_name, - lookup_type, - tzname, - ) - if lookup_type == "week": - # ...then add a day to get from Sunday to Monday. - sql = "TIMESTAMP_ADD(" + sql + ", INTERVAL 1 DAY)" - return sql + :rtype: str + :returns: A SQL statement for casting. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#date + tzname = tzname if settings.USE_TZ and tzname else "UTC" + return 'DATE(%s, "%s")' % (field_name, tzname) - def time_trunc_sql(self, lookup_type, field_name, tzname="UTC"): - """Truncate time in the lookup. + else: - :type lookup_type: str - :param lookup_type: A type of the lookup. + def datetime_cast_date_sql(self, field_name, params, tzname): + """Cast date in the lookup. - :type field_name: str - :param field_name: The name of the field. + :type field_name: str + :param field_name: The name of the field. - :type tzname: str - :param tzname: The name of the timezone. Defaults to 'UTC' For backward compatability. + :type params: list(str) + :param params: list of query params. - :rtype: str - :returns: A SQL statement for truncating. - """ - # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc - tzname = tzname if settings.USE_TZ and tzname else "UTC" - return 'TIMESTAMP_TRUNC(%s, %s, "%s")' % ( - field_name, - lookup_type, - tzname, - ) + :type tzname: str + :param tzname: The time zone name. If using of time zone is not + allowed in settings default will be UTC. - def datetime_cast_date_sql(self, field_name, tzname): - """Cast date in the lookup. + :rtype: str + :returns: A SQL statement for casting. + """ + # https://cloud.google.com/spanner/docs/functions-and-operators#date + tzname = tzname if settings.USE_TZ and tzname else "UTC" + return 'DATE(%s, "%s")' % (field_name, tzname), params - :type field_name: str - :param field_name: The name of the field. + if USING_DJANGO_3: - :type tzname: str - :param tzname: The time zone name. If using of time zone is not - allowed in settings default will be UTC. + def datetime_cast_time_sql(self, field_name, tzname): + """Cast time in the lookup. - :rtype: str - :returns: A SQL statement for casting. - """ - # https://cloud.google.com/spanner/docs/functions-and-operators#date - tzname = tzname if settings.USE_TZ and tzname else "UTC" - return 'DATE(%s, "%s")' % (field_name, tzname) + :type field_name: str + :param field_name: The name of the field. - def datetime_cast_time_sql(self, field_name, tzname): - """Cast time in the lookup. + :type tzname: str + :param tzname: The time zone name. If using of time zone is not + allowed in settings default will be UTC. - :type field_name: str - :param field_name: The name of the field. + :rtype: str + :returns: A SQL statement for casting. + """ + tzname = tzname if settings.USE_TZ and tzname else "UTC" + # Cloud Spanner doesn't have a function for converting + # TIMESTAMP to another time zone. + return ( + "TIMESTAMP(FORMAT_TIMESTAMP(" + "'%%Y-%%m-%%d %%R:%%E9S %%Z', %s, '%s'))" + % (field_name, tzname) + ) - :type tzname: str - :param tzname: The time zone name. If using of time zone is not - allowed in settings default will be UTC. + else: - :rtype: str - :returns: A SQL statement for casting. - """ - tzname = tzname if settings.USE_TZ and tzname else "UTC" - # Cloud Spanner doesn't have a function for converting - # TIMESTAMP to another time zone. - return ( - "TIMESTAMP(FORMAT_TIMESTAMP(" - "'%%Y-%%m-%%d %%R:%%E9S %%Z', %s, '%s'))" % (field_name, tzname) - ) + def datetime_cast_time_sql(self, field_name, params, tzname): + """Cast time in the lookup. + + :type field_name: str + :param field_name: The name of the field. + + :type params: list(str) + :param params: list of query params. + + :type tzname: str + :param tzname: The time zone name. If using of time zone is not + allowed in settings default will be UTC. + + :rtype: str + :returns: A SQL statement for casting. + """ + tzname = tzname if settings.USE_TZ and tzname else "UTC" + # Cloud Spanner doesn't have a function for converting + # TIMESTAMP to another time zone. + return ( + "TIMESTAMP(FORMAT_TIMESTAMP(" + "'%%Y-%%m-%%d %%R:%%E9S %%Z', %s, '%s'))" + % (field_name, tzname) + ), params def date_interval_sql(self, timedelta): """Get a date interval in microseconds. diff --git a/django_spanner/schema.py b/django_spanner/schema.py index eb82ab689d..dd4832b180 100644 --- a/django_spanner/schema.py +++ b/django_spanner/schema.py @@ -117,14 +117,24 @@ def create_model(self, model): # Create a unique constraint separately because Spanner doesn't # allow them inline on a column. if field.unique and not field.primary_key: - self.deferred_sql.append( - self._create_unique_sql(model, [field.column]) - ) + if USING_DJANGO_3: + self.deferred_sql.append( + self._create_unique_sql(model, [field.column]) + ) + else: + self.deferred_sql.append( + self._create_unique_sql(model, [field]) + ) # Add any unique_togethers (always deferred, as some fields might be # created afterwards, like geometry fields with some backends) for fields in model._meta.unique_together: - columns = [model._meta.get_field(field).column for field in fields] + if USING_DJANGO_3: + columns = [ + model._meta.get_field(field).column for field in fields + ] + else: + columns = [model._meta.get_field(field) for field in fields] self.deferred_sql.append(self._create_unique_sql(model, columns)) constraints = [ constraint.constraint_sql(model, self) @@ -280,9 +290,14 @@ def add_field(self, model, field): # Create a unique constraint separately because Spanner doesn't allow # them inline on a column. if field.unique and not field.primary_key: - self.deferred_sql.append( - self._create_unique_sql(model, [field.column]) - ) + if USING_DJANGO_3: + self.deferred_sql.append( + self._create_unique_sql(model, [field.column]) + ) + else: + self.deferred_sql.append( + self._create_unique_sql(model, [field]) + ) # Add any FK constraints later if ( field.remote_field @@ -492,7 +507,15 @@ def _alter_field( ): self.execute(self._create_index_sql(model, fields=[new_field])) - def _alter_column_type_sql(self, model, old_field, new_field, new_type): + def _alter_column_type_sql( + self, + model, + old_field, + new_field, + new_type, + old_collation=None, + new_collation=None, + ): # Spanner needs to use sql_alter_column_not_null if the field is # NOT NULL, otherwise the constraint is dropped. sql = ( @@ -530,6 +553,7 @@ def _unique_sql( deferrable=None, # Spanner does not require this parameter include=None, opclasses=None, + expressions=None, ): # Inline constraints aren't supported, so create the index separately. if USING_DJANGO_3: @@ -543,7 +567,13 @@ def _unique_sql( ) else: sql = self._create_unique_sql( - model, fields, name=name, condition=condition + model, + fields, + name=name, + condition=condition, + include=include, + opclasses=opclasses, + expressions=expressions, ) if sql: self.deferred_sql.append(sql) diff --git a/django_test_suite.sh b/django_test_suite_4.2.sh similarity index 96% rename from django_test_suite.sh rename to django_test_suite_4.2.sh index c558f3030a..fcd47ec7c9 100755 --- a/django_test_suite.sh +++ b/django_test_suite_4.2.sh @@ -18,7 +18,7 @@ mkdir -p $DJANGO_TESTS_DIR if [ $SPANNER_EMULATOR_HOST != 0 ] then pip3 install . - git clone --depth 1 --single-branch --branch "django/stable/2.2.x" https://github.com/googleapis/python-spanner-django.git $DJANGO_TESTS_DIR/django + git clone --depth 1 --single-branch --branch "django/stable/4.2.x" https://github.com/googleapis/python-spanner-django.git $DJANGO_TESTS_DIR/django fi # Install dependencies for Django tests. @@ -56,6 +56,7 @@ DATABASES = { 'NAME': "$TEST_DBNAME_OTHER", }, } +USE_TZ = False SECRET_KEY = 'spanner_tests_secret_key' PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', diff --git a/noxfile.py b/noxfile.py index e8b2a05000..bbc909ad6d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -26,7 +26,7 @@ DEFAULT_PYTHON_VERSION = "3.8" SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"] -UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] +UNIT_TEST_PYTHON_VERSIONS = ["3.8", "3.9", "3.10"] CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() @@ -66,7 +66,7 @@ def lint_setup_py(session): ) -def default(session, django_version="2.2"): +def default(session, django_version="3.2"): # Install all test dependencies, then install this package in-place. session.install( "django~={}".format(django_version), @@ -75,7 +75,7 @@ def default(session, django_version="2.2"): "pytest", "pytest-cov", "coverage", - "sqlparse==0.3.0", + "sqlparse==0.3.1", "google-cloud-spanner>=3.13.0", "opentelemetry-api==1.1.0", "opentelemetry-sdk==1.1.0", @@ -92,7 +92,7 @@ def default(session, django_version="2.2"): "--cov-append", "--cov-config=.coveragerc", "--cov-report=", - "--cov-fail-under=80", + "--cov-fail-under=75", os.path.join("tests", "unit"), *session.posargs, ) @@ -101,13 +101,13 @@ def default(session, django_version="2.2"): @nox.session(python=UNIT_TEST_PYTHON_VERSIONS) def unit(session): """Run the unit test suite.""" - print("Unit tests with django 2.2") - default(session) print("Unit tests with django 3.2") - default(session, django_version="3.2") + default(session) + print("Unit tests with django 4.2") + default(session, django_version="4.2") -def system_test(session, django_version="2.2"): +def system_test(session, django_version="3.2"): """Run the system test suite.""" constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" @@ -157,10 +157,10 @@ def system_test(session, django_version="2.2"): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def system(session): - print("System tests with django 2.2") - system_test(session) print("System tests with django 3.2") - system_test(session, django_version="3.2") + system_test(session) + print("System tests with django 4.2") + system_test(session, django_version="4.2") @nox.session(python=DEFAULT_PYTHON_VERSION) @@ -194,7 +194,7 @@ def docs(session): "sphinx==4.5.0", "alabaster", "recommonmark", - "django==2.2", + "django==3.2", ) shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) @@ -231,7 +231,7 @@ def docfx(session): "gcp-sphinx-docfx-yaml", "alabaster", "recommonmark", - "django==2.2", + "django==3.2", ) shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) diff --git a/run_testing_worker.py b/run_testing_worker.py index fc91863ea7..721c6be54b 100644 --- a/run_testing_worker.py +++ b/run_testing_worker.py @@ -71,7 +71,7 @@ def __exit__(self, exc, exc_value, traceback): with TestInstance() as instance_name: os.system( - """DJANGO_TEST_APPS="{apps}" SPANNER_TEST_INSTANCE={instance} bash ./django_test_suite.sh""".format( + """DJANGO_TEST_APPS="{apps}" SPANNER_TEST_INSTANCE={instance} bash ./django_test_suite_4.2.sh""".format( apps=" ".join(test_apps), instance=instance_name ) ) diff --git a/setup.py b/setup.py index 8d88553793..7676b8d577 100644 --- a/setup.py +++ b/setup.py @@ -59,16 +59,14 @@ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Utilities", "Framework :: Django", - "Framework :: Django :: 2.2", "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", ], extras_require=extras, - python_requires=">=3.6", + python_requires=">=3.8", ) diff --git a/tests/unit/django_spanner/test_compiler.py b/tests/unit/django_spanner/test_compiler.py index 11fd1222a0..3b887b0b4f 100644 --- a/tests/unit/django_spanner/test_compiler.py +++ b/tests/unit/django_spanner/test_compiler.py @@ -8,6 +8,7 @@ from django.db.utils import DatabaseError from django_spanner.compiler import SQLCompiler from django.db.models.query import QuerySet +from django_spanner import USING_DJANGO_3 from tests.unit.django_spanner.simple_test import SpannerSimpleTestClass from .models import Number @@ -38,14 +39,24 @@ def test_get_combinator_sql_all_union_sql_generated(self): compiler = SQLCompiler(qs4.query, self.connection, "default") sql_compiled, params = compiler.get_combinator_sql("union", True) - self.assertEqual( - sql_compiled, - [ - "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num <= %s UNION ALL SELECT tests_number.num " - + "FROM tests_number WHERE tests_number.num >= %s" - ], - ) + if USING_DJANGO_3: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num <= %s UNION ALL SELECT tests_number.num " + + "FROM tests_number WHERE tests_number.num >= %s" + ], + ) + else: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num <= %s UNION ALL SELECT tests_number.num " + + "AS col1 FROM tests_number WHERE tests_number.num >= %s" + ], + ) self.assertEqual(params, [1, 8]) def test_get_combinator_sql_distinct_union_sql_generated(self): @@ -59,15 +70,26 @@ def test_get_combinator_sql_distinct_union_sql_generated(self): compiler = SQLCompiler(qs4.query, self.connection, "default") sql_compiled, params = compiler.get_combinator_sql("union", False) - self.assertEqual( - sql_compiled, - [ - "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num <= %s UNION DISTINCT SELECT " - + "tests_number.num FROM tests_number WHERE " - + "tests_number.num >= %s" - ], - ) + if USING_DJANGO_3: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num <= %s UNION DISTINCT SELECT " + + "tests_number.num FROM tests_number WHERE " + + "tests_number.num >= %s" + ], + ) + else: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num <= %s UNION DISTINCT SELECT " + + "tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num >= %s" + ], + ) self.assertEqual(params, [1, 8]) def test_get_combinator_sql_difference_all_sql_generated(self): @@ -81,14 +103,24 @@ def test_get_combinator_sql_difference_all_sql_generated(self): compiler = SQLCompiler(qs4.query, self.connection, "default") sql_compiled, params = compiler.get_combinator_sql("difference", True) - self.assertEqual( - sql_compiled, - [ - "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num <= %s EXCEPT ALL SELECT tests_number.num " - + "FROM tests_number WHERE tests_number.num >= %s" - ], - ) + if USING_DJANGO_3: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num <= %s EXCEPT ALL SELECT tests_number.num " + + "FROM tests_number WHERE tests_number.num >= %s" + ], + ) + else: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num <= %s EXCEPT ALL SELECT tests_number.num " + + "AS col1 FROM tests_number WHERE tests_number.num >= %s" + ], + ) self.assertEqual(params, [1, 8]) def test_get_combinator_sql_difference_distinct_sql_generated(self): @@ -102,15 +134,26 @@ def test_get_combinator_sql_difference_distinct_sql_generated(self): compiler = SQLCompiler(qs4.query, self.connection, "default") sql_compiled, params = compiler.get_combinator_sql("difference", False) - self.assertEqual( - sql_compiled, - [ - "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num <= %s EXCEPT DISTINCT SELECT " - + "tests_number.num FROM tests_number WHERE " - + "tests_number.num >= %s" - ], - ) + if USING_DJANGO_3: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num <= %s EXCEPT DISTINCT SELECT " + + "tests_number.num FROM tests_number WHERE " + + "tests_number.num >= %s" + ], + ) + else: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num <= %s EXCEPT DISTINCT SELECT " + + "tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num >= %s" + ], + ) self.assertEqual(params, [1, 8]) def test_get_combinator_sql_union_and_difference_query_together(self): @@ -124,17 +167,30 @@ def test_get_combinator_sql_union_and_difference_query_together(self): compiler = SQLCompiler(qs4.query, self.connection, "default") sql_compiled, params = compiler.get_combinator_sql("union", False) - self.assertEqual( - sql_compiled, - [ - "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num <= %s UNION DISTINCT SELECT * FROM (" - + "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num >= %s EXCEPT DISTINCT " - + "SELECT tests_number.num FROM tests_number " - + "WHERE tests_number.num = %s)" - ], - ) + if USING_DJANGO_3: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num <= %s UNION DISTINCT SELECT * FROM (" + + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num >= %s EXCEPT DISTINCT " + + "SELECT tests_number.num FROM tests_number " + + "WHERE tests_number.num = %s)" + ], + ) + else: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num <= %s UNION DISTINCT SELECT * FROM (" + + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num >= %s EXCEPT DISTINCT " + + "SELECT tests_number.num AS col1 FROM tests_number " + + "WHERE tests_number.num = %s)" + ], + ) self.assertEqual(params, [1, 8, 10]) def test_get_combinator_sql_parentheses_in_compound_not_supported(self): @@ -151,17 +207,30 @@ def test_get_combinator_sql_parentheses_in_compound_not_supported(self): compiler = SQLCompiler(qs4.query, self.connection, "default") compiler.connection.features.supports_parentheses_in_compound = False sql_compiled, params = compiler.get_combinator_sql("union", False) - self.assertEqual( - sql_compiled, - [ - "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num <= %s UNION DISTINCT SELECT * FROM (" - + "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.num >= %s EXCEPT DISTINCT " - + "SELECT tests_number.num FROM tests_number " - + "WHERE tests_number.num = %s)" - ], - ) + if USING_DJANGO_3: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num <= %s UNION DISTINCT SELECT * FROM (" + + "SELECT tests_number.num FROM tests_number WHERE " + + "tests_number.num >= %s EXCEPT DISTINCT " + + "SELECT tests_number.num FROM tests_number " + + "WHERE tests_number.num = %s)" + ], + ) + else: + self.assertEqual( + sql_compiled, + [ + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num <= %s UNION DISTINCT SELECT * FROM (" + + "SELECT tests_number.num AS col1 FROM tests_number WHERE " + + "tests_number.num >= %s EXCEPT DISTINCT " + + "SELECT tests_number.num AS col1 FROM tests_number " + + "WHERE tests_number.num = %s)" + ], + ) self.assertEqual(params, [1, 8, 10]) def test_get_combinator_sql_empty_queryset_raises_exception(self): diff --git a/tests/unit/django_spanner/test_introspection.py b/tests/unit/django_spanner/test_introspection.py index afb5147046..fd5fd64301 100644 --- a/tests/unit/django_spanner/test_introspection.py +++ b/tests/unit/django_spanner/test_introspection.py @@ -87,60 +87,33 @@ def get_table_column_schema(*args, **kwargs): table_description = db_introspection.get_table_description( cursor=cursor, table_name="Table_1" ) - if USING_DJANGO_3: - self.assertEqual( - table_description, - [ - FieldInfo( - name="name", - type_code=TypeCode.STRING, - display_size=None, - internal_size=10, - precision=None, - scale=None, - null_ok=False, - default=None, - collation=None, - ), - FieldInfo( - name="age", - type_code=TypeCode.INT64, - display_size=None, - internal_size=None, - precision=None, - scale=None, - null_ok=True, - default=None, - collation=None, - ), - ], - ) - else: - self.assertEqual( - table_description, - [ - FieldInfo( - name="name", - type_code=TypeCode.STRING, - display_size=None, - internal_size=10, - precision=None, - scale=None, - null_ok=False, - default=None, - ), - FieldInfo( - name="age", - type_code=TypeCode.INT64, - display_size=None, - internal_size=None, - precision=None, - scale=None, - null_ok=True, - default=None, - ), - ], - ) + self.assertEqual( + table_description, + [ + FieldInfo( + name="name", + type_code=TypeCode.STRING, + display_size=None, + internal_size=10, + precision=None, + scale=None, + null_ok=False, + default=None, + collation=None, + ), + FieldInfo( + name="age", + type_code=TypeCode.INT64, + display_size=None, + internal_size=None, + precision=None, + scale=None, + null_ok=True, + default=None, + collation=None, + ), + ], + ) def test_get_primary_key_column(self): """ diff --git a/tests/unit/django_spanner/test_operations.py b/tests/unit/django_spanner/test_operations.py index ed52e1b49b..a1d87520a3 100644 --- a/tests/unit/django_spanner/test_operations.py +++ b/tests/unit/django_spanner/test_operations.py @@ -11,6 +11,8 @@ from django.core.management.color import no_style from django.db.utils import DatabaseError from google.cloud.spanner_dbapi.types import DateStr + +from django_spanner import USING_DJANGO_3 from tests.unit.django_spanner.simple_test import SpannerSimpleTestClass import uuid @@ -111,67 +113,162 @@ def test_convert_uuidfield_value_none(self): ) def test_date_extract_sql(self): - self.assertEqual( - self.db_operations.date_extract_sql("week", "dummy_field"), - "EXTRACT(isoweek FROM dummy_field)", - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.date_extract_sql("week", "dummy_field"), + "EXTRACT(isoweek FROM dummy_field)", + ) + else: + self.assertEqual( + self.db_operations.date_extract_sql("week", "dummy_field"), + ("EXTRACT(isoweek FROM dummy_field)", None), + ) def test_date_extract_sql_lookup_type_dayofweek(self): - self.assertEqual( - self.db_operations.date_extract_sql("dayofweek", "dummy_field"), - "EXTRACT(dayofweek FROM dummy_field)", - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.date_extract_sql( + "dayofweek", "dummy_field" + ), + "EXTRACT(dayofweek FROM dummy_field)", + ) + else: + self.assertEqual( + self.db_operations.date_extract_sql( + "dayofweek", "dummy_field" + ), + ("EXTRACT(dayofweek FROM dummy_field)", None), + ) def test_datetime_extract_sql(self): settings.USE_TZ = True - self.assertEqual( - self.db_operations.datetime_extract_sql( - "dayofweek", "dummy_field", "IST" - ), - 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "IST")', - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.datetime_extract_sql( + "dayofweek", "dummy_field", "IST" + ), + 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "IST")', + ) + else: + self.assertEqual( + self.db_operations.datetime_extract_sql( + "dayofweek", "dummy_field", None, "IST" + ), + ( + 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "IST")', + None, + ), + ) def test_datetime_extract_sql_use_tz_false(self): settings.USE_TZ = False - self.assertEqual( - self.db_operations.datetime_extract_sql( - "dayofweek", "dummy_field", "IST" - ), - 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "UTC")', - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.datetime_extract_sql( + "dayofweek", "dummy_field", "IST" + ), + 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "UTC")', + ) + else: + self.assertEqual( + self.db_operations.datetime_extract_sql( + "dayofweek", "dummy_field", None, "IST" + ), + ( + 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "UTC")', + None, + ), + ) settings.USE_TZ = True # reset changes. def test_time_extract_sql(self): - self.assertEqual( - self.db_operations.time_extract_sql("dayofweek", "dummy_field"), - 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "UTC")', - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.time_extract_sql( + "dayofweek", "dummy_field" + ), + 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "UTC")', + ) + else: + self.assertEqual( + self.db_operations.time_extract_sql( + "dayofweek", "dummy_field" + ), + ( + 'EXTRACT(dayofweek FROM dummy_field AT TIME ZONE "UTC")', + None, + ), + ) def test_time_trunc_sql(self): - self.assertEqual( - self.db_operations.time_trunc_sql("dayofweek", "dummy_field"), - 'TIMESTAMP_TRUNC(dummy_field, dayofweek, "UTC")', - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.time_trunc_sql("dayofweek", "dummy_field"), + 'TIMESTAMP_TRUNC(dummy_field, dayofweek, "UTC")', + ) + else: + self.assertEqual( + self.db_operations.time_trunc_sql( + "dayofweek", "dummy_field", None + ), + ('TIMESTAMP_TRUNC(dummy_field, dayofweek, "UTC")', None), + ) def test_datetime_cast_date_sql(self): - self.assertEqual( - self.db_operations.datetime_cast_date_sql("dummy_field", "IST"), - 'DATE(dummy_field, "IST")', - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.datetime_cast_date_sql( + "dummy_field", "IST" + ), + 'DATE(dummy_field, "IST")', + ) + else: + self.assertEqual( + self.db_operations.datetime_cast_date_sql( + "dummy_field", None, "IST" + ), + ('DATE(dummy_field, "IST")', None), + ) def test_datetime_cast_time_sql(self): settings.USE_TZ = True - self.assertEqual( - self.db_operations.datetime_cast_time_sql("dummy_field", "IST"), - "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', dummy_field, 'IST'))", - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.datetime_cast_time_sql( + "dummy_field", "IST" + ), + "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', dummy_field, 'IST'))", + ) + else: + self.assertEqual( + self.db_operations.datetime_cast_time_sql( + "dummy_field", None, "IST" + ), + ( + "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', dummy_field, 'IST'))", + None, + ), + ) def test_datetime_cast_time_sql_use_tz_false(self): settings.USE_TZ = False - self.assertEqual( - self.db_operations.datetime_cast_time_sql("dummy_field", "IST"), - "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', dummy_field, 'UTC'))", - ) + if USING_DJANGO_3: + self.assertEqual( + self.db_operations.datetime_cast_time_sql( + "dummy_field", "IST" + ), + "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', dummy_field, 'UTC'))", + ) + else: + self.assertEqual( + self.db_operations.datetime_cast_time_sql( + "dummy_field", None, "IST" + ), + ( + "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', dummy_field, 'UTC'))", + None, + ), + ) settings.USE_TZ = True # reset changes. def test_date_interval_sql(self): From 681a1790a63b2b452cc985aa1cfdbb8d1ebd32a8 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:35:34 +0530 Subject: [PATCH 2/2] chore(main): release 4.0.0 (#882) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 11 +++++++++++ version.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6beff29ca6..955d617d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [4.0.0](https://github.com/googleapis/python-spanner-django/compare/v3.1.0...v4.0.0) (2024-04-25) + + +### ⚠ BREAKING CHANGES + +* Support Django 4.2 ([#865](https://github.com/googleapis/python-spanner-django/issues/865)) + +### Features + +* Support Django 4.2 ([#865](https://github.com/googleapis/python-spanner-django/issues/865)) ([75cd9bc](https://github.com/googleapis/python-spanner-django/commit/75cd9bc25bed283f9e22cf2b4b3a9791f5fc5059)) + ## [3.1.0](https://github.com/googleapis/python-spanner-django/compare/v3.0.2...v3.1.0) (2024-04-23) diff --git a/version.py b/version.py index d814134281..69c0395ae8 100644 --- a/version.py +++ b/version.py @@ -4,4 +4,4 @@ # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd -__version__ = "3.1.0" +__version__ = "4.0.0" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy