Skip to content

Commit dd6063b

Browse files
committed
Fix test db name when NAME not provided in DATABASES setting.
Fix Enum error when provided an IntegerChoices value. Add document about multiple db settings.
1 parent 6b9b6ac commit dd6063b

File tree

17 files changed

+180
-135
lines changed

17 files changed

+180
-135
lines changed

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1+
### 1.0.2 (2023-02-28)
2+
- Fix test db name when NAME not provided in DATABASES setting.
3+
- Fix Enum error when provided an IntegerChoices value.
4+
- Add document about multiple db settings.
5+
6+
### 1.0.1 (2023-02-23)
7+
8+
- Add `return_int` parameter to `Enum[8|16]Field` to control whether to get an int or str value when querying from the database.
9+
- Fix TupleField container_class.
10+
- Add fields documentation.
11+
12+
13+
### 1.0.0 (2023-02-21)
14+
15+
- Add tests for migrations.
16+
- Fix bytes escaping.
17+
- Fix date and datetime lookup.
18+
- Add documentations.
19+
- Add lots of new field types:
20+
- Float32/64
21+
- [U]Int8/16/32/64/128/256
22+
- Date/Date32/DateTime('timezone')/DateTime64('timezone')
23+
- String/FixedString(N)
24+
- Enum8/16
25+
- Array(T)
26+
- Bool
27+
- UUID
28+
- Decimal
29+
- IPv4/IPv6
30+
- LowCardinality(T)
31+
- Tuple(T1, T2, ...)
32+
- Map(key, value)
33+
134
0.2.1 (2022-10-30)
235
---
336

README.md

Lines changed: 78 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ $ python setup.py install
5656

5757
### Configuration
5858

59-
we can use the docker compose file under the project for test and try
6059

61-
Only `ENGINE` is required, other options have default values.
60+
Only `ENGINE` is required in database setting, other options have default values.
6261

6362
- ENGINE: required, set to `clickhouse_backend.backend`.
6463
- NAME: database name, default `default`.
@@ -67,21 +66,82 @@ Only `ENGINE` is required, other options have default values.
6766
- USER: database user, default `default`.
6867
- PASSWORD: database password, default empty.
6968

