diff --git a/.github/workflows/django_tests_against_emulator0.yml b/.github/workflows/django_tests_against_emulator0.yml index 2b31049e72..54d44caef6 100644 --- a/.github/workflows/django_tests_against_emulator0.yml +++ b/.github/workflows/django_tests_against_emulator0.yml @@ -29,4 +29,4 @@ jobs: 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/README.rst b/README.rst index 62739a3182..4f0d9ca886 100644 --- a/README.rst +++ b/README.rst @@ -253,118 +253,11 @@ Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. See the `Code of Conduct `_ for more information. -Current limitations -------------------- -``AutoField`` generates random IDs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Spanner doesn't have support for auto-generating primary key values. -Therefore, ``django-google-spanner`` monkey-patches ``AutoField`` to generate a -random UUID4. It generates a default using ``Field``'s ``default`` option which -means ``AutoField``\ s will have a value when a model instance is created. For -example: - -:: - - >>> ExampleModel() - >>> ExampleModel.pk - 4229421414948291880 - -To avoid -`hotspotting `__, -these IDs are not monotonically increasing. This means that sorting -models by ID isn't guaranteed to return them in the order in which they -were created. - -``ForeignKey`` constraints aren't created (`#313 `__) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Spanner does not support ``ON DELETE CASCADE`` when creating foreign-key -constraints, so this is not supported in ``django-google-spanner``. - -``Unsigned`` datatypes are not supported -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Spanner does not support ``Unsigned`` datatypes so `PositiveIntegerField -`__ -and `PositiveSmallIntegerField -`__ -are both stored as `Integer type -`__ -. - -``Meta.order_with_respect_to`` model option isn't supported -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This feature uses a column name that starts with an underscore -(``_order``) which Spanner doesn't allow. - -Random ``QuerySet`` ordering isn't supported -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Spanner does not support it and will throw an exception. For example: - -:: - - >>> ExampleModel.objects.order_by('?') - ... - django.db.utils.ProgrammingError: 400 Function not found: RANDOM ... FROM - example_model ORDER BY RANDOM() ASC - -Schema migrations -~~~~~~~~~~~~~~~~~ - -There are some limitations on schema changes to consider: - -- No support for renaming tables and columns; -- A column's type can't be changed; -- A table's primary key can't be altered. - -``DurationField`` arithmetic doesn't work with ``DateField`` values (`#253 `__) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Spanner requires using different functions for arithmetic depending on -the column type: - -- ``TIMESTAMP`` columns (``DateTimeField``) require ``TIMESTAMP_ADD`` - or ``TIMESTAMP_SUB`` -- ``DATE`` columns (``DateField``) require ``DATE_ADD`` or ``DATE_SUB`` - -Django does not provide ways to determine which database function to -use. ``DatabaseOperations.combine_duration_expression()`` arbitrarily uses -``TIMESTAMP_ADD`` and ``TIMESTAMP_SUB``. Therefore, if you use a -``DateField`` in a ``DurationField`` expression, you'll likely see an error -such as: - -:: - - "No matching signature for function TIMESTAMP\_ADD for argument types: - DATE, INTERVAL INT64 DATE\_TIME\_PART." - -Computations that yield FLOAT64 values cannot be assigned to INT64 columns -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Spanner does not support this (`#331 -`__) and will -throw an error: - -:: - - >>> ExampleModel.objects.update(integer=F('integer') / 2) - ... - django.db.utils.ProgrammingError: 400 Value of type FLOAT64 cannot be - assigned to integer, which has type INT64 [at 1:46]\nUPDATE - example_model SET integer = (example_model.integer /... - -Addition with null values crash -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Additions cannot include ``None`` values. For example: +LIMITATIONS +----------------- -:: +Spanner has certain limitations of it's own and a full set of limitations are documented over `here `_ +It is recommended that you go through that list. - >>> Book.objects.annotate(adjusted_rating=F('rating') + None) - ... - google.api_core.exceptions.InvalidArgument: 400 Operands of + cannot be literal - NULL ... +Django spanner has a set of limitations as well, please go through the `list `_. diff --git a/django_spanner/__init__.py b/django_spanner/__init__.py index a26703d5a5..0d88ffac91 100644 --- a/django_spanner/__init__.py +++ b/django_spanner/__init__.py @@ -12,7 +12,12 @@ from uuid import uuid4 import pkg_resources -from django.db.models.fields import AutoField, Field +from django.db.models.fields import ( + AutoField, + SmallAutoField, + BigAutoField, + Field, +) # Monkey-patch google.DatetimeWithNanoseconds's __eq__ compare against # datetime.datetime. @@ -45,6 +50,14 @@ def autofield_init(self, *args, **kwargs): AutoField.__init__ = autofield_init +SmallAutoField.__init__ = autofield_init +BigAutoField.__init__ = autofield_init +AutoField.db_returning = False +SmallAutoField.db_returning = False +BigAutoField.db_returning = False +AutoField.validators = [] +SmallAutoField.validators = [] +BigAutoField.validators = [] old_datetimewithnanoseconds_eq = getattr( DatetimeWithNanoseconds, "__eq__", None diff --git a/django_spanner/base.py b/django_spanner/base.py index 4a4b86ff7d..00033bf223 100644 --- a/django_spanner/base.py +++ b/django_spanner/base.py @@ -45,6 +45,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): "GenericIPAddressField": "STRING(39)", "NullBooleanField": "BOOL", "OneToOneField": "INT64", + "PositiveBigIntegerField": "INT64", "PositiveIntegerField": "INT64", "PositiveSmallIntegerField": "INT64", "SlugField": "STRING(%(max_length)s)", @@ -96,6 +97,12 @@ class DatabaseWrapper(BaseDatabaseWrapper): "iendswith": "", } + data_type_check_constraints = { + "PositiveBigIntegerField": "%(column)s >= 0", + "PositiveIntegerField": "%(column)s >= 0", + "PositiveSmallIntegerField": "%(column)s >= 0", + } + Database = spanner_dbapi SchemaEditorClass = DatabaseSchemaEditor creation_class = DatabaseCreation diff --git a/django_spanner/features.py b/django_spanner/features.py index a0ae6299c3..2417b0b95f 100644 --- a/django_spanner/features.py +++ b/django_spanner/features.py @@ -23,6 +23,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): # https://cloud.google.com/spanner/quotas#query_limits max_query_params = 900 supports_foreign_keys = False + can_create_inline_fk = False supports_ignore_conflicts = False supports_partial_indexes = False supports_regex_backreferencing = False @@ -30,21 +31,107 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_sequence_reset = False supports_timezones = False supports_transactions = False - supports_column_check_constraints = False - supports_table_check_constraints = False + supports_column_check_constraints = True + supports_table_check_constraints = True + supports_order_by_nulls_modifier = False + # Spanner does not support json + supports_json_field = False + supports_primitives_in_json_field = False + # Spanner does not support SELECTing an arbitrary expression that also + # appears in the GROUP BY clause. + supports_subqueries_in_group_by = False uses_savepoints = False + # Spanner does not support expression indexes + # example: CREATE INDEX index_name ON table (LOWER(column_name)) + supports_expression_indexes = False # Django tests that aren't supported by Spanner. skip_tests = ( + # 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 objects + "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 foreign key constraints in Spanner. + "backends.tests.FkConstraintsTests.test_check_constraints_sql_keywords", "backends.tests.FkConstraintsTests.test_check_constraints", # Spanner does not support empty list of DML statement. "backends.tests.BackendTestCase.test_cursor_executemany_with_empty_params_list", "fixtures_regress.tests.TestFixtures.test_loaddata_raises_error_when_fixture_has_invalid_foreign_key", # No Django transaction management in Spanner. + "transactions.tests.DisableDurabiltityCheckTests.test_nested_both_durable", + "transactions.tests.DisableDurabiltityCheckTests.test_nested_inner_durable", "basic.tests.SelectOnSaveTests.test_select_on_save_lying_update", # django_spanner monkey patches AutoField to have a default value. + # 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", "basic.tests.ModelTest.test_hash", + "bulk_create.tests.BulkCreateTests.test_unsaved_parent", "custom_managers.tests.CustomManagerTests.test_slow_removal_through_specified_fk_related_manager", "custom_managers.tests.CustomManagerTests.test_slow_removal_through_default_fk_related_manager", "generic_relations.test_forms.GenericInlineFormsetTests.test_options", @@ -151,12 +238,13 @@ class DatabaseFeatures(BaseDatabaseFeatures): "prefetch_related.test_prefetch_related_objects.PrefetchRelatedObjectsTests.test_m2m_then_m2m", "prefetch_related.tests.CustomPrefetchTests.test_custom_qs", "prefetch_related.tests.CustomPrefetchTests.test_nested_prefetch_related_are_not_overwritten", - "prefetch_related.tests.DirectPrefechedObjectCacheReuseTests.test_detect_is_fetched", - "prefetch_related.tests.DirectPrefechedObjectCacheReuseTests.test_detect_is_fetched_with_to_attr", + "prefetch_related.tests.DirectPrefetchedObjectCacheReuseTests.test_detect_is_fetched", + "prefetch_related.tests.DirectPrefetchedObjectCacheReuseTests.test_detect_is_fetched_with_to_attr", "prefetch_related.tests.ForeignKeyToFieldTest.test_m2m", "queries.test_bulk_update.BulkUpdateNoteTests.test_multiple_fields", "queries.test_bulk_update.BulkUpdateTests.test_inherited_fields", - "queries.tests.Queries1Tests.test_ticket9411", + "lookup.tests.LookupTests.test_exact_query_rhs_with_selected_columns", + # "queries.tests.Queries1Tests.test_ticket9411", "queries.tests.Queries4Tests.test_ticket15316_exclude_true", "queries.tests.Queries5Tests.test_ticket7256", "queries.tests.SubqueryTests.test_related_sliced_subquery", @@ -171,6 +259,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): "syndication_tests.tests.SyndicationFeedTest.test_template_feed", # 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", "datetimes.tests.DateTimesTests.test_21432", "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_func_with_timezone", "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_timezone_applied_before_truncation", # noqa @@ -235,16 +324,12 @@ class DatabaseFeatures(BaseDatabaseFeatures): "queries.test_bulk_update.BulkUpdateTests.test_large_batch", # Spanner doesn't support random ordering. "ordering.tests.OrderingTests.test_random_ordering", + "aggregation.tests.AggregateTestCase.test_aggregation_random_ordering", # casting DateField to DateTimeField adds an unexpected hour: # https://github.com/googleapis/python-spanner-django/issues/260 "db_functions.comparison.test_cast.CastTests.test_cast_from_db_date_to_datetime", # Tests that fail during tear down on databases that don't support # transactions: https://github.com/googleapis/python-spanner-django/issues/271 - "admin_views.test_multidb.MultiDatabaseTests.test_add_view", - "admin_views.test_multidb.MultiDatabaseTests.test_change_view", - "admin_views.test_multidb.MultiDatabaseTests.test_delete_view", - "auth_tests.test_admin_multidb.MultiDatabaseTests.test_add_view", - "auth_tests.test_remote_user_deprecation.RemoteUserCustomTest.test_configure_user_deprecation_warning", "contenttypes_tests.test_models.ContentTypesMultidbTests.test_multidb", # Tests that by-pass using django_spanner and generate # invalid DDL: https://github.com/googleapis/python-spanner-django/issues/298 @@ -261,6 +346,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): "transaction_hooks.tests.TestConnectionOnCommit.test_hooks_cleared_on_reconnect", "transaction_hooks.tests.TestConnectionOnCommit.test_no_hooks_run_from_failed_transaction", "transaction_hooks.tests.TestConnectionOnCommit.test_no_savepoints_atomic_merged_with_outer", + "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", # Tests that require savepoints. "get_or_create.tests.UpdateOrCreateTests.test_integrity", "get_or_create.tests.UpdateOrCreateTests.test_manual_primary_key_test", @@ -272,6 +361,11 @@ class DatabaseFeatures(BaseDatabaseFeatures): # Spanner doesn't support views. "inspectdb.tests.InspectDBTransactionalTests.test_include_views", "introspection.tests.IntrospectionTests.test_table_names_with_views", + # Fields: JSON, GenericIPAddressField are mapped to String in Spanner + "inspectdb.tests.InspectDBTestCase.test_field_types", + "inspectdb.tests.InspectDBTestCase.test_json_field", + # BigIntegerField is mapped to IntegerField in Spanner + "inspectdb.tests.InspectDBTestCase.test_number_field_types", # No sequence for AutoField in Spanner. "introspection.tests.IntrospectionTests.test_sequence_list", # DatabaseIntrospection.get_key_columns() is only required if this @@ -289,12 +383,15 @@ class DatabaseFeatures(BaseDatabaseFeatures): "migrations.test_commands.MigrateTests.test_migrate_initial_false", "migrations.test_executor.ExecutorTests.test_soft_apply", # Spanner limitation: Cannot change type of column. + "schema.tests.SchemaTests.test_char_field_pk_to_auto_field", "migrations.test_executor.ExecutorTests.test_alter_id_type_with_fk", "schema.tests.SchemaTests.test_alter_auto_field_to_char_field", "schema.tests.SchemaTests.test_alter_text_field_to_date_field", "schema.tests.SchemaTests.test_alter_text_field_to_datetime_field", "schema.tests.SchemaTests.test_alter_text_field_to_time_field", + "schema.tests.SchemaTests.test_ci_cs_db_collation", # Spanner limitation: Cannot rename tables and columns. + "migrations.test_operations.OperationTests.test_rename_field_case", "contenttypes_tests.test_operations.ContentTypeOperationsTests", "migrations.test_operations.OperationTests.test_alter_fk_non_fk", "migrations.test_operations.OperationTests.test_alter_model_table", @@ -367,6 +464,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): # os.chmod() doesn't work on Kokoro? "file_uploads.tests.DirectoryCreationTests.test_readonly_root", # Tests that sometimes fail on Kokoro for unknown reasons. + "migrations.test_operations.OperationTests.test_add_constraint_combinable", "contenttypes_tests.test_models.ContentTypesTests.test_cache_not_shared_between_managers", "migration_test_data_persistence.tests.MigrationDataNormalPersistenceTestCase.test_persistence", "servers.test_liveserverthread.LiveServerThreadTest.test_closes_connections", @@ -374,6 +472,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): "view_tests.tests.test_csrf.CsrfViewTests.test_no_cookies", "view_tests.tests.test_csrf.CsrfViewTests.test_no_referer", "view_tests.tests.test_i18n.SetLanguageTests.test_lang_from_translated_i18n_pattern", + # Tests that fail but are not related to spanner. + "test_utils.test_testcase.TestDataTests.test_undeepcopyable_warning", ) if os.environ.get("SPANNER_EMULATOR_HOST", None): @@ -381,6 +481,27 @@ class DatabaseFeatures(BaseDatabaseFeatures): skip_tests += ( # Untyped parameters are not supported: # https://github.com/GoogleCloudPlatform/cloud-spanner-emulator#features-and-limitations + "auth_tests.test_views.PasswordResetTest.test_confirm_custom_reset_url_token_link_redirects_to_set_password_page", # noqa + "admin_docs.test_views.AdminDocViewDefaultEngineOnly.test_template_detail_path_traversal", # noqa + "queries.tests.Queries1Tests.test_excluded_intermediary_m2m_table_joined", # noqa + "queries.tests.Queries4Tests.test_combine_or_filter_reuse", # noqa + "queries.tests.Queries1Tests.test_negate_field", # noqa + "queries.tests.Queries1Tests.test_field_with_filterable", # noqa + "queries.tests.Queries1Tests.test_order_by_raw_column_alias_warning", # noqa + "queries.tests.Queries1Tests.test_order_by_rawsql", # noqa + "queries.tests.Queries4Tests.test_filter_reverse_non_integer_pk", # noqa + "admin_views.test_multidb.MultiDatabaseTests.test_delete_view", # noqa + "auth_tests.test_admin_multidb.MultiDatabaseTests.test_add_view", # noqa + "admin_docs.test_middleware.XViewMiddlewareTest.test_no_auth_middleware", # noqa + "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_total_ordering_optimization_meta_constraints", # 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_changelist.test_date_hierarchy.DateHierarchyTests.test_bounded_params", # noqa "admin_changelist.test_date_hierarchy.DateHierarchyTests.test_bounded_params_with_time_zone", # noqa "admin_changelist.test_date_hierarchy.DateHierarchyTests.test_invalid_params", # noqa @@ -392,14 +513,14 @@ class DatabaseFeatures(BaseDatabaseFeatures): "admin_changelist.tests.ChangeListTests.test_custom_paginator", # noqa "admin_changelist.tests.ChangeListTests.test_deterministic_order_for_model_ordered_by_its_manager", # noqa "admin_changelist.tests.ChangeListTests.test_deterministic_order_for_unordered_model", # 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_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_dynamic_list_display", # noqa "admin_changelist.tests.ChangeListTests.test_dynamic_list_display_links", # noqa "admin_changelist.tests.ChangeListTests.test_dynamic_list_filter", # noqa @@ -409,7 +530,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): "admin_changelist.tests.ChangeListTests.test_get_list_editable_queryset_with_regex_chars_in_prefix", # noqa "admin_changelist.tests.ChangeListTests.test_get_select_related_custom_method", # noqa "admin_changelist.tests.ChangeListTests.test_multiuser_edit", # noqa - "admin_changelist.tests.ChangeListTests.test_no_distinct_for_m2m_in_list_filter_without_params", # noqa + "admin_changelist.tests.ChangeListTests.test_no_exists_for_m2m_in_list_filter_without_params", # noqa "admin_changelist.tests.ChangeListTests.test_no_list_display_links", # noqa "admin_changelist.tests.ChangeListTests.test_object_tools_displayed_no_add_permission", # noqa "admin_changelist.tests.ChangeListTests.test_pagination", # noqa @@ -572,7 +693,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "aggregation.tests.AggregateTestCase.test_filter_aggregate", # noqa "aggregation.tests.AggregateTestCase.test_fkey_aggregate", # noqa "aggregation.tests.AggregateTestCase.test_grouped_annotation_in_group_by", # noqa - "aggregation.tests.AggregateTestCase.test_missing_output_field_raises_error", # noqa "aggregation.tests.AggregateTestCase.test_more_aggregation", # noqa "aggregation.tests.AggregateTestCase.test_multi_arg_aggregate", # noqa "aggregation.tests.AggregateTestCase.test_multiple_aggregates", # noqa @@ -651,10 +771,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): "auth_tests.test_context_processors.AuthContextProcessorTests.test_session_is_accessed", # noqa "auth_tests.test_context_processors.AuthContextProcessorTests.test_session_not_accessed", # noqa "auth_tests.test_context_processors.AuthContextProcessorTests.test_user_attrs", # 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_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_decorators.PermissionsRequiredDecoratorTest.test_many_permissions_in_set_pass", # noqa "auth_tests.test_decorators.PermissionsRequiredDecoratorTest.test_many_permissions_pass", # noqa "auth_tests.test_decorators.PermissionsRequiredDecoratorTest.test_permissioned_denied_exception_raised", # noqa @@ -826,20 +946,19 @@ class DatabaseFeatures(BaseDatabaseFeatures): "auth_tests.test_remote_user.RemoteUserTest.test_last_login", # noqa "auth_tests.test_remote_user.RemoteUserTest.test_unknown_user", # noqa "auth_tests.test_remote_user.RemoteUserTest.test_user_switch_forces_new_login", # noqa - "auth_tests.test_remote_user_deprecation.RemoteUserCustomTest.test_configure_user_deprecation_warning", # noqa "auth_tests.test_signals.SignalTestCase.test_failed_login_without_request", # noqa "auth_tests.test_signals.SignalTestCase.test_login", # noqa "auth_tests.test_signals.SignalTestCase.test_login_with_custom_user_without_last_login_field", # noqa "auth_tests.test_signals.SignalTestCase.test_logout", # noqa "auth_tests.test_signals.SignalTestCase.test_logout_anonymous", # noqa "auth_tests.test_signals.SignalTestCase.test_update_last_login", # 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 + "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_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_10265", # noqa "auth_tests.test_tokens.TokenGeneratorTest.test_check_token_with_nonexistent_token_and_user", # noqa "auth_tests.test_tokens.TokenGeneratorTest.test_make_token", # noqa @@ -1172,7 +1291,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): "generic_inline_admin.tests.GenericAdminViewTest.test_basic_add_POST", # noqa "generic_inline_admin.tests.GenericAdminViewTest.test_basic_edit_GET", # noqa "generic_inline_admin.tests.GenericAdminViewTest.test_basic_edit_POST", # noqa - "generic_inline_admin.tests.GenericInlineAdminParametersTest.testMaxNumParam", # noqa + "generic_inline_admin.tests.GenericInlineAdminParametersTest.test_max_num_param", # noqa "generic_inline_admin.tests.GenericInlineAdminParametersTest.test_get_extra", # noqa "generic_inline_admin.tests.GenericInlineAdminParametersTest.test_extra_param", # noqa "generic_inline_admin.tests.GenericInlineAdminParametersTest.test_get_max_num", # noqa @@ -1288,7 +1407,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "migrations.test_operations.OperationTests.test_alter_model_table_noop", # noqa "migrations.test_operations.OperationTests.test_alter_unique_together", # noqa "migrations.test_operations.OperationTests.test_alter_unique_together_remove", # noqa - "migrations.test_operations.OperationTests.test_autofield_foreignfield_growth", # noqa "migrations.test_operations.OperationTests.test_column_name_quoting", # noqa "migrations.test_operations.OperationTests.test_create_model", # noqa "migrations.test_operations.OperationTests.test_create_model_inheritance", # noqa @@ -1395,7 +1513,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "null_queries.tests.NullQueriesTests.test_reverse_relations", # noqa "ordering.tests.OrderingTests.test_default_ordering", # noqa "ordering.tests.OrderingTests.test_default_ordering_override", # noqa - "ordering.tests.OrderingTests.test_deprecated_values_annotate", # noqa "ordering.tests.OrderingTests.test_extra_ordering", # noqa "ordering.tests.OrderingTests.test_extra_ordering_quoting", # noqa "ordering.tests.OrderingTests.test_extra_ordering_with_table_name", # noqa @@ -1471,7 +1588,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): "queries.tests.Queries1Tests.test_ticket2306", # noqa "queries.tests.Queries1Tests.test_ticket2400", # noqa "queries.tests.Queries1Tests.test_ticket2496", # noqa - "queries.tests.Queries1Tests.test_ticket2902", # noqa + # "queries.tests.Queries1Tests.test_ticket2902", # noqa "queries.tests.Queries1Tests.test_ticket3037", # noqa "queries.tests.Queries1Tests.test_ticket3141", # noqa "queries.tests.Queries1Tests.test_ticket4358", # noqa @@ -1600,7 +1717,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): "schema.tests.SchemaTests.test_alter_auto_field_to_integer_field", # noqa "schema.tests.SchemaTests.test_alter_charfield_to_null", # noqa "schema.tests.SchemaTests.test_alter_field_add_index_to_integerfield", # noqa - "schema.tests.SchemaTests.test_alter_field_default_doesnt_perfom_queries", # noqa + "schema.tests.SchemaTests.test_alter_field_default_doesnt_perform_queries", # noqa "schema.tests.SchemaTests.test_alter_field_default_dropped", # noqa "schema.tests.SchemaTests.test_alter_field_fk_keeps_index", # noqa "schema.tests.SchemaTests.test_alter_field_fk_to_o2o", # noqa @@ -1695,7 +1812,11 @@ class DatabaseFeatures(BaseDatabaseFeatures): "sitemaps_tests.test_http.HTTPSitemapTests.test_paged_sitemap", # noqa "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_i18nsitemap_index", # noqa + # "sitemaps_tests.test_http.HTTPSitemapTests.test_simple_i18nsitemap_index", # 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 "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 @@ -1744,9 +1865,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): "fixtures.tests.FixtureLoadingTests.test_loading_and_dumping", # noqa "fixtures.tests.FixtureLoadingTests.test_loading_stdin", # noqa "fixtures.tests.FixtureLoadingTests.test_output_formats", # noqa - "fixtures.tests.TestCaseFixtureLoadingTests.testClassFixtures", # noqa + "fixtures.tests.TestCaseFixtureLoadingTests.test_class_fixtures", # noqa "fixtures_model_package.tests.FixtureTestCase.test_loaddata", # noqa - "fixtures_model_package.tests.SampleTestCase.testClassFixtures", # noqa "get_or_create.tests.UpdateOrCreateTests.test_create_twice", # noqa "get_or_create.tests.UpdateOrCreateTests.test_defaults_exact", # noqa "get_or_create.tests.UpdateOrCreateTests.test_update", # noqa @@ -1758,6 +1878,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): "model_inheritance.tests.ModelInheritanceTests.test_update_parent_filtering", # noqa "model_inheritance.tests.ModelInheritanceDataTests.test_update_query_counts", # noqa "model_inheritance.tests.ModelInheritanceDataTests.test_update_inherited_model", # 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 "test_client.tests.ClientTest.test_empty_post", # noqa "test_client.tests.ClientTest.test_exception_following_nested_client_request", # noqa "test_client.tests.ClientTest.test_external_redirect", # noqa @@ -1878,4 +2002,66 @@ 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 + "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_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_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.UserWithPermTestCase.test_backend_without_with_perm", # 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_reset_confirm_view_custom_username_hint", # 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_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_models.UserManagerTestCase.test_runpython_manager_methods", # noqa + "datetimes.tests.DateTimesTests.test_datetimes_ambiguous_and_invalid_times", # noqa + "db_functions.comparison.test_cast.CastTests.test_cast_to_duration", # noqa ) diff --git a/django_spanner/introspection.py b/django_spanner/introspection.py index b95ea3e629..95db6723d5 100644 --- a/django_spanner/introspection.py +++ b/django_spanner/introspection.py @@ -96,6 +96,7 @@ def get_table_description(self, cursor, table_name): None, # scale details.null_ok, None, # default + None, # collation ) ) diff --git a/django_spanner/lookups.py b/django_spanner/lookups.py index d9a54982f2..8f837733c5 100644 --- a/django_spanner/lookups.py +++ b/django_spanner/lookups.py @@ -100,10 +100,18 @@ def iexact(self, compiler, connection): # lhs_sql is the expression/column to use as the regular expression. # Use concat to make the value case-insensitive. lhs_sql = "CONCAT('^(?i)', " + lhs_sql + ", '$')" - if not self.rhs_is_direct_value() and not params: - # If rhs is not a direct value and parameter is not present we want - # to have only 1 formatable argument in rhs_sql else we need 2. - rhs_sql = rhs_sql.replace("%%s", "%s") + if not self.rhs_is_direct_value(): + # If rhs is not a direct value + if not params: + # if params is not present, then we have only 1 formatable + # argument in rhs_sql. + rhs_sql = rhs_sql.replace("%%s", "%s") + else: + # If params is present and rhs_sql is to be replaced as well. + # Example: model_fields.test_uuid.TestQuerying.test_iexact. + rhs_sql = rhs_sql.replace("%%s", "__PLACEHOLDER_FOR_LHS_SQL__") + rhs_sql = rhs_sql.replace("%s", "%%s") + rhs_sql = rhs_sql.replace("__PLACEHOLDER_FOR_LHS_SQL__", "%s") # rhs_sql is REGEXP_CONTAINS(%s, %%s), and lhs_sql is the column name. return rhs_sql % lhs_sql, params diff --git a/django_spanner/operations.py b/django_spanner/operations.py index 48a3e3cef3..a57c794973 100644 --- a/django_spanner/operations.py +++ b/django_spanner/operations.py @@ -377,7 +377,7 @@ def datetime_extract_sql(self, lookup_type, field_name, tzname): :rtype: str :returns: A SQL statement for extracting. """ - tzname = tzname if settings.USE_TZ else "UTC" + 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, @@ -403,7 +403,7 @@ def time_extract_sql(self, lookup_type, field_name): field_name, ) - def date_trunc_sql(self, lookup_type, field_name): + def date_trunc_sql(self, lookup_type, field_name, tzname=None): """Truncate date in the lookup. :type lookup_type: str @@ -412,6 +412,10 @@ def date_trunc_sql(self, lookup_type, field_name): :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. """ @@ -429,7 +433,7 @@ def date_trunc_sql(self, lookup_type, field_name): sql = "DATE_ADD(CAST(" + sql + " AS DATE), INTERVAL 1 DAY)" return sql - def datetime_trunc_sql(self, lookup_type, field_name, tzname): + def datetime_trunc_sql(self, lookup_type, field_name, tzname="UTC"): """Truncate datetime in the lookup. :type lookup_type: str @@ -438,11 +442,14 @@ def datetime_trunc_sql(self, lookup_type, field_name, tzname): :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 else "UTC" + 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 @@ -458,7 +465,7 @@ def datetime_trunc_sql(self, lookup_type, field_name, tzname): sql = "TIMESTAMP_ADD(" + sql + ", INTERVAL 1 DAY)" return sql - def time_trunc_sql(self, lookup_type, field_name): + def time_trunc_sql(self, lookup_type, field_name, tzname="UTC"): """Truncate time in the lookup. :type lookup_type: str @@ -467,11 +474,19 @@ def time_trunc_sql(self, lookup_type, field_name): :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 - return 'TIMESTAMP_TRUNC(%s, %s, "UTC")' % (field_name, lookup_type) + tzname = tzname if settings.USE_TZ and tzname else "UTC" + return 'TIMESTAMP_TRUNC(%s, %s, "%s")' % ( + field_name, + lookup_type, + tzname, + ) def datetime_cast_date_sql(self, field_name, tzname): """Cast date in the lookup. @@ -487,7 +502,7 @@ def datetime_cast_date_sql(self, field_name, tzname): :returns: A SQL statement for casting. """ # https://cloud.google.com/spanner/docs/functions-and-operators#date - tzname = tzname if settings.USE_TZ else "UTC" + tzname = tzname if settings.USE_TZ and tzname else "UTC" return 'DATE(%s, "%s")' % (field_name, tzname) def datetime_cast_time_sql(self, field_name, tzname): @@ -503,7 +518,7 @@ def datetime_cast_time_sql(self, field_name, tzname): :rtype: str :returns: A SQL statement for casting. """ - tzname = tzname if settings.USE_TZ else "UTC" + 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 ( @@ -549,6 +564,10 @@ def combine_expression(self, connector, sub_expressions): return "MOD(%s)" % ", ".join(sub_expressions) elif connector == "^": return "POWER(%s)" % ", ".join(sub_expressions) + elif connector == "#": + # Connector '#' represents Bit Xor in django. + # Spanner represents the same fuction with '^' symbol. + return super().combine_expression("^", sub_expressions) elif connector == ">>": lhs, rhs = sub_expressions # Use an alternate computation because Cloud Sapnner's '>>' @@ -626,8 +645,6 @@ def prep_for_like_query(self, x): """ return re.escape(str(x)) - prep_for_iexact_query = prep_for_like_query - def no_limit_value(self): """The largest INT64: (2**63) - 1 diff --git a/django_spanner/schema.py b/django_spanner/schema.py index 247358857a..1004302f08 100644 --- a/django_spanner/schema.py +++ b/django_spanner/schema.py @@ -4,10 +4,10 @@ # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd +import uuid from django.db import NotSupportedError from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django_spanner._opentelemetry_tracing import trace_call -from django_spanner import USE_EMULATOR class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): @@ -60,7 +60,15 @@ def create_model(self, model): # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) if db_params["check"]: - definition += " " + self.sql_check_constraint % db_params + definition += ( + ", CONSTRAINT constraint_%s_%s_%s " + % ( + model._meta.db_table, + self.quote_name(field.name), + uuid.uuid4().hex[:6].lower(), + ) + + self.sql_check_constraint % db_params + ) # Autoincrement SQL (for backends with inline variant) col_type_suffix = field.db_type_suffix(connection=self.connection) if col_type_suffix: @@ -124,6 +132,7 @@ def create_model(self, model): trace_attributes = { "model_name": self.quote_name(model._meta.db_table) } + with trace_call( "CloudSpannerDjango.create_model", self.connection, @@ -206,7 +215,15 @@ def add_field(self, model, field): # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) if db_params["check"]: - definition += " " + self.sql_check_constraint % db_params + definition += ( + ", CONSTRAINT constraint_%s_%s_%s " + % ( + model._meta.db_table, + self.quote_name(field.name), + uuid.uuid4().hex[:6].lower(), + ) + + self.sql_check_constraint % db_params + ) # Build the SQL and run it sql = self.sql_create_column % { "table": self.quote_name(model._meta.db_table), @@ -378,6 +395,8 @@ def add_index(self, model, index): def quote_value(self, value): # A more complete implementation isn't currently required. + if isinstance(value, str): + return "'%s'" % value.replace("'", "''") return str(value) def _alter_field( @@ -450,7 +469,7 @@ def _alter_field( self.connection, trace_attributes, ): - self.execute(self._create_index_sql(model, [new_field])) + self.execute(self._create_index_sql(model, fields=[new_field])) def _alter_column_type_sql(self, model, old_field, new_field, new_type): # Spanner needs to use sql_alter_column_not_null if the field is @@ -472,19 +491,24 @@ def _alter_column_type_sql(self, model, old_field, new_field, new_type): [], ) - def _check_sql(self, name, check): - # Emulator does not support check constraints yet. - if USE_EMULATOR: - return None - return self.sql_constraint % { - "name": self.quote_name(name), - "constraint": self.sql_check_constraint % {"check": check}, - } - - def _unique_sql(self, model, fields, name, condition=None): + def _unique_sql( + self, + model, + fields, + name, + condition=None, + deferrable=None, # Spanner does not require this parameter + include=None, + opclasses=None, + ): # Inline constraints aren't supported, so create the index separately. sql = self._create_unique_sql( - model, fields, name=name, condition=condition + model, + fields, + name=name, + condition=condition, + include=include, + opclasses=opclasses, ) if sql: self.deferred_sql.append(sql) diff --git a/django_test_suite.sh b/django_test_suite.sh index 17173cc2f9..b70ec99d17 100755 --- a/django_test_suite.sh +++ b/django_test_suite.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 "spanner/stable/2.2.x" https://github.com/c24t/django.git $DJANGO_TESTS_DIR/django + git clone --depth 1 --single-branch --branch "stable/spanner/3.2.x" https://github.com/vi3k6i5/django.git $DJANGO_TESTS_DIR/django fi # Install dependencies for Django tests. @@ -60,6 +60,7 @@ SECRET_KEY = 'spanner_tests_secret_key' PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', ] +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' ! } diff --git a/docs/index.rst b/docs/index.rst index ec9b23a055..950bb65f28 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,3 +25,11 @@ For a list of all ``google-cloud-spanner-django`` releases: :maxdepth: 2 changelog + +Limitations +--------- + +.. toctree:: + :maxdepth: 1 + + limitations \ No newline at end of file diff --git a/docs/limitations.rst b/docs/limitations.rst new file mode 100644 index 0000000000..2909743922 --- /dev/null +++ b/docs/limitations.rst @@ -0,0 +1,165 @@ +Current limitations +------------------- + +``AutoField`` generates random IDs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spanner doesn't have support for auto-generating primary key values. +Therefore, ``django-google-spanner`` monkey-patches ``AutoField`` to generate a +random UUID4. It generates a default using ``Field``'s ``default`` option which +means ``AutoField``\ s will have a value when a model instance is created. For +example: + +:: + + >>> ExampleModel() + >>> ExampleModel.pk + 4229421414948291880 + +To avoid +`hotspotting `__, +these IDs are not monotonically increasing. This means that sorting +models by ID isn't guaranteed to return them in the order in which they +were created. + +``ForeignKey`` constraints aren't created (`#313 `__) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spanner does not support ``ON DELETE CASCADE`` when creating foreign-key +constraints, so this is not supported in ``django-google-spanner``. + + +No native support for ``DecimalField`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spanner's support for `Decimal `__ +types is limited to +`NUMERIC `__ +precision. Higher-precision values can be stored as strings instead. + + +``Meta.order_with_respect_to`` model option isn't supported +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This feature uses a column name that starts with an underscore +(``_order``) which Spanner doesn't allow. + +Random ``QuerySet`` ordering isn't supported +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spanner does not support it and will throw an exception. For example: + +:: + + >>> ExampleModel.objects.order_by('?') + ... + django.db.utils.ProgrammingError: 400 Function not found: RANDOM ... FROM + example_model ORDER BY RANDOM() ASC + +Schema migrations +~~~~~~~~~~~~~~~~~ + +There are some limitations on schema changes to consider: + +- No support for renaming tables and columns; +- A column's type can't be changed; +- A table's primary key can't be altered. + +``DurationField`` arithmetic doesn't work with ``DateField`` values (`#253 `__) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spanner requires using different functions for arithmetic depending on +the column type: + +- ``TIMESTAMP`` columns (``DateTimeField``) require ``TIMESTAMP_ADD`` + or ``TIMESTAMP_SUB`` +- ``DATE`` columns (``DateField``) require ``DATE_ADD`` or ``DATE_SUB`` + +Django does not provide ways to determine which database function to +use. ``DatabaseOperations.combine_duration_expression()`` arbitrarily uses +``TIMESTAMP_ADD`` and ``TIMESTAMP_SUB``. Therefore, if you use a +``DateField`` in a ``DurationField`` expression, you'll likely see an error +such as: + +:: + + "No matching signature for function TIMESTAMP\_ADD for argument types: + DATE, INTERVAL INT64 DATE\_TIME\_PART." + +Computations that yield FLOAT64 values cannot be assigned to INT64 columns +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spanner does not support this (`#331 +`__) and will +throw an error: + +:: + + >>> ExampleModel.objects.update(integer=F('integer') / 2) + ... + django.db.utils.ProgrammingError: 400 Value of type FLOAT64 cannot be + assigned to integer, which has type INT64 [at 1:46]\nUPDATE + example_model SET integer = (example_model.integer /... + +Addition with null values crash +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Additions cannot include ``None`` values. For example: + +:: + + >>> Book.objects.annotate(adjusted_rating=F('rating') + None) + ... + google.api_core.exceptions.InvalidArgument: 400 Operands of + cannot be literal + NULL ... + +stddev() and variance() function call with sample population only +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spanner supports `stddev()` and `variance()` functions (`link `__). + +Django’s Variance and StdDev database functions have 2 modes. +One with full population `STDDEV_POP` and another with sample population `STDDEV_SAMP` and `VAR_SAMP`. +Currently spanner only supports these functions with samples and not the full population `STDDEV_POP`, + + +Interleaving is not supported currently +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Interleaving is a feature that is supported by spanner database `link `_. +But currently django spanner does not support this feature, more details on this is discussed in this `github issue `_. + +Update object by passing primary key +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In django3.1 a new feature was introduced, `._state.adding`, +this allowed spanner to resolve `this bug `_. + +But introduced a new issue with spanner django. Calling `instance.save()` an object after setting it's primary key to an existing primary key value, +will cause a `IntegrityError` as follows: `django.db.utils.IntegrityError: (1062, "Duplicate entry ....` + +The workaround for this is to update `._state.adding` to `False`. +Example: +.. code:: python + + >>> # This test case passes. + >>> def test_update_primary_with_default(self): + >>> obj = PrimaryKeyWithDefault() + >>> obj.save() + >>> obj_2 = PrimaryKeyWithDefault(uuid=obj.uuid) + >>> obj_2._state.adding = False + >>> obj_2.save() + + >>> # This test case fails with `IntegrityError`. + >>> def test_update_primary_with_default(self): + >>> obj = PrimaryKeyWithDefault() + >>> obj.save() + >>> obj_2 = PrimaryKeyWithDefault(uuid=obj.uuid) + >>> obj_2.save() + +More details about this issue can be tracked `here `_. + +Support for Json field is currently not there +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This library currently does not support json. For more info on json support support follow this `github issue `_. + diff --git a/noxfile.py b/noxfile.py index 3b51d73841..dfa030e25c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -69,7 +69,7 @@ def lint_setup_py(session): def default(session): # Install all test dependencies, then install this package in-place. session.install( - "django~=2.2", + "django~=3.2", "mock", "mock-import", "pytest", @@ -136,7 +136,7 @@ def system(session): # Install all test dependencies, then install this package into the # virtualenv's dist-packages. session.install( - "django~=2.2", + "django~=3.2", "mock", "pytest", "google-cloud-testutils", @@ -172,7 +172,7 @@ def docs(session): """Build the docs for this library.""" session.install("-e", ".[tracing]") - session.install("sphinx", "alabaster", "recommonmark", "django==2.2") + session.install("sphinx", "alabaster", "recommonmark", "django==3.2") shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) # Warnings as errors is disabled for `sphinx-build` because django module @@ -200,7 +200,7 @@ def docfx(session): "alabaster", "recommonmark", "gcp-sphinx-docfx-yaml", - "django==2.2", + "django==3.2", ) shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) diff --git a/setup.py b/setup.py index a3143566bb..25e19457b3 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ "Programming Language :: Python :: 3.9", "Topic :: Utilities", "Framework :: Django", - "Framework :: Django :: 2.2", + "Framework :: Django :: 3.2", ], extras_require=extras, python_requires=">=3.6", diff --git a/tests/unit/django_spanner/test_expressions.py b/tests/unit/django_spanner/test_expressions.py index 0efc99ce08..aeeebb5075 100644 --- a/tests/unit/django_spanner/test_expressions.py +++ b/tests/unit/django_spanner/test_expressions.py @@ -20,7 +20,8 @@ def test_order_by_sql_query_with_order_by_null_last(self): self.assertEqual( sql_compiled, "SELECT tests_report.name FROM tests_report ORDER BY " - + "tests_report.name IS NULL, tests_report.name DESC", + + "tests_report.name IS NULL, tests_report.name IS NULL, " + + "tests_report.name DESC", ) def test_order_by_sql_query_with_order_by_null_first(self): @@ -32,7 +33,8 @@ def test_order_by_sql_query_with_order_by_null_first(self): self.assertEqual( sql_compiled, "SELECT tests_report.name FROM tests_report ORDER BY " - + "tests_report.name IS NOT NULL, tests_report.name DESC", + + "tests_report.name IS NOT NULL, tests_report.name " + + "IS NOT NULL, tests_report.name DESC", ) def test_order_by_sql_query_with_order_by_name(self): diff --git a/tests/unit/django_spanner/test_functions.py b/tests/unit/django_spanner/test_functions.py index 00b431b73b..b24a2290e9 100644 --- a/tests/unit/django_spanner/test_functions.py +++ b/tests/unit/django_spanner/test_functions.py @@ -179,7 +179,7 @@ def test_pi(self): self.assertEqual( sql_query, "SELECT tests_author.num FROM tests_author WHERE tests_author.num " - + "= (3.141592653589793)", + + "= 3.141592653589793", ) self.assertEqual(params, ()) diff --git a/tests/unit/django_spanner/test_introspection.py b/tests/unit/django_spanner/test_introspection.py index c90288f3b3..03b5b67ca9 100644 --- a/tests/unit/django_spanner/test_introspection.py +++ b/tests/unit/django_spanner/test_introspection.py @@ -98,6 +98,7 @@ def get_table_column_schema(*args, **kwargs): scale=None, null_ok=False, default=None, + collation=None, ), FieldInfo( name="age", @@ -108,6 +109,7 @@ def get_table_column_schema(*args, **kwargs): scale=None, null_ok=True, default=None, + collation=None, ), ], ) diff --git a/tests/unit/django_spanner/test_lookups.py b/tests/unit/django_spanner/test_lookups.py index 53604691cc..3eb4812a6f 100644 --- a/tests/unit/django_spanner/test_lookups.py +++ b/tests/unit/django_spanner/test_lookups.py @@ -59,7 +59,7 @@ def test_cast_param_to_float_with_no_params_query(self): self.assertEqual( sql_compiled, "SELECT tests_number.num FROM tests_number WHERE " - + "tests_number.item_id = (tests_number.num)", + + "tests_number.item_id = tests_number.num", ) self.assertEqual(params, ()) @@ -111,7 +111,7 @@ def test_startswith_endswith_sql_query_with_bileteral_transform(self): sql_compiled, "SELECT tests_author.name FROM tests_author WHERE " + "REGEXP_CONTAINS(CAST(UPPER(tests_author.name) AS STRING), " - + "REPLACE(REPLACE(REPLACE(CONCAT('^', (UPPER(%s))), " + + "REPLACE(REPLACE(REPLACE(CONCAT('^', UPPER(%s)), " + '"\\\\", "\\\\\\\\"), "%%", r"\\%%"), "_", r"\\_"))', ) self.assertEqual(params, ("abc",)) @@ -128,7 +128,7 @@ def test_startswith_endswith_case_insensitive_transform_sql_query(self): sql_compiled, "SELECT tests_author.name FROM tests_author WHERE " + "REGEXP_CONTAINS(CAST(UPPER(tests_author.name) AS STRING), " - + "REPLACE(REPLACE(REPLACE(CONCAT('^(?i)', (UPPER(%s))), " + + "REPLACE(REPLACE(REPLACE(CONCAT('^(?i)', UPPER(%s)), " + '"\\\\", "\\\\\\\\"), "%%", r"\\%%"), "_", r"\\_"))', ) self.assertEqual(params, ("abc",)) @@ -144,7 +144,7 @@ def test_startswith_endswith_endswith_sql_query_with_transform(self): sql_compiled, "SELECT tests_author.name FROM tests_author WHERE " + "REGEXP_CONTAINS(CAST(UPPER(tests_author.name) AS STRING), " - + "REPLACE(REPLACE(REPLACE(CONCAT('', (UPPER(%s)), '$'), " + + "REPLACE(REPLACE(REPLACE(CONCAT('', UPPER(%s), '$'), " + '"\\\\", "\\\\\\\\"), "%%", r"\\%%"), "_", r"\\_"))', ) self.assertEqual(params, ("abc",)) @@ -183,7 +183,7 @@ def test_regex_sql_query_case_sensitive_with_transform(self): sql_compiled, "SELECT tests_author.num FROM tests_author WHERE " + "REGEXP_CONTAINS(CAST(UPPER(tests_author.name) AS STRING), " - + "(UPPER(%s)))", + + "UPPER(%s))", ) self.assertEqual(params, ("abc",)) @@ -197,7 +197,7 @@ def test_regex_sql_query_case_insensitive_with_transform(self): sql_compiled, "SELECT tests_author.num FROM tests_author WHERE " + "REGEXP_CONTAINS(CAST(UPPER(tests_author.name) AS STRING), " - + "CONCAT('(?i)', (UPPER(%s))))", + + "CONCAT('(?i)', UPPER(%s)))", ) self.assertEqual(params, ("abc",)) @@ -236,7 +236,7 @@ def test_contains_sql_query_case_insensitive_transform(self): sql_compiled, "SELECT tests_author.name FROM tests_author WHERE " + "REGEXP_CONTAINS(CAST(UPPER(tests_author.name) AS STRING), " - + "REPLACE(REPLACE(REPLACE(CONCAT('(?i)', (UPPER(%s))), " + + "REPLACE(REPLACE(REPLACE(CONCAT('(?i)', UPPER(%s)), " + '"\\\\", "\\\\\\\\"), "%%", r"\\%%"), "_", r"\\_"))', ) self.assertEqual(params, ("abc",)) @@ -250,7 +250,7 @@ def test_contains_sql_query_case_sensitive_transform(self): sql_compiled, "SELECT tests_author.name FROM tests_author WHERE " + "REGEXP_CONTAINS(CAST(UPPER(tests_author.name) AS STRING), " - + 'REPLACE(REPLACE(REPLACE((UPPER(%s)), "\\\\", "\\\\\\\\"), ' + + 'REPLACE(REPLACE(REPLACE(UPPER(%s), "\\\\", "\\\\\\\\"), ' + '"%%", r"\\%%"), "_", r"\\_"))', ) self.assertEqual(params, ("abc",)) @@ -279,7 +279,7 @@ def test_iexact_sql_query_case_insensitive_function_transform(self): self.assertEqual( sql_compiled, "SELECT tests_author.name FROM tests_author WHERE " - + "REGEXP_CONTAINS((UPPER(tests_author.last_name)), " + + "REGEXP_CONTAINS(UPPER(tests_author.last_name), " + "CONCAT('^(?i)', CAST(UPPER(tests_author.name) AS STRING), '$'))", ) self.assertEqual(params, ()) @@ -293,7 +293,7 @@ def test_iexact_sql_query_case_insensitive_value_match(self): self.assertEqual( sql_compiled, "SELECT tests_author.name FROM tests_author WHERE " - + "REGEXP_CONTAINS((UPPER(CONCAT('^(?i)', " - + "CAST(UPPER(tests_author.name) AS STRING), '$'))), %s)", + + "REGEXP_CONTAINS(UPPER(CONCAT('^(?i)', " + + "CAST(UPPER(tests_author.name) AS STRING), '$')), %s)", ) self.assertEqual(params, ("abc",)) diff --git a/version.py b/version.py index 32ec82411a..715a618af0 100644 --- a/version.py +++ b/version.py @@ -4,4 +4,4 @@ # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd -__version__ = "2.2.1b4" +__version__ = "3.2.1"