1+ from itertools import zip_longest
2+
13from django .db .models .expressions import Func , Value
4+ from django .utils .itercompat import is_iterable
25
36__all__ = [
47 "Engine" ,
2023]
2124
2225
26+ def _check_positive (value , name ):
27+ if not isinstance (value , int ) and value <= 0 :
28+ raise ValueError (f"{ name } must be positive integer." )
29+ return value
30+
31+
32+ def _check_not_negative (value , name ):
33+ if not isinstance (value , int ) and value < 0 :
34+ raise ValueError (f"{ name } must not be negative." )
35+ return value
36+
37+
38+ def _check_bool (value , name ):
39+ if value not in (0 , 1 ):
40+ raise ValueError (f"{ name } must be one of (0, 1, True, False)" )
41+ return int (value )
42+
43+
44+ def _check_str (value , name ):
45+ if not isinstance (value , str ):
46+ raise ValueError (f"{ name } must be string" )
47+ return value
48+
49+
2350class Engine (Func ):
51+ @property
52+ def function (self ):
53+ return self .__class__ .__name__
54+
2455 def deconstruct (self ):
2556 path , args , kwargs = super ().deconstruct ()
2657 if path .startswith ("clickhouse_backend.models.engines" ):
@@ -31,99 +62,197 @@ def deconstruct(self):
3162
3263
3364class BaseMergeTree (Engine ):
34- def __init__ (self , * expressions , output_field = None , ** extra ):
35- self .order_by = extra .pop ("order_by" , None )
36- assert self .order_by is not None , "order_by is required by MergeTree family."
37- self .partition_by = extra .pop ("partition_by" , None )
38- self .primary_key = extra .pop ("primary_key" , None )
39- super ().__init__ (* expressions , output_field = output_field , ** extra )
65+ max_arity = None # The max number of arguments the function accepts.
66+ # https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree#settings
67+ setting_types = {
68+ _check_positive : [
69+ "index_granularity" ,
70+ "min_index_granularity_bytes" ,
71+ "merge_max_block_size" ,
72+ "min_bytes_for_wide_part" ,
73+ "min_rows_for_wide_part" ,
74+ "max_parts_in_total" ,
75+ "max_compress_block_size" ,
76+ "min_compress_block_size" ,
77+ "max_partitions_to_read" ,
78+ ],
79+ _check_not_negative : [
80+ "index_granularity_bytes" ,
81+ "min_merge_bytes_to_use_direct_io" ,
82+ "merge_with_ttl_timeout" ,
83+ "merge_with_recompression_ttl_timeout" ,
84+ "try_fetch_recompressed_part_timeout" ,
85+ ],
86+ _check_bool : [
87+ "enable_mixed_granularity_parts" ,
88+ "use_minimalistic_part_header_in_zookeeper" ,
89+ "write_final_mark" ,
90+ ],
91+ _check_str : ["storage_policy" ],
92+ }
93+
94+ def __init__ (
95+ self ,
96+ * expressions ,
97+ order_by = None ,
98+ partition_by = None ,
99+ primary_key = None ,
100+ ** settings ,
101+ ):
102+ if self .max_arity is not None and len (expressions ) > self .max_arity :
103+ raise TypeError (
104+ "'%s' takes at most %s %s (%s given)"
105+ % (
106+ self .__class__ .__name__ ,
107+ self .max_arity ,
108+ "argument" if self .max_arity == 1 else "arguments" ,
109+ len (expressions ),
110+ )
111+ )
112+
113+ assert (
114+ order_by is not None or primary_key is not None
115+ ), "order_by or primary_key is missing."
116+ self .order_by = order_by
117+ self .primary_key = primary_key
118+ self .partition_by = partition_by
119+
120+ if order_by is not None :
121+ if isinstance (order_by , str ) or not is_iterable (order_by ):
122+ self .order_by = [order_by ]
123+ if any (o is None for o in self .order_by ):
124+ raise ValueError ("None is not allowed in order_by" )
125+
126+ if primary_key is not None :
127+ if isinstance (primary_key , str ) or not is_iterable (primary_key ):
128+ self .primary_key = [primary_key ]
129+ if any (o is None for o in self .primary_key ):
130+ raise ValueError ("None is not allowed in primary_key" )
131+
132+ # https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree#choosing-a-primary-key-that-differs-from-the-sorting-key
133+ # primary key expression tuple must be a prefix of the sorting key expression tuple.
134+ if self .order_by is not None and self .primary_key is not None :
135+ for o , p in zip_longest (self .order_by , self .primary_key ):
136+ if p is None :
137+ break
138+ if p != o :
139+ raise ValueError ("primary_key must be a prefix of order_by" )
140+
141+ normalized_settings = {}
142+ for setting , value in settings .items ():
143+ for validate , keys in self .setting_types .items ():
144+ if setting in keys :
145+ normalized_settings [setting ] = validate (value , setting )
146+ break
147+ else :
148+ raise TypeError (f"{ setting } is not a valid setting." )
149+ self .settings = normalized_settings
150+ super ().__init__ (* expressions )
40151
41152
42153class MergeTree (BaseMergeTree ):
43- function = "MergeTree"
44154 arity = 0
45155
46156
47157class ReplacingMergeTree (BaseMergeTree ):
48- function = "ReplacingMergeTree"
158+ max_arity = 2
49159
50160
51161class SummingMergeTree (BaseMergeTree ):
52- function = "SummingMergeTree"
162+ pass
53163
54164
55165class AggregatingMergeTree (BaseMergeTree ):
56- function = "AggregatingMergeTree"
57166 arity = 0
58167
59168
60169class CollapsingMergeTree (BaseMergeTree ):
61- function = "CollapsingMergeTree"
62170 arity = 1
63171
64172
65173class VersionedCollapsingMergeTree (BaseMergeTree ):
66- function = "CollapsingMergeTree"
67174 arity = 2
68175
69176
70177class GraphiteMergeTree (BaseMergeTree ):
71- function = "GraphiteMergeTree"
72178 arity = 1
73179
180+ def __init__ (self , * expressions , ** extra ):
181+ if expressions :
182+ expressions = (Value (expressions [0 ]), * expressions [1 :])
183+ super ().__init__ (* expressions , ** extra )
184+
74185
75186class ReplicatedMixin :
76187 def __init__ (self , * expressions , ** extra ):
77188 if self .arity is not None and len (expressions ) != self .arity + 2 :
78189 raise TypeError (
79- "'%s' takes exactly %s %s (%s given)"
190+ "'%s' takes exactly %s arguments (%s given)"
80191 % (
81192 self .__class__ .__name__ ,
82193 self .arity + 2 ,
83- "arguments" ,
84194 len (expressions ),
85195 )
86196 )
87197 if self .arity is None and len (expressions ) < 2 :
88198 raise TypeError (
89- "'%s' takes at least %s %s (%s given)"
199+ "'%s' takes at least 2 arguments (%s given)"
90200 % (
91201 self .__class__ .__name__ ,
92- 2 ,
93- "arguments" ,
94202 len (expressions ),
95203 )
96204 )
97- replicated_params = tuple (Value (arg ) for arg in self .expressions [:2 ])
205+ if self .max_arity is not None and len (expressions ) > self .max_arity + 2 :
206+ raise TypeError (
207+ "'%s' takes at most %s arguments (%s given)"
208+ % (
209+ self .__class__ .__name__ ,
210+ self .max_arity + 2 ,
211+ len (expressions ),
212+ )
213+ )
214+ replicated_params = map (Value , self .expressions [:2 ])
98215 super ().__init__ (* expressions [2 :], ** extra )
99- self .expressions = replicated_params + self .expressions
216+ self .expressions = ( * replicated_params , * self .expressions )
100217
101218
102- class ReplicatedMergeTree (ReplicatedMixin , MergeTree ):
103- function = "ReplicatedMergeTree"
219+ class ReplicatedMergeTree (MergeTree ):
220+ def __init__ (self , * expressions , ** extra ):
221+ # https://github.com/ClickHouse/ClickHouse/issues/8675
222+ # https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication#creating-replicated-tables
223+ # You can specify default arguments for Replicated table engine in the server configuration file.
224+ # In this case, you can omit arguments when creating tables.
225+ if expressions :
226+ if len (expressions ) != 2 :
227+ raise TypeError (
228+ "'ReplicatedMergeTree' takes at 0 or 2 arguments (%s given)"
229+ % len (expressions )
230+ )
231+ expressions = map (Value , expressions )
232+ super ().__init__ (* expressions , ** extra )
104233
105234
106235class ReplicatedReplacingMergeTree (ReplicatedMixin , ReplacingMergeTree ):
107- function = "ReplicatedReplacingMergeTree"
236+ pass
108237
109238
110239class ReplicatedSummingMergeTree (ReplicatedMixin , SummingMergeTree ):
111- function = "ReplicatedSummingMergeTree"
240+ pass
112241
113242
114243class ReplicatedAggregatingMergeTree (ReplicatedMixin , AggregatingMergeTree ):
115- function = "ReplicatedAggregatingMergeTree"
244+ pass
116245
117246
118247class ReplicatedCollapsingMergeTree (ReplicatedMixin , CollapsingMergeTree ):
119- function = "ReplicatedCollapsingMergeTree"
248+ pass
120249
121250
122251class ReplicatedVersionedCollapsingMergeTree (
123252 ReplicatedMixin , VersionedCollapsingMergeTree
124253):
125- function = "ReplicatedCollapsingMergeTree"
254+ pass
126255
127256
128257class ReplicatedGraphiteMergeTree (ReplicatedMixin , GraphiteMergeTree ):
129- function = "ReplicatedGraphiteMergeTree"
258+ pass
0 commit comments