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):