diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index fde506095c..e42555ed80 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -971,7 +971,7 @@ class Subquery(Expression): clone.queryset.query = clone.queryset.query.relabeled_clone(change_map) clone.queryset.query.external_aliases.update( alias for alias in change_map.values() - if alias not in clone.queryset.query.tables + if alias not in clone.queryset.query.alias_map ) return clone diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index c2fa0bab29..b4b27a5b56 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -31,7 +31,7 @@ class SQLCompiler: self.ordering_parts = re.compile(r'(.*)\s(ASC|DESC)(.*)') def setup_query(self): - if all(self.query.alias_refcount[a] == 0 for a in self.query.tables): + if all(self.query.alias_refcount[a] == 0 for a in self.query.alias_map): self.query.get_initial_alias() self.select, self.klass_info, self.annotation_col_map = self.get_select() self.col_count = len(self.select) @@ -141,7 +141,7 @@ class SQLCompiler: # Is this a reference to query's base table primary key? If the # expression isn't a Col-like, then skip the expression. if (getattr(expr, 'target', None) == self.query.model._meta.pk and - getattr(expr, 'alias', None) == self.query.tables[0]): + getattr(expr, 'alias', None) == self.query.base_table): pk = expr break # If the main model's primary key is in the query, group by that @@ -681,7 +681,7 @@ class SQLCompiler: """ result = [] params = [] - for alias in self.query.tables: + for alias in self.query.alias_map: if not self.query.alias_refcount[alias]: continue try: @@ -1149,10 +1149,10 @@ class SQLDeleteCompiler(SQLCompiler): Create the SQL for this query. Return the SQL string and list of parameters. """ - assert len([t for t in self.query.tables if self.query.alias_refcount[t] > 0]) == 1, \ + assert len([t for t in self.query.alias_map if self.query.alias_refcount[t] > 0]) == 1, \ "Can only delete from one table at a time." qn = self.quote_name_unless_alias - result = ['DELETE FROM %s' % qn(self.query.tables[0])] + result = ['DELETE FROM %s' % qn(self.query.base_table)] where, params = self.compile(self.query.where) if where: result.append('WHERE %s' % where) @@ -1205,7 +1205,7 @@ class SQLUpdateCompiler(SQLCompiler): update_params.append(val) else: values.append('%s = NULL' % qn(name)) - table = self.query.tables[0] + table = self.query.base_table result = [ 'UPDATE %s SET' % qn(table), ', '.join(values), diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index d3ab487ba7..b4a87938f7 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -30,6 +30,7 @@ from django.db.models.sql.where import ( AND, OR, ExtraWhere, NothingNode, WhereNode, ) from django.utils.encoding import force_text +from django.utils.functional import cached_property from django.utils.tree import Node __all__ = ['Query', 'RawQuery'] @@ -144,7 +145,6 @@ class Query: # clause to contain other than default fields (values(), subqueries...) # Note that annotations go to annotations dictionary. self.select = () - self.tables = () # Aliases in the order they are created. self.where = where() self.where_class = where # The group_by attribute can have one of the following forms: @@ -217,6 +217,10 @@ class Query: def has_select_fields(self): return bool(self.select or self.annotation_select_mask or self.extra_select_mask) + @cached_property + def base_table(self): + return list(self.alias_map)[0] if self.alias_map else None + def __str__(self): """ Return the query as a string of SQL with the parameter values @@ -274,7 +278,6 @@ class Query: obj.default_ordering = self.default_ordering obj.standard_ordering = self.standard_ordering obj.select = self.select - obj.tables = self.tables obj.where = self.where.clone() obj.where_class = self.where_class obj.group_by = self.group_by @@ -431,7 +434,7 @@ class Query: inner_query.group_by = (self.model._meta.pk.get_col(inner_query.get_initial_alias()),) inner_query.default_cols = False - relabels = {t: 'subquery' for t in inner_query.tables} + relabels = {t: 'subquery' for t in inner_query.alias_map} relabels[None] = 'subquery' # Remove any aggregates marked for reduction from the subquery # and move them to the outer AggregateQuery. @@ -539,7 +542,7 @@ class Query: # Note that we will be creating duplicate joins for non-m2m joins in # the AND case. The results will be correct but this creates too many # joins. This is something that could be fixed later on. - reuse = set() if conjunction else set(self.tables) + reuse = set() if conjunction else set(self.alias_map) # Base table must be present in the query - this is the same # table on both sides. self.get_initial_alias() @@ -549,7 +552,8 @@ class Query: rhs_votes = set() # Now, add the joins from rhs query into the new query (skipping base # table). - for alias in rhs.tables[1:]: + rhs_tables = list(rhs.alias_map)[1:] + for alias in rhs_tables: join = rhs.alias_map[alias] # If the left side of the join was already relabeled, use the # updated alias. @@ -714,7 +718,6 @@ class Query: alias = table_name self.table_map[alias] = [alias] self.alias_refcount[alias] = 1 - self.tables += (alias,) return alias, True def ref_alias(self, alias): @@ -864,12 +867,9 @@ class Query: self.subq_aliases = self.subq_aliases.union([self.alias_prefix]) outer_query.subq_aliases = outer_query.subq_aliases.union(self.subq_aliases) change_map = OrderedDict() - tables = list(self.tables) - for pos, alias in enumerate(tables): + for pos, alias in enumerate(self.alias_map): new_alias = '%s%d' % (self.alias_prefix, pos) change_map[alias] = new_alias - tables[pos] = new_alias - self.tables = tuple(tables) self.change_aliases(change_map) def get_initial_alias(self): @@ -877,8 +877,8 @@ class Query: Return the first alias for this query, after increasing its reference count. """ - if self.tables: - alias = self.tables[0] + if self.alias_map: + alias = self.base_table self.ref_alias(alias) else: alias = self.join(BaseTable(self.get_meta().db_table, None)) @@ -1910,7 +1910,10 @@ class Query: # Trim and operate only on tables that were generated for # the lookup part of the query. That is, avoid trimming # joins generated for F() expressions. - lookup_tables = [t for t in self.tables if t in self._lookup_joins or t == self.tables[0]] + lookup_tables = [ + t for t in self.alias_map + if t in self._lookup_joins or t == self.base_table + ] for trimmed_paths, path in enumerate(all_paths): if path.m2m: break @@ -1950,7 +1953,7 @@ class Query: select_alias = lookup_tables[trimmed_paths] # The found starting point is likely a Join instead of a BaseTable reference. # But the first entry in the query's FROM clause must not be a JOIN. - for table in self.tables: + for table in self.alias_map: if self.alias_refcount[table] > 0: self.alias_map[table] = BaseTable(self.alias_map[table].table_name, table) break diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 53f975a1e9..0011a0dadd 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -19,7 +19,7 @@ class DeleteQuery(Query): compiler = 'SQLDeleteCompiler' def do_query(self, table, where, using): - self.tables = (table,) + self.alias_map = {table: self.alias_map[table]} self.where = where cursor = self.get_compiler(using).execute_sql(CURSOR) return cursor.rowcount if cursor else 0 @@ -52,8 +52,8 @@ class DeleteQuery(Query): innerq.get_initial_alias() # The same for our new query. self.get_initial_alias() - innerq_used_tables = tuple([t for t in innerq.tables if innerq.alias_refcount[t]]) - if not innerq_used_tables or innerq_used_tables == self.tables: + innerq_used_tables = tuple([t for t in innerq.alias_map if innerq.alias_refcount[t]]) + if not innerq_used_tables or innerq_used_tables == tuple(self.alias_map): # There is only the base table in use in the query. self.where = innerq.where else: diff --git a/tests/queries/tests.py b/tests/queries/tests.py index b2d5ef5e95..75425f86ab 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -272,7 +272,7 @@ class Queries1Tests(TestCase): list(q2) combined_query = (q1 & q2).order_by('name').query self.assertEqual(len([ - t for t in combined_query.tables if combined_query.alias_refcount[t] + t for t in combined_query.alias_map if combined_query.alias_refcount[t] ]), 1) def test_order_by_join_unref(self): @@ -499,7 +499,7 @@ class Queries1Tests(TestCase): qs, ['', '', '', ''] ) - self.assertEqual(len(qs.query.tables), 1) + self.assertEqual(len(qs.query.alias_map), 1) def test_tickets_2874_3002(self): qs = Item.objects.select_related().order_by('note__note', 'name')