tinybird-cli 6.5.6.dev0__tar.gz → 6.5.8.dev0__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 (49) hide show
  1. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/PKG-INFO +15 -1
  2. tinybird_cli-6.5.8.dev0/pyproject.toml +246 -0
  3. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/__cli__.py +2 -2
  4. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/ch_utils/constants.py +1 -0
  5. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/ch_utils/engine.py +90 -54
  6. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/client.py +80 -89
  7. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/config.py +0 -4
  8. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/datafile_common.py +72 -45
  9. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/datatypes.py +34 -12
  10. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/feedback_manager.py +5 -0
  11. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/git_settings.py +1 -3
  12. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/sql.py +3 -10
  13. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/sql_template.py +128 -98
  14. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/sql_template_fmt.py +10 -15
  15. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/sql_toolset.py +63 -11
  16. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/syncasync.py +9 -54
  17. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/branch.py +4 -25
  18. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/cicd.py +2 -0
  19. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/cli.py +5 -9
  20. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/common.py +33 -87
  21. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/config.py +1 -2
  22. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/datasource.py +15 -55
  23. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/pipe.py +49 -34
  24. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +4 -4
  25. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/token.py +1 -2
  26. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/workspace.py +11 -13
  27. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tornado_template.py +227 -83
  28. tinybird_cli-6.5.8.dev0/tinybird/utils/__init__.py +0 -0
  29. tinybird_cli-6.5.8.dev0/tinybird/utils/bools.py +19 -0
  30. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird_cli.egg-info/PKG-INFO +15 -1
  31. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird_cli.egg-info/SOURCES.txt +3 -0
  32. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/setup.cfg +0 -0
  33. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/check_pypi.py +0 -0
  34. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/context.py +0 -0
  35. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli.py +0 -0
  36. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
  37. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
  38. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  39. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
  40. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/job.py +0 -0
  41. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
  42. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
  43. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  44. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/test.py +0 -0
  45. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  46. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  47. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird_cli.egg-info/entry_points.txt +0 -0
  48. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird_cli.egg-info/requires.txt +0 -0
  49. {tinybird_cli-6.5.6.dev0 → tinybird_cli-6.5.8.dev0}/tinybird_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird_cli
3
- Version: 6.5.6.dev0
3
+ Version: 6.5.8.dev0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli
6
6
  Author: Tinybird
@@ -43,6 +43,20 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
43
43
  Changelog
44
44
  ----------
45
45
 
46
+
47
+ 6.5.7
48
+ ***********
49
+
50
+ - `Added` ``Markdown`` is now a valid ``FORMAT`` for SQL queries (for example ``tb sql "SELECT 1 FORMAT Markdown"``), returned as a Markdown table.
51
+ - `Changed` ``tb datasource append`` and ``tb datasource replace`` submit remote CSV, NDJSON, and Parquet URLs through the v1 import endpoint by default.
52
+ - `Added` Copy pipes accept ``ON_DEMAND_COMPUTE true`` so scheduled runs use on-demand compute. Manual ``tb pipe copy run`` still requires ``--on-demand-compute``, and asks for confirmation when the local datafile does not enable it (skip it with ``--yes``).
53
+ - `Removed` The ``--main`` flag from ``tb branch regression-tests``. It ran the comparison against the main Branch for a custom CI workflow.
54
+
55
+ 6.5.6
56
+ ***********
57
+
58
+ - `Changed` ``tb datasource append`` and ``tb datasource replace`` submit local CSV, NDJSON, and Parquet files through the v1 import endpoint by default. ``tb datasource append --events`` also submits its request body through v1. Commands wait for completion by default to preserve the existing output; pass ``--no-wait`` to print the queued job ID instead. Remote URL imports continue to use their existing route.
59
+
46
60
  6.5.5
47
61
  ***********
48
62
 
