tinybird-toolset 2.5.4__tar.gz → 2.6.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.5.4/src/tinybird_toolset.egg-info → tinybird_toolset-2.6.0}/PKG-INFO +2 -2
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/README.md +8 -8
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/conf.py +1 -1
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/setup.py +2 -2
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/chtoolset/query.py +2 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0/src/tinybird_toolset.egg-info}/PKG-INFO +2 -2
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/tinybird_toolset.egg-info/SOURCES.txt +2 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_check_compatible_types.py +56 -0
- tinybird_toolset-2.6.0/tests/test_check_ttl_partition_compatibility.py +175 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_get_columns_from_create_query.py +101 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_normalize_table_column.py +75 -0
- tinybird_toolset-2.6.0/tests/test_validate_partition_key.py +150 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_validate_ttl.py +248 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/LICENSE +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/MANIFEST.in +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/setup.cfg +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/chtoolset/__init__.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/tinybird_toolset.egg-info/dependency_links.txt +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/tinybird_toolset.egg-info/requires.txt +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/tinybird_toolset.egg-info/top_level.txt +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_check_write_query.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_chquery.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_convert_to_row_binary.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_explain_ast.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_get_left_table.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_internal_cache.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_normalize_query_keep_names.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_parse_create_materialized_view_target_table.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_replace_tables.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_replace_tables_backward_compat.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_rewrite_aggregation_states.py +0 -0
- {tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_tables.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinybird-toolset
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Home-page: https://gitlab.com/tinybird/clickhouse-toolset
|
|
5
5
|
Author: Tinybird.co
|
|
6
6
|
Author-email: support@tinybird.co
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.13, <3.15
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Provides-Extra: test
|
|
10
10
|
Requires-Dist: pytest; extra == "test"
|
|
@@ -70,9 +70,9 @@ You'll also need `objcopy` or `llvm-objcopy` in your path. The latter is more pr
|
|
|
70
70
|
in the bin path of your homebrew llvm installation, so you'll have to do something like: `export PATH="/opt/homebrew/opt/llvm@19/bin:$PATH"`
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
make build-3.
|
|
74
|
-
make test-3.
|
|
75
|
-
OMIT_PATCHES=1 make build-3.
|
|
73
|
+
make build-3.13 # Build for Python 3.13
|
|
74
|
+
make test-3.13 # Build and test
|
|
75
|
+
OMIT_PATCHES=1 make build-3.13 # Skip patches on rebuild
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
### Key Profiling Targets
|
|
@@ -113,8 +113,8 @@ make build # Build for all Python versions
|
|
|
113
113
|
make test # Run tests for all Python versions
|
|
114
114
|
|
|
115
115
|
# Or for a specific Python version
|
|
116
|
-
make build-3.
|
|
117
|
-
make test-3.
|
|
116
|
+
make build-3.13 # Build only for Python 3.13
|
|
117
|
+
make test-3.13 # Test only for Python 3.13
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
The Docker image includes:
|
|
@@ -139,7 +139,7 @@ sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
|
|
|
139
139
|
|
|
140
140
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
|
141
141
|
sudo apt update
|
|
142
|
-
sudo apt install python3.
|
|
142
|
+
sudo apt install python3.13-dev python3.14-dev
|
|
143
143
|
|
|
144
144
|
pip3 install virtualenv
|
|
145
145
|
```
|
|
@@ -199,7 +199,7 @@ source ~/.zshrc
|
|
|
199
199
|
|
|
200
200
|
* Follow the normal build. When validating a new machine or toolchain, prefer building a single Python version first:
|
|
201
201
|
```bash
|
|
202
|
-
make build-3.
|
|
202
|
+
make build-3.13
|
|
203
203
|
```
|
|
204
204
|
Then run the full build only after that succeeds:
|
|
205
205
|
```bash
|
|
@@ -283,7 +283,7 @@ export TWINE_PASSWORD=pypi-... # PyPI API token
|
|
|
283
283
|
```
|
|
284
284
|
|
|
285
285
|
The script will:
|
|
286
|
-
- Build wheels for Python 3.
|
|
286
|
+
- Build wheels for Python 3.13-3.14
|
|
287
287
|
- Delocate them (bundle dynamic libraries, set minimum macOS version to 11.0)
|
|
288
288
|
- Run tests for each Python version
|
|
289
289
|
- Upload to PyPI
|
|
@@ -417,7 +417,7 @@ class CustomBuildWithFromCH(build_ext):
|
|
|
417
417
|
|
|
418
418
|
compiler = getattr(self.compiler, "compiler_so")
|
|
419
419
|
if sys.platform == 'darwin':
|
|
420
|
-
# Fix for problem in Apple Silicon Mac with Python 3.
|
|
420
|
+
# Fix for problem in Apple Silicon Mac with Python 3.x
|
|
421
421
|
# both -arch arm64 and -arch x86_64 are included and this collides with
|
|
422
422
|
# other -march option from cflags() (generated by Ninja)
|
|
423
423
|
if 'x86_64' in compiler and 'arm64' in compiler:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from setuptools import setup, Extension
|
|
2
2
|
|
|
3
3
|
NAME = 'tinybird-toolset'
|
|
4
|
-
VERSION = '2.
|
|
4
|
+
VERSION = '2.6.0'
|
|
5
5
|
|
|
6
6
|
# Shared metadata for both the full (extension) build and the metadata-only
|
|
7
7
|
# fallback below, so the two setup() calls can't drift apart.
|
|
@@ -13,7 +13,7 @@ common_kwargs = dict(
|
|
|
13
13
|
author_email="support@tinybird.co",
|
|
14
14
|
packages=["chtoolset"],
|
|
15
15
|
package_dir={"": "src"},
|
|
16
|
-
python_requires=">=3.
|
|
16
|
+
python_requires=">=3.13, <3.15",
|
|
17
17
|
install_requires=[],
|
|
18
18
|
)
|
|
19
19
|
|
|
@@ -21,6 +21,8 @@ from chtoolset._query import replace_tables, \
|
|
|
21
21
|
get_columns_from_create_query, \
|
|
22
22
|
normalize_table_column, \
|
|
23
23
|
validate_ttl, \
|
|
24
|
+
validate_partition_key, \
|
|
25
|
+
check_ttl_partition_compatibility, \
|
|
24
26
|
parse_create_materialized_view_target_table as _parse_create_materialized_view_target_table
|
|
25
27
|
|
|
26
28
|
from typing import Optional, TypedDict
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinybird-toolset
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Home-page: https://gitlab.com/tinybird/clickhouse-toolset
|
|
5
5
|
Author: Tinybird.co
|
|
6
6
|
Author-email: support@tinybird.co
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.13, <3.15
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Provides-Extra: test
|
|
10
10
|
Requires-Dist: pytest; extra == "test"
|
|
@@ -11,6 +11,7 @@ src/tinybird_toolset.egg-info/dependency_links.txt
|
|
|
11
11
|
src/tinybird_toolset.egg-info/requires.txt
|
|
12
12
|
src/tinybird_toolset.egg-info/top_level.txt
|
|
13
13
|
tests/test_check_compatible_types.py
|
|
14
|
+
tests/test_check_ttl_partition_compatibility.py
|
|
14
15
|
tests/test_check_write_query.py
|
|
15
16
|
tests/test_chquery.py
|
|
16
17
|
tests/test_convert_to_row_binary.py
|
|
@@ -25,4 +26,5 @@ tests/test_replace_tables.py
|
|
|
25
26
|
tests/test_replace_tables_backward_compat.py
|
|
26
27
|
tests/test_rewrite_aggregation_states.py
|
|
27
28
|
tests/test_tables.py
|
|
29
|
+
tests/test_validate_partition_key.py
|
|
28
30
|
tests/test_validate_ttl.py
|
|
@@ -357,6 +357,21 @@ class TestCast(unittest.TestCase):
|
|
|
357
357
|
self.assertTrue(chquery.check_compatible_types('AggregateFunction(sum, Int16)', 'AggregateFunction(sum, Int8)'))
|
|
358
358
|
self.assertEqual(str(error.exception), """Different #0 argument: Int16 vs Int8""")
|
|
359
359
|
|
|
360
|
+
def test_works_with_aggregate_functions_over_json(self):
|
|
361
|
+
# Regression test for PLTF-842: resolving an AggregateFunction/
|
|
362
|
+
# SimpleAggregateFunction over a JSON value type used to segfault.
|
|
363
|
+
# DataTypeObject::doGetDefaultSerialization() unconditionally
|
|
364
|
+
# dereferences Context::getGlobalContextInstance() to read the
|
|
365
|
+
# allow_simdjson setting, which is null since chtoolset never runs
|
|
366
|
+
# a real clickhouse-server. See CheckCompatibleTypes.cpp.
|
|
367
|
+
self.assertTrue(chquery.check_compatible_types(
|
|
368
|
+
'AggregateFunction(argMax, JSON, DateTime)',
|
|
369
|
+
'AggregateFunction(argMax, JSON, DateTime)'))
|
|
370
|
+
|
|
371
|
+
self.assertTrue(chquery.check_compatible_types(
|
|
372
|
+
'SimpleAggregateFunction(any, JSON)',
|
|
373
|
+
'SimpleAggregateFunction(any, JSON)'))
|
|
374
|
+
|
|
360
375
|
def test_rejects_if_agg_parameters_are_incompatible(self):
|
|
361
376
|
with self.assertRaises(ValueError) as error:
|
|
362
377
|
self.assertTrue(chquery.check_compatible_types('AggregateFunction(topK, String)', 'AggregateFunction(topK(100), String)'))
|
|
@@ -365,3 +380,44 @@ class TestCast(unittest.TestCase):
|
|
|
365
380
|
with self.assertRaises(ValueError) as error:
|
|
366
381
|
self.assertTrue(chquery.check_compatible_types('AggregateFunction(topK(99), String)', 'AggregateFunction(topK(100), String)'))
|
|
367
382
|
self.assertEqual(str(error.exception), """Different #0 parameter: 99 vs 100""")
|
|
383
|
+
|
|
384
|
+
# Nested/composed types that must resolve without crashing. Covers PLTF-842
|
|
385
|
+
# (JSON nested inside Array/Map/Tuple/Nested/AggregateFunction combinators,
|
|
386
|
+
# which all previously segfaulted resolving JSON's default serialization
|
|
387
|
+
# without a global ClickHouse Context) plus general composed-type coverage.
|
|
388
|
+
NESTED_AND_COMPOSED_TYPES = [
|
|
389
|
+
'Array(JSON)',
|
|
390
|
+
'Map(String, JSON)',
|
|
391
|
+
'Tuple(JSON, String)',
|
|
392
|
+
'Tuple(a JSON, b String)',
|
|
393
|
+
'Nullable(JSON)',
|
|
394
|
+
'Nested(a JSON, b String)',
|
|
395
|
+
'AggregateFunction(argMax, Array(JSON), DateTime)',
|
|
396
|
+
'AggregateFunction(argMax, Map(String, JSON), DateTime)',
|
|
397
|
+
'AggregateFunction(argMax, Tuple(JSON, String), DateTime)',
|
|
398
|
+
'SimpleAggregateFunction(any, Array(JSON))',
|
|
399
|
+
'Array(AggregateFunction(argMax, JSON, DateTime))',
|
|
400
|
+
'Array(LowCardinality(String))',
|
|
401
|
+
'Map(String, LowCardinality(String))',
|
|
402
|
+
'Array(Nullable(Int64))',
|
|
403
|
+
'Map(String, Nullable(Int64))',
|
|
404
|
+
'Map(String, AggregateFunction(sum, Int64))',
|
|
405
|
+
'Nested(a UInt64, b String)',
|
|
406
|
+
'Tuple(x Float64, y Float64)',
|
|
407
|
+
'Array(Tuple(UInt8, String))',
|
|
408
|
+
'Array(Array(String))',
|
|
409
|
+
'Map(String, Array(String))',
|
|
410
|
+
'Map(String, Map(String, Int64))',
|
|
411
|
+
'Variant(String, Int64)',
|
|
412
|
+
'Dynamic',
|
|
413
|
+
'LowCardinality(Nullable(String))',
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
def test_nested_and_composed_types_self_compatible(self):
|
|
417
|
+
for type_str in self.NESTED_AND_COMPOSED_TYPES:
|
|
418
|
+
with self.subTest(type_str):
|
|
419
|
+
self.assertTrue(chquery.check_compatible_types(type_str, type_str))
|
|
420
|
+
|
|
421
|
+
def test_low_cardinality_of_json_is_rejected(self):
|
|
422
|
+
with self.assertRaisesRegex(ValueError, 'DataTypeLowCardinality is supported only for'):
|
|
423
|
+
chquery.check_compatible_types('LowCardinality(JSON)', 'LowCardinality(JSON)')
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from chtoolset import query as chquery
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestCheckTTLPartitionCompatibility(unittest.TestCase):
|
|
6
|
+
def test_checked_and_compatible(self):
|
|
7
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
8
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
9
|
+
ttl="ts + INTERVAL 1 MONTH",
|
|
10
|
+
sorting_key="ts",
|
|
11
|
+
primary_key="ts",
|
|
12
|
+
partition_key="toYYYYMM(ts)",
|
|
13
|
+
)
|
|
14
|
+
self.assertTrue(result["checked"])
|
|
15
|
+
self.assertIsNone(result["error"])
|
|
16
|
+
|
|
17
|
+
def test_checked_and_compatible_uses_interval_count(self):
|
|
18
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
19
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
20
|
+
ttl="ts + INTERVAL 90 DAY",
|
|
21
|
+
sorting_key="ts",
|
|
22
|
+
primary_key="ts",
|
|
23
|
+
partition_key="toYYYYMM(ts)",
|
|
24
|
+
)
|
|
25
|
+
self.assertTrue(result["checked"])
|
|
26
|
+
self.assertIsNone(result["error"])
|
|
27
|
+
|
|
28
|
+
def test_checked_and_incompatible(self):
|
|
29
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
30
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
31
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
32
|
+
sorting_key="ts",
|
|
33
|
+
primary_key="ts",
|
|
34
|
+
partition_key="toYYYYMM(ts)",
|
|
35
|
+
)
|
|
36
|
+
self.assertTrue(result["checked"])
|
|
37
|
+
self.assertIsNotNone(result["error"])
|
|
38
|
+
self.assertIn("shorter than the partition key period", result["error"])
|
|
39
|
+
|
|
40
|
+
def test_mixed_where_clause_ttl_is_not_checked(self):
|
|
41
|
+
# A WHERE clause makes the whole statement ineligible for ttl_only_drop_parts,
|
|
42
|
+
# including when it also contains an unconditional TTL.
|
|
43
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
44
|
+
columns=[
|
|
45
|
+
{"name": "ts", "type": "DateTime"},
|
|
46
|
+
{"name": "status", "type": "String"},
|
|
47
|
+
],
|
|
48
|
+
ttl="ts + INTERVAL 1 MONTH, ts + INTERVAL 1 DAY WHERE status = 'expired'",
|
|
49
|
+
sorting_key="ts",
|
|
50
|
+
primary_key="ts",
|
|
51
|
+
partition_key="toYYYYMM(ts)",
|
|
52
|
+
)
|
|
53
|
+
self.assertFalse(result["checked"])
|
|
54
|
+
self.assertIsNone(result["error"])
|
|
55
|
+
|
|
56
|
+
def test_not_checked_when_a_ttl_clause_is_unrecognized(self):
|
|
57
|
+
# An unrecognized unconditional TTL remains unchecked; the WHERE clause also makes the
|
|
58
|
+
# whole statement ineligible for the optimization.
|
|
59
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
60
|
+
columns=[
|
|
61
|
+
{"name": "ts", "type": "DateTime"},
|
|
62
|
+
{"name": "other_col", "type": "DateTime"},
|
|
63
|
+
{"name": "flag", "type": "UInt8"},
|
|
64
|
+
],
|
|
65
|
+
ttl="toDateTime(other_col) + INTERVAL 1 DAY, ts + INTERVAL 1 MONTH WHERE flag = 1",
|
|
66
|
+
sorting_key="ts",
|
|
67
|
+
primary_key="ts",
|
|
68
|
+
partition_key="toYYYYMM(ts)",
|
|
69
|
+
)
|
|
70
|
+
self.assertFalse(result["checked"])
|
|
71
|
+
self.assertIsNone(result["error"])
|
|
72
|
+
|
|
73
|
+
# validate_ttl, by contrast, silently accepts it - confirming the gap this function closes.
|
|
74
|
+
self.assertIsNone(
|
|
75
|
+
chquery.validate_ttl(
|
|
76
|
+
columns=[
|
|
77
|
+
{"name": "ts", "type": "DateTime"},
|
|
78
|
+
{"name": "other_col", "type": "DateTime"},
|
|
79
|
+
{"name": "flag", "type": "UInt8"},
|
|
80
|
+
],
|
|
81
|
+
ttl="toDateTime(other_col) + INTERVAL 1 DAY, ts + INTERVAL 1 MONTH WHERE flag = 1",
|
|
82
|
+
sorting_key="ts",
|
|
83
|
+
primary_key="ts",
|
|
84
|
+
partition_key="toYYYYMM(ts)",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def test_not_checked_when_only_where_ttl_exists(self):
|
|
89
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
90
|
+
columns=[
|
|
91
|
+
{"name": "ts", "type": "DateTime"},
|
|
92
|
+
{"name": "flag", "type": "UInt8"},
|
|
93
|
+
],
|
|
94
|
+
ttl="ts + INTERVAL 1 DAY WHERE flag = 1",
|
|
95
|
+
sorting_key="ts",
|
|
96
|
+
primary_key="ts",
|
|
97
|
+
partition_key="toYYYYMM(ts)",
|
|
98
|
+
)
|
|
99
|
+
self.assertFalse(result["checked"])
|
|
100
|
+
self.assertIsNone(result["error"])
|
|
101
|
+
|
|
102
|
+
def test_not_checked_when_ttl_column_differs_from_partition_column(self):
|
|
103
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
104
|
+
columns=[
|
|
105
|
+
{"name": "ts", "type": "DateTime"},
|
|
106
|
+
{"name": "updated_at", "type": "DateTime"},
|
|
107
|
+
],
|
|
108
|
+
ttl="updated_at + INTERVAL 7 DAY",
|
|
109
|
+
sorting_key="ts",
|
|
110
|
+
primary_key="ts",
|
|
111
|
+
partition_key="toYYYYMM(ts)",
|
|
112
|
+
)
|
|
113
|
+
self.assertFalse(result["checked"])
|
|
114
|
+
self.assertIsNone(result["error"])
|
|
115
|
+
|
|
116
|
+
def test_not_checked_for_composite_partition_key(self):
|
|
117
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
118
|
+
columns=[
|
|
119
|
+
{"name": "ts", "type": "DateTime"},
|
|
120
|
+
{"name": "k", "type": "String"},
|
|
121
|
+
],
|
|
122
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
123
|
+
sorting_key="ts",
|
|
124
|
+
primary_key="ts",
|
|
125
|
+
partition_key="toYYYYMM(ts), k",
|
|
126
|
+
)
|
|
127
|
+
self.assertFalse(result["checked"])
|
|
128
|
+
self.assertIsNone(result["error"])
|
|
129
|
+
|
|
130
|
+
def test_not_checked_for_unrecognized_partition_function(self):
|
|
131
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
132
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
133
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
134
|
+
sorting_key="ts",
|
|
135
|
+
primary_key="ts",
|
|
136
|
+
partition_key="intDiv(toUInt32(ts), 86400)",
|
|
137
|
+
)
|
|
138
|
+
self.assertFalse(result["checked"])
|
|
139
|
+
self.assertIsNone(result["error"])
|
|
140
|
+
|
|
141
|
+
def test_not_checked_when_no_partition_key(self):
|
|
142
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
143
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
144
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
145
|
+
sorting_key="ts",
|
|
146
|
+
primary_key="ts",
|
|
147
|
+
)
|
|
148
|
+
self.assertFalse(result["checked"])
|
|
149
|
+
self.assertIsNone(result["error"])
|
|
150
|
+
|
|
151
|
+
def test_not_checked_when_no_ttl(self):
|
|
152
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
153
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
154
|
+
ttl="",
|
|
155
|
+
sorting_key="ts",
|
|
156
|
+
primary_key="ts",
|
|
157
|
+
partition_key="toYYYYMM(ts)",
|
|
158
|
+
)
|
|
159
|
+
self.assertFalse(result["checked"])
|
|
160
|
+
self.assertIsNone(result["error"])
|
|
161
|
+
|
|
162
|
+
def test_not_checked_on_malformed_ttl(self):
|
|
163
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
164
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
165
|
+
ttl="this is not a valid ttl !!!",
|
|
166
|
+
sorting_key="ts",
|
|
167
|
+
primary_key="ts",
|
|
168
|
+
partition_key="toYYYYMM(ts)",
|
|
169
|
+
)
|
|
170
|
+
self.assertFalse(result["checked"])
|
|
171
|
+
self.assertIsNone(result["error"])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
unittest.main()
|
{tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_get_columns_from_create_query.py
RENAMED
|
@@ -670,6 +670,107 @@ class TestGetColumnsFromCreateQuery(unittest.TestCase):
|
|
|
670
670
|
'is_primary_key': False,
|
|
671
671
|
})
|
|
672
672
|
|
|
673
|
+
def test_aggregate_function_over_json_type(self):
|
|
674
|
+
"""Regression test for PLTF-842: resolving an AggregateFunction over a
|
|
675
|
+
JSON value type used to segfault inside columnInfoFromColumnDeclaration
|
|
676
|
+
(functions/NormalizeColumnsFromDict.cpp), which calls DataTypeFactory
|
|
677
|
+
without a global ClickHouse Context. JSON's default serialization reads
|
|
678
|
+
that context unconditionally to check the allow_simdjson setting."""
|
|
679
|
+
sql = """
|
|
680
|
+
CREATE TABLE test (
|
|
681
|
+
id UInt64,
|
|
682
|
+
payload AggregateFunction(argMax, JSON, DateTime)
|
|
683
|
+
) ENGINE = AggregatingMergeTree() ORDER BY id
|
|
684
|
+
"""
|
|
685
|
+
cols = chquery.get_columns_from_create_query(sql)
|
|
686
|
+
self.assertEqual(len(cols), 2)
|
|
687
|
+
|
|
688
|
+
self.assertColumnEquals(cols[1], {
|
|
689
|
+
'name': 'payload',
|
|
690
|
+
'type': 'AggregateFunction(argMax, JSON, DateTime)',
|
|
691
|
+
'nullable': False,
|
|
692
|
+
'default_specifier': '',
|
|
693
|
+
'default_expression': None,
|
|
694
|
+
'codec': None,
|
|
695
|
+
'comment': None,
|
|
696
|
+
'ttl': None,
|
|
697
|
+
'is_primary_key': False,
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
# Nested/composed type -> expected (normalized_type, nullable). Covers PLTF-842
|
|
701
|
+
# (JSON nested inside Array/Map/Tuple/Nested/AggregateFunction combinators, which
|
|
702
|
+
# all previously segfaulted resolving JSON's default serialization without a
|
|
703
|
+
# global ClickHouse Context) plus general composed-type coverage so any similar
|
|
704
|
+
# gap for other types would show up here too.
|
|
705
|
+
NESTED_AND_COMPOSED_TYPES = [
|
|
706
|
+
('Array(JSON)', ('Array(JSON)', False)),
|
|
707
|
+
('Map(String, JSON)', ('Map(String, JSON)', False)),
|
|
708
|
+
('Tuple(JSON, String)', ('Tuple(JSON, String)', False)),
|
|
709
|
+
('Tuple(a JSON, b String)', ('Tuple(a JSON, b String)', False)),
|
|
710
|
+
('Nullable(JSON)', ('Nullable(JSON)', True)),
|
|
711
|
+
('Nested(a JSON, b String)', ('Nested(a JSON, b String)', False)),
|
|
712
|
+
('AggregateFunction(argMax, JSON, DateTime)', ('AggregateFunction(argMax, JSON, DateTime)', False)),
|
|
713
|
+
('AggregateFunction(argMax, Array(JSON), DateTime)', ('AggregateFunction(argMax, Array(JSON), DateTime)', False)),
|
|
714
|
+
('AggregateFunction(argMax, Map(String, JSON), DateTime)', ('AggregateFunction(argMax, Map(String, JSON), DateTime)', False)),
|
|
715
|
+
('AggregateFunction(argMax, Tuple(JSON, String), DateTime)', ('AggregateFunction(argMax, Tuple(JSON, String), DateTime)', False)),
|
|
716
|
+
('SimpleAggregateFunction(any, JSON)', ('SimpleAggregateFunction(any, JSON)', False)),
|
|
717
|
+
('SimpleAggregateFunction(any, Array(JSON))', ('SimpleAggregateFunction(any, Array(JSON))', False)),
|
|
718
|
+
('Array(AggregateFunction(argMax, JSON, DateTime))', ('Array(AggregateFunction(argMax, JSON, DateTime))', False)),
|
|
719
|
+
('Array(LowCardinality(String))', ('Array(LowCardinality(String))', False)),
|
|
720
|
+
('Map(String, LowCardinality(String))', ('Map(String, LowCardinality(String))', False)),
|
|
721
|
+
('Array(Nullable(Int64))', ('Array(Nullable(Int64))', False)),
|
|
722
|
+
('Map(String, Nullable(Int64))', ('Map(String, Nullable(Int64))', False)),
|
|
723
|
+
('Map(String, AggregateFunction(sum, Int64))', ('Map(String, AggregateFunction(sum, Int64))', False)),
|
|
724
|
+
('Nested(a UInt64, b String)', ('Nested(a UInt64, b String)', False)),
|
|
725
|
+
('Tuple(x Float64, y Float64)', ('Tuple(x Float64, y Float64)', False)),
|
|
726
|
+
('Array(Tuple(UInt8, String))', ('Array(Tuple(UInt8, String))', False)),
|
|
727
|
+
('Array(Array(String))', ('Array(Array(String))', False)),
|
|
728
|
+
('Map(String, Array(String))', ('Map(String, Array(String))', False)),
|
|
729
|
+
('Map(String, Map(String, Int64))', ('Map(String, Map(String, Int64))', False)),
|
|
730
|
+
('Variant(String, Int64)', ('Variant(String, Int64)', False)),
|
|
731
|
+
('Dynamic', ('Dynamic', False)),
|
|
732
|
+
('LowCardinality(Nullable(String))', ('LowCardinality(Nullable(String))', True)),
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
def test_nested_and_composed_types(self):
|
|
736
|
+
"""Regression test for PLTF-842 plus general coverage: resolving nested
|
|
737
|
+
and composed types (Array/Map/Tuple/Nested/AggregateFunction combinators,
|
|
738
|
+
including JSON nested inside them) must not crash columnInfoFromColumnDeclaration
|
|
739
|
+
(functions/NormalizeColumnsFromDict.cpp)."""
|
|
740
|
+
for type_str, (expected_type, expected_nullable) in self.NESTED_AND_COMPOSED_TYPES:
|
|
741
|
+
with self.subTest(type_str):
|
|
742
|
+
sql = f"""
|
|
743
|
+
CREATE TABLE test (
|
|
744
|
+
id UInt64,
|
|
745
|
+
col {type_str}
|
|
746
|
+
) ENGINE = AggregatingMergeTree() ORDER BY id
|
|
747
|
+
"""
|
|
748
|
+
cols = chquery.get_columns_from_create_query(sql)
|
|
749
|
+
self.assertEqual(len(cols), 2)
|
|
750
|
+
self.assertColumnEquals(cols[1], {
|
|
751
|
+
'name': 'col',
|
|
752
|
+
'type': expected_type,
|
|
753
|
+
'nullable': expected_nullable,
|
|
754
|
+
'default_specifier': '',
|
|
755
|
+
'default_expression': None,
|
|
756
|
+
'codec': None,
|
|
757
|
+
'comment': None,
|
|
758
|
+
'ttl': None,
|
|
759
|
+
'is_primary_key': False,
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
def test_low_cardinality_of_json_is_rejected(self):
|
|
763
|
+
"""LowCardinality only supports numbers, strings, Date or DateTime; JSON
|
|
764
|
+
must be rejected with a clear error rather than crashing."""
|
|
765
|
+
sql = """
|
|
766
|
+
CREATE TABLE test (
|
|
767
|
+
id UInt64,
|
|
768
|
+
col LowCardinality(JSON)
|
|
769
|
+
) ENGINE = AggregatingMergeTree() ORDER BY id
|
|
770
|
+
"""
|
|
771
|
+
with self.assertRaisesRegex(ValueError, 'DataTypeLowCardinality is supported only for'):
|
|
772
|
+
chquery.get_columns_from_create_query(sql)
|
|
773
|
+
|
|
673
774
|
def test_invalid_query(self):
|
|
674
775
|
"""Test that invalid queries raise appropriate errors"""
|
|
675
776
|
with self.assertRaises(ValueError):
|
|
@@ -390,6 +390,81 @@ class TestNormalizeTableColumn(unittest.TestCase):
|
|
|
390
390
|
|
|
391
391
|
self.assertEqual(column["type"], "Float64")
|
|
392
392
|
|
|
393
|
+
def test_aggregate_function_over_json_type(self):
|
|
394
|
+
# Regression test for PLTF-842: normalizeColumnInfo() calls
|
|
395
|
+
# columnInfoFromColumnDeclaration() (functions/NormalizeColumnsFromDict.cpp),
|
|
396
|
+
# which used to segfault resolving an AggregateFunction over a JSON value
|
|
397
|
+
# type via DataTypeFactory without a global ClickHouse Context.
|
|
398
|
+
column = chquery.normalize_table_column(
|
|
399
|
+
column={
|
|
400
|
+
"name": "payload",
|
|
401
|
+
"normalized_name": "payload",
|
|
402
|
+
"type": "AggregateFunction(argMax, JSON, DateTime)",
|
|
403
|
+
"nullable": False,
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
self.assertEqual(column["type"], "AggregateFunction(argMax, JSON, DateTime)")
|
|
408
|
+
self.assertFalse(column["nullable"])
|
|
409
|
+
|
|
410
|
+
# Nested/composed type -> expected (normalized_type, nullable). Covers PLTF-842
|
|
411
|
+
# (JSON nested inside Array/Map/Tuple/Nested/AggregateFunction combinators)
|
|
412
|
+
# plus general composed-type coverage for normalizeColumnInfo()
|
|
413
|
+
# (functions/NormalizeColumnsFromDict.cpp).
|
|
414
|
+
NESTED_AND_COMPOSED_TYPES = [
|
|
415
|
+
('Array(JSON)', ('Array(JSON)', False)),
|
|
416
|
+
('Map(String, JSON)', ('Map(String, JSON)', False)),
|
|
417
|
+
('Tuple(JSON, String)', ('Tuple(JSON, String)', False)),
|
|
418
|
+
('Tuple(a JSON, b String)', ('Tuple(a JSON, b String)', False)),
|
|
419
|
+
('Nullable(JSON)', ('Nullable(JSON)', True)),
|
|
420
|
+
('Nested(a JSON, b String)', ('Nested(a JSON, b String)', False)),
|
|
421
|
+
('AggregateFunction(argMax, Array(JSON), DateTime)', ('AggregateFunction(argMax, Array(JSON), DateTime)', False)),
|
|
422
|
+
('AggregateFunction(argMax, Map(String, JSON), DateTime)', ('AggregateFunction(argMax, Map(String, JSON), DateTime)', False)),
|
|
423
|
+
('AggregateFunction(argMax, Tuple(JSON, String), DateTime)', ('AggregateFunction(argMax, Tuple(JSON, String), DateTime)', False)),
|
|
424
|
+
('SimpleAggregateFunction(any, JSON)', ('SimpleAggregateFunction(any, JSON)', False)),
|
|
425
|
+
('SimpleAggregateFunction(any, Array(JSON))', ('SimpleAggregateFunction(any, Array(JSON))', False)),
|
|
426
|
+
('Array(AggregateFunction(argMax, JSON, DateTime))', ('Array(AggregateFunction(argMax, JSON, DateTime))', False)),
|
|
427
|
+
('Array(LowCardinality(String))', ('Array(LowCardinality(String))', False)),
|
|
428
|
+
('Map(String, LowCardinality(String))', ('Map(String, LowCardinality(String))', False)),
|
|
429
|
+
('Array(Nullable(Int64))', ('Array(Nullable(Int64))', False)),
|
|
430
|
+
('Map(String, Nullable(Int64))', ('Map(String, Nullable(Int64))', False)),
|
|
431
|
+
('Map(String, AggregateFunction(sum, Int64))', ('Map(String, AggregateFunction(sum, Int64))', False)),
|
|
432
|
+
('Nested(a UInt64, b String)', ('Nested(a UInt64, b String)', False)),
|
|
433
|
+
('Tuple(x Float64, y Float64)', ('Tuple(x Float64, y Float64)', False)),
|
|
434
|
+
('Array(Tuple(UInt8, String))', ('Array(Tuple(UInt8, String))', False)),
|
|
435
|
+
('Array(Array(String))', ('Array(Array(String))', False)),
|
|
436
|
+
('Map(String, Array(String))', ('Map(String, Array(String))', False)),
|
|
437
|
+
('Map(String, Map(String, Int64))', ('Map(String, Map(String, Int64))', False)),
|
|
438
|
+
('Variant(String, Int64)', ('Variant(String, Int64)', False)),
|
|
439
|
+
('Dynamic', ('Dynamic', False)),
|
|
440
|
+
('LowCardinality(Nullable(String))', ('LowCardinality(Nullable(String))', True)),
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
def test_nested_and_composed_types(self):
|
|
444
|
+
for type_str, (expected_type, expected_nullable) in self.NESTED_AND_COMPOSED_TYPES:
|
|
445
|
+
with self.subTest(type_str):
|
|
446
|
+
column = chquery.normalize_table_column(
|
|
447
|
+
column={
|
|
448
|
+
"name": "col",
|
|
449
|
+
"normalized_name": "col",
|
|
450
|
+
"type": type_str,
|
|
451
|
+
"nullable": False,
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
self.assertEqual(column["type"], expected_type)
|
|
455
|
+
self.assertEqual(column["nullable"], expected_nullable)
|
|
456
|
+
|
|
457
|
+
def test_low_cardinality_of_json_is_rejected(self):
|
|
458
|
+
with self.assertRaisesRegex(ValueError, 'DataTypeLowCardinality is supported only for'):
|
|
459
|
+
chquery.normalize_table_column(
|
|
460
|
+
column={
|
|
461
|
+
"name": "col",
|
|
462
|
+
"normalized_name": "col",
|
|
463
|
+
"type": "LowCardinality(JSON)",
|
|
464
|
+
"nullable": False,
|
|
465
|
+
},
|
|
466
|
+
)
|
|
467
|
+
|
|
393
468
|
def test_default_now_for_created_at(self):
|
|
394
469
|
column = chquery.normalize_table_column(
|
|
395
470
|
column={
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from chtoolset import query as chquery
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestValidatePartitionKey(unittest.TestCase):
|
|
6
|
+
def test_valid_empty_partition_key(self):
|
|
7
|
+
self.assertIsNone(
|
|
8
|
+
chquery.validate_partition_key(
|
|
9
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
10
|
+
partition_key="",
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def test_valid_hourly_partition_key(self):
|
|
15
|
+
# Hour is the floor - allowed.
|
|
16
|
+
self.assertIsNone(
|
|
17
|
+
chquery.validate_partition_key(
|
|
18
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
19
|
+
partition_key="toStartOfHour(ts)",
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def test_valid_daily_partition_key(self):
|
|
24
|
+
self.assertIsNone(
|
|
25
|
+
chquery.validate_partition_key(
|
|
26
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
27
|
+
partition_key="toDate(ts)",
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_valid_monthly_partition_key(self):
|
|
32
|
+
self.assertIsNone(
|
|
33
|
+
chquery.validate_partition_key(
|
|
34
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
35
|
+
partition_key="toYYYYMM(ts)",
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def test_invalid_minute_partition_key(self):
|
|
40
|
+
err = chquery.validate_partition_key(
|
|
41
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
42
|
+
partition_key="toStartOfMinute(ts)",
|
|
43
|
+
)
|
|
44
|
+
self.assertIsNotNone(err)
|
|
45
|
+
self.assertIn("finer than hourly", err)
|
|
46
|
+
|
|
47
|
+
def test_invalid_second_partition_key(self):
|
|
48
|
+
err = chquery.validate_partition_key(
|
|
49
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
50
|
+
partition_key="toStartOfSecond(ts)",
|
|
51
|
+
)
|
|
52
|
+
self.assertIsNotNone(err)
|
|
53
|
+
self.assertIn("finer than hourly", err)
|
|
54
|
+
|
|
55
|
+
def test_invalid_five_minute_partition_key(self):
|
|
56
|
+
err = chquery.validate_partition_key(
|
|
57
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
58
|
+
partition_key="toStartOfFiveMinutes(ts)",
|
|
59
|
+
)
|
|
60
|
+
self.assertIsNotNone(err)
|
|
61
|
+
self.assertIn("finer than hourly", err)
|
|
62
|
+
|
|
63
|
+
def test_invalid_ten_minute_partition_key(self):
|
|
64
|
+
err = chquery.validate_partition_key(
|
|
65
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
66
|
+
partition_key="toStartOfTenMinutes(ts)",
|
|
67
|
+
)
|
|
68
|
+
self.assertIsNotNone(err)
|
|
69
|
+
self.assertIn("finer than hourly", err)
|
|
70
|
+
|
|
71
|
+
def test_invalid_fifteen_minute_partition_key(self):
|
|
72
|
+
err = chquery.validate_partition_key(
|
|
73
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
74
|
+
partition_key="toStartOfFifteenMinutes(ts)",
|
|
75
|
+
)
|
|
76
|
+
self.assertIsNotNone(err)
|
|
77
|
+
self.assertIn("finer than hourly", err)
|
|
78
|
+
|
|
79
|
+
def test_invalid_five_minute_singular_alias_partition_key(self):
|
|
80
|
+
# toStartOfFiveMinute (singular) is a registered alias for toStartOfFiveMinutes.
|
|
81
|
+
err = chquery.validate_partition_key(
|
|
82
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
83
|
+
partition_key="toStartOfFiveMinute(ts)",
|
|
84
|
+
)
|
|
85
|
+
self.assertIsNotNone(err)
|
|
86
|
+
self.assertIn("finer than hourly", err)
|
|
87
|
+
|
|
88
|
+
def test_invalid_millisecond_partition_key(self):
|
|
89
|
+
err = chquery.validate_partition_key(
|
|
90
|
+
columns=[{"name": "ts", "type": "DateTime64(3)"}],
|
|
91
|
+
partition_key="toStartOfMillisecond(ts)",
|
|
92
|
+
)
|
|
93
|
+
self.assertIsNotNone(err)
|
|
94
|
+
self.assertIn("finer than hourly", err)
|
|
95
|
+
|
|
96
|
+
def test_invalid_microsecond_partition_key(self):
|
|
97
|
+
err = chquery.validate_partition_key(
|
|
98
|
+
columns=[{"name": "ts", "type": "DateTime64(6)"}],
|
|
99
|
+
partition_key="toStartOfMicrosecond(ts)",
|
|
100
|
+
)
|
|
101
|
+
self.assertIsNotNone(err)
|
|
102
|
+
self.assertIn("finer than hourly", err)
|
|
103
|
+
|
|
104
|
+
def test_invalid_nanosecond_partition_key(self):
|
|
105
|
+
err = chquery.validate_partition_key(
|
|
106
|
+
columns=[{"name": "ts", "type": "DateTime64(9)"}],
|
|
107
|
+
partition_key="toStartOfNanosecond(ts)",
|
|
108
|
+
)
|
|
109
|
+
self.assertIsNotNone(err)
|
|
110
|
+
self.assertIn("finer than hourly", err)
|
|
111
|
+
|
|
112
|
+
def test_invalid_yyyymmddhhmmss_partition_key(self):
|
|
113
|
+
# Packs the full date+time down to the second - no coarsening at all.
|
|
114
|
+
err = chquery.validate_partition_key(
|
|
115
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
116
|
+
partition_key="toYYYYMMDDhhmmss(ts)",
|
|
117
|
+
)
|
|
118
|
+
self.assertIsNotNone(err)
|
|
119
|
+
self.assertIn("finer than hourly", err)
|
|
120
|
+
|
|
121
|
+
def test_valid_skipped_for_composite_partition_key(self):
|
|
122
|
+
self.assertIsNone(
|
|
123
|
+
chquery.validate_partition_key(
|
|
124
|
+
columns=[
|
|
125
|
+
{"name": "ts", "type": "DateTime"},
|
|
126
|
+
{"name": "k", "type": "String"},
|
|
127
|
+
],
|
|
128
|
+
partition_key="toStartOfMinute(ts), k",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def test_valid_skipped_for_unrecognized_function(self):
|
|
133
|
+
self.assertIsNone(
|
|
134
|
+
chquery.validate_partition_key(
|
|
135
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
136
|
+
partition_key="intDiv(toUInt32(ts), 60)",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def test_valid_skipped_for_bare_column(self):
|
|
141
|
+
self.assertIsNone(
|
|
142
|
+
chquery.validate_partition_key(
|
|
143
|
+
columns=[{"name": "k", "type": "String"}],
|
|
144
|
+
partition_key="k",
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
unittest.main()
|
|
@@ -389,6 +389,254 @@ class TestValidateTTL(unittest.TestCase):
|
|
|
389
389
|
primary_key="ts",
|
|
390
390
|
)
|
|
391
391
|
|
|
392
|
+
# --- Partition key / TTL granularity ---
|
|
393
|
+
def test_invalid_ttl_finer_than_partition_month_vs_day(self):
|
|
394
|
+
err = chquery.validate_ttl(
|
|
395
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
396
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
397
|
+
sorting_key="ts",
|
|
398
|
+
primary_key="ts",
|
|
399
|
+
partition_key="toYYYYMM(ts)",
|
|
400
|
+
)
|
|
401
|
+
self.assertIsNotNone(err)
|
|
402
|
+
self.assertIn("shorter than the partition key period", err)
|
|
403
|
+
|
|
404
|
+
def test_invalid_ttl_finer_than_partition_month_vs_week(self):
|
|
405
|
+
err = chquery.validate_ttl(
|
|
406
|
+
ttl="ts + INTERVAL 1 WEEK",
|
|
407
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
408
|
+
sorting_key="ts",
|
|
409
|
+
primary_key="ts",
|
|
410
|
+
partition_key="toYYYYMM(ts)",
|
|
411
|
+
)
|
|
412
|
+
self.assertIsNotNone(err)
|
|
413
|
+
self.assertIn("shorter than the partition key period", err)
|
|
414
|
+
|
|
415
|
+
def test_valid_ttl_interval_count_is_included_in_duration(self):
|
|
416
|
+
# 90 DAY is comparable to 3 MONTH for a monthly partition; the DAY unit alone
|
|
417
|
+
# must not cause a rejection.
|
|
418
|
+
self.assertIsNone(
|
|
419
|
+
chquery.validate_ttl(
|
|
420
|
+
ttl="ts + INTERVAL 90 DAY",
|
|
421
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
422
|
+
sorting_key="ts",
|
|
423
|
+
primary_key="ts",
|
|
424
|
+
partition_key="toYYYYMM(ts)",
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def test_invalid_ttl_interval_does_not_cover_longest_partition_period(self):
|
|
429
|
+
err = chquery.validate_ttl(
|
|
430
|
+
ttl="ts + INTERVAL 30 DAY",
|
|
431
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
432
|
+
sorting_key="ts",
|
|
433
|
+
primary_key="ts",
|
|
434
|
+
partition_key="toYYYYMM(ts)",
|
|
435
|
+
)
|
|
436
|
+
self.assertIsNotNone(err)
|
|
437
|
+
self.assertIn("shorter than the partition key period", err)
|
|
438
|
+
|
|
439
|
+
def test_valid_ttl_interval_covers_longest_partition_period(self):
|
|
440
|
+
self.assertIsNone(
|
|
441
|
+
chquery.validate_ttl(
|
|
442
|
+
ttl="ts + INTERVAL 31 DAY",
|
|
443
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
444
|
+
sorting_key="ts",
|
|
445
|
+
primary_key="ts",
|
|
446
|
+
partition_key="toYYYYMM(ts)",
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def test_valid_ttl_coarser_than_partition_month_vs_day(self):
|
|
451
|
+
# TTL is monthly, partition is daily: whole day-partitions still expire together.
|
|
452
|
+
self.assertIsNone(
|
|
453
|
+
chquery.validate_ttl(
|
|
454
|
+
ttl="ts + INTERVAL 3 MONTH",
|
|
455
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
456
|
+
sorting_key="ts",
|
|
457
|
+
primary_key="ts",
|
|
458
|
+
partition_key="toDate(ts)",
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def test_valid_ttl_matches_partition_granularity(self):
|
|
463
|
+
self.assertIsNone(
|
|
464
|
+
chquery.validate_ttl(
|
|
465
|
+
ttl="ts + INTERVAL 1 MONTH",
|
|
466
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
467
|
+
sorting_key="ts",
|
|
468
|
+
primary_key="ts",
|
|
469
|
+
partition_key="toYYYYMM(ts)",
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def test_valid_no_partition_key_skips_granularity_check(self):
|
|
474
|
+
self.assertIsNone(
|
|
475
|
+
chquery.validate_ttl(
|
|
476
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
477
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
478
|
+
sorting_key="ts",
|
|
479
|
+
primary_key="ts",
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def test_valid_granularity_check_skipped_for_different_column(self):
|
|
484
|
+
# TTL and partition key are on different columns; the relationship is unknown, so no check is applied.
|
|
485
|
+
self.assertIsNone(
|
|
486
|
+
chquery.validate_ttl(
|
|
487
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
488
|
+
columns=[
|
|
489
|
+
{"name": "ts", "type": "DateTime"},
|
|
490
|
+
{"name": "inserted_at", "type": "DateTime"},
|
|
491
|
+
],
|
|
492
|
+
sorting_key="ts",
|
|
493
|
+
primary_key="ts",
|
|
494
|
+
partition_key="toYYYYMM(inserted_at)",
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def test_valid_granularity_check_skipped_for_composite_partition_key(self):
|
|
499
|
+
self.assertIsNone(
|
|
500
|
+
chquery.validate_ttl(
|
|
501
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
502
|
+
columns=[
|
|
503
|
+
{"name": "ts", "type": "DateTime"},
|
|
504
|
+
{"name": "k", "type": "String"},
|
|
505
|
+
],
|
|
506
|
+
sorting_key="ts",
|
|
507
|
+
primary_key="ts",
|
|
508
|
+
partition_key="toYYYYMM(ts), k",
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def test_valid_granularity_check_skipped_for_unrecognized_partition_function(self):
|
|
513
|
+
self.assertIsNone(
|
|
514
|
+
chquery.validate_ttl(
|
|
515
|
+
ttl="ts + INTERVAL 7 DAY",
|
|
516
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
517
|
+
sorting_key="ts",
|
|
518
|
+
primary_key="ts",
|
|
519
|
+
partition_key="intDiv(toUInt32(ts), 86400)",
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def test_invalid_bare_column_ttl_with_partition_key(self):
|
|
524
|
+
# No INTERVAL at all: the TTL fires the instant the raw column value is reached,
|
|
525
|
+
# which is finer than any partition granularity.
|
|
526
|
+
err = chquery.validate_ttl(
|
|
527
|
+
ttl="ts",
|
|
528
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
529
|
+
sorting_key="ts",
|
|
530
|
+
primary_key="ts",
|
|
531
|
+
partition_key="toYYYYMM(ts)",
|
|
532
|
+
)
|
|
533
|
+
self.assertIsNotNone(err)
|
|
534
|
+
self.assertIn("shorter than the partition key period", err)
|
|
535
|
+
|
|
536
|
+
def test_valid_mixed_where_clause_ttl_skips_part_drop_check(self):
|
|
537
|
+
self.assertIsNone(
|
|
538
|
+
chquery.validate_ttl(
|
|
539
|
+
ttl="ts + INTERVAL 1 DAY, ts + INTERVAL 1 DAY WHERE status = 'expired'",
|
|
540
|
+
columns=[
|
|
541
|
+
{"name": "ts", "type": "DateTime"},
|
|
542
|
+
{"name": "status", "type": "String"},
|
|
543
|
+
],
|
|
544
|
+
sorting_key="ts",
|
|
545
|
+
primary_key="ts",
|
|
546
|
+
partition_key="toYYYYMM(ts)",
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
def test_invalid_fixed_interval_can_be_shorter_than_calendar_truncation(self):
|
|
551
|
+
# A month can contain 31 days, so late rows in that month can be born expired.
|
|
552
|
+
err = chquery.validate_ttl(
|
|
553
|
+
ttl="toStartOfMonth(ts) + INTERVAL 30 DAY",
|
|
554
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
555
|
+
sorting_key="ts",
|
|
556
|
+
primary_key="ts",
|
|
557
|
+
partition_key="toYYYYMM(ts)",
|
|
558
|
+
)
|
|
559
|
+
self.assertIsNotNone(err)
|
|
560
|
+
self.assertIn("shorter than its truncation period", err)
|
|
561
|
+
|
|
562
|
+
def test_valid_matching_calendar_interval_and_truncation(self):
|
|
563
|
+
self.assertIsNone(
|
|
564
|
+
chquery.validate_ttl(
|
|
565
|
+
ttl="toStartOfMonth(ts) + INTERVAL 1 MONTH",
|
|
566
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
567
|
+
sorting_key="ts",
|
|
568
|
+
primary_key="ts",
|
|
569
|
+
partition_key="toYYYYMM(ts)",
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def test_valid_equivalent_calendar_interval_and_truncation(self):
|
|
574
|
+
self.assertIsNone(
|
|
575
|
+
chquery.validate_ttl(
|
|
576
|
+
ttl="toStartOfQuarter(ts) + INTERVAL 3 MONTH",
|
|
577
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
578
|
+
sorting_key="ts",
|
|
579
|
+
primary_key="ts",
|
|
580
|
+
partition_key="toStartOfQuarter(ts)",
|
|
581
|
+
)
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
def test_valid_long_interval_with_truncated_base_finer_than_partition(self):
|
|
585
|
+
self.assertIsNone(
|
|
586
|
+
chquery.validate_ttl(
|
|
587
|
+
ttl="toStartOfDay(ts) + INTERVAL 1 YEAR",
|
|
588
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
589
|
+
sorting_key="ts",
|
|
590
|
+
primary_key="ts",
|
|
591
|
+
partition_key="toYYYYMM(ts)",
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
def test_invalid_interval_shorter_than_ttl_truncation(self):
|
|
596
|
+
err = chquery.validate_ttl(
|
|
597
|
+
ttl="toStartOfMonth(ts) + INTERVAL 5 MINUTE",
|
|
598
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
599
|
+
sorting_key="ts",
|
|
600
|
+
primary_key="ts",
|
|
601
|
+
partition_key="toYYYYMM(ts)",
|
|
602
|
+
)
|
|
603
|
+
self.assertIsNotNone(err)
|
|
604
|
+
self.assertIn("shorter than its truncation period", err)
|
|
605
|
+
self.assertIn("expire as soon as they are inserted", err)
|
|
606
|
+
|
|
607
|
+
def test_invalid_interval_shorter_than_truncation_without_partition_key(self):
|
|
608
|
+
err = chquery.validate_ttl(
|
|
609
|
+
ttl="toStartOfHour(ts) + INTERVAL 5 MINUTE",
|
|
610
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
611
|
+
sorting_key="ts",
|
|
612
|
+
primary_key="ts",
|
|
613
|
+
)
|
|
614
|
+
self.assertIsNotNone(err)
|
|
615
|
+
self.assertIn("shorter than its truncation period", err)
|
|
616
|
+
|
|
617
|
+
def test_valid_granularity_check_skipped_for_multi_minute_partition(self):
|
|
618
|
+
# toStartOfFiveMinutes isn't a recognized partition granularity (IntervalKind has no
|
|
619
|
+
# level between Minute and Hour), so the check is skipped rather than risk a false accept.
|
|
620
|
+
self.assertIsNone(
|
|
621
|
+
chquery.validate_ttl(
|
|
622
|
+
ttl="ts + INTERVAL 1 MINUTE",
|
|
623
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
624
|
+
sorting_key="ts",
|
|
625
|
+
primary_key="ts",
|
|
626
|
+
partition_key="toStartOfFiveMinutes(ts)",
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
def test_valid_granularity_check_skipped_for_multi_minute_ttl_truncation(self):
|
|
631
|
+
self.assertIsNone(
|
|
632
|
+
chquery.validate_ttl(
|
|
633
|
+
ttl="toStartOfFiveMinutes(ts) + INTERVAL 1 HOUR",
|
|
634
|
+
columns=[{"name": "ts", "type": "DateTime"}],
|
|
635
|
+
sorting_key="ts",
|
|
636
|
+
primary_key="ts",
|
|
637
|
+
partition_key="toStartOfTenMinutes(ts)",
|
|
638
|
+
)
|
|
639
|
+
)
|
|
392
640
|
|
|
393
641
|
# --- Regression guard: sequential throwing calls must not crash ---
|
|
394
642
|
def test_sequential_throwing_calls_do_not_crash(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/tinybird_toolset.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/src/tinybird_toolset.egg-info/requires.txt
RENAMED
|
File without changes
|
{tinybird_toolset-2.5.4 → tinybird_toolset-2.6.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_toolset-2.5.4 → tinybird_toolset-2.6.0}/tests/test_replace_tables_backward_compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|