Skip to content

Commit 871526e

Browse files
committed
Add tests for backends and fix bugs that find by them. Remove redundant code. Correct database features.
1 parent 95f9186 commit 871526e

29 files changed

+1905
-141
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
0.2.1 (2022-10-30)
2+
---
3+
4+
- Add tests for backends.
5+
- Remove redundant code.
6+
- Correct database features.
7+
- Fix bugs that find by new tests.
8+
19
0.2.0 (2022-10-26)
210
---
311

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Django ClickHouse Database Backend
22
===
33

4-
[中文文档](README_cn.md)
4+
[中文文档](https://github.com/jayvynl/django-clickhouse-backend/blob/main/README_cn.md)
55

66
Django clickhouse backend is a [django database backend](https://docs.djangoproject.com/en/4.1/ref/databases/) for
77
[clickhouse](https://clickhouse.com/docs/en/home/) database. This project allows using django ORM to interact with
@@ -26,7 +26,7 @@ Get started
2626
### Installation
2727

2828
```shell
29-
pip install git+https://github.com/jayvynl/django-clickhouse-backend
29+
pip install django-clickhouse-backend
3030
```
3131

3232
or

clickhouse_backend/VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.0
1+
0.2.1

clickhouse_backend/backend/base.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .introspection import DatabaseIntrospection # NOQA
1515
from .operations import DatabaseOperations # NOQA
1616
from .schema import DatabaseSchemaEditor # NOQA
17+
from django.utils.functional import cached_property
1718

1819

1920
class DatabaseWrapper(BaseDatabaseWrapper):
@@ -58,8 +59,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
5859
'lte': '<= %s',
5960
'startswith': 'LIKE %s',
6061
'endswith': 'LIKE %s',
61-
'istartswith': 'LIKE UPPER(%s)',
62-
'iendswith': 'LIKE UPPER(%s)',
62+
'istartswith': 'ILIKE %s',
63+
'iendswith': 'ILIKE %s',
6364
}
6465

6566
# The patterns below are used to generate SQL pattern lookup clauses when
@@ -73,11 +74,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
7374
pattern_esc = r"replaceAll(replaceAll(replaceAll({}, '\\', '\\\\'), '%', '\\%'), '_', '\\_')"
7475
pattern_ops = {
7576
'contains': "LIKE '%%' || {} || '%%'",
76-
'icontains': "LIKE '%%' || UPPER({}) || '%%'",
77+
'icontains': "ILIKE '%%' || {} || '%%'",
7778
'startswith': "LIKE {} || '%%'",
78-
'istartswith': "LIKE UPPER({}) || '%%'",
79+
'istartswith': "ILIKE {} || '%%'",
7980
'endswith': "LIKE '%%' || {}",
80-
'iendswith': "LIKE '%%' || UPPER({})",
81+
'iendswith': "ILIKE '%%' || {}",
8182
}
8283

8384
Database = Database
@@ -151,17 +152,24 @@ def _set_autocommit(self, autocommit):
151152

152153
def is_usable(self):
153154
try:
154-
# Use a psycopg cursor directly, bypassing Django's utilities.
155+
# Use a clickhouse_driver cursor directly, bypassing Django's utilities.
155156
with self.connection.cursor() as cursor:
156157
cursor.execute('SELECT 1')
157158
except Database.Error:
158159
return False
159160
else:
160161
return True
161162

162-
def make_debug_cursor(self, cursor):
163-
return CursorDebugWrapper(cursor, self)
164-
165-
166-
class CursorDebugWrapper(BaseCursorDebugWrapper):
167-
pass
163+
@cached_property
164+
def ch_version(self):
165+
with self.temporary_connection() as cursor:
166+
cursor.execute("SELECT version()")
167+
row = cursor.fetchone()
168+
return row[0]
169+
170+
def get_database_version(self):
171+
"""
172+
Return a tuple of the database's version.
173+
E.g. for ch_version "22.9.3.18", return (22, 9, 3, 18).
174+
"""
175+
return tuple(map(int, self.ch_version.split(".")))

clickhouse_backend/backend/creation.py

+15-19
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,37 @@ class DatabaseCreation(BaseDatabaseCreation):
1010
def _quote_name(self, name):
1111
return self.connection.ops.quote_name(name)
1212

13-
def _get_database_create_suffix(self, engine=None):
14-
suffix = ""
15-
if engine:
16-
suffix += "ENGINE = {}".format(engine)
17-
return suffix
18-
1913
def sql_table_creation_suffix(self):
20-
test_settings = self.connection.settings_dict['TEST']
21-
return self._get_database_create_suffix(
22-
engine=test_settings.get('ENGINE'),
23-
)
14+
test_settings = self.connection.settings_dict["TEST"]
15+
engine = test_settings.get("ENGINE")
16+
if engine:
17+
return "ENGINE = %s" % engine
18+
return ""
2419

2520
def _database_exists(self, cursor, database_name):
26-
cursor.execute('SELECT 1 FROM system.databases WHERE name = %s', [strip_quotes(database_name)])
21+
cursor.execute(
22+
"SELECT 1 FROM system.databases WHERE name = %s",
23+
[strip_quotes(database_name)]
24+
)
2725
return cursor.fetchone() is not None
2826

2927
def create_test_db(self, verbosity=1, autoclobber=False, serialize=True, keepdb=False):
3028
super().create_test_db(verbosity, autoclobber, serialize, keepdb)
31-
self.connection.settings_dict.setdefault(
32-
'fake_transaction',
33-
self.connection.settings_dict['TEST'].get('fake_transaction', False)
34-
)
35-
self.connection.fake_transaction = self.connection.settings_dict['fake_transaction']
29+
test_settings = self.connection.settings_dict["TEST"]
30+
if "fake_transaction" in test_settings:
31+
self.connection.fake_transaction = test_settings["fake_transaction"]
3632

3733
def _execute_create_test_db(self, cursor, parameters, keepdb=False):
3834
try:
39-
if keepdb and self._database_exists(cursor, parameters['dbname']):
35+
if keepdb and self._database_exists(cursor, parameters["dbname"]):
4036
# If the database should be kept and it already exists, don't
4137
# try to create a new one.
4238
return
4339
super()._execute_create_test_db(cursor, parameters, keepdb)
4440
except Exception as e:
45-
if not e.args or getattr(e.args[0], 'code', '') != ErrorCodes.DATABASE_ALREADY_EXISTS:
41+
if not e.args or getattr(e.args[0], "code", "") != ErrorCodes.DATABASE_ALREADY_EXISTS:
4642
# All errors except "database already exists" cancel tests.
47-
self.log('Got an error creating the test database: %s' % e)
43+
self.log("Got an error creating the test database: %s" % e)
4844
sys.exit(2)
4945
elif not keepdb:
5046
# If the database should be kept, ignore "database already

clickhouse_backend/backend/features.py

+11-24
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55

66
class DatabaseFeatures(BaseDatabaseFeatures):
7+
# TODO: figure out compatible clickhouse version.
8+
minimum_database_version = None
79
# Use this class attribute control whether using fake transaction.
810
# Fake transaction is used in test, prevent other database such as postgresql
911
# from flush at the end of each testcase. Only use this feature when you are
@@ -17,33 +19,31 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1719
# https://clickhouse.com/docs/en/sql-reference/data-types/string/
1820
# allows_group_by_lob = True
1921

20-
# In clickhouse_backend MergeTree table family, PK have different meaning as in RDBMS
21-
# PK is used for efficient range scans, pk can be duplicated, no auto incr pk.
22-
# https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree/#primary-keys-and-indexes-in-queries
23-
allows_group_by_pk = False
24-
allows_group_by_selected_pks = False
25-
2622
# There is no unique constraint in clickhouse_backend.
2723
# supports_nullable_unique_constraints = True
2824
# supports_partially_nullable_unique_constraints = True
29-
supports_deferrable_unique_constraints = True
25+
# supports_deferrable_unique_constraints = False
3026

31-
# Transaction is not supported, and limited transaction is under development.
27+
# Clickhouse only supports limited transaction.
3228
# https://clickhouse.com/docs/en/sql-reference/ansi/
3329
# https://github.com/ClickHouse/ClickHouse/issues/32513
30+
# https://clickhouse.com/docs/en/guides/developer/transactional
3431
@cached_property
3532
def uses_savepoints(self):
3633
return self.fake_transaction
34+
3735
can_release_savepoints = False
3836

3937
# Is there a true datatype for uuid?
4038
has_native_uuid_field = True
4139

4240
# Clickhouse use re2 syntax which does not support backreference.
41+
# https://clickhouse.com/docs/en/sql-reference/functions/string-search-functions#matchhaystack-pattern
42+
# https://github.com/google/re2/wiki/Syntax
4343
supports_regex_backreferencing = False
4444

4545
# Can date/datetime lookups be performed using a string?
46-
supports_date_lookup_using_string = False
46+
supports_date_lookup_using_string = True
4747

4848
# Confirm support for introspected foreign keys
4949
# Every database can do this reliably, except MySQL,
@@ -57,32 +57,27 @@ def uses_savepoints(self):
5757
'BinaryField': 'BinaryField',
5858
'BooleanField': 'BooleanField',
5959
'CharField': 'CharField',
60-
'DurationField': 'DurationField',
6160
'GenericIPAddressField': 'GenericIPAddressField',
6261
'IntegerField': 'IntegerField',
6362
'PositiveBigIntegerField': 'PositiveBigIntegerField',
6463
'PositiveIntegerField': 'PositiveIntegerField',
6564
'PositiveSmallIntegerField': 'PositiveSmallIntegerField',
6665
'SmallIntegerField': 'SmallIntegerField',
67-
'TimeField': 'TimeField',
6866
}
6967

7068
# https://clickhouse.com/docs/en/sql-reference/statements/alter/index/
7169
# Index manipulation is supported only for tables with *MergeTree* engine (including replicated variants).
7270
supports_index_column_ordering = False
7371

7472
# Does the backend support introspection of materialized views?
75-
can_introspect_materialized_views = False
73+
can_introspect_materialized_views = True
7674

7775
# Support for the DISTINCT ON clause
7876
can_distinct_on_fields = True
7977

8078
# Does the backend prevent running SQL queries in broken transactions?
8179
atomic_transactions = False
8280

83-
# Does it support operations requiring references rename in a transaction?
84-
supports_atomic_references_rename = False
85-
8681
# Can we issue more than one ALTER COLUMN clause in an ALTER TABLE?
8782
supports_combined_alters = True
8883

@@ -93,14 +88,11 @@ def uses_savepoints(self):
9388
supports_column_check_constraints = False
9489
supports_table_check_constraints = True
9590
# Does the backend support introspection of CHECK constraints?
96-
can_introspect_check_constraints = False
91+
can_introspect_check_constraints = True
9792

9893
# What kind of error does the backend throw when accessing closed cursor?
9994
closed_cursor_error_class = InterfaceError
10095

101-
# Does 'a' LIKE 'A' match?
102-
has_case_insensitive_like = False
103-
10496
# https://clickhouse.com/docs/en/sql-reference/statements/insert-into/#constraints
10597
supports_ignore_conflicts = False
10698

@@ -119,11 +111,6 @@ def uses_savepoints(self):
119111
# SQL template override for tests.aggregation.tests.NowUTC
120112
test_now_utc_template = 'now64()'
121113

122-
@cached_property
123-
def supports_explaining_query_execution(self):
124-
"""Does this backend support explaining query execution?"""
125-
return True
126-
127114
@cached_property
128115
def supports_transactions(self):
129116
return self.fake_transaction

clickhouse_backend/backend/introspection.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
1717
# Maps type codes to Django Field types.
1818
data_types_reverse = {
1919
# 'String': 'BinaryField',
20-
# The best type for String is BinaryField, but sometime you may need TextField.
20+
# The best type for String is BinaryField, but sometimes you may need TextField.
2121
'String': 'TextField',
2222
'Int64': 'BigIntegerField',
2323
'Int16': 'SmallIntegerField',
@@ -27,7 +27,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
2727
'UInt32': 'PositiveIntegerField',
2828
'Float32': 'FloatField',
2929
'Float64': 'FloatField',
30-
'IPv4': 'IPAddressField',
30+
'IPv4': 'GenericIPAddressField',
3131
'IPv6': 'GenericIPAddressField',
3232
'Date': 'DateField',
3333
'Date32': 'DateField',
@@ -61,23 +61,18 @@ def get_table_list(self, cursor):
6161

6262
def get_table_description(self, cursor, table_name):
6363
"""
64-
Return a description of the table with the DB-API cursor.description
65-
interface.
64+
Return a description of the table.
6665
"""
67-
# Query the pg_catalog tables as cursor.description does not reliably
68-
# return the nullable property and information_schema.columns does not
69-
# contain details of materialized views.
66+
# Query the INFORMATION_SCHEMA.COLUMNS table.
7067
cursor.execute("""
7168
SELECT column_name, data_type, NULL, character_maximum_length,
7269
coalesce(numeric_precision, datetime_precision),
73-
numeric_scale, is_nullable, column_default
70+
numeric_scale, is_nullable, column_default, NULL
7471
FROM INFORMATION_SCHEMA.COLUMNS
7572
WHERE table_catalog = currentDatabase() AND table_name = %s
7673
""", [table_name])
7774
return [
78-
FieldInfo(
79-
*line, None
80-
)
75+
FieldInfo(*line)
8176
for line in cursor.fetchall()
8277
]
8378

@@ -118,7 +113,10 @@ def get_constraints(self, cursor, table_name):
118113
return constraints
119114

120115
@cached_property
121-
def settings(self):
116+
def settings(self) -> set:
117+
"""
118+
Get all available settings.
119+
"""
122120
with self.connection.cursor() as cursor:
123121
cursor.execute("SELECT name from system.settings")
124122
rows = cursor.fetchall()

clickhouse_backend/backend/operations.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.conf import settings
44
from django.db.backends.base.operations import BaseDatabaseOperations
5+
56
from clickhouse_backend import compat
67

78

@@ -211,10 +212,6 @@ def lookup_cast(self, lookup_type, internal_type=None):
211212
else:
212213
lookup = "CAST(%s, 'Nullable(String)')"
213214

214-
# Use UPPER(x) for case-insensitive lookups; it"s faster.
215-
if lookup_type in ("iexact", "icontains", "istartswith", "iendswith"):
216-
lookup = "UPPER(%s)" % lookup
217-
218215
return lookup
219216

220217
def max_name_length(self):
@@ -352,6 +349,13 @@ def savepoint_rollback_sql(self, sid):
352349
return "SELECT 1"
353350
return super().savepoint_rollback_sql(sid)
354351

352+
def last_executed_query(self, cursor, sql, params):
353+
if sql.startswith("INSERT INTO"):
354+
return "%s %s" % (sql, ", ".join(map(str, params)))
355+
if params:
356+
return sql % params
357+
return sql
358+
355359
def settings_sql(self, **settings):
356360
result = []
357361
params = []

0 commit comments

Comments
 (0)