Completed
Push — master ( 8124bf...d2c927 )
by Bertrand
01:15
created

cachalot._get_tables()   F

Complexity

Conditions 12

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 12
dl 0
loc 23
rs 2.9693

How to fix   Complexity   

Complexity

Complex classes like cachalot._get_tables() 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 import VERSION as django_version
11
from django.db import connections
12
from django.db.models.sql import Query
13
from django.db.models.sql.where import ExtraWhere, SubqueryConstraint
14
from django.utils.module_loading import import_string
15
from django.utils.six import text_type, binary_type
16
17
from .settings import cachalot_settings
18
from .signals import post_invalidation
19
from .transaction import AtomicCache
20
21
22
DJANGO_LTE_1_8 = django_version[:2] <= (1, 8)
23
DJANGO_GTE_1_9 = django_version[:2] >= (1, 9)
24
25
26
class UncachableQuery(Exception):
27
    pass
28
29
30
TUPLE_OR_LIST = {tuple, list}
31
32
CACHABLE_PARAM_TYPES = {
33
    bool, int, float, Decimal, binary_type, text_type, type(None),
34
    datetime.date, datetime.time, datetime.datetime, datetime.timedelta, UUID,
35
}
36
37
UNCACHABLE_FUNCS = set()
38
if DJANGO_GTE_1_9:
39
    from django.db.models.functions import Now
40
    from django.contrib.postgres.functions import TransactionNow
41
    UNCACHABLE_FUNCS.update((Now, TransactionNow))
42
43
try:
44
    from psycopg2.extras import (
45
        NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet, Json)
46
except ImportError:
47
    pass
48
else:
49
    CACHABLE_PARAM_TYPES.update((
50
        NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet, Json))
51
52
53
def check_parameter_types(params):
54
    for p in params:
55
        cl = p.__class__
56
        if cl not in CACHABLE_PARAM_TYPES:
57
            if cl in TUPLE_OR_LIST:
58
                check_parameter_types(p)
59
            elif cl is dict:
60
                check_parameter_types(p.items())
61
            else:
62
                raise UncachableQuery
63
64
65
def get_query_cache_key(compiler):
66
    """
67
    Generates a cache key from a SQLCompiler.
68
69
    This cache key is specific to the SQL query and its context
70
    (which database is used).  The same query in the same context
71
    (= the same database) must generate the same cache key.
72
73
    :arg compiler: A SQLCompiler that will generate the SQL query
74
    :type compiler: django.db.models.sql.compiler.SQLCompiler
75
    :return: A cache key
76
    :rtype: int
77
    """
78
    sql, params = compiler.as_sql()
79
    check_parameter_types(params)
80
    cache_key = '%s:%s:%s' % (compiler.using, sql, params)
81
    return sha1(cache_key.encode('utf-8')).hexdigest()
82
83
84
def get_table_cache_key(db_alias, table):
85
    """
86
    Generates a cache key from a SQL table.
87
88
    :arg db_alias: Alias of the used database
89
    :type db_alias: str or unicode
90
    :arg table: Name of the SQL table
91
    :type table: str or unicode
92
    :return: A cache key
93
    :rtype: int
94
    """
95
    cache_key = '%s:%s' % (db_alias, table)
96
    return sha1(cache_key.encode('utf-8')).hexdigest()
97
98
99
def _get_query_cache_key(compiler):
100
    return import_string(cachalot_settings.CACHALOT_QUERY_KEYGEN)(compiler)
101
102
103
def _get_table_cache_key(db_alias, table):
104
    return import_string(cachalot_settings.CACHALOT_TABLE_KEYGEN)(db_alias, table)
105
106
107
def _get_tables_from_sql(connection, lowercased_sql):
108
    return [t for t in connection.introspection.django_table_names()
109
            if t in lowercased_sql]
110
111
112
def _find_subqueries(children):
113
    for child in children:
114
        if child.__class__ is SubqueryConstraint:
115
            if child.query_object.__class__ is Query:
116
                yield child.query_object
117
            else:
118
                yield child.query_object.query
119
        else:
120
            rhs = None
121
            if hasattr(child, 'rhs'):
122
                rhs = child.rhs
123
            rhs_class = rhs.__class__
124
            if rhs_class is Query:
125
                yield rhs
126
            elif hasattr(rhs, 'query'):
127
                yield rhs.query
128
            elif rhs_class in UNCACHABLE_FUNCS:
129
                raise UncachableQuery
130
        if hasattr(child, 'children'):
131
            for grand_child in _find_subqueries(child.children):
132
                yield grand_child
133
134
135
def _get_tables(query, db_alias):
136
    if '?' in query.order_by and not cachalot_settings.CACHALOT_CACHE_RANDOM:
137
        raise UncachableQuery
138
139
    tables = set(query.table_map)
140
    tables.add(query.get_meta().db_table)
141
    subquery_constraints = _find_subqueries(
142
        query.where.children + query.having.children if DJANGO_LTE_1_8
143
        else query.where.children)
144
    for subquery in subquery_constraints:
145
        tables.update(_get_tables(subquery, db_alias))
146
    if query.extra_select or hasattr(query, 'subquery') \
147
            or any(c.__class__ is ExtraWhere for c in query.where.children):
148
        sql = query.get_compiler(db_alias).as_sql()[0].lower()
149
        additional_tables = _get_tables_from_sql(connections[db_alias], sql)
150
        tables.update(additional_tables)
151
152
    whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
153
    blacklist = cachalot_settings.CACHALOT_UNCACHABLE_TABLES
154
    if (whitelist and not tables.issubset(whitelist)) \
155
            or not tables.isdisjoint(blacklist):
156
        raise UncachableQuery
157
    return tables
158
159
160
def _get_table_cache_keys(compiler):
161
    db_alias = compiler.using
162
    return [_get_table_cache_key(db_alias, t)
163
            for t in _get_tables(compiler.query, db_alias)]
164
165
166
def _invalidate_tables(cache, db_alias, tables):
167
    now = time()
168
    cache.set_many(
169
        {_get_table_cache_key(db_alias, t): now for t in tables}, None)
170
171
    if isinstance(cache, AtomicCache):
172
        cache.to_be_invalidated.update(tables)
173
174
175
def _invalidate_table(cache, db_alias, table):
176
    cache.set(_get_table_cache_key(db_alias, table), time(), None)
177
178
    if isinstance(cache, AtomicCache):
179
        cache.to_be_invalidated.add(table)
180
    else:
181
        post_invalidation.send(table, db_alias=db_alias)
182