@@ -0,0 +1,246 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=61.0",
4
+ "wheel==0.37.1",
5
+ "cffi~=2.0",
6
+ ]
7
+ build-backend = "setuptools.build_meta"
8
+
9
+ [project]
10
+ name = "tinybird"
11
+ version = "1.0"
12
+ description = "tinybird"
13
+ readme = { text = "tinybird analytics", content-type = "text/markdown" }
14
+ requires-python = ">=3.13,<3.15"
15
+ authors = [
16
+ { name = "tinybird.co" },
17
+ ]
18
+ dependencies = [
19
+ "GitPython==3.1.44",
20
+ "Markdown==3.5.1",
21
+ "aioboto3~=15.0",
22
+ "aiofiles==24.1.0",
23
+ "aiohttp~=3.11",
24
+ "anthropic~=1.9",
25
+ "async-lru==2.3.0",
26
+ "aws-msk-iam-sasl-signer-python==1.0.2",
27
+ "cachetools~=6.0",
28
+ "cffi~=2.0",
29
+ "chardet==4.0.0",
30
+ "ciso8601==2.2.0",
31
+ "click~=8.5",
32
+ "clickhouse-cityhash==1.0.2.5",
33
+ "clickhouse-driver==0.2.9",
34
+ "confluent-kafka[avro,json,schemaregistry]==2.12.2",
35
+ "contextvars==2.4",
36
+ "cramjam==2.11.0",
37
+ "croniter==6.2.2",
38
+ "cryptography~=50.0",
39
+ "datasketch==1.6.5",
40
+ "dnspython==2.8.0",
41
+ "docker==7.1.0",
42
+ "fastavro==1.12.2",
43
+ "fastmcp==2.12.2",
44
+ "google-cloud-aiplatform~=1.148",
45
+ "google-cloud-compute==1.48.0",
46
+ "google-cloud-scheduler==2.20.0",
47
+ "google-cloud-secret-manager==2.29.0",
48
+ "google-cloud-storage~=3.4",
49
+ "google_api_core~=2.25",
50
+ "googleapis_common_protos==1.66.0",
51
+ "grpcio_status==1.67.1",
52
+ "humanfriendly==10.0",
53
+ "isal==1.8.0",
54
+ "jinja2",
55
+ "jsondiff==2.2.1",
56
+ "jsonschema==4.23.0",
57
+ "litellm~=1.97",
58
+ "logfire-api==4.2.0",
59
+ "lru-dict==1.3.0",
60
+ "lz4==4.4.5",
61
+ "memray==1.20.0",
62
+ "msgspec==0.21.1",
63
+ "multidict==6.9.1",
64
+ "networkx==3.4.2",
65
+ "numpy==1.26.4",
66
+ "objgraph==3.6.2",
67
+ "opentelemetry-api~=1.38",
68
+ "opentelemetry-exporter-otlp-proto-http~=1.38",
69
+ "opentelemetry-sdk~=1.38",
70
+ "orb-billing==4.80.1",
71
+ "orjson==3.11.9",
72
+ "packaging~=26.3",
73
+ "passlib==1.7.1",
74
+ "posthog==7.28.0",
75
+ "pre-commit==4.6.2",
76
+ "prometheus_client==0.26.0",
77
+ "prompt_toolkit==3.0.48",
78
+ "propcache==0.3.2",
79
+ "proto_plus==1.25.0",
80
+ "protobuf==5.29.5",
81
+ "psutil==7.2.2",
82
+ "psycopg2-binary==2.9.12",
83
+ "py-spy==0.4.2",
84
+ "pyarrow==23.0.1",
85
+ "pycurl==7.45.7",
86
+ "pydantic-ai-slim[anthropic]~=2.0",
87
+ "pydantic-ai-slim[google]~=2.0",
88
+ "pydantic-ai-slim[retries]~=2.0",
89
+ "pydantic~=2.12",
90
+ "pyjwt[crypto]==2.9.0",
91
+ "pympler==1.1",
92
+ "pynacl==1.5.0",
93
+ "pyperclip==1.9.0",
94
+ "python-dateutil==2.9.0.post0",
95
+ "python-dotenv==1.1.0",
96
+ "python-ulid==2.7.0",
97
+ "pyyaml~=6.0",
98
+ "redis[hiredis]==5.3.1",
99
+ "requests~=2.32.5",
100
+ "sentry-sdk==2.69.2",
101
+ "setproctitle==1.3.3",
102
+ "shandy-sqlfmt==0.11.1",
103
+ "shandy-sqlfmt[jinjafmt]==0.11.1",
104
+ "sqlparse==0.4.3",
105
+ "starlette==1.0.1",
106
+ "streaming-form-data==1.20.0",
107
+ "stripe==3.1.0",
108
+ "tabulate==0.8.10",
109
+ "thefuzz==0.22.1",
110
+ "tinybird-python-sdk[ai]==0.3.7",
111
+ "tinybird-toolset==2.7.1",
112
+ "toposort==1.10",
113
+ "tornado==6.5.5",
114
+ "urllib3~=2.8",
115
+ "uvicorn[standard]~=0.53.0",
116
+ "virtualenv==20.21.1",
117
+ "watchdog==6.0.0",
118
+ "wheel==0.37.1",
119
+ "zlib-ng==1.0.0",
120
+ "zstandard==0.25.0",
121
+ ]
122
+
123
+ [dependency-groups]
124
+ deploy = [
125
+ "GitPython==3.1.44",
126
+ "ansible==8.5.0",
127
+ "ara==1.6.1",
128
+ "boto3~=1.40",
129
+ "fabric==3.2.2",
130
+ "google-auth~=2.47",
131
+ "jsonnet",
132
+ "prettytable==3.8.0",
133
+ "requests~=2.32.5",
134
+ "requests[security]",
135
+ "xmltodict",
136
+ ]
137
+ devtools = [
138
+ "flameprof",
139
+ "matplotlib",
140
+ "memory_profiler",
141
+ "rope",
142
+ ]
143
+ doc = [
144
+ "Pallets-Sphinx-Themes==2.3.0",
145
+ "pyOpenSSL~=26.4",
146
+ "sphinx-autobuild==2024.2.4",
147
+ "sphinx-sitemap",
148
+ "sphinx==7.3.7",
149
+ "sphinxcontrib-httpdomain",
150
+ "sphinxcontrib-jquery==4.1",
151
+ ]
152
+ lint = [
153
+ "botocore-stubs~=1.40",
154
+ "celery-types~=0.20.0",
155
+ "flake8~=7.4",
156
+ "flake8-async~=26.8",
157
+ "mypy==2.3.1",
158
+ "ruff==0.16.10",
159
+ "types-Markdown",
160
+ "types-PyYAML",
161
+ "types-aiofiles==24.1.0.20241221",
162
+ "types-boto3-lite~=1.40",
163
+ "types-cachetools",
164
+ "types-chardet",
165
+ "types-paramiko",
166
+ "types-pycurl",
167
+ "types-python-dateutil",
168
+ "types-pytz",
169
+ "types-requests",
170
+ "types-tabulate",
171
+ "types_croniter",
172
+ ]
173
+ test = [
174
+ "coverage~=7.16",
175
+ "freezegun==1.5.5",
176
+ "google-auth~=2.47",
177
+ "gsutil==4.58",
178
+ "httpx2~=2.13",
179
+ "hypothesis==6.156.9",
180
+ "oauth2client==3.0.0",
181
+ "py==1.11.0",
182
+ "pytest-asyncio~=1.4",
183
+ "pytest-cov~=7.0",
184
+ "pytest-env==1.1.3",
185
+ "pytest-flakefinder==1.1.0",
186
+ "pytest-timeout==2.3.1",
187
+ "pytest-xdist==3.6.1",
188
+ "pytest~=8.1",
189
+ "rangehttpserver==1.4.0",
190
+ "yappi==1.7.6",
191
+ ]
192
+ # Synced automatically by plain `uv run`/`uv sync`, no `--group` needed.
193
+ dev = [
194
+ { include-group = "test" },
195
+ { include-group = "deploy" },
196
+ { include-group = "lint" },
197
+ ]
198
+
199
+ [project.scripts]
200
+ tinybird_server = "tinybird.app:run"
201
+ tinybird_tool = "tinybird.tinybird_tool.main:cli"
202
+ tb_old = "tinybird.tb_cli:cli"
203
+ tb = "tinybird.tb.cli:cli"
204
+ colibri = "tinybird.colibri.cli:cli"
205
+
206
+ [tool.uv.workspace]
207
+ members = [
208
+ "chbackups",
209
+ "clickhouse",
210
+ "data_connector",
211
+ "gatherer",
212
+ "platform-api",
213
+ "tinybird_orchestrator",
214
+ ]
215
+
216
+ [tool.setuptools]
217
+ include-package-data = true
218
+
219
+ [tool.setuptools.packages.find]
220
+ namespaces = false
221
+ include = ["tinybird", "tinybird.*", "tinybird_shared", "tinybird_shared.*", "scripts"]
222
+ exclude = ["tests*"]
223
+
224
+ [tool.setuptools.package-data]
225
+ tinybird = [
226
+ "templates/**/*",
227
+ "static/**/*",
228
+ "default_pipes/**/*",
229
+ "sql/**/*",
230
+ "monitors/**/*.sql",
231
+ "scripts/**/*",
232
+ "data_projects/**/*",
233
+ "data_projects/**/.*",
234
+ "raw_events/**/*",
235
+ "query_booster/**/*",
236
+ "deployments/**/*",
237
+ ]
238
+ tinybird_shared = [
239
+ "redis_client/**/*",
240
+ "retry/**/*",
241
+ "metrics/**/*",
242
+ "gatherer_settings/**/*",
243
+ "clickhouse/**/*",
244
+ "gatherer_backpressure/**/*",
245
+ "data_connector_circuit_breaker/**/*",
246
+ ]
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '6.5.6.dev0'
8
- __revision__ = '5a6cd1d'
7
+ __version__ = '6.5.8.dev0'
8
+ __revision__ = '57adbc63'
@@ -263,4 +263,5 @@ VALID_QUERY_FORMATS = (
263
263
  "JSONCompact",
264
264
  "JSONStringsEachRowWithProgress",
265
265
  "ODBCDriver2",
266
+ "Markdown",
266
267
  )
