tinybird-toolset 2.5.5__tar.gz → 2.6.1__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.
Files changed (32) hide show
  1. {tinybird_toolset-2.5.5/src/tinybird_toolset.egg-info → tinybird_toolset-2.6.1}/PKG-INFO +1 -1
  2. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/setup.py +1 -1
  3. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/src/chtoolset/query.py +2 -0
  4. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1/src/tinybird_toolset.egg-info}/PKG-INFO +1 -1
  5. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/src/tinybird_toolset.egg-info/SOURCES.txt +2 -0
  6. tinybird_toolset-2.6.1/tests/test_check_ttl_partition_compatibility.py +175 -0
  7. tinybird_toolset-2.6.1/tests/test_validate_partition_key.py +150 -0
  8. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_validate_ttl.py +248 -0
  9. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/LICENSE +0 -0
  10. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/MANIFEST.in +0 -0
  11. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/README.md +0 -0
  12. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/conf.py +0 -0
  13. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/setup.cfg +0 -0
  14. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/src/chtoolset/__init__.py +0 -0
  15. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/src/tinybird_toolset.egg-info/dependency_links.txt +0 -0
  16. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/src/tinybird_toolset.egg-info/requires.txt +0 -0
  17. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/src/tinybird_toolset.egg-info/top_level.txt +0 -0
  18. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_check_compatible_types.py +0 -0
  19. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_check_write_query.py +0 -0
  20. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_chquery.py +0 -0
  21. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_convert_to_row_binary.py +0 -0
  22. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_explain_ast.py +0 -0
  23. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_get_columns_from_create_query.py +0 -0
  24. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_get_left_table.py +0 -0
  25. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_internal_cache.py +0 -0
  26. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_normalize_query_keep_names.py +0 -0
  27. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_normalize_table_column.py +0 -0
  28. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_parse_create_materialized_view_target_table.py +0 -0
  29. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_replace_tables.py +0 -0
  30. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_replace_tables_backward_compat.py +0 -0
  31. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_rewrite_aggregation_states.py +0 -0
  32. {tinybird_toolset-2.5.5 → tinybird_toolset-2.6.1}/tests/test_tables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinybird-toolset
3
- Version: 2.5.5
3
+ Version: 2.6.1
4
4
  Home-page: https://gitlab.com/tinybird/clickhouse-toolset
5
5
  Author: Tinybird.co
6
6
  Author-email: support@tinybird.co
@@ -1,7 +1,7 @@
1
1
  from setuptools import setup, Extension
2
2
 
3
3
  NAME = 'tinybird-toolset'
4
- VERSION = '2.5.5'
4
+ VERSION = '2.6.1'
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.
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinybird-toolset
3
- Version: 2.5.5
3
+ Version: 2.6.1
4
4
  Home-page: https://gitlab.com/tinybird/clickhouse-toolset
5
5
  Author: Tinybird.co
6
6
  Author-email: support@tinybird.co
@@ -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
@@ -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()
@@ -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):