mirror of
https://github.com/Hopiu/django-cachalot.git
synced 2026-05-24 12:13:43 +00:00
This PR upgrades cachalot to version 2.2.0 allowing for anyone to install cachalot at any Django package above 1.11. All tests currently function now. * Added mysql as a service to fix Travis CI * Removed tox dependency * Removed upper limit on Django and support 2.2-3.0 CI Test * Cachalot will only say "officially supports" while removing upper limit to not hinder anyone's progress. * Added Python 3.8 and Django 2.2 and 3.0 to CI tests - Max is 200 and we'll get there quickly if we add another Python version. Utilizes #127 - Keep dependencies for six and django.utils.six * Correctly run Travis test by updating tox.ini * Originally using tox syntax but changed testenv to travis so that travis env and travis dependencies and stuff are used * Tox tests should start running command * testenv without dependencies broke travis. New spot in Tox * testenv without dependencies broke travis * I can't figure out where testenv is supposed to be * Good thing there's squash * Added missing six in test * Also added six package to Tox for Django 3.0 coverage. * The missing six package caused several problems in several tests due to using an incorrect DB * Resolves #138 * Need to add the panels here in order to satisfy tests * Adds CachalotPanel to Python 2 backwards compatibility * Forgot super class took those like save() * Added databases to certain test cases * APITestCase, DebugToolbarTestCase, SignalsTestCase required a database set * Fixes errors like these: https://travis-ci.org/noripyt/django-cachalot/jobs/648691385#L4892 * Dropped OFFICIAL support for Python 3.4 + MySQL * MySQL and its client on 3.4 is f--king up a lot of the testing and I can't bear with it. We can revisit it later, but, honestly, who uses Python 3.4... and if an organization is, then they will either have no problem with Postgres and SQLite or know that MySQL doesn't work. * Drop Py3.4 from running. Fixed databases * Now goes with setting's databases rather than hardcoded since some tests don't have other databases. * Fix ReadTestCase Django 2.2+ changed self.queryset at line 1000 with self.query=queryset.query * Django Dependency Removed from utils.py * Last commit used Django version. This one used try except * Fixed assertNumQuery; added allow_failures for 3.4 * Primary: More information in test_utils.py with method is_django_21_below_and_is_sqlite * The problem with the entire build lie in the self.is_sqlite for the tests. SQLite will make 2 queries when creating or destroying or updating instead of 1. * In summary, if SQLite is the DB and Django * Changed build matrix to allow failures for 3.4 instead of taking it out completely so that we can get it to work soon. * Remove -> From function return * I love backwards compatibility * SQLite2 in multi_db.py returned incorrect bool * Django 2.1 and SQLite checker doesn't work for some tests * Some tests DO HAVE the BEGIN query that is returned... Not sure why. If the next test show completely different WriteTestCase problems, then we need to find alternative. * Removed check from atomic * Adjust Django and is_sqlite errors * CI testing the Tox errors: https://travis-ci.org/noripyt/django-cachalot?utm_medium=notification&utm_source=github_status * Adjust Django and is_sqlite errors (590 CI) * CI testing the Tox errors: https://travis-ci.org/noripyt/django-cachalot?utm_medium=notification&utm_source=github_status * Adjustment 2 * Make include matrix and Adjust Django and is_sqlite errors (594 CI) * CI testing the Tox errors: https://travis-ci.org/noripyt/django-cachalot?utm_medium=notification&utm_source=github_status * Adjustment 3 * Adjust Django and is_sqlite errors (596 CI) * CI testing the Tox errors: https://travis-ci.org/noripyt/django-cachalot?utm_medium=notification&utm_source=github_status * Adjustment 4 * Added my intro to ReadTheDocs * Adjust Django and is_sqlite errors (598 CI) * CI testing the Tox errors: https://travis-ci.org/noripyt/django-cachalot?utm_medium=notification&utm_source=github_status * Adjustment 5 * Drop Python 3.4 from tests since PyLibMC not cooperating at that level. * Bump package version up 1 minor for Django 2.2 and 3.0
210 lines
6.8 KiB
Python
210 lines
6.8 KiB
Python
# coding: utf-8
|
|
|
|
from __future__ import unicode_literals
|
|
import datetime
|
|
from decimal import Decimal
|
|
from hashlib import sha1
|
|
from time import time
|
|
from uuid import UUID
|
|
|
|
from django.contrib.postgres.functions import TransactionNow
|
|
from django.db import connections
|
|
from django.db.models import QuerySet, Subquery, Exists
|
|
from django.db.models.functions import Now
|
|
from django.db.models.sql import Query, AggregateQuery
|
|
from django.db.models.sql.where import ExtraWhere, WhereNode, NothingNode
|
|
try:
|
|
from django.utils.six import text_type, binary_type, integer_types
|
|
except ImportError:
|
|
from six import text_type, binary_type, integer_types
|
|
|
|
from .settings import ITERABLES, cachalot_settings
|
|
from .transaction import AtomicCache
|
|
|
|
|
|
class UncachableQuery(Exception):
|
|
pass
|
|
|
|
|
|
class IsRawQuery(Exception):
|
|
pass
|
|
|
|
|
|
CACHABLE_PARAM_TYPES = {
|
|
bool, int, float, Decimal, bytearray, binary_type, text_type, type(None),
|
|
datetime.date, datetime.time, datetime.datetime, datetime.timedelta, UUID,
|
|
}
|
|
CACHABLE_PARAM_TYPES.update(integer_types) # Adds long for Python 2
|
|
UNCACHABLE_FUNCS = {Now, TransactionNow}
|
|
|
|
try:
|
|
from psycopg2 import Binary
|
|
from psycopg2.extras import (
|
|
NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet, Json)
|
|
from django.contrib.postgres.fields.jsonb import JsonAdapter
|
|
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
CACHABLE_PARAM_TYPES.update((
|
|
Binary, NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet,
|
|
Json, JsonAdapter))
|
|
|
|
|
|
def check_parameter_types(params):
|
|
for p in params:
|
|
cl = p.__class__
|
|
if cl not in CACHABLE_PARAM_TYPES:
|
|
if cl in ITERABLES:
|
|
check_parameter_types(p)
|
|
elif cl is dict:
|
|
check_parameter_types(p.items())
|
|
else:
|
|
raise UncachableQuery
|
|
|
|
|
|
def get_query_cache_key(compiler):
|
|
"""
|
|
Generates a cache key from a SQLCompiler.
|
|
|
|
This cache key is specific to the SQL query and its context
|
|
(which database is used). The same query in the same context
|
|
(= the same database) must generate the same cache key.
|
|
|
|
:arg compiler: A SQLCompiler that will generate the SQL query
|
|
:type compiler: django.db.models.sql.compiler.SQLCompiler
|
|
:return: A cache key
|
|
:rtype: int
|
|
"""
|
|
sql, params = compiler.as_sql()
|
|
check_parameter_types(params)
|
|
cache_key = '%s:%s:%s' % (compiler.using, sql,
|
|
[text_type(p) for p in params])
|
|
return sha1(cache_key.encode('utf-8')).hexdigest()
|
|
|
|
|
|
def get_table_cache_key(db_alias, table):
|
|
"""
|
|
Generates a cache key from a SQL table.
|
|
|
|
:arg db_alias: Alias of the used database
|
|
:type db_alias: str or unicode
|
|
:arg table: Name of the SQL table
|
|
:type table: str or unicode
|
|
:return: A cache key
|
|
:rtype: int
|
|
"""
|
|
cache_key = '%s:%s' % (db_alias, table)
|
|
return sha1(cache_key.encode('utf-8')).hexdigest()
|
|
|
|
|
|
def _get_tables_from_sql(connection, lowercased_sql):
|
|
return {t for t in connection.introspection.django_table_names()
|
|
if t in lowercased_sql}
|
|
|
|
|
|
def _find_subqueries_in_where(children):
|
|
for child in children:
|
|
child_class = child.__class__
|
|
if child_class is WhereNode:
|
|
for grand_child in _find_subqueries_in_where(child.children):
|
|
yield grand_child
|
|
elif child_class is ExtraWhere:
|
|
raise IsRawQuery
|
|
elif child_class is NothingNode:
|
|
pass
|
|
else:
|
|
rhs = child.rhs
|
|
rhs_class = rhs.__class__
|
|
if rhs_class is Query:
|
|
yield rhs
|
|
elif rhs_class is QuerySet:
|
|
yield rhs.query
|
|
elif rhs_class is Subquery or rhs_class is Exists:
|
|
yield rhs.queryset.query
|
|
elif rhs_class in UNCACHABLE_FUNCS:
|
|
raise UncachableQuery
|
|
|
|
|
|
def is_cachable(table):
|
|
whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
|
|
if whitelist and table not in whitelist:
|
|
return False
|
|
return table not in cachalot_settings.CACHALOT_UNCACHABLE_TABLES
|
|
|
|
|
|
def are_all_cachable(tables):
|
|
whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
|
|
if whitelist and not tables.issubset(whitelist):
|
|
return False
|
|
return tables.isdisjoint(cachalot_settings.CACHALOT_UNCACHABLE_TABLES)
|
|
|
|
|
|
def filter_cachable(tables):
|
|
whitelist = cachalot_settings.CACHALOT_ONLY_CACHABLE_TABLES
|
|
tables = tables.difference(cachalot_settings.CACHALOT_UNCACHABLE_TABLES)
|
|
if whitelist:
|
|
return tables.intersection(whitelist)
|
|
return tables
|
|
|
|
|
|
def _get_tables(db_alias, query):
|
|
if query.select_for_update or (
|
|
not cachalot_settings.CACHALOT_CACHE_RANDOM
|
|
and '?' in query.order_by):
|
|
raise UncachableQuery
|
|
|
|
try:
|
|
if query.extra_select:
|
|
raise IsRawQuery
|
|
# Gets all tables already found by the ORM.
|
|
tables = set(query.table_map)
|
|
tables.add(query.get_meta().db_table)
|
|
# Gets tables in subquery annotations.
|
|
for annotation in query.annotations.values():
|
|
if isinstance(annotation, Subquery):
|
|
# Django 2.2+ removed queryset in favor of simply using query
|
|
try:
|
|
tables.update(_get_tables(db_alias, annotation.queryset.query))
|
|
except AttributeError:
|
|
tables.update(_get_tables(db_alias, annotation.query))
|
|
# Gets tables in WHERE subqueries.
|
|
for subquery in _find_subqueries_in_where(query.where.children):
|
|
tables.update(_get_tables(db_alias, subquery))
|
|
# Gets tables in HAVING subqueries.
|
|
if isinstance(query, AggregateQuery):
|
|
tables.update(
|
|
_get_tables_from_sql(connections[db_alias], query.subquery))
|
|
# Gets tables in combined queries
|
|
# using `.union`, `.intersection`, or `difference`.
|
|
if query.combined_queries:
|
|
for combined_query in query.combined_queries:
|
|
tables.update(_get_tables(db_alias, combined_query))
|
|
except IsRawQuery:
|
|
sql = query.get_compiler(db_alias).as_sql()[0].lower()
|
|
tables = _get_tables_from_sql(connections[db_alias], sql)
|
|
|
|
if not are_all_cachable(tables):
|
|
raise UncachableQuery
|
|
return tables
|
|
|
|
|
|
def _get_table_cache_keys(compiler):
|
|
db_alias = compiler.using
|
|
get_table_cache_key = cachalot_settings.CACHALOT_TABLE_KEYGEN
|
|
return [get_table_cache_key(db_alias, t)
|
|
for t in _get_tables(db_alias, compiler.query)]
|
|
|
|
|
|
def _invalidate_tables(cache, db_alias, tables):
|
|
tables = filter_cachable(set(tables))
|
|
if not tables:
|
|
return
|
|
now = time()
|
|
get_table_cache_key = cachalot_settings.CACHALOT_TABLE_KEYGEN
|
|
cache.set_many(
|
|
{get_table_cache_key(db_alias, t): now for t in tables},
|
|
cachalot_settings.CACHALOT_TIMEOUT)
|
|
|
|
if isinstance(cache, AtomicCache):
|
|
cache.to_be_invalidated.update(tables)
|