diff options
| author | Brian Rosner <brosner@gmail.com> | 2008-06-30 15:38:16 +0000 |
|---|---|---|
| committer | Brian Rosner <brosner@gmail.com> | 2008-06-30 15:38:16 +0000 |
| commit | 829fd5a9670a581309797f56bfc7e5140550e873 (patch) | |
| tree | 6efe30b93fd5fc6564392f1dbaaf04c95a4b6bc2 /django/db/models/sql | |
| parent | c349ba4cfc2d137da393bddbda6e78fdb98c9c81 (diff) | |
newforms-admin: Merged from trunk up to [7808]. Fixed #7519, #7573
git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7809 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/db/models/sql')
| -rw-r--r-- | django/db/models/sql/query.py | 178 | ||||
| -rw-r--r-- | django/db/models/sql/subqueries.py | 12 | ||||
| -rw-r--r-- | django/db/models/sql/where.py | 26 |
3 files changed, 165 insertions, 51 deletions
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index e8d10bc55b..6c06609969 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -7,6 +7,7 @@ databases). The abstraction barrier only works one way: this module has to know all about the internals of models in order to get the information it needs. """ +import datetime from copy import deepcopy from django.utils.tree import Node @@ -14,9 +15,10 @@ from django.utils.datastructures import SortedDict from django.dispatch import dispatcher from django.db import connection from django.db.models import signals +from django.db.models.fields import FieldDoesNotExist +from django.db.models.query_utils import select_related_descend from django.db.models.sql.where import WhereNode, EverythingNode, AND, OR from django.db.models.sql.datastructures import Count -from django.db.models.fields import FieldDoesNotExist from django.core.exceptions import FieldError from datastructures import EmptyResultSet, Empty, MultiJoin from constants import * @@ -56,6 +58,7 @@ class Query(object): self.start_meta = None self.select_fields = [] self.related_select_fields = [] + self.dupe_avoidance = {} # SQL-related attributes self.select = [] @@ -164,6 +167,7 @@ class Query(object): obj.start_meta = self.start_meta obj.select_fields = self.select_fields[:] obj.related_select_fields = self.related_select_fields[:] + obj.dupe_avoidance = self.dupe_avoidance.copy() obj.select = self.select[:] obj.tables = self.tables[:] obj.where = deepcopy(self.where) @@ -214,7 +218,7 @@ class Query(object): obj.select_related = False obj.related_select_cols = [] obj.related_select_fields = [] - if obj.distinct and len(obj.select) > 1: + if len(obj.select) > 1: obj = self.clone(CountQuery, _query=obj, where=self.where_class(), distinct=False) obj.select = [] @@ -362,10 +366,21 @@ class Query(object): item.relabel_aliases(change_map) self.select.append(item) self.select_fields = rhs.select_fields[:] - self.extra_select = rhs.extra_select.copy() - self.extra_tables = rhs.extra_tables - self.extra_where = rhs.extra_where - self.extra_params = rhs.extra_params + + if connector == OR: + # It would be nice to be able to handle this, but the queries don't + # really make sense (or return consistent value sets). Not worth + # the extra complexity when you can write a real query instead. + if self.extra_select and rhs.extra_select: + raise ValueError("When merging querysets using 'or', you " + "cannot have extra(select=...) on both sides.") + if self.extra_where and rhs.extra_where: + raise ValueError("When merging querysets using 'or', you " + "cannot have extra(where=...) on both sides.") + self.extra_select.update(rhs.extra_select) + self.extra_tables += rhs.extra_tables + self.extra_where += rhs.extra_where + self.extra_params += rhs.extra_params # Ordering uses the 'rhs' ordering, unless it has none, in which case # the current ordering is used. @@ -439,28 +454,39 @@ class Query(object): self._select_aliases = aliases return result - def get_default_columns(self, with_aliases=False, col_aliases=None): + def get_default_columns(self, with_aliases=False, col_aliases=None, + start_alias=None, opts=None, as_pairs=False): """ Computes the default columns for selecting every field in the base model. Returns a list of strings, quoted appropriately for use in SQL - directly, as well as a set of aliases used in the select statement. + directly, as well as a set of aliases used in the select statement (if + 'as_pairs' is True, returns a list of (alias, col_name) pairs instead + of strings as the first component and None as the second component). """ result = [] - table_alias = self.tables[0] - root_pk = self.model._meta.pk.column + if opts is None: + opts = self.model._meta + if start_alias: + table_alias = start_alias + else: + table_alias = self.tables[0] + root_pk = opts.pk.column seen = {None: table_alias} qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name aliases = set() - for field, model in self.model._meta.get_fields_with_model(): + for field, model in opts.get_fields_with_model(): try: alias = seen[model] except KeyError: alias = self.join((table_alias, model._meta.db_table, root_pk, model._meta.pk.column)) seen[model] = alias + if as_pairs: + result.append((alias, field.column)) + continue if with_aliases and field.column in col_aliases: c_alias = 'Col%d' % len(col_aliases) result.append('%s.%s AS %s' % (qn(alias), @@ -473,6 +499,8 @@ class Query(object): aliases.add(r) if with_aliases: col_aliases.add(field.column) + if as_pairs: + return result, None return result, aliases def get_from_clause(self): @@ -609,6 +637,11 @@ class Query(object): alias, False) alias = joins[-1] col = target.column + if not field.rel: + # To avoid inadvertent trimming of a necessary alias, use the + # refcount to show that we are referencing a non-relation field on + # the model. + self.ref_alias(alias) # Must use left outer joins for nullable fields. for join in joins: @@ -829,8 +862,8 @@ class Query(object): if reuse and always_create and table in self.table_map: # Convert the 'reuse' to case to be "exclude everything but the - # reusable set for this table". - exclusions = set(self.table_map[table]).difference(reuse) + # reusable set, minus exclusions, for this table". + exclusions = set(self.table_map[table]).difference(reuse).union(set(exclusions)) always_create = False t_ident = (lhs_table, table, lhs_col, col) if not always_create: @@ -865,7 +898,8 @@ class Query(object): return alias def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1, - used=None, requested=None, restricted=None, nullable=None): + used=None, requested=None, restricted=None, nullable=None, + dupe_set=None): """ Fill in the information needed for a select_related query. The current depth is measured as the number of connections away from the root model @@ -875,6 +909,7 @@ class Query(object): if not restricted and self.max_depth and cur_depth > self.max_depth: # We've recursed far enough; bail out. return + if not opts: opts = self.get_meta() root_alias = self.get_initial_alias() @@ -882,6 +917,10 @@ class Query(object): self.related_select_fields = [] if not used: used = set() + if dupe_set is None: + dupe_set = set() + orig_dupe_set = dupe_set + orig_used = used # Setup for the case when only particular related fields should be # included in the related selection. @@ -893,9 +932,10 @@ class Query(object): restricted = False for f, model in opts.get_fields_with_model(): - if (not f.rel or (restricted and f.name not in requested) or - (not restricted and f.null) or f.rel.parent_link): + if not select_related_descend(f, restricted, requested): continue + dupe_set = orig_dupe_set.copy() + used = orig_used.copy() table = f.rel.to._meta.db_table if nullable or f.null: promote = True @@ -906,18 +946,32 @@ class Query(object): alias = root_alias for int_model in opts.get_base_chain(model): lhs_col = int_opts.parents[int_model].column + dedupe = lhs_col in opts.duplicate_targets + if dedupe: + used.update(self.dupe_avoidance.get(id(opts), lhs_col), + ()) + dupe_set.add((opts, lhs_col)) int_opts = int_model._meta alias = self.join((alias, int_opts.db_table, lhs_col, int_opts.pk.column), exclusions=used, promote=promote) + for (dupe_opts, dupe_col) in dupe_set: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) else: alias = root_alias + + dedupe = f.column in opts.duplicate_targets + if dupe_set or dedupe: + used.update(self.dupe_avoidance.get((id(opts), f.column), ())) + if dedupe: + dupe_set.add((opts, f.column)) + alias = self.join((alias, table, f.column, f.rel.get_related_field().column), exclusions=used, promote=promote) used.add(alias) - self.related_select_cols.extend([(alias, f2.column) - for f2 in f.rel.to._meta.fields]) + self.related_select_cols.extend(self.get_default_columns( + start_alias=alias, opts=f.rel.to._meta, as_pairs=True)[0]) self.related_select_fields.extend(f.rel.to._meta.fields) if restricted: next = requested.get(f.name, {}) @@ -927,8 +981,10 @@ class Query(object): new_nullable = f.null else: new_nullable = None + for dupe_opts, dupe_col in dupe_set: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1, - used, next, restricted, new_nullable) + used, next, restricted, new_nullable, dupe_set) def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, can_reuse=None): @@ -1048,7 +1104,19 @@ class Query(object): # that's harmless. self.promote_alias(table) - self.where.add((alias, col, field, lookup_type, value), connector) + # To save memory and copying time, convert the value from the Python + # object to the actual value used in the SQL query. + if field: + params = field.get_db_prep_lookup(lookup_type, value) + else: + params = Field().get_db_prep_lookup(lookup_type, value) + if isinstance(value, datetime.datetime): + annotation = datetime.datetime + else: + annotation = bool(value) + + self.where.add((alias, col, field.db_type(), lookup_type, annotation, + params), connector) if negate: for alias in join_list: @@ -1058,7 +1126,8 @@ class Query(object): for alias in join_list: if self.alias_map[alias][JOIN_TYPE] == self.LOUTER: j_col = self.alias_map[alias][RHS_JOIN_COL] - entry = Node([(alias, j_col, None, 'isnull', True)]) + entry = Node([(alias, j_col, None, 'isnull', True, + [True])]) entry.negate() self.where.add(entry, AND) break @@ -1066,7 +1135,7 @@ class Query(object): # Leaky abstraction artifact: We have to specifically # exclude the "foo__in=[]" case from this handling, because # it's short-circuited in the Where class. - entry = Node([(alias, col, field, 'isnull', True)]) + entry = Node([(alias, col, None, 'isnull', True, [True])]) entry.negate() self.where.add(entry, AND) @@ -1114,7 +1183,9 @@ class Query(object): (which gives the table we are joining to), 'alias' is the alias for the table we are joining to. If dupe_multis is True, any many-to-many or many-to-one joins will always create a new alias (necessary for - disjunctive filters). + disjunctive filters). If can_reuse is not None, it's a list of aliases + that can be reused in these joins (nothing else can be reused in this + case). Returns the final field involved in the join, the target database column (used for any 'where' constraint), the final 'opts' value and the @@ -1122,7 +1193,14 @@ class Query(object): """ joins = [alias] last = [0] + dupe_set = set() + exclusions = set() for pos, name in enumerate(names): + try: + exclusions.add(int_alias) + except NameError: + pass + exclusions.add(alias) last.append(len(joins)) if name == 'pk': name = opts.pk.name @@ -1141,6 +1219,7 @@ class Query(object): names = opts.get_all_field_names() raise FieldError("Cannot resolve keyword %r into field. " "Choices are: %s" % (name, ", ".join(names))) + if not allow_many and (m2m or not direct): for alias in joins: self.unref_alias(alias) @@ -1150,12 +1229,27 @@ class Query(object): alias_list = [] for int_model in opts.get_base_chain(model): lhs_col = opts.parents[int_model].column + dedupe = lhs_col in opts.duplicate_targets + if dedupe: + exclusions.update(self.dupe_avoidance.get( + (id(opts), lhs_col), ())) + dupe_set.add((opts, lhs_col)) opts = int_model._meta alias = self.join((alias, opts.db_table, lhs_col, - opts.pk.column), exclusions=joins) + opts.pk.column), exclusions=exclusions) joins.append(alias) + exclusions.add(alias) + for (dupe_opts, dupe_col) in dupe_set: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) cached_data = opts._join_cache.get(name) orig_opts = opts + dupe_col = direct and field.column or field.field.column + dedupe = dupe_col in opts.duplicate_targets + if dupe_set or dedupe: + if dedupe: + dupe_set.add((opts, dupe_col)) + exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col), + ())) if direct: if m2m: @@ -1177,9 +1271,11 @@ class Query(object): target) int_alias = self.join((alias, table1, from_col1, to_col1), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) alias = self.join((int_alias, table2, from_col2, to_col2), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) joins.extend([int_alias, alias]) elif field.rel: # One-to-one or many-to-one field @@ -1195,7 +1291,7 @@ class Query(object): opts, target) alias = self.join((alias, table, from_col, to_col), - exclusions=joins, nullable=field.null) + exclusions=exclusions, nullable=field.null) joins.append(alias) else: # Non-relation fields. @@ -1223,9 +1319,11 @@ class Query(object): target) int_alias = self.join((alias, table1, from_col1, to_col1), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) alias = self.join((int_alias, table2, from_col2, to_col2), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) joins.extend([int_alias, alias]) else: # One-to-many field (ForeignKey defined on the target model) @@ -1243,14 +1341,34 @@ class Query(object): opts, target) alias = self.join((alias, table, from_col, to_col), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) joins.append(alias) + for (dupe_opts, dupe_col) in dupe_set: + try: + self.update_dupe_avoidance(dupe_opts, dupe_col, int_alias) + except NameError: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) + if pos != len(names) - 1: raise FieldError("Join on field %r not permitted." % name) return field, target, opts, joins, last + def update_dupe_avoidance(self, opts, col, alias): + """ + For a column that is one of multiple pointing to the same table, update + the internal data structures to note that this alias shouldn't be used + for those other columns. + """ + ident = id(opts) + for name in opts.duplicate_targets[col]: + try: + self.dupe_avoidance[ident, name].add(alias) + except KeyError: + self.dupe_avoidance[ident, name] = set([alias]) + def split_exclude(self, filter_expr, prefix): """ When doing an exclude against any kind of N-to-many relation, we need diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 0bb741d706..d8c5b074ec 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -49,7 +49,7 @@ class DeleteQuery(Query): for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): where = self.where_class() where.add((None, related.field.m2m_reverse_name(), - related.field, 'in', + related.field.db_type(), 'in', True, pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]), AND) self.do_query(related.field.m2m_db_table(), where) @@ -59,11 +59,11 @@ class DeleteQuery(Query): if isinstance(f, generic.GenericRelation): from django.contrib.contenttypes.models import ContentType field = f.rel.to._meta.get_field(f.content_type_field_name) - w1.add((None, field.column, field, 'exact', - ContentType.objects.get_for_model(cls).id), AND) + w1.add((None, field.column, field.db_type(), 'exact', True, + [ContentType.objects.get_for_model(cls).id]), AND) for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): where = self.where_class() - where.add((None, f.m2m_column_name(), f, 'in', + where.add((None, f.m2m_column_name(), f.db_type(), 'in', True, pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND) if w1: @@ -81,7 +81,7 @@ class DeleteQuery(Query): for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): where = self.where_class() field = self.model._meta.pk - where.add((None, field.column, field, 'in', + where.add((None, field.column, field.db_type(), 'in', True, pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND) self.do_query(self.model._meta.db_table, where) @@ -204,7 +204,7 @@ class UpdateQuery(Query): for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): self.where = self.where_class() f = self.model._meta.pk - self.where.add((None, f.column, f, 'in', + self.where.add((None, f.column, f.db_type(), 'in', True, pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND) self.values = [(related_field.column, None, '%s')] diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 14e54487a3..bf45bceb4b 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -21,8 +21,9 @@ class WhereNode(tree.Node): the correct SQL). The children in this tree are usually either Q-like objects or lists of - [table_alias, field_name, field_class, lookup_type, value]. However, a - child could also be any class with as_sql() and relabel_aliases() methods. + [table_alias, field_name, db_type, lookup_type, value_annotation, + params]. However, a child could also be any class with as_sql() and + relabel_aliases() methods. """ default = AND @@ -88,29 +89,24 @@ class WhereNode(tree.Node): def make_atom(self, child, qn): """ - Turn a tuple (table_alias, field_name, field_class, lookup_type, value) - into valid SQL. + Turn a tuple (table_alias, field_name, db_type, lookup_type, + value_annot, params) into valid SQL. Returns the string for the SQL fragment and the parameters to use for it. """ - table_alias, name, field, lookup_type, value = child + table_alias, name, db_type, lookup_type, value_annot, params = child if table_alias: lhs = '%s.%s' % (qn(table_alias), qn(name)) else: lhs = qn(name) - db_type = field and field.db_type() or None field_sql = connection.ops.field_cast_sql(db_type) % lhs - if isinstance(value, datetime.datetime): + if value_annot is datetime.datetime: cast_sql = connection.ops.datetime_cast_sql() else: cast_sql = '%s' - if field: - params = field.get_db_prep_lookup(lookup_type, value) - else: - params = Field().get_db_prep_lookup(lookup_type, value) if isinstance(params, QueryWrapper): extra, params = params.data else: @@ -123,11 +119,11 @@ class WhereNode(tree.Node): connection.operators[lookup_type] % cast_sql), params) if lookup_type == 'in': - if not value: + if not value_annot: raise EmptyResultSet if extra: return ('%s IN %s' % (field_sql, extra), params) - return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(value))), + return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(params))), params) elif lookup_type in ('range', 'year'): return ('%s BETWEEN %%s and %%s' % field_sql, params) @@ -135,8 +131,8 @@ class WhereNode(tree.Node): return ('%s = %%s' % connection.ops.date_extract_sql(lookup_type, field_sql), params) elif lookup_type == 'isnull': - return ('%s IS %sNULL' % (field_sql, (not value and 'NOT ' or '')), - params) + return ('%s IS %sNULL' % (field_sql, + (not value_annot and 'NOT ' or '')), ()) elif lookup_type == 'search': return (connection.ops.fulltext_search_sql(field_sql), params) elif lookup_type in ('regex', 'iregex'): |
