Completed
Push — master ( 6e429f...74e000 )
by Bertrand
30s
created

_find_subqueries_in_where()   C

Complexity

Conditions 10

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
dl 0
loc 19
rs 6
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like _find_subqueries_in_where() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# coding: utf-8
2
3
from __future__ import unicode_literals
4
import datetime
5
from decimal import Decimal
6
from hashlib import sha1
7
from time import time
8
from uuid import UUID
9
10
from django.contrib.postgres.functions import TransactionNow
11
from django.db import connections
12
from django.db.models import QuerySet, Subquery, Exists
13
from django.db.models.functions import Now
14
from django.db.models.sql import Query, AggregateQuery
15
from django.db.models.sql.where import ExtraWhere, WhereNode
16
from django.utils.six import text_type, binary_type, integer_types
17
18
from .settings import ITERABLES, cachalot_settings
19
from .transaction import AtomicCache
20
21
22
class UncachableQuery(Exception):
23
    pass
24
25
26
class IsRawQuery(Exception):
27
    pass
28
29
30
CACHABLE_PARAM_TYPES = {
31
    bool, int, float, Decimal, bytearray, binary_type, text_type, type(None),
32
    datetime.date, datetime.time, datetime.datetime, datetime.timedelta, UUID,
33
}
34
CACHABLE_PARAM_TYPES.update(integer_types)  # Adds long for Python 2
35
UNCACHABLE_FUNCS = {Now, TransactionNow}
36
37
try:
38
    from psycopg2 import Binary
39
    from psycopg2.extras import (
40
        NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet, Json)
41
    from django.contrib.postgres.fields.jsonb import JsonAdapter
42
43
except ImportError:
44
    pass
45
else:
46
    CACHABLE_PARAM_TYPES.update((
47
        Binary, NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet,
48
        Json, JsonAdapter))
49
50
51
def check_parameter_types(params):
52
    for p in params:
53
        cl = p.__class__
54
        if cl not in CACHABLE_PARAM_TYPES:
55
            if cl in ITERABLES:
56
                check_parameter_types(p)
57
            elif cl is dict:
58
                check_parameter_types(p.items())
59
            else:
60
                raise UncachableQuery
61
62
63
def get_query_cache_key(compiler):
64
    """
65
    Generates a cache key from a SQLCompiler.
66
67
    This cache key is specific to the SQL query and its context
68
    (which database is used).  The same query in the same context
69
    (= the same database) must generate the same cache key.
70
71
    :arg compiler: A SQLCompiler that will generate the SQL query
72
    :type compiler: django.db.models.sql.compiler.SQLCompiler
73
    :return: A cache key
74
    :rtype: int
75
    """
76
    sql, params = compiler.as_sql()
77
    check_parameter_types(params)
78
    cache_key = '%s:%s:%s' % (compiler.using, sql,
79
                              [text_type(p) for p in params])
80
    return sha1(cache_key.encode('utf-8')).hexdigest()
81
82
83
def get_table_cache_key(db_alias, table):
84
    """
85
    Generates a cache key from a SQL table.
86
87
    :arg db_alias: Alias of the used database
88
    :type db_alias: str or unicode
89
    :arg table: Name of the SQL table
90
    :type table: str or unicode
91
    :return: A cache key
92
    :rtype: int
93
    """
94
    cache_key = '%s:%s' % (db_alias, table)
95
    return sha1(cache_key.encode('utf-8')).hexdigest()
96
97
98
def _get_tables_from_sql(connection, lowercased_sql):
99
    return {t for t in connection.introspection.django_table_names()
100
            if t in lowercased_sql}
101
102
103
def _find_subqueries_in_where(children):
104
    for child in children:
105
        child_class = child.__class__
106
        if child_class is WhereNode:
107
            for grand_child in _find_subqueries_in_where(child.children):
108
                yield grand_child
109
        elif child_class is ExtraWhere:
110
            raise IsRawQuery
111
        else:
112
            rhs = child.rhs
113
            rhs_class = rhs.__class__
114
            if rhs_class is Query:
115
                yield rhs
116
            elif rhs_class is QuerySet:
117
                yield rhs.query
118
            elif rhs_class is Subquery or rhs_class is Exists:
119
                yield rhs.queryset.query
120
            elif rhs_class in UNCACHABLE_FUNCS:
121
                raise UncachableQuery
122
123
124
def is_cachable(table):
125
    whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
126
    if whitelist and table not in whitelist:
127
        return False
128
    return table not in cachalot_settings.CACHALOT_UNCACHABLE_TABLES
129
130
131
def are_all_cachable(tables):
132
    whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
133
    if whitelist and not tables.issubset(whitelist):
134
        return False
135
    return tables.isdisjoint(cachalot_settings.CACHALOT_UNCACHABLE_TABLES)
136
137
138
def filter_cachable(tables):
139
    whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
140
    tables = tables.difference(cachalot_settings.CACHALOT_UNCACHABLE_TABLES)
141
    if whitelist:
142
        return tables.intersection(whitelist)
143
    return tables
144
145
146
def _get_tables(db_alias, query):
147
    if query.select_for_update or (
148
            not cachalot_settings.CACHALOT_CACHE_RANDOM
149
            and '?' in query.order_by):
150
        raise UncachableQuery
151
152
    try:
153
        if query.extra_select:
154
            raise IsRawQuery
155
        # Gets all tables already found by the ORM.
156
        tables = set(query.table_map)
157
        tables.add(query.get_meta().db_table)
158
        # Gets tables in subquery annotations.
159
        for annotation in query.annotations.values():
160
            if isinstance(annotation, Subquery):
161
                tables.update(_get_tables(db_alias, annotation.queryset.query))
162
        # Gets tables in WHERE subqueries.
163
        for subquery in _find_subqueries_in_where(query.where.children):
164
            tables.update(_get_tables(db_alias, subquery))
165
        # Gets tables in HAVING subqueries.
166
        if isinstance(query, AggregateQuery):
167
            tables.update(
168
                _get_tables_from_sql(connections[db_alias], query.subquery))
169
    except IsRawQuery:
170
        sql = query.get_compiler(db_alias).as_sql()[0].lower()
171
        tables = _get_tables_from_sql(connections[db_alias], sql)
172
173
    if not are_all_cachable(tables):
174
        raise UncachableQuery
175
    return tables
176
177
178
def _get_table_cache_keys(compiler):
179
    db_alias = compiler.using
180
    get_table_cache_key = cachalot_settings.CACHALOT_TABLE_KEYGEN
181
    return [get_table_cache_key(db_alias, t)
182
            for t in _get_tables(db_alias, compiler.query)]
183
184
185
def _invalidate_tables(cache, db_alias, tables):
186
    tables = filter_cachable(set(tables))
187
    if not tables:
188
        return
189
    now = time()
190
    get_table_cache_key = cachalot_settings.CACHALOT_TABLE_KEYGEN
191
    cache.set_many(
192
        {get_table_cache_key(db_alias, t): now for t in tables},
193
        cachalot_settings.CACHALOT_TIMEOUT)
194
195
    if isinstance(cache, AtomicCache):
196
        cache.to_be_invalidated.update(tables)
197