@@ -1,3 +1,4 @@
1
+ import functools
1
2
  import logging
2
3
  import re
3
4
  from collections import defaultdict
@@ -25,6 +26,9 @@ DEFAULT_EMPTY_PARAMETERS = ["ttl", "partition_key", "sorting_key"]
25
26
  # * `N` is a number
26
27
  SIMPLE_TTL_DEFINITION = re.compile(r"""^([a-zA-Z0-9_\-\.\(\)\ \/\*]*) \+ (toInterval[a-zA-Z]*\([0-9]+\))$""")
27
28
 
29
+ # ClickHouse 25.8 quotes the engine name `Null`, as `null` is a keyword.
30
+ _BACKQUOTED_ENGINE_NAME = re.compile(r"^`(\w+)`")
31
+
28
32
 
29
33
  class TableDetails:
30
34
  """
@@ -78,6 +82,16 @@ class TableDetails:
78
82
 
79
83
  @property
80
84
  def engine_full(self) -> Optional[str]:
85
+ """
86
+ >>> TableDetails({"engine_full": "`Null`", "engine": "Null"}).engine_full
87
+ 'Null'
88
+ >>> TableDetails({"engine_full": "`Null`()", "engine": "Null"}).engine_full
89
+ 'Null()'
90
+ >>> TableDetails({"engine_full": "Null", "engine": "Null"}).to_json()
91
+ {'engine_full': 'Null', 'engine': 'Null'}
92
+ >>> TableDetails({"engine_full": "`Null`", "engine": "Null"}).to_json()
93
+ {'engine_full': 'Null', 'engine': 'Null'}
94
+ """
81
95
  _engine_full: Optional[str] = self.details.get("engine_full", None)
82
96
  if not _engine_full:
83
97
  return None
@@ -86,7 +100,7 @@ class TableDetails:
86
100
  # We cannot remove index_granularity = 8192 blindly because it might be followed by other settings
87
101
  if settings and settings.strip().lower() == "index_granularity = 8192":
88
102
  _engine_full = _engine_full.replace(" SETTINGS index_granularity = 8192", "")
89
- return engine_replicated_to_local(_engine_full)
103
+ return engine_replicated_to_local(_BACKQUOTED_ENGINE_NAME.sub(r"\1", _engine_full))
90
104
 
91
105
  @property
92
106
  def original_engine_full(self) -> Optional[str]:
@@ -111,8 +125,7 @@ class TableDetails:
111
125
 
112
126
  @property
113
127
  def version(self):
114
- _version = self.details.get("version", None)
115
- return _version
128
+ return self.details.get("version", None)
116
129
 
117
130
  def is_replicated(self) -> bool:
118
131
  engine: Optional[str] = self.details.get("engine", None)
@@ -183,6 +196,7 @@ class TableDetails:
183
196
  settings = self.details.get("settings", None)
184
197
  if settings and settings.strip().lower() != "index_granularity = 8192":
185
198
  return settings
199
+ return None
186
200
 
187
201
  @property
188
202
  def ttl(self):
@@ -190,23 +204,19 @@ class TableDetails:
190
204
 
191
205
  @property
192
206
  def ver(self):
193
- _ver = self.details.get("ver", None)
194
- return _ver
207
+ return self.details.get("ver", None)
195
208
 
196
209
  @property
197
210
  def is_deleted(self):
198
- _is_deleted = self.details.get("is_deleted", None)
199
- return _is_deleted
211
+ return self.details.get("is_deleted", None)
200
212
 
201
213
  @property
202
214
  def columns(self):
203
- _columns = self.details.get("columns", None)
204
- return _columns
215
+ return self.details.get("columns", None)
205
216
 
206
217
  @property
207
218
  def sign(self):
208
- _sign = self.details.get("sign", None)
209
- return _sign
219
+ return self.details.get("sign", None)
210
220
 
211
221
  @property
212
222
  def statistics(self) -> Dict[str, Any]:
@@ -444,38 +454,70 @@ def get_engine_config(engine: str):
444
454
  )
445
455
 
446
456
 
447
- def engine_params(columns, params: List[EngineParam], args: Dict):
448
- params_values = []
457
+ def validate_engine_config(
458
+ engine: str, args: dict, schema: Optional[str] = None, columns: Optional[List[Dict[str, Any]]] = None
459
+ ) -> None:
460
+ if schema is not None and columns is not None:
461
+ raise ValueError("You can not use 'schema' and 'columns' at the same time")
462
+ _, (params, options) = get_engine_config(engine)
463
+ schema_columns = parse_table_structure(schema) if schema is not None else (columns or [])
464
+ engine_settings = {key.replace("engine_", ""): value for key, value in args.items()}
465
+
466
+ for arg in engine_settings:
467
+ if not hasattr(TableDetails, arg):
468
+ raise ValueError(f"engine_{arg} is not a valid option")
469
+
449
470
  for p in params:
450
- if p.required and p.name not in args:
471
+ if p.required and p.name not in engine_settings:
451
472
  raise ValueError(f"Missing required parameter '{p.name}'")
452
- param_value = args.get(p.name, None) or p.default_value
473
+ param_value = engine_settings.get(p.name) or p.default_value
453
474
  if not param_value:
454
475
  continue
455
476
  if p.is_valid:
456
477
  check_is_valid(
457
- valid_check=p.is_valid, check_type="parameter", columns=columns, tb_param=p.tb_param, value=param_value
478
+ valid_check=p.is_valid,
479
+ check_type="parameter",
480
+ columns=schema_columns,
481
+ tb_param=p.tb_param,
482
+ value=param_value,
458
483
  )
484
+
485
+ for o in options:
486
+ if o.required and o.name not in engine_settings:
487
+ raise ValueError(f"Missing required option '{o.name}'")
488
+ option_value = engine_settings.get(o.name) or o.default_value
489
+ if o.is_valid:
490
+ check_is_valid(
491
+ valid_check=o.is_valid,
492
+ check_type="option",
493
+ columns=schema_columns,
494
+ tb_param=o.tb_param,
495
+ value=option_value,
496
+ )
497
+
498
+
499
+ def engine_params(params: List[EngineParam], args: Dict) -> List[str]:
500
+ params_values = []
501
+ for p in params:
502
+ param_value = args.get(p.name, None) or p.default_value
503
+ if not param_value:
504
+ continue
459
505
  params_values.append(param_value)
460
506
  return params_values
461
507
 
462
508
 
463
- def engine_options(columns, options: List[EngineOption], args: Dict):
509
+ def engine_options(options: List[EngineOption], args: Dict) -> List[str]:
464
510
  options_values = []
465
511
  engine_settings = ""
466
512
 
467
513
  for o in options:
468
- if o.required and o.name not in args:
469
- raise ValueError(f"Missing required option '{o.name}'")
470
514
  option_value = args.get(o.name) or o.default_value
471
- if o.is_valid:
472
- check_is_valid(
473
- valid_check=o.is_valid, check_type="option", columns=columns, tb_param=o.tb_param, value=option_value
474
- )
475
-
476
515
  if option_value:
477
516
  if o.sql.lower() == "settings":
478
517
  engine_settings = f"{o.sql} {option_value}"
518
+ # Conditional TTLs fail if we wrap the value in parentheses
519
+ elif o.sql.lower() == "ttl":
520
+ options_values.append(f"{o.sql} {option_value}")
479
521
  else:
480
522
  options_values.append(f"{o.sql} ({option_value})")
481
523
 
@@ -491,7 +533,7 @@ def check_is_valid(
491
533
  columns: List[Dict[str, Any]],
492
534
  tb_param: str,
493
535
  value: str,
494
- ):
536
+ ) -> None:
495
537
  """
