tinybird-toolset 2.4.13__tar.gz → 2.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tinybird_toolset-2.4.13/src/tinybird_toolset.egg-info → tinybird_toolset-2.5.0}/PKG-INFO +1 -1
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/conf.py +31 -3
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/setup.py +2 -1
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/chtoolset/query.py +1 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0/src/tinybird_toolset.egg-info}/PKG-INFO +1 -1
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/tinybird_toolset.egg-info/SOURCES.txt +2 -1
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_convert_to_row_binary.py +6 -4
- tinybird_toolset-2.5.0/tests/test_validate_ttl.py +394 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/LICENSE +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/MANIFEST.in +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/README.md +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/setup.cfg +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/chtoolset/__init__.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/tinybird_toolset.egg-info/dependency_links.txt +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/tinybird_toolset.egg-info/requires.txt +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/tinybird_toolset.egg-info/top_level.txt +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_check_compatible_types.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_check_write_query.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_chquery.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_explain_ast.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_get_columns_from_create_query.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_get_left_table.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_internal_cache.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_normalize_query_keep_names.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_normalize_table_column.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_parse_create_materialized_view_target_table.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_replace_tables.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_replace_tables_backward_compat.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_rewrite_aggregation_states.py +0 -0
- {tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_tables.py +0 -0
|
@@ -94,9 +94,11 @@ class ClickHouseBuildExt(build_ext):
|
|
|
94
94
|
if not os.path.exists(os.path.join(CLICKHOUSE_PATH, 'CMakeLists.txt')):
|
|
95
95
|
raise RuntimeError('Git submodules are not initialized. Run: `git submodule update --init --recursive`.')
|
|
96
96
|
|
|
97
|
-
# One of the libraries, chosen randomly
|
|
97
|
+
# One of the libraries, chosen randomly (+ the TTL functions one, added later)
|
|
98
98
|
if os.path.isfile(os.path.join(CLICKHOUSE_BUILD_PATH,
|
|
99
|
-
'src/AggregateFunctions/libclickhouse_aggregate_functions.a'))
|
|
99
|
+
'src/AggregateFunctions/libclickhouse_aggregate_functions.a')) \
|
|
100
|
+
and os.path.isfile(os.path.join(CLICKHOUSE_BUILD_PATH,
|
|
101
|
+
'src/Functions/libclickhouse_ttl_functions.a')):
|
|
100
102
|
return
|
|
101
103
|
|
|
102
104
|
# Avoid segfault caused by missing global context
|
|
@@ -138,9 +140,18 @@ class ClickHouseBuildExt(build_ext):
|
|
|
138
140
|
# Removes TSA warnings (we don't compile with a libc++ with annotations)
|
|
139
141
|
apply_patch("patches/tsa.patch")
|
|
140
142
|
|
|
141
|
-
#
|
|
143
|
+
# Builds the functions needed by validate_ttl into libclickhouse_ttl_functions.a,
|
|
144
|
+
# which is linked with --whole-archive/-force_load (see extra_libs).
|
|
145
|
+
# NOTE: after editing this patch (or any other), the early-return above skips the
|
|
146
|
+
# ClickHouse build; delete ch_build/src/Functions/libclickhouse_ttl_functions.a
|
|
147
|
+
# (or the whole ch_build) to force it to run again.
|
|
142
148
|
apply_patch("patches/dbmsfunctions.patch")
|
|
143
149
|
|
|
150
|
+
# Range-check narrow-integer Field inserts so JSON/Dynamic paths widen instead of
|
|
151
|
+
# silently wrapping (1944 -> -104). Backport of the ColumnVector::tryInsert fix;
|
|
152
|
+
# remove once the fix lands in the pinned ClickHouse submodule.
|
|
153
|
+
apply_patch("patches/ColumnVectorTryInsert.patch")
|
|
154
|
+
|
|
144
155
|
if sys.platform == 'darwin':
|
|
145
156
|
# We disable whole_archive options destined for AppleClang, which is unsupported but keeps being a PITA
|
|
146
157
|
apply_patch("patches/darwin.patch")
|
|
@@ -243,6 +254,14 @@ class ClickHouseBuildExt(build_ext):
|
|
|
243
254
|
['--config', 'Release',
|
|
244
255
|
'--target', 'src/AggregateFunctions/libclickhouse_aggregate_functions.a'])
|
|
245
256
|
|
|
257
|
+
# Functions for TTL validation, also with MinSize as performance is not a concern.
|
|
258
|
+
# This library is linked with --whole-archive/-force_load (see extra_libs) so that
|
|
259
|
+
# its REGISTER_FUNCTION static registrars run and DB::registerFunctions() can
|
|
260
|
+
# register all of them without listing them one by one.
|
|
261
|
+
subprocess.check_call([cmake_cmd, '--build', CLICKHOUSE_BUILD_PATH] +
|
|
262
|
+
['--config', 'Release',
|
|
263
|
+
'--target', 'src/Functions/libclickhouse_ttl_functions.a'])
|
|
264
|
+
|
|
246
265
|
# Now build with Release just the parser, which is where we want speed
|
|
247
266
|
subprocess.check_call([cmake_cmd, CLICKHOUSE_PATH] + cmake_args + ['-DCMAKE_BUILD_TYPE=Release'],
|
|
248
267
|
cwd=CLICKHOUSE_BUILD_PATH)
|
|
@@ -375,6 +394,15 @@ class CustomBuildWithFromCH(build_ext):
|
|
|
375
394
|
# - Remove .o. These files only contain function declaration and we don't need them
|
|
376
395
|
ch_libs = list(filter(lambda lib: lib and lib.find('whole-archive') == -1 and lib.find('-lm') == -1, ch_libs))
|
|
377
396
|
|
|
397
|
+
# Force-load the TTL validation functions so their REGISTER_FUNCTION static
|
|
398
|
+
# registrars run even though nothing references their symbols directly.
|
|
399
|
+
# DB::registerFunctions() (called from ValidateTTL.cpp) registers all of them.
|
|
400
|
+
ttl_functions_lib = os.path.join(CLICKHOUSE_BUILD_PATH, 'src/Functions/libclickhouse_ttl_functions.a')
|
|
401
|
+
if sys.platform == 'darwin':
|
|
402
|
+
ch_libs = [f'-Wl,-force_load,{ttl_functions_lib}'] + ch_libs
|
|
403
|
+
else:
|
|
404
|
+
ch_libs = ['-Wl,--whole-archive', ttl_functions_lib, '-Wl,--no-whole-archive'] + ch_libs
|
|
405
|
+
|
|
378
406
|
if sys.platform == 'linux':
|
|
379
407
|
ch_libs = ['-Wl,--no-whole-archive'] + ch_libs
|
|
380
408
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from setuptools import setup, Extension
|
|
2
2
|
|
|
3
3
|
NAME = 'tinybird-toolset'
|
|
4
|
-
VERSION = '2.
|
|
4
|
+
VERSION = '2.5.0'
|
|
5
5
|
|
|
6
6
|
try:
|
|
7
7
|
from conf import *
|
|
@@ -20,6 +20,7 @@ try:
|
|
|
20
20
|
"functions/ReplaceTables.h",
|
|
21
21
|
"functions/Tables.h",
|
|
22
22
|
"functions/TBQueryParser.h",
|
|
23
|
+
"functions/ValidateTTL.h",
|
|
23
24
|
"functions/Validation.h",
|
|
24
25
|
"functions/simdjsonHelpers.h",
|
|
25
26
|
"functions/JSONPathQuery.h",
|
|
@@ -20,6 +20,7 @@ from chtoolset._query import replace_tables, \
|
|
|
20
20
|
set_row_binary_encoder_thread_buffer_size, \
|
|
21
21
|
get_columns_from_create_query, \
|
|
22
22
|
normalize_table_column, \
|
|
23
|
+
validate_ttl, \
|
|
23
24
|
parse_create_materialized_view_target_table as _parse_create_materialized_view_target_table
|
|
24
25
|
|
|
25
26
|
from typing import Optional, TypedDict
|
|
@@ -8346,20 +8346,22 @@ TEST_CASES = [
|
|
|
8346
8346
|
1, 0),
|
|
8347
8347
|
|
|
8348
8348
|
|
|
8349
|
-
# schemaless JSON encoded as string
|
|
8349
|
+
# schemaless JSON encoded as string widens too-narrow integer types instead of
|
|
8350
|
+
# overflowing: y=22 first pins the path to Int8, then y=220 widens it to Int16
|
|
8351
|
+
# rather than wrapping to -36 (CH-440, ColumnVector::tryInsert range check)
|
|
8350
8352
|
(ConversionMode.ONLY_LEGACY,
|
|
8351
8353
|
[('v', 'Map(String, JSON)', '$', False)],
|
|
8352
8354
|
'{"a":{"x":11,"y":22},"b":{"x":110,"y":220}}',
|
|
8353
|
-
b'\x00\x02\x01a\x0f{"x":11,"y":22}\x01b\x11{"x":110,"y"
|
|
8355
|
+
b'\x00\x02\x01a\x0f{"x":11,"y":22}\x01b\x11{"x":110,"y":220}',
|
|
8354
8356
|
b'',
|
|
8355
8357
|
1, 0),
|
|
8356
8358
|
(ConversionMode.ONLY_LEGACY,
|
|
8357
8359
|
[('json', 'Map(String, JSON)', "$", False)],
|
|
8358
8360
|
'{"a":"{\\"x\\":11,\\"y\\":22}","b":"{\\"x\\":110,\\"y\\":220}"}',
|
|
8359
|
-
b'\x00\x02\x01a\x0f{"x":11,"y":22}\x01b\x11{"x":110,"y"
|
|
8361
|
+
b'\x00\x02\x01a\x0f{"x":11,"y":22}\x01b\x11{"x":110,"y":220}',
|
|
8360
8362
|
b'',
|
|
8361
8363
|
1, 0),
|
|
8362
|
-
#
|
|
8364
|
+
# binary JSON encoding stores per-value type tags, so it already preserved the value
|
|
8363
8365
|
(ConversionMode.ONLY_NONLEGACY,
|
|
8364
8366
|
[('v', 'Map(String, JSON)', '$', False)],
|
|
8365
8367
|
'{"a":{"x":11,"y":22},"b":{"x":110,"y":220}}',
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from chtoolset import query as chquery
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestValidateTTL(unittest.TestCase):
|
|
6
|
+
def test_empty_ttl_is_valid(self):
|
|
7
|
+
self.assertIsNone(
|
|
8
|
+
chquery.validate_ttl(
|
|
9
|
+
ttl="",
|
|
10
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
11
|
+
sorting_key="ts",
|
|
12
|
+
primary_key="ts",
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def test_valid_no_primary_key_uses_sorting_key(self):
|
|
17
|
+
self.assertIsNone(
|
|
18
|
+
chquery.validate_ttl(
|
|
19
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
20
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
21
|
+
sorting_key="ts",
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def test_valid_datetime_plus_interval(self):
|
|
26
|
+
self.assertIsNone(
|
|
27
|
+
chquery.validate_ttl(
|
|
28
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
29
|
+
columns=[{"name": "ts", "type": "DateTime"}, {"name": "v", "type": "Int32"}],
|
|
30
|
+
sorting_key="ts",
|
|
31
|
+
primary_key="ts",
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def test_valid_datetime_plus_to_interval(self):
|
|
36
|
+
self.assertIsNone(
|
|
37
|
+
chquery.validate_ttl(
|
|
38
|
+
ttl="ts + toIntervalDay(7)",
|
|
39
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
40
|
+
sorting_key="ts",
|
|
41
|
+
primary_key="ts",
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def test_valid_date_column(self):
|
|
46
|
+
self.assertIsNone(
|
|
47
|
+
chquery.validate_ttl(
|
|
48
|
+
ttl="d + INTERVAL 30 DAY",
|
|
49
|
+
columns=[{"name": "d", "type": "Date"}],
|
|
50
|
+
sorting_key="d",
|
|
51
|
+
primary_key="d",
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def test_valid_wrapped_datetime64(self):
|
|
56
|
+
self.assertIsNone(
|
|
57
|
+
chquery.validate_ttl(
|
|
58
|
+
ttl="toDateTime(event_time) + toIntervalDay(7)",
|
|
59
|
+
columns=[{"name": "event_time", "type": "DateTime64(3)"}],
|
|
60
|
+
sorting_key="event_time",
|
|
61
|
+
primary_key="event_time",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def test_invalid_non_date_result_type(self):
|
|
66
|
+
err = chquery.validate_ttl(
|
|
67
|
+
ttl="counter + toIntervalDay(7)",
|
|
68
|
+
columns=[{"name": "counter", "type": "Int32"}],
|
|
69
|
+
sorting_key="counter",
|
|
70
|
+
primary_key="counter",
|
|
71
|
+
)
|
|
72
|
+
self.assertIsNotNone(err)
|
|
73
|
+
self.assertIn("Illegal types", err)
|
|
74
|
+
|
|
75
|
+
def test_valid_datetime_plus_integer_expression(self):
|
|
76
|
+
self.assertIsNone(
|
|
77
|
+
chquery.validate_ttl(
|
|
78
|
+
ttl="ts + 7",
|
|
79
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
80
|
+
sorting_key="ts",
|
|
81
|
+
primary_key="ts",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def test_invalid_unwrapped_datetime64(self):
|
|
86
|
+
err = chquery.validate_ttl(
|
|
87
|
+
ttl="event_time + toIntervalDay(7)",
|
|
88
|
+
columns=[{"name": "event_time", "type": "DateTime64(3)"}],
|
|
89
|
+
sorting_key="event_time",
|
|
90
|
+
primary_key="event_time",
|
|
91
|
+
)
|
|
92
|
+
self.assertIsNotNone(err)
|
|
93
|
+
self.assertIn("TTL expression result column should have DateTime or Date type", err)
|
|
94
|
+
|
|
95
|
+
def test_invalid_unknown_column(self):
|
|
96
|
+
err = chquery.validate_ttl(
|
|
97
|
+
ttl="missing + INTERVAL 7 DAY",
|
|
98
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
99
|
+
sorting_key="ts",
|
|
100
|
+
primary_key="ts",
|
|
101
|
+
)
|
|
102
|
+
self.assertIsNotNone(err)
|
|
103
|
+
self.assertIn("Missing columns: 'missing'", err)
|
|
104
|
+
|
|
105
|
+
def test_invalid_malformed_ttl(self):
|
|
106
|
+
err = chquery.validate_ttl(
|
|
107
|
+
ttl="this is not a valid ttl !!!",
|
|
108
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
109
|
+
sorting_key="ts",
|
|
110
|
+
primary_key="ts",
|
|
111
|
+
)
|
|
112
|
+
self.assertIsNotNone(err)
|
|
113
|
+
self.assertIn("Syntax error", err)
|
|
114
|
+
|
|
115
|
+
# --- Date/time functions supported by ClickHouse in TTL expressions ---
|
|
116
|
+
def test_valid_from_unix_timestamp(self):
|
|
117
|
+
self.assertIsNone(
|
|
118
|
+
chquery.validate_ttl(
|
|
119
|
+
ttl="fromUnixTimestamp(uts) + INTERVAL 7 DAY",
|
|
120
|
+
columns=[{"name": "uts", "type": "UInt32"}],
|
|
121
|
+
sorting_key="uts",
|
|
122
|
+
primary_key="uts",
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def test_valid_from_unix_timestamp64_milli(self):
|
|
127
|
+
self.assertIsNone(
|
|
128
|
+
chquery.validate_ttl(
|
|
129
|
+
ttl="toDateTime(fromUnixTimestamp64Milli(uts)) + INTERVAL 7 DAY",
|
|
130
|
+
columns=[{"name": "uts", "type": "Int64"}],
|
|
131
|
+
sorting_key="uts",
|
|
132
|
+
primary_key="uts",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def test_valid_from_unix_timestamp64_micro_and_nano(self):
|
|
137
|
+
for fn in ("fromUnixTimestamp64Micro", "fromUnixTimestamp64Nano"):
|
|
138
|
+
with self.subTest(fn=fn):
|
|
139
|
+
self.assertIsNone(
|
|
140
|
+
chquery.validate_ttl(
|
|
141
|
+
ttl=f"toDateTime({fn}(uts)) + INTERVAL 1 DAY",
|
|
142
|
+
columns=[{"name": "uts", "type": "Int64"}],
|
|
143
|
+
sorting_key="uts",
|
|
144
|
+
primary_key="uts",
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def test_valid_to_start_of_functions(self):
|
|
149
|
+
for fn in (
|
|
150
|
+
"toStartOfDay",
|
|
151
|
+
"toStartOfHour",
|
|
152
|
+
"toStartOfMinute",
|
|
153
|
+
"toStartOfFifteenMinutes",
|
|
154
|
+
"toStartOfMonth",
|
|
155
|
+
"toStartOfQuarter",
|
|
156
|
+
"toStartOfYear",
|
|
157
|
+
"toStartOfWeek",
|
|
158
|
+
"toMonday",
|
|
159
|
+
"toLastDayOfMonth",
|
|
160
|
+
):
|
|
161
|
+
with self.subTest(fn=fn):
|
|
162
|
+
self.assertIsNone(
|
|
163
|
+
chquery.validate_ttl(
|
|
164
|
+
ttl=f"{fn}(ts) + INTERVAL 30 DAY",
|
|
165
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
166
|
+
sorting_key="ts",
|
|
167
|
+
primary_key="ts",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def test_valid_to_start_of_interval(self):
|
|
172
|
+
self.assertIsNone(
|
|
173
|
+
chquery.validate_ttl(
|
|
174
|
+
ttl="toStartOfInterval(ts, INTERVAL 1 DAY) + INTERVAL 90 DAY",
|
|
175
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
176
|
+
sorting_key="ts",
|
|
177
|
+
primary_key="ts",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def test_valid_date_trunc(self):
|
|
182
|
+
self.assertIsNone(
|
|
183
|
+
chquery.validate_ttl(
|
|
184
|
+
ttl="dateTrunc('month', ts) + INTERVAL 6 MONTH",
|
|
185
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
186
|
+
sorting_key="ts",
|
|
187
|
+
primary_key="ts",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def test_valid_to_timezone(self):
|
|
192
|
+
self.assertIsNone(
|
|
193
|
+
chquery.validate_ttl(
|
|
194
|
+
ttl="toTimezone(ts, 'UTC') + INTERVAL 7 DAY",
|
|
195
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
196
|
+
sorting_key="ts",
|
|
197
|
+
primary_key="ts",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def test_valid_make_date(self):
|
|
202
|
+
self.assertIsNone(
|
|
203
|
+
chquery.validate_ttl(
|
|
204
|
+
ttl="makeDate(y, m, d) + INTERVAL 7 DAY",
|
|
205
|
+
columns=[
|
|
206
|
+
{"name": "y", "type": "UInt16"},
|
|
207
|
+
{"name": "m", "type": "UInt8"},
|
|
208
|
+
{"name": "d", "type": "UInt8"},
|
|
209
|
+
],
|
|
210
|
+
sorting_key="y",
|
|
211
|
+
primary_key="y",
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# --- Conditional / multi-clause TTLs ---
|
|
216
|
+
def test_valid_ttl_with_where(self):
|
|
217
|
+
self.assertIsNone(
|
|
218
|
+
chquery.validate_ttl(
|
|
219
|
+
ttl="ts + INTERVAL 7 DAY WHERE status = 'expired'",
|
|
220
|
+
columns=[
|
|
221
|
+
{"name": "ts", "type": "DateTime"},
|
|
222
|
+
{"name": "status", "type": "String"},
|
|
223
|
+
],
|
|
224
|
+
sorting_key="ts",
|
|
225
|
+
primary_key="ts",
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def test_valid_ttl_with_where_numeric(self):
|
|
230
|
+
self.assertIsNone(
|
|
231
|
+
chquery.validate_ttl(
|
|
232
|
+
ttl="ts + INTERVAL 30 DAY WHERE x % 10 == 0 AND y > 5",
|
|
233
|
+
columns=[
|
|
234
|
+
{"name": "ts", "type": "DateTime"},
|
|
235
|
+
{"name": "x", "type": "Int32"},
|
|
236
|
+
{"name": "y", "type": "Int32"},
|
|
237
|
+
],
|
|
238
|
+
sorting_key="ts",
|
|
239
|
+
primary_key="ts",
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def test_invalid_ttl_where_unknown_column(self):
|
|
244
|
+
err = chquery.validate_ttl(
|
|
245
|
+
ttl="ts + INTERVAL 7 DAY WHERE missing = 1",
|
|
246
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
247
|
+
sorting_key="ts",
|
|
248
|
+
primary_key="ts",
|
|
249
|
+
)
|
|
250
|
+
self.assertIsNotNone(err)
|
|
251
|
+
self.assertIn("Missing columns: 'missing'", err)
|
|
252
|
+
|
|
253
|
+
def test_valid_ttl_where_string_expression(self):
|
|
254
|
+
self.assertIsNone(
|
|
255
|
+
chquery.validate_ttl(
|
|
256
|
+
ttl="ts + INTERVAL 7 DAY WHERE status",
|
|
257
|
+
columns=[
|
|
258
|
+
{"name": "ts", "type": "DateTime"},
|
|
259
|
+
{"name": "status", "type": "String"},
|
|
260
|
+
],
|
|
261
|
+
sorting_key="ts",
|
|
262
|
+
primary_key="ts",
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def test_valid_multiple_ttls_comma_separated(self):
|
|
267
|
+
self.assertIsNone(
|
|
268
|
+
chquery.validate_ttl(
|
|
269
|
+
ttl="ts + INTERVAL 7 DAY WHERE status = 'a', ts + INTERVAL 14 DAY WHERE status = 'b'",
|
|
270
|
+
columns=[
|
|
271
|
+
{"name": "ts", "type": "DateTime"},
|
|
272
|
+
{"name": "status", "type": "String"},
|
|
273
|
+
],
|
|
274
|
+
sorting_key="ts",
|
|
275
|
+
primary_key="ts",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def test_valid_ttl_group_by(self):
|
|
280
|
+
# GROUP BY rollup; must group by a prefix of the primary key
|
|
281
|
+
self.assertIsNone(
|
|
282
|
+
chquery.validate_ttl(
|
|
283
|
+
ttl="ts + INTERVAL 1 DAY GROUP BY k SET v = sum(v)",
|
|
284
|
+
columns=[
|
|
285
|
+
{"name": "k", "type": "String"},
|
|
286
|
+
{"name": "ts", "type": "DateTime"},
|
|
287
|
+
{"name": "v", "type": "Int64"},
|
|
288
|
+
],
|
|
289
|
+
sorting_key="k",
|
|
290
|
+
primary_key="k",
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def test_invalid_ttl_group_by_not_in_primary_key(self):
|
|
295
|
+
err = chquery.validate_ttl(
|
|
296
|
+
ttl="ts + INTERVAL 1 DAY GROUP BY missing SET v = sum(v)",
|
|
297
|
+
columns=[
|
|
298
|
+
{"name": "k", "type": "String"},
|
|
299
|
+
{"name": "ts", "type": "DateTime"},
|
|
300
|
+
{"name": "v", "type": "Int64"},
|
|
301
|
+
],
|
|
302
|
+
sorting_key="k",
|
|
303
|
+
primary_key="k",
|
|
304
|
+
)
|
|
305
|
+
self.assertIsNotNone(err)
|
|
306
|
+
self.assertIn("GROUP BY key should be a prefix of primary key", err)
|
|
307
|
+
|
|
308
|
+
def test_valid_where_string_functions(self):
|
|
309
|
+
columns = [
|
|
310
|
+
{"name": "ts", "type": "DateTime"},
|
|
311
|
+
{"name": "s", "type": "String"},
|
|
312
|
+
{"name": "n", "type": "Int32"},
|
|
313
|
+
]
|
|
314
|
+
predicates = {
|
|
315
|
+
"concat": "concat(s, 'x') = 'ax'",
|
|
316
|
+
"startsWith": "startsWith(s, 'a')",
|
|
317
|
+
"endsWith": "endsWith(s, 'z')",
|
|
318
|
+
"length": "length(s) > 3",
|
|
319
|
+
"lower": "lower(s) = 'a'",
|
|
320
|
+
"substring": "substring(s, 1, 2) = 'ab'",
|
|
321
|
+
"match": "match(s, '^a')",
|
|
322
|
+
"in": "n IN (1, 2, 3)",
|
|
323
|
+
}
|
|
324
|
+
for name, predicate in predicates.items():
|
|
325
|
+
with self.subTest(fn=name):
|
|
326
|
+
self.assertIsNone(
|
|
327
|
+
chquery.validate_ttl(
|
|
328
|
+
ttl=f"ts + INTERVAL 7 DAY WHERE {predicate}",
|
|
329
|
+
columns=columns,
|
|
330
|
+
sorting_key="ts",
|
|
331
|
+
primary_key="ts",
|
|
332
|
+
),
|
|
333
|
+
f"{name} should be a known function in TTL WHERE clauses",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# --- Input validation ---
|
|
337
|
+
def test_columns_must_be_list_of_dicts(self):
|
|
338
|
+
with self.assertRaises(TypeError):
|
|
339
|
+
chquery.validate_ttl(ttl="ts + INTERVAL 7 DAY", columns="ts DateTime", sorting_key="ts", primary_key="ts")
|
|
340
|
+
|
|
341
|
+
def test_column_dict_requires_name_and_type(self):
|
|
342
|
+
with self.assertRaises((TypeError, ValueError)):
|
|
343
|
+
chquery.validate_ttl(
|
|
344
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
345
|
+
columns=[{"name": "ts"}],
|
|
346
|
+
sorting_key="ts",
|
|
347
|
+
primary_key="ts",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# --- Regression guard: sequential throwing calls must not crash ---
|
|
352
|
+
def test_sequential_throwing_calls_do_not_crash(self):
|
|
353
|
+
# A validate_ttl call that throws must not corrupt shared state and take
|
|
354
|
+
# down a subsequent call. validateTTL parses against a private per-call
|
|
355
|
+
# context copy (functions/ValidateTTL.cpp) precisely so this holds.
|
|
356
|
+
# Run in a subprocess so a hypothetical crash doesn't kill pytest; the
|
|
357
|
+
# child must exit 0.
|
|
358
|
+
import signal
|
|
359
|
+
import subprocess
|
|
360
|
+
import sys
|
|
361
|
+
import textwrap
|
|
362
|
+
repro = textwrap.dedent("""\
|
|
363
|
+
from chtoolset import query as q
|
|
364
|
+
err1 = q.validate_ttl(
|
|
365
|
+
ttl="counter + toIntervalDay(7)",
|
|
366
|
+
columns=[{"name": "counter", "type": "Int32"}],
|
|
367
|
+
sorting_key="counter",
|
|
368
|
+
primary_key="counter",
|
|
369
|
+
)
|
|
370
|
+
assert err1 is not None and "Illegal types" in err1, (
|
|
371
|
+
f"call 1 should return an 'Illegal types' error, got: {err1!r}")
|
|
372
|
+
q.validate_ttl(
|
|
373
|
+
ttl="ts + INTERVAL 7 DAY WHERE missing = 1",
|
|
374
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
375
|
+
sorting_key="ts",
|
|
376
|
+
primary_key="ts",
|
|
377
|
+
)
|
|
378
|
+
""")
|
|
379
|
+
result = subprocess.run([sys.executable, "-c", repro],
|
|
380
|
+
capture_output=True, text=True, timeout=30)
|
|
381
|
+
if result.returncode != 0 and result.returncode != -signal.SIGABRT:
|
|
382
|
+
raise RuntimeError(
|
|
383
|
+
f"Unexpected subprocess exit {result.returncode} "
|
|
384
|
+
f"(expected 0 or SIGABRT {-signal.SIGABRT})."
|
|
385
|
+
f"\nSTDERR:\n{result.stderr}"
|
|
386
|
+
f"\nSTDOUT:\n{result.stdout}")
|
|
387
|
+
self.assertEqual(result.returncode, 0,
|
|
388
|
+
f"Subprocess crashed with SIGABRT."
|
|
389
|
+
f"\nSTDERR:\n{result.stderr}"
|
|
390
|
+
f"\nSTDOUT:\n{result.stdout}")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
if __name__ == "__main__":
|
|
394
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/tinybird_toolset.egg-info/requires.txt
RENAMED
|
File without changes
|
{tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/src/tinybird_toolset.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_get_columns_from_create_query.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_toolset-2.4.13 → tinybird_toolset-2.5.0}/tests/test_replace_tables_backward_compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|