Compare commits

...

16 Commits

Author SHA1 Message Date
M1ha Shvn
3b79ad697a
Merge pull request #53 from carrotquest/fix-lock-hard-release
Fix bug in Storage lock hard release system if sync process works on multiple hosts
2025-02-19 15:34:25 +05:00
M1ha Shvn
95f1bdb26a
Merge pull request #54 from carrotquest/change-test-versions
Update versions in tests
2025-02-19 15:34:09 +05:00
M1ha-Shvn
85d27a65f9 Update versions in tests 2025-02-19 15:23:41 +05:00
M1ha-Shvn
43a1821408 Fix bug in Storage lock hard release system if sync process works on multiple hosts 2025-02-19 15:18:16 +05:00
M1ha Shvn
1b247e95fe
Update overview.md
Added README note by [issue 51](https://github.com/carrotquest/django-clickhouse/issues/51)
2024-01-05 15:31:07 +05:00
M1ha Shvn
244b4fbd37
Added latest software versions to testing in GitHub actions (#48)
1. Added latest software versions to testing in GitHub actions
2. Fixed timezone mistakes in tests
3. Fixed naive datetime warnings
2024-01-05 15:20:10 +05:00
M1ha Shvn
0ac4615649
Merge pull request #50 from mahdi-jfri/patch-1
Fix typo in overview docs
2023-05-12 10:07:41 +05:00
Mahdi Jafari
ee723fca2a
Fix typo in overview docs 2023-05-08 11:22:30 +03:30
M1ha Shvn
b70ec45edf
Fixed conflict of model parameter and hints in DefaultRouter.allow_migrate (#46)
Fixed conflict of model parameter and hints in DefaultRouter.allow_migrate
2022-10-20 11:47:32 +03:00
M1ha Shvn
fc362e852f
Added tests on issue-42 (#45)
Fixed issue https://github.com/carrotquest/django-clickhouse/issues/42
2022-10-19 11:09:10 +03:00
M1ha Shvn
b2cb098349
Merge pull request #44 from carrotquest/run-migrations-hints
Added ability to add hints to RunPython and RunSQL migration operations
2022-10-19 10:43:38 +03:00
M1ha.Shvn
9fb93060ef Bug fix 2022-10-19 12:32:30 +05:00
M1ha.Shvn
ac8d34c261 Added ability to add hints to RunPython and RunSQL migration operations 2022-10-19 12:24:53 +05:00
M1ha Shvn
01cec7c999
Update clickhouse_migrate.py
Fixed error in manage.py clickhouse_migrate
2021-12-16 14:22:43 +05:00
M1ha Shvn
2beb449b67
Merge pull request #39 from carrotquest/test-versions
Update python-tests.yml
2021-11-06 08:56:15 +03:00
M1ha Shvn
404584fc01
Update python-tests.yml
Added python 3.10 and django 3.2 to testing
2021-11-06 08:08:41 +03:00
22 changed files with 265 additions and 56 deletions

View File

@ -11,12 +11,56 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9"]
postgres-version: ["9.6", "10", "11", "12"]
django-version: ["2.1", "2.2", "3.0", "3.1", "3.2"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
postgres-version: ["9.6", "10", "11", "12", "13", "14", "15", "16"]
django-version: ["3.2", "4.0", "4.1", "4.2", "5.0", "5.1"]
clickhouse-version: ["latest"]
redis-version: ["latest"]
exclude:
# Django 4.0+ doesn't support PostgreSQL 9.6
- django-version: "4.0"
postgres-version: "9.6"
- django-version: "4.1"
postgres-version: "9.6"
- django-version: "4.2"
postgres-version: "9.6"
- django-version: "5.0"
postgres-version: "9.6"
- django-version: "5.1"
postgres-version: "9.6"
# Django 4.1+ doesn't support PostgreSQL 10
- django-version: "4.1"
postgres-version: "10"
- django-version: "4.2"
postgres-version: "10"
- django-version: "5.0"
postgres-version: "10"
- django-version: "5.1"
postgres-version: "10"
# Django 4.2+ doesn't support PostgreSQL 11
- django-version: "4.2"
postgres-version: "11"
- django-version: "5.0"
postgres-version: "11"
- django-version: "5.1"
postgres-version: "11"
# Django 5.1+ doesn't support PostgreSQL 12
- django-version: "5.1"
postgres-version: "12"
# Django 5.0+ does not support python 3.8, 3.9
- django-version: "5.0"
python-version: "3.8"
- django-version: "5.0"
python-version: "3.9"
- django-version: "5.1"
python-version: "3.8"
- django-version: "5.1"
python-version: "3.9"
services:
postgres:
image: postgres:${{ matrix.postgres-version }}

View File

@ -24,7 +24,7 @@ services:
build:
context: .
args:
- PYTHON_VER=latest
- PYTHON_IMAGE_TAG=latest
environment:
- REDIS_HOST=redis_db
- PGHOST=postgres_db

View File

@ -75,6 +75,12 @@ Parameters
* `--verbosity: Optional[int] = 1` - Level of debug output. See [here](https://docs.djangoproject.com/en/3.2/ref/django-admin/#cmdoption-verbosity) for more details.
* `--help` - Print help
## Migration operations enhancements
* `RunSQL`, `RunPython`
Can accept `hints: dict = {}` parameter in order to set migration database alias (`force_migrate_on_databases: List[str]` key) or model (`model: Union[str, Type[ClickHouseModel]]` key)
## Migration algorithm
- Get a list of databases from `CLICKHOUSE_DATABASES` setting. Migrate them one by one.
- Find all django apps from `INSTALLED_APPS` setting, which have no `readonly=True` attribute and have `migrate=True` attribute. Migrate them one by one.

View File

@ -96,6 +96,19 @@ class ClickHouseUser(ClickHouseModel):
engine = MergeTree('birthday', ('birthday',))
```
**Important note**: `clickhouse_model.py` file is not anyhow imported by django initialization code. So if your models are not used anywhere excluding this file, you should import it somewhere in your code if you want synchroniztion working correctly. For instance, you can customise [AppConfig](https://docs.djangoproject.com/en/5.0/ref/applications/#django.apps.AppConfig.ready) like:
```python
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'my_app'
def ready(self):
from my_app.clickhouse_models import ClickHouseUser
```
## Migration to create table in ClickHouse
1. Read [migrations](migrations.md) section
2. Create `clickhouse_migrations` package in your django app
@ -112,7 +125,7 @@ class ClickHouseUser(ClickHouseModel):
4. Add content to file `0001_initial.py`:
```python
from django_clickhouse import migrations
from my_app.cilckhouse_models import ClickHouseUser
from my_app.clickhouse_models import ClickHouseUser
class Migration(migrations.Migration):
operations = [

View File

@ -25,12 +25,17 @@ Router is a class, defining 3 methods:
Returns `database alias` to use for given `model` for `SELECT` queries.
* `def db_for_write(self, model: ClickHouseModel, **hints) -> str`
Returns `database alias` to use for given `model` for `INSERT` queries.
* `def allow_migrate(self, db_alias: str, app_label: str, operation: Operation, model: Optional[ClickHouseModel] = None, **hints: dict) -> bool`
* `def allow_migrate(self, db_alias: str, app_label: str, operation: Operation, **hints: dict) -> bool`
Checks if migration `operation` should be applied in django application `app_label` on database `db_alias`.
Optional `model` field can be used to determine migrations on concrete model.
Optional `hints` help to pass additional info which can be used to test migrations availability on concrete model.
By default [CLICKHOUSE_DATABASE_ROUTER](configuration.md#clickhouse_database_router) is used.
It gets routing information from model fields, described below.
It also gives ability to use 2 kinds of hints:
* `force_migrate_on_databases: Iterable[str]` - concrete database aliases where migration should be applied
* `model: Type[ClickHouseModel]` - a model class, to read routing attributes from.
Can be set as class or its string name.
If name is set, class is searched in current `<app_label>.<config.MODELS_MODULE>` package.
## ClickHouseModel routing attributes
Default database router reads routing settings from model attributes.

View File

@ -13,7 +13,7 @@ with open('requirements.txt') as f:
setup(
name='django-clickhouse',
version='1.1.2',
version='1.2.2',
packages=['django_clickhouse', 'django_clickhouse.management.commands'],
package_dir={'': 'src'},
url='https://github.com/carrotquest/django-clickhouse',

View File

@ -9,6 +9,7 @@ from itertools import chain
from typing import List, Tuple, Iterable, Set, Any, Optional
from django.db.models import Model as DjangoModel, QuerySet as DjangoQuerySet
from django.utils.timezone import now
from infi.clickhouse_orm.engines import CollapsingMergeTree
from infi.clickhouse_orm.models import Model as InfiModel, ModelBase as InfiModelBase
from statsd.defaults.django import statsd
@ -290,7 +291,7 @@ class ClickHouseModel(InfiModel, metaclass=ClickHouseModelMeta):
res = (datetime.datetime.now() - last_sync_time).total_seconds() >= cls.get_sync_delay()
logger.debug('django-clickhouse: need_sync returned %s for class %s as no last sync found'
' (now: %s, last: %s, delay: %d)'
% (res, cls.__name__, datetime.datetime.now().isoformat(), last_sync_time.isoformat(),
% (res, cls.__name__, now().isoformat(), last_sync_time.isoformat(),
cls.get_sync_delay()))
return res

View File

@ -23,7 +23,7 @@ class Command(BaseCommand):
' By default all available migrations are applied.'
' Note that library currently have no ability rollback migrations')
parser.add_argument('--database', '-d', nargs='?', type=str, required=False, choices=config.DATABASES.keys(),
parser.add_argument('--database', '-d', nargs='?', type=str, required=False, choices=list(config.DATABASES.keys()),
help='ClickHouse database alias key from CLICKHOUSE_DATABASES django setting.'
' By default migrations are applied to all databases.')

View File

@ -10,6 +10,7 @@ from django.dispatch import receiver
# In order to support all operations import here
from infi.clickhouse_orm.migrations import * # noqa F401, F403
from infi.clickhouse_orm.migrations import RunSQL as LibRunSQL, RunPython as LibRunPython
from infi.clickhouse_orm.database import ServerError, DatabaseException
from infi.clickhouse_orm.fields import StringField, DateField
@ -39,10 +40,9 @@ class Migration:
database = database or connections[db_alias]
for op in self.operations:
model_class = getattr(op, 'model_class', None)
hints = getattr(op, 'hints', {})
if db_router.allow_migrate(db_alias, self.__module__, op, model_class, **hints):
if db_router.allow_migrate(db_alias, self.__module__, op, **hints):
op.apply(database)
@ -176,3 +176,15 @@ class MigrationHistory(ClickHouseModel):
@classmethod
def table_name(cls):
return 'django_clickhouse_migrations'
class RunSQL(LibRunSQL):
def __init__(self, *args, hints: Optional[dict] = None, **kwargs):
super().__init__(*args, **kwargs)
self.hints = hints or {}
class RunPython(LibRunPython):
def __init__(self, *args, hints: Optional[dict] = None, **kwargs):
super().__init__(*args, **kwargs)
self.hints = hints or {}

View File

@ -64,7 +64,7 @@ class ClickHouseSyncBulkUpdateQuerySetMixin(ClickHouseSyncRegisterMixin, BulkUpd
if returning is None:
returning = pk_name
elif isinstance(returning, str):
returning = [pk_name, returning]
returning = [pk_name, returning] if returning != '*' else '*'
else:
returning = list(returning) + [pk_name]

View File

@ -30,24 +30,26 @@ class DefaultRouter:
"""
return random.choice(model.write_db_aliases)
def allow_migrate(self, db_alias: str, app_label: str, operation: Operation,
model=None, **hints) -> bool:
def allow_migrate(self, db_alias: str, app_label: str, operation: Operation, **hints) -> bool:
"""
Checks if migration can be applied to given database
:param db_alias: Database alias to check
:param app_label: App from which migration is got
:param operation: Operation object to perform
:param model: Model migration is applied to
:param hints: Hints to make correct decision
:return: boolean
"""
if hints.get("force_migrate_on_databases", None):
return db_alias in hints["force_migrate_on_databases"]
if hints.get('model'):
model = '%s.%s.%s' % (app_label, config.MODELS_MODULE, hints['model']) \
if isinstance(hints['model'], str) else hints['model']
model = hints.get('model') or getattr(operation, 'model_class', None)
if model is None:
raise ValueError('"model_class" attribute is not defined for operation "%s". '
'Please provide "force_migrate_on_databases" or "model" in hints.'
% operation.__class__.__name__)
model = '%s.%s.%s' % (app_label, config.MODELS_MODULE, model) \
if isinstance(model, str) else model
model = lazy_class_import(model)
if operation.__class__ not in {CreateTable, DropTable}:

View File

@ -11,12 +11,15 @@ import logging
from typing import Any, Optional, List, Tuple
import os
from celery.utils.nodenames import gethostname
from django.utils.timezone import now
from statsd.defaults.django import statsd
from .configuration import config
from .exceptions import ConfigurationError, RedisLockTimeoutError
from .redis import redis_zadd
from .utils import check_pid, get_subclasses, SingletonMeta
from .utils import check_pid_exists, get_subclasses, SingletonMeta
logger = logging.getLogger('django-clickhouse')
@ -186,8 +189,7 @@ class RedisStorage(Storage, metaclass=SingletonMeta):
def get_operations(self, import_key, count, **kwargs):
ops_key = self.REDIS_KEY_OPS_TEMPLATE.format(import_key=import_key)
res = self._redis.zrangebyscore(ops_key, '-inf', datetime.datetime.now().timestamp(), start=0, num=count,
withscores=True)
res = self._redis.zrangebyscore(ops_key, '-inf', now().timestamp(), start=0, num=count, withscores=True)
if res:
ops, scores = zip(*res)
@ -214,19 +216,31 @@ class RedisStorage(Storage, metaclass=SingletonMeta):
# Block process to be single threaded. Default sync delay is 10 * default sync delay.
# It can be changed for model, by passing `lock_timeout` argument to pre_sync
lock = self.get_lock(import_key, **kwargs)
current_host_name = gethostname()
lock_pid_key = self.REDIS_KEY_LOCK_PID.format(import_key=import_key)
try:
lock.acquire()
self._redis.set(lock_pid_key, os.getpid())
self._redis.set(lock_pid_key, '%s:%s' % (current_host_name, os.getpid()))
except RedisLockTimeoutError:
statsd.incr('%s.sync.%s.lock.timeout' % (config.STATSD_PREFIX, import_key))
# Lock is busy. But If the process has been killed, I don't want to wait any more.
# Let's check if pid exists
pid = int(self._redis.get(lock_pid_key) or 0)
if pid and not check_pid(pid):
# I assume that lock has been killed if it works on the same host (other than localhost)
# and there is no process alive.
# I also assume that there are no hosts with same hostname other than localhost.
# Note: previously value contained only pid. Let's support old value for back compatibility
active_lock_data = self._redis.get(lock_pid_key).split(b":")
active_pid = int(active_lock_data[-1] or 0)
active_host_name = active_lock_data[0] \
if len(active_lock_data) > 1 and active_lock_data[0] != "localhost" else None
if (
active_pid and active_host_name
and active_host_name == current_host_name and not check_pid_exists(active_pid)
):
statsd.incr('%s.sync.%s.lock.hard_release' % (config.STATSD_PREFIX, import_key))
logger.warning('django-clickhouse: hard releasing lock "%s" locked by pid %d (process is dead)'
% (import_key, pid))
% (import_key, active_pid))
self._redis.delete(lock_pid_key)
lock.hard_release()
self.pre_sync(import_key, **kwargs)

View File

@ -127,7 +127,7 @@ def model_to_dict(instance: DjangoModel, fields: Optional[Iterable[str]] = None
return data
def check_pid(pid):
def check_pid_exists(pid):
"""
Check For the existence of a unix pid.
"""

View File

@ -5,7 +5,7 @@
"fields": {
"value": 100,
"created_date": "2018-01-01",
"created": "2018-01-01 00:00:00"
"created": "2018-01-01 00:00:00+0000"
}
},
{
@ -14,7 +14,7 @@
"fields": {
"value": 200,
"created_date": "2018-02-01",
"created": "2018-02-01 00:00:00"
"created": "2018-02-01 00:00:00+0000"
}
},
{
@ -23,7 +23,7 @@
"fields": {
"value": 300,
"created_date": "2018-03-01",
"created": "2018-03-01 00:00:00"
"created": "2018-03-01 00:00:00+0000"
}
},
{
@ -32,7 +32,7 @@
"fields": {
"value": 400,
"created_date": "2018-04-01",
"created": "2018-04-01 00:00:00"
"created": "2018-04-01 00:00:00+0000"
}
},
{
@ -41,7 +41,7 @@
"fields": {
"value": 500,
"created_date": "2018-05-01",
"created": "2018-05-01 00:00:00"
"created": "2018-05-01 00:00:00+0000"
}
}
]

View File

@ -5,7 +5,7 @@
"fields": {
"value": 100,
"created_date": "2018-01-01",
"created": "2018-02-01 00:00:00"
"created": "2018-02-01 00:00:00+0000"
}
},
{
@ -14,7 +14,7 @@
"fields": {
"value": 200,
"created_date": "2018-02-01",
"created": "2018-02-01 00:00:00"
"created": "2018-02-01 00:00:00+0000"
}
}
]

View File

@ -8,6 +8,7 @@ from time import sleep
import datetime
from django.utils.timezone import now
# set Django environment
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@ -25,7 +26,7 @@ logger = logging.getLogger('django-clickhouse')
def create(batch_size=1000, test_time=60, period=1, **kwargs):
for iteration in range(int(test_time / period)):
res = TestModel.objects.db_manager('test_db').bulk_create([
TestModel(created=datetime.datetime.now(), created_date='2018-01-01', value=iteration * batch_size + i)
TestModel(created=now(), created_date='2018-01-01', value=iteration * batch_size + i)
for i in range(batch_size)
])
logger.info('django-clickhouse: test created %d records' % len(res))
@ -54,8 +55,8 @@ def sync(period=1, test_time=60, **kwargs):
if kwargs['once']:
ClickHouseCollapseTestModel.sync_batch_from_storage()
else:
start = datetime.datetime.now()
while (datetime.datetime.now() - start).total_seconds() < test_time:
start = now()
while (now() - start).total_seconds() < test_time:
ClickHouseCollapseTestModel.sync_batch_from_storage()
sleep(period)

View File

@ -4,6 +4,7 @@ This file contains django settings to run tests with runtests.py
from os import environ
SECRET_KEY = 'fake-key'
USE_TZ = True
DATABASES = {
'default': {

View File

@ -1,6 +1,7 @@
import datetime
from django.test import TestCase
from django.utils.timezone import now
from tests.clickhouse_models import ClickHouseTestModel
@ -20,11 +21,11 @@ class ClickHouseModelTest(TestCase):
self.assertTrue(ClickHouseTestModel.need_sync())
# Time hasn't passed - no sync
self.storage.set_last_sync_time(ClickHouseTestModel.get_import_key(), datetime.datetime.now())
self.storage.set_last_sync_time(ClickHouseTestModel.get_import_key(), now())
self.assertFalse(ClickHouseTestModel.need_sync())
# Time has passed
sync_delay = ClickHouseTestModel.get_sync_delay()
self.storage.set_last_sync_time(ClickHouseTestModel.get_import_key(),
datetime.datetime.now() - datetime.timedelta(seconds=sync_delay + 1))
now() - datetime.timedelta(seconds=sync_delay + 1))
self.assertTrue(ClickHouseTestModel.need_sync())

View File

@ -13,7 +13,7 @@ from tests.clickhouse_models import ClickHouseTestModel
class NoMigrateRouter(DefaultRouter):
def allow_migrate(self, db_alias, app_label, operation, model=None, **hints):
def allow_migrate(self, db_alias, app_label, operation, **hints):
return False

View File

@ -40,7 +40,7 @@ class TestOperations(TransactionTestCase):
def test_save(self):
# INSERT operation
instance = self.django_model(created_date=datetime.date.today(), created=datetime.datetime.now(), value=2)
instance = self.django_model(created_date=datetime.date.today(), created=now(), value=2)
instance.save()
self.assertListEqual([('insert', "%s.%d" % (self.db_alias, instance.pk))],
self.storage.get_operations(self.clickhouse_model.get_import_key(), 10))
@ -52,13 +52,13 @@ class TestOperations(TransactionTestCase):
self.storage.get_operations(self.clickhouse_model.get_import_key(), 10))
def test_create(self):
instance = self.django_model.objects.create(pk=100555, created_date=datetime.date.today(),
created=datetime.datetime.now(), value=2)
instance = self.django_model.objects.create(pk=100555, created_date=datetime.date.today(), created=now(),
value=2)
self.assertListEqual([('insert', "%s.%d" % (self.db_alias, instance.pk))],
self.storage.get_operations(self.clickhouse_model.get_import_key(), 10))
def test_bulk_create(self):
items = [self.django_model(created_date=datetime.date.today(), created=datetime.datetime.now(), value=i)
items = [self.django_model(created_date=datetime.date.today(), created=now(), value=i)
for i in range(5)]
items = self.django_model.objects.bulk_create(items)
self.assertEqual(5, len(items))
@ -99,6 +99,22 @@ class TestOperations(TransactionTestCase):
self.assertSetEqual({('insert', "%s.%d" % (self.db_alias, instance.pk)) for instance in items},
set(self.storage.get_operations(self.clickhouse_model.get_import_key(), 10)))
def test_pg_bulk_create_returning(self):
now_dt = now()
res = self.django_model.objects.pg_bulk_create([
{'value': i, 'created': now_dt, 'created_date': now_dt.date()}
for i in range(5)
], returning='*')
self.assertEqual(5, len(res))
for i, instance in enumerate(res):
self.assertEqual(instance.created, now_dt)
self.assertEqual(instance.created_date, now_dt.date())
self.assertEqual(i, instance.value)
self.assertSetEqual({('insert', "%s.%d" % (self.db_alias, instance.pk)) for instance in res},
set(self.storage.get_operations(self.clickhouse_model.get_import_key(), 10)))
def test_pg_bulk_update(self):
items = list(self.django_model.objects.filter(pk__in={1, 2}))
@ -115,6 +131,21 @@ class TestOperations(TransactionTestCase):
self.assertSetEqual({('update', "%s.%d" % (self.db_alias, instance.pk)) for instance in items},
set(self.storage.get_operations(self.clickhouse_model.get_import_key(), 10)))
def test_pg_bulk_update_returning(self):
items = list(self.django_model.objects.filter(pk__in={1, 2}))
res = self.django_model.objects.pg_bulk_update([
{'id': instance.pk, 'value': instance.pk * 10}
for instance in items
], returning='*')
self.assertEqual(2, len(res))
for instance in res:
self.assertEqual(instance.value, instance.pk * 10)
self.assertSetEqual({('update', "%s.%d" % (self.db_alias, instance.pk)) for instance in items},
set(self.storage.get_operations(self.clickhouse_model.get_import_key(), 10)))
def test_pg_bulk_update_or_create(self):
items = list(self.django_model.objects.filter(pk__in={1, 2}))
@ -135,9 +166,28 @@ class TestOperations(TransactionTestCase):
self.assertSetEqual({('update', "%s.%d" % (self.db_alias, instance.pk)) for instance in items},
set(self.storage.get_operations(self.clickhouse_model.get_import_key(), 10)))
def test_pg_bulk_update_or_create_returning(self):
items = list(self.django_model.objects.filter(pk__in={1, 2}))
data = [{
'id': instance.pk,
'value': instance.pk * 10,
'created_date': instance.created_date,
'created': instance.created
} for instance in items] + [{'id': 11, 'value': 110, 'created_date': datetime.date.today(), 'created': now()}]
res = self.django_model.objects.pg_bulk_update_or_create(data, returning='*')
self.assertEqual(3, len(res))
for instance in res:
self.assertEqual(instance.value, instance.pk * 10)
self.assertSetEqual({('update', "%s.%d" % (self.db_alias, instance.pk)) for instance in res},
set(self.storage.get_operations(self.clickhouse_model.get_import_key(), 10)))
def test_get_or_create(self):
instance, created = self.django_model.objects. \
get_or_create(pk=100, defaults={'created_date': datetime.date.today(), 'created': datetime.datetime.now(),
get_or_create(pk=100, defaults={'created_date': datetime.date.today(), 'created': now(),
'value': 2})
self.assertTrue(created)
@ -153,8 +203,7 @@ class TestOperations(TransactionTestCase):
def test_update_or_create(self):
instance, created = self.django_model.objects. \
update_or_create(pk=100, defaults={'created_date': datetime.date.today(),
'created': datetime.datetime.now(), 'value': 2})
update_or_create(pk=100, defaults={'created_date': datetime.date.today(), 'created': now(), 'value': 2})
self.assertTrue(created)
self.assertListEqual([('insert', "%s.%d" % (self.db_alias, instance.pk))],
self.storage.get_operations(self.clickhouse_model.get_import_key(), 10))
@ -179,7 +228,7 @@ class TestOperations(TransactionTestCase):
def test_bulk_create_returning(self):
items = [
self.django_model(created_date=datetime.date.today(), created=datetime.datetime.now(), value=i)
self.django_model(created_date=datetime.date.today(), created=now(), value=i)
for i in range(5)
]
items = self.django_model.objects.bulk_create_returning(items)
@ -210,7 +259,7 @@ class TestOperations(TransactionTestCase):
def test_save_returning(self):
# INSERT operation
instance = self.django_model(created_date=datetime.date.today(), created=datetime.datetime.now(), value=2)
instance = self.django_model(created_date=datetime.date.today(), created=now(), value=2)
instance.save_returning()
self.assertListEqual([('insert', "%s.%d" % (self.db_alias, instance.pk))],
self.storage.get_operations(self.clickhouse_model.get_import_key(), 10))
@ -234,6 +283,7 @@ class TestOperations(TransactionTestCase):
class TestSecondaryOperations(TestOperations):
# from django.db.models.fields import *
fixtures = ['test_secondary_model']
django_model = SecondaryTestModel
clickhouse_model = ClickHouseSecondTestModel

59
tests/test_routers.py Normal file
View File

@ -0,0 +1,59 @@
from django.test import SimpleTestCase
from django_clickhouse.migrations import RunSQL, CreateTable
from django_clickhouse.routers import DefaultRouter
from tests.clickhouse_models import ClickHouseTestModel
class DefaultRouterAllowMigrateTest(SimpleTestCase):
def setUp(self):
self.router = DefaultRouter()
self.operation = RunSQL('SELECT 1')
def test_hints_model_class(self):
hints = {'model': ClickHouseTestModel}
with self.subTest('Allow migrate'):
res = self.router.allow_migrate('default', 'tests', self.operation, **hints)
self.assertTrue(res)
with self.subTest('Reject migrate'):
res = self.router.allow_migrate('other', 'tests', self.operation, **hints)
self.assertFalse(res)
def test_hints_model_name(self):
hints = {'model': 'ClickHouseTestModel'}
with self.subTest('Allow migrate'):
res = self.router.allow_migrate('default', 'tests', self.operation, **hints)
self.assertTrue(res)
with self.subTest('Reject migrate'):
res = self.router.allow_migrate('other', 'tests', self.operation, **hints)
self.assertFalse(res)
def test_hints_force_migrate_on_databases(self):
hints = {'force_migrate_on_databases': ['secondary']}
with self.subTest('Allow migrate'):
res = self.router.allow_migrate('secondary', 'apps', self.operation, **hints)
self.assertTrue(res)
with self.subTest('Reject migrate'):
res = self.router.allow_migrate('default', 'apps', self.operation, **hints)
self.assertFalse(res)
def test_model_operation(self):
with self.subTest('Allow migrate'):
operation = CreateTable(ClickHouseTestModel)
res = self.router.allow_migrate('default', 'apps', operation)
self.assertTrue(res)
with self.subTest('Reject migrate'):
operation = CreateTable(ClickHouseTestModel)
res = self.router.allow_migrate('other', 'apps', operation)
self.assertFalse(res)
def test_no_model(self):
with self.assertRaises(ValueError):
self.router.allow_migrate('default', 'apps', self.operation)

View File

@ -30,7 +30,7 @@ class SyncTest(TransactionTestCase):
ClickHouseTestModel.get_storage().flush()
def test_simple(self):
obj = TestModel.objects.create(value=1, created=datetime.datetime.now(), created_date=datetime.date.today())
obj = TestModel.objects.create(value=1, created=now(), created_date=datetime.date.today())
ClickHouseTestModel.sync_batch_from_storage()
synced_data = list(ClickHouseTestModel.objects.all())
@ -40,7 +40,7 @@ class SyncTest(TransactionTestCase):
self.assertEqual(obj.id, synced_data[0].id)
def test_collapsing_update_by_final(self):
obj = TestModel.objects.create(value=1, created=datetime.datetime.now(), created_date=datetime.date.today())
obj = TestModel.objects.create(value=1, created=now(), created_date=datetime.date.today())
obj.value = 2
obj.save()
ClickHouseCollapseTestModel.sync_batch_from_storage()
@ -63,7 +63,7 @@ class SyncTest(TransactionTestCase):
def test_collapsing_update_by_version(self):
ClickHouseCollapseTestModel.engine.version_col = 'version'
obj = TestModel.objects.create(value=1, created=datetime.datetime.now(), created_date=datetime.date.today())
obj = TestModel.objects.create(value=1, created=now(), created_date=datetime.date.today())
obj.value = 2
obj.save()
ClickHouseCollapseTestModel.sync_batch_from_storage()
@ -97,7 +97,7 @@ class SyncTest(TransactionTestCase):
self.assertEqual(0, len(synced_data))
def test_multi_model(self):
obj = TestModel.objects.create(value=1, created=datetime.datetime.now(), created_date=datetime.date.today())
obj = TestModel.objects.create(value=1, created=now(), created_date=datetime.date.today())
obj.value = 2
obj.save()
ClickHouseMultiTestModel.sync_batch_from_storage()
@ -268,7 +268,7 @@ class ProfileTest(TransactionTestCase):
ClickHouseTestModel.sync_enabled = False
TestModel.objects.bulk_create([
TestModel(created=datetime.datetime.now(), created_date='2018-01-01', value=i)
TestModel(created=now(), created_date='2018-01-01', value=i)
for i in range(self.BATCH_SIZE)
])