70-
```python
71-
DATABASES = {
72-
'default': {
73-
'ENGINE': 'clickhouse_backend.backend',
74-
'NAME': 'default',
75-
'HOST': 'localhost',
76-
'USER': 'DB_USER',
77-
'PASSWORD': 'DB_PASSWORD',
78-
'TEST': {
79-
'fake_transaction': True
80-
}
81-
}
82-
}
83-
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
84-
```
69+
In the most cases, you may just use clickhouse to store some big events tables,
70+
and use some RDBMS to store other tables.
71+
Here I give an example setting for clickhouse and postgresql.
72+
73+
```python
74+
DATABASES = {
75+
'default': {
76+
'ENGINE': 'django.db.backends.postgresql',
77+
'HOST': 'localhost',
78+
'USER': 'postgres',
79+
'PASSWORD': '123456',
80+
'NAME': 'postgres',
81+
},
82+
'clickhouse': {
83+
'ENGINE': 'clickhouse_backend.backend',
84+
'NAME': 'default',
85+
'HOST': 'localhost',
86+
'USER': 'DB_USER',
87+
'PASSWORD': 'DB_PASSWORD',
88+
'TEST': {
89+
'fake_transaction': True
90+
}
91+
}
92+
}
93+
DATABASE_ROUTERS = ['dbrouters.ClickHouseRouter']
94+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
95+
```
96+
97+
```python
98+
# dbrouters.py
99+
from clickhouse_backend.models import ClickhouseModel
100+
101+
def get_subclasses(class_):
102+
classes = class_.__subclasses__()
103+
104+
index = 0
105+
while index < len(classes):
106+
classes.extend(classes[index].__subclasses__())
107+
index += 1
108+
109+
return list(set(classes))
110+
111+
112+
class ClickHouseRouter:
113+
def __init__(self):
114+
self.route_model_names = set()
115+
for model in get_subclasses(ClickhouseModel):
116+
if model._meta.abstract:
117+
continue
118+
self.route_model_names.add(model._meta.label_lower)
119+
120+
def db_for_read(self, model, **hints):
121+
if (model._meta.label_lower in self.route_model_names
122+
or hints.get('clickhouse')):
123+
return 'clickhouse'
124+
return None
125+
126+
def db_for_write(self, model, **hints):
127+
if (model._meta.label_lower in self.route_model_names
128+
or hints.get('clickhouse')):
129+
return 'clickhouse'
130+
return None
131+
132+
def allow_migrate(self, db, app_label, model_name=None, **hints):
133+
if (f'{app_label}.{model_name}' in self.route_model_names
134+
or hints.get('clickhouse')):
135+
return db == 'clickhouse'
136+
elif db == 'clickhouse':
137+
return False
138+
return None
139+
```
140+
141+
You should use [database router](https://docs.djangoproject.com/en/4.1/topics/db/multi-db/#automatic-database-routing) to
142+
automatically route your queries to the right database. In the preceding example, I write a database router which route all
143+
queries from subclasses of `clickhouse_backend.models.ClickhouseModel` or custom migrations with a `clickhouse` hint key to clickhouse.
144+
All other queries are routed to the default database (postgresql).
85145

86146
`DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'` is required to working with django migration.
87147
More details will be covered in [DEFAULT_AUTO_FIELD](https://github.com/jayvynl/django-clickhouse-backend/blob/main/docs/Configurations.md#default_auto_field).
@@ -317,33 +377,7 @@ $ tox
317377
Changelog
318378
---
319379

320-
### 1.0.1 (2023-02-23)
321-
322-
- Add `return_int` parameter to `Enum[8|16]Field` to control whether to get an int or str value when querying from the database.
323-
- Fix TupleField container_class.
324-
- Add fields documentation.
325-
326-
327-
### 1.0.0 (2023-02-21)
328-
329-
- Add tests for migrations.
330-
- Fix bytes escaping.
331-
- Fix date and datetime lookup.
332-
- Add documentations.
333-
- Add lots of new field types:
334-
- Float32/64
335-
- [U]Int8/16/32/64/128/256
336-
- Date/Date32/DateTime('timezone')/DateTime64('timezone')
337-
- String/FixedString(N)
338-
- Enum8/16
339-
- Array(T)
340-
- Bool
341-
- UUID
342-
- Decimal
343-
- IPv4/IPv6
344-
- LowCardinality(T)
345-
- Tuple(T1, T2, ...)
346-
- Map(key, value)
380+
[All changelogs](https://github.com/jayvynl/django-clickhouse-backend/blob/main/CHANGELOG.md).
347381

348382

349383
License

clickhouse_backend/backend/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS):
151151
# from flush at the end of each testcase. Only use this feature when you are
152152
# aware of the effect in TransactionTestCase.
153153
self.fake_transaction = settings_dict.get("fake_transaction", False)
154+
if not self.settings_dict["NAME"]:
155+
self.settings_dict["NAME"] = "default"
154156

155157
@property
156158
def fake_transaction(self):

clickhouse_backend/backend/schema.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,12 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
406406
if old_field.null and not new_field.null:
407407
old_default = self.effective_default(old_field)
408408
new_default = self.effective_default(new_field)
409+
if hasattr(self, 'skip_default_on_alter'):
410+
skip = self.skip_default_on_alter(new_field)
411+
else:
412+
skip = self.skip_default(new_field)
409413
if (
410-
not self.skip_default_on_alter(new_field) and
414+
not skip and
411415
old_default != new_default
412416
and new_default is not None
413417
):

clickhouse_backend/models/fields/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.core import checks
55
from django.core import exceptions
6+
from django.db.models import IntegerChoices
67
from django.db.models import fields
78
from django.utils.functional import cached_property
89
from django.utils.itercompat import is_iterable
@@ -345,6 +346,8 @@ def get_prep_value(self, value):
345346
value = value.decode("utf-8")
346347
except UnicodeDecodeError:
347348
pass
349+
elif isinstance(value, IntegerChoices):
350+
value = value.value
348351
return value
349352

350353
def from_db_value(self, value, expression, connection):

clickhouse_backend/models/sql/compiler.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ def as_sql(self, *args, **kwargs):
1818
sql = "%s %s" % (prefix, sql.lstrip())
1919
if suffix:
2020
sql = "%s %s" % (sql, suffix)
21-
setting_info = getattr(self.query, "setting_info", None)
22-
if setting_info:
21+
if getattr(self.query, "setting_info", None):
2322
setting_sql, setting_params = self.connection.ops.settings_sql(
24-
**setting_info
23+
**self.query.setting_info
2524
)
2625
sql = "%s %s" % (sql, setting_sql)
2726
params = (*params, *setting_params)
@@ -169,7 +168,7 @@ def as_sql(self):
169168
result.append("WHERE 1")
170169

171170
params = (*update_params, *params)
172-
if hasattr(self.query, "setting_info") and self.query.setting_info:
171+
if getattr(self.query, "setting_info", None):
173172
setting_sql, setting_params = self.connection.ops.settings_sql(
174173
**self.query.setting_info
175174
)

clickhouse_backend/models/sql/query.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self, model, where=query.WhereNode, alias_cols=True):
1818

1919
def clone(self):
2020
obj = super().clone()
21-
obj.settings = self.setting_info.copy()
21+
obj.setting_info = self.setting_info.copy()
2222
return obj
2323

2424
def explain(self, using, format=None, type=None, **settings):
@@ -28,15 +28,18 @@ def explain(self, using, format=None, type=None, **settings):
2828
return "\n".join(compiler.explain_query())
2929

3030

31-
def clone_factory(cls):
32-
old = cls.clone
31+
def clone_decorator(cls):
32+
old_clone = cls.clone
3333

3434
def clone(self):
35-
obj = old(self)
36-
obj.settings = self.setting_info.copy()
35+
obj = old_clone(self)
36+
if hasattr(obj, "setting_info"):
37+
obj.setting_info = self.setting_info.copy()
3738
return obj
38-
return clone
3939

40+
cls.clone = clone
41+
return cls
4042

41-
subqueries.UpdateQuery.clone = clone_factory(subqueries.UpdateQuery)
42-
subqueries.DeleteQuery.clone = clone_factory(subqueries.DeleteQuery)
43+
44+
clone_decorator(subqueries.UpdateQuery)
45+
clone_decorator(subqueries.DeleteQuery)

example/config/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@
7171
DATABASES = {
7272
'default': {
7373
'ENGINE': 'clickhouse_backend.backend',
74+
'TEST': {
75+
'MIGRATE': False
76+
}
7477
}
7578
}
7679

example/testapp/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Action(IntegerChoices):
1010
DROP = 2
1111
ALERT = 3
1212
ip = models.GenericIPAddressField(default='::')
13-
ipv4 = models.GenericIPAddressField(default="127.0.0.1")
13+
ipv4 = models.IPv4Field(default="127.0.0.1")
1414
ip_nullable = models.GenericIPAddressField(null=True)
1515
port = models.UInt16Field(default=0)
1616
protocol = models.StringField(default='', low_cardinality=True)

example/testapp/tests.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
11
from django.test import TestCase
22

3-
# Create your tests here.
3+
from . import models
4+
5+
6+
class Tests(TestCase):
7+
@classmethod
8+
def setUpTestData(cls):
9+
models.Event.objects.bulk_create([
10+
models.Event() for _ in range(10)
11+
])
12+
13+
def test_create(self):
14+
models.Event.objects.create()
15+
self.assertEqual(models.Event.objects.count(), 11)
16+
17+
def test_update(self):
18+
event = models.Event.objects.create()
19+
models.Event.objects.filter(id=event.id).settings(mutations_sync=1).update(protocol="TCP")
20+
self.assertTrue(models.Event.objects.filter(protocol="TCP").exists())
21+
22+
def test_delete(self):
23+
models.Event.objects.settings(mutations_sync=1).delete()
24+
self.assertFalse(models.Event.objects.exists())

0 commit comments

Comments
 (0)