496
538
  >>> check_is_valid(sorting_key_is_valid, 'option', ['column-name'], 'sorting_key', 'date')
497
539
 
@@ -508,15 +550,13 @@ def check_is_valid(
508
550
  raise ValueError(f"Invalid value '{value}' for {check_type} '{tb_param}', reason: {e}")
509
551
 
510
552
 
511
- def build_engine(
512
- engine: str, columns: Optional[List], params: List[EngineParam], options: List[EngineOption], args: Dict
513
- ):
514
- return f"{engine}({', '.join(engine_params(columns, params, args))}) {' '.join(engine_options(columns, options, args))}".strip()
553
+ def build_engine(engine: str, params: List[EngineParam], options: List[EngineOption], args: Dict) -> str:
554
+ return f"{engine}({', '.join(engine_params(params, args))}) {' '.join(engine_options(options, args))}".strip()
515
555
 
516
556
 
517
557
  def engine_full_from_dict(
518
558
  engine: str, args: dict, schema: Optional[str] = None, columns: Optional[List[Dict[str, Any]]] = None
519
- ):
559
+ ) -> str:
520
560
  """
521
561
  >>> schema = ''
522
562
  >>> engine_full_from_dict('wadus', {}, schema=schema)
@@ -536,7 +576,9 @@ def engine_full_from_dict(
536
576
  >>> engine_full_from_dict('MergeTree', {'sorting_key': 'local_date, cod_store'}, schema=schema)
537
577
  'MergeTree() ORDER BY (local_date, cod_store)'
538
578
  >>> engine_full_from_dict('MergeTree', {'partition_key': 'toDate(timestamp)', 'sorting_key': 'local_date, cod_store', 'settings': 'index_granularity = 32, index_granularity_bytes = 2048', 'ttl': 'toDate(local_date) + INTERVAL 1 DAY'}, schema=schema)
539
- 'MergeTree() PARTITION BY (toDate(timestamp)) ORDER BY (local_date, cod_store) TTL (toDate(local_date) + INTERVAL 1 DAY) SETTINGS index_granularity = 32, index_granularity_bytes = 2048'
579
+ 'MergeTree() PARTITION BY (toDate(timestamp)) ORDER BY (local_date, cod_store) TTL toDate(local_date) + INTERVAL 1 DAY SETTINGS index_granularity = 32, index_granularity_bytes = 2048'
580
+ >>> engine_full_from_dict('MergeTree', {'sorting_key': 'timestamp', 'ttl': 'timestamp + INTERVAL 1 DAY DELETE WHERE is_deleted = 1'}, schema=schema)
581
+ 'MergeTree() ORDER BY (timestamp) TTL timestamp + INTERVAL 1 DAY DELETE WHERE is_deleted = 1'
540
582
 
541
583
  >>> schema = ''
542
584
  >>> engine_full_from_dict('CollapsingMergeTree', {'sign': 'sign_column'}, schema=schema)
@@ -605,20 +647,10 @@ def engine_full_from_dict(
605
647
  'ReplacingMergeTree(updated_at) PARTITION BY (tuple()) ORDER BY (project_id) SETTINGS index_granularity = 32'
606
648
  """
