1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
|
from itertools import product
from funcy import group_by, join_with, lcat, lmap, cat
from django.db.models import Subquery
from django.db.models.query import QuerySet
from django.db.models.sql import OR
from django.db.models.sql.datastructures import Join
from django.db.models.sql.query import Query, ExtraWhere
from django.db.models.sql.where import NothingNode
from django.db.models.lookups import Lookup, Exact, In, IsNull
from django.db.models.expressions import BaseExpression, Exists
from .conf import settings
from .invalidation import serializable_fields
# This existed prior to Django 5.2
try:
from django.db.models.sql.where import SubqueryConstraint
except ImportError:
class SubqueryConstraint(object):
pass
def dnfs(qs):
"""
Converts query condition tree into a DNF of eq conds.
Separately for each alias.
Any negations, conditions with lookups other than __exact or __in,
conditions on joined models and subrequests are ignored.
__in is converted into = or = or = ...
"""
SOME = Some()
SOME_TREE = {frozenset({(None, None, SOME, True)})}
def negate(term):
return (term[0], term[1], term[2], not term[3])
def _dnf(where):
"""
Constructs DNF of where tree consisting of terms in form:
(alias, attribute, value, negation)
meaning `alias.attribute = value`
or `not alias.attribute = value` if negation is False
Any conditions other then eq are dropped.
"""
if isinstance(where, Lookup):
# If where.lhs don't refer to a field then don't bother
if not hasattr(where.lhs, 'target'):
return SOME_TREE
# Don't bother with complex right hand side either
if isinstance(where.rhs, (QuerySet, Query, BaseExpression)):
return SOME_TREE
# Skip conditions on non-serialized fields
if where.lhs.target not in serializable_fields(where.lhs.target.model):
return SOME_TREE
attname = where.lhs.target.attname
if isinstance(where, Exact):
return {frozenset({(where.lhs.alias, attname, where.rhs, True)})}
elif isinstance(where, IsNull):
return {frozenset({(where.lhs.alias, attname, None, where.rhs)})}
elif isinstance(where, In) and len(where.rhs) < settings.CACHEOPS_LONG_DISJUNCTION:
return {frozenset({(where.lhs.alias, attname, v, True)}) for v in where.rhs}
else:
return SOME_TREE
elif isinstance(where, NothingNode):
return set()
elif isinstance(where, (ExtraWhere, SubqueryConstraint, Exists)):
return SOME_TREE
elif len(where) == 0:
return {frozenset()}
else:
children_dnfs = lmap(_dnf, where.children)
if len(children_dnfs) == 0:
return {frozenset()}
elif len(children_dnfs) == 1:
result = children_dnfs[0]
else:
# Just unite children joined with OR
if where.connector == OR:
result = set(cat(children_dnfs))
# Use Cartesian product to AND children
else:
result = {frozenset(cat(conjs)) for conjs in product(*children_dnfs)}
# Negating and expanding brackets
if where.negated:
result = {frozenset(map(negate, conjs)) for conjs in product(*result)}
return result
def clean_conj(conj, for_alias):
conds = {}
for alias, attname, value, negation in conj:
# "SOME" conds, negated conds and conds for other aliases should be stripped
if value is not SOME and negation and alias == for_alias:
# Conjs with fields eq 2 different values will never cause invalidation
if attname in conds and conds[attname] != value:
return None
conds[attname] = value
return conds
def clean_dnf(tree, aliases):
cleaned = [clean_conj(conj, alias) for conj in tree for alias in aliases]
# Remove deleted conjunctions
cleaned = [conj for conj in cleaned if conj is not None]
# Any empty conjunction eats up the rest
# NOTE: a more elaborate DNF reduction is not really needed,
# just keep your querysets sane.
if not all(cleaned):
return [{}]
return cleaned
def add_join_conds(dnf, query):
from collections import defaultdict
# A cond on parent (alias, col) means the same cond applies to target and vice a versa
join_exts = defaultdict(list)
for alias, join in query.alias_map.items():
if query.alias_refcount[alias] and isinstance(join, Join):
for parent_col, target_col in join.join_cols:
join_exts[join.parent_alias, parent_col].append((join.table_alias, target_col))
join_exts[join.table_alias, target_col].append((join.parent_alias, parent_col))
if not join_exts:
return dnf
return {
conj | {
(join_alias, join_col, v, negation)
for alias, col, v, negation in conj
for join_alias, join_col in join_exts[alias, col]
}
for conj in dnf
}
def query_dnf(query):
def table_for(alias):
return alias if alias == main_alias else query.alias_map[alias].table_name
dnf = _dnf(query.where)
dnf = add_join_conds(dnf, query)
# NOTE: we exclude content_type as it never changes and will hold dead invalidation info
main_alias = query.model._meta.db_table
aliases = {alias for alias, join in query.alias_map.items()
if query.alias_refcount[alias]} \
| {main_alias} - {'django_content_type'}
tables = group_by(table_for, aliases)
return {table: clean_dnf(dnf, table_aliases) for table, table_aliases in tables.items()}
if qs.query.combined_queries:
dnfs_ = join_with(lcat, (query_dnf(q) for q in qs.query.combined_queries))
else:
dnfs_ = query_dnf(qs.query)
# Add any subqueries used for annotation
if qs.query.annotations:
subqueries = (query_dnf(getattr(q, 'query', None))
for q in qs.query.annotations.values() if isinstance(q, Subquery))
dnfs_.update(join_with(lcat, subqueries))
return dnfs_
class Some:
def __str__(self):
return 'SOME'
__repr__ = __str__
|