From bf524d229f3c1008f41450e2750b85395aa75fe6 Mon Sep 17 00:00:00 2001 From: Gagaro Date: Mon, 31 Jan 2022 15:51:38 +0100 Subject: [PATCH] Refs #30581 -- Allowed sql.Query to be used without model. --- django/db/models/expressions.py | 15 +++++--- django/db/models/sql/compiler.py | 13 +++++-- django/db/models/sql/query.py | 13 +++++-- tests/queries/test_query.py | 65 ++++++++++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 25c2803085a..edd644da54b 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -921,12 +921,15 @@ class RawSQL(Expression): self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False ): # Resolve parents fields used in raw SQL. - for parent in query.model._meta.get_parent_list(): - for parent_field in parent._meta.local_fields: - _, column_name = parent_field.get_attname_column() - if column_name.lower() in self.sql.lower(): - query.resolve_ref(parent_field.name, allow_joins, reuse, summarize) - break + if query.model: + for parent in query.model._meta.get_parent_list(): + for parent_field in parent._meta.local_fields: + _, column_name = parent_field.get_attname_column() + if column_name.lower() in self.sql.lower(): + query.resolve_ref( + parent_field.name, allow_joins, reuse, summarize + ) + break return super().resolve_expression( query, allow_joins, reuse, summarize, for_save ) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 28d412e11af..ce2787b6c0e 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -310,8 +310,8 @@ class SQLCompiler: ordering = self.query.order_by elif self.query.order_by: ordering = self.query.order_by - elif self.query.get_meta().ordering: - ordering = self.query.get_meta().ordering + elif (meta := self.query.get_meta()) and meta.ordering: + ordering = meta.ordering self._meta_ordering = ordering else: ordering = [] @@ -645,7 +645,11 @@ class SQLCompiler: params.extend(s_params) out_cols.append(s_sql) - result += [", ".join(out_cols), "FROM", *from_] + result += [", ".join(out_cols)] + if from_: + result += ["FROM", *from_] + elif self.connection.features.bare_select_suffix: + result += [self.connection.features.bare_select_suffix] params.extend(f_params) if self.query.select_for_update and features.has_select_for_update: @@ -796,7 +800,8 @@ class SQLCompiler: """ result = [] if opts is None: - opts = self.query.get_meta() + if (opts := self.query.get_meta()) is None: + return result only_load = self.deferred_to_columns() start_alias = start_alias or self.query.get_initial_alias() # The 'seen_models' is used to optimize checking the needed parent diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 31a66cec467..54f3258eacc 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -46,6 +46,8 @@ __all__ = ["Query", "RawQuery"] def get_field_names_from_opts(opts): + if opts is None: + return set() return set( chain.from_iterable( (f.name, f.attname) if f.concrete else (f.name,) for f in opts.get_fields() @@ -301,7 +303,8 @@ class Query(BaseExpression): processing. Normally, this is self.model._meta, but it can be changed by subclasses. """ - return self.model._meta + if self.model: + return self.model._meta def clone(self): """ @@ -994,8 +997,10 @@ class Query(BaseExpression): if self.alias_map: alias = self.base_table self.ref_alias(alias) - else: + elif self.model: alias = self.join(self.base_table_class(self.get_meta().db_table, None)) + else: + alias = None return alias def count_active_tables(self): @@ -1619,6 +1624,8 @@ class Query(BaseExpression): field = None filtered_relation = None try: + if opts is None: + raise FieldDoesNotExist field = opts.get_field(name) except FieldDoesNotExist: if name in self.annotation_select: @@ -1673,7 +1680,7 @@ class Query(BaseExpression): # Check if we need any joins for concrete inheritance cases (the # field lives in parent, but we are currently in one of its # children) - if model is not opts.model: + if opts is not None and model is not opts.model: path_to_parent = opts.get_path_to_parent(model) if path_to_parent: path.extend(path_to_parent) diff --git a/tests/queries/test_query.py b/tests/queries/test_query.py index e69d569ac30..9884116cd02 100644 --- a/tests/queries/test_query.py +++ b/tests/queries/test_query.py @@ -1,14 +1,23 @@ from datetime import datetime from django.core.exceptions import FieldError +from django.db import DEFAULT_DB_ALIAS, connection from django.db.models import BooleanField, CharField, F, Q -from django.db.models.expressions import Col, Func +from django.db.models.expressions import ( + Col, + Exists, + ExpressionWrapper, + Func, + RawSQL, + Value, +) from django.db.models.fields.related_lookups import RelatedIsNull from django.db.models.functions import Lower from django.db.models.lookups import Exact, GreaterThan, IsNull, LessThan -from django.db.models.sql.query import JoinPromoter, Query +from django.db.models.sql.constants import SINGLE +from django.db.models.sql.query import JoinPromoter, Query, get_field_names_from_opts from django.db.models.sql.where import OR -from django.test import SimpleTestCase +from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test.utils import register_lookup from .models import Author, Item, ObjectC, Ranking @@ -152,6 +161,56 @@ class TestQuery(SimpleTestCase): query.build_where(Func(output_field=CharField())) +class TestQueryNoModel(TestCase): + def test_rawsql_annotation(self): + query = Query(None) + sql = "%s IS NULL" + # Wrap with a CASE WHEN expression if a database backend (e.g. Oracle) + # doesn't support boolean expression in SELECT list. + if not connection.features.supports_boolean_expr_in_select_clause: + sql = f"CASE WHEN {sql} THEN 1 ELSE 0 END" + query.add_annotation(RawSQL(sql, (None,), BooleanField()), "_check") + result = query.get_compiler(using=DEFAULT_DB_ALIAS).execute_sql(SINGLE) + self.assertEqual(result[0], 1) + + def test_subquery_annotation(self): + query = Query(None) + query.add_annotation(Exists(Item.objects.all()), "_check") + result = query.get_compiler(using=DEFAULT_DB_ALIAS).execute_sql(SINGLE) + self.assertEqual(result[0], 0) + + @skipUnlessDBFeature("supports_boolean_expr_in_select_clause") + def test_q_annotation(self): + query = Query(None) + check = ExpressionWrapper( + Q(RawSQL("%s IS NULL", (None,), BooleanField())) + | Q(Exists(Item.objects.all())), + BooleanField(), + ) + query.add_annotation(check, "_check") + result = query.get_compiler(using=DEFAULT_DB_ALIAS).execute_sql(SINGLE) + self.assertEqual(result[0], 1) + + def test_names_to_path_field(self): + query = Query(None) + query.add_annotation(Value(True), "value") + path, final_field, targets, names = query.names_to_path(["value"], opts=None) + self.assertEqual(path, []) + self.assertIsInstance(final_field, BooleanField) + self.assertEqual(len(targets), 1) + self.assertIsInstance(targets[0], BooleanField) + self.assertEqual(names, []) + + def test_names_to_path_field_error(self): + query = Query(None) + msg = "Cannot resolve keyword 'nonexistent' into field." + with self.assertRaisesMessage(FieldError, msg): + query.names_to_path(["nonexistent"], opts=None) + + def test_get_field_names_from_opts(self): + self.assertEqual(get_field_names_from_opts(None), set()) + + class JoinPromoterTest(SimpleTestCase): def test_repr(self): self.assertEqual(