607
649
 
608
- if schema is not None and columns is not None:
609
- raise ValueError("You can not use 'schema' and 'columns' at the same time")
610
- engine_config = get_engine_config(engine)
611
- name, (params, options) = engine_config
612
- if columns is None and schema is not None:
613
- columns = parse_table_structure(schema)
614
-
650
+ validate_engine_config(engine, args, schema=schema, columns=columns)
651
+ name, (params, options) = get_engine_config(engine)
615
652
  engine_settings = {key.replace("engine_", ""): value for key, value in args.items()}
616
-
617
- for arg in engine_settings:
618
- if not hasattr(TableDetails, arg):
619
- raise ValueError(f"engine_{arg} is not a valid option")
620
-
621
- return build_engine(name, columns, params, options, engine_settings)
653
+ return build_engine(name, params, options, engine_settings)
622
654
 
623
655
 
624
656
  def engine_params_from_engine_full(engine_full: str) -> Dict[str, Any]:
@@ -694,8 +726,7 @@ def ttl_from_engine(engine: str) -> Optional[str]:
694
726
  return None
695
727
  settings_array = engine.split(" SETTINGS ")
696
728
  settings = " SETTINGS " + settings_array[1] if len(settings_array) > 1 else None
697
- ttl = ttl_array[1][: -(len(settings))] if settings else ttl_array[1]
698
- return ttl
729
+ return ttl_array[1][: -(len(settings))] if settings else ttl_array[1]
699
730
 
700
731
 
701
732
  def ttl_condition_from_engine_full(engine_full: Optional[str]) -> Optional[str]:
@@ -759,17 +790,22 @@ def ttl_condition_from_engine_full(engine_full: Optional[str]) -> Optional[str]:
759
790
  return None
760
791
 
761
792
 
793
+ @functools.lru_cache(maxsize=1024)
794
+ def _index_lines(create_table_query_expr: str) -> tuple[str, ...]:
795
+ from tinybird.sql_toolset import format_sql
796
+
797
+ return tuple(
798
+ line.strip() for line in format_sql(create_table_query_expr).splitlines() if line.strip().startswith("INDEX")
799
+ )
800
+
801
+
762
802
  def _parse_indexes(create_table_query_expr: str) -> List[TableIndex]:
763
- if create_table_query_expr == "":
803
+ # format_sql is CPU-heavy and runs on the event loop. ClickHouse writes the
804
+ # INDEX keyword in uppercase in create_table_query.
805
+ if "INDEX" not in create_table_query_expr:
764
806
  return []
765
807
  try:
766
- from tinybird.sql_toolset import format_sql
767
-
768
- indexes = [
769
- line.strip()
770
- for line in format_sql(create_table_query_expr).splitlines()
771
- if line.strip().startswith("INDEX")
772
- ]
808
+ indexes = list(_index_lines(create_table_query_expr))
773
809
  except ModuleNotFoundError:
774
810
  # this is not needed from CLI
775
811
  return []