tinybird-cli 6.5.6.dev0__py3-none-any.whl → 6.5.8.dev0__py3-none-any.whl
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/__cli__.py +2 -2
- tinybird/ch_utils/constants.py +1 -0
- tinybird/ch_utils/engine.py +90 -54
- tinybird/client.py +80 -89
- tinybird/config.py +0 -4
- tinybird/datafile_common.py +72 -45
- tinybird/datatypes.py +34 -12
- tinybird/feedback_manager.py +5 -0
- tinybird/git_settings.py +1 -3
- tinybird/sql.py +3 -10
- tinybird/sql_template.py +128 -98
- tinybird/sql_template_fmt.py +10 -15
- tinybird/sql_toolset.py +63 -11
- tinybird/syncasync.py +9 -54
- tinybird/tb_cli_modules/branch.py +4 -25
- tinybird/tb_cli_modules/cicd.py +2 -0
- tinybird/tb_cli_modules/cli.py +5 -9
- tinybird/tb_cli_modules/common.py +33 -87
- tinybird/tb_cli_modules/config.py +1 -2
- tinybird/tb_cli_modules/datasource.py +15 -55
- tinybird/tb_cli_modules/pipe.py +49 -34
- tinybird/tb_cli_modules/tinyunit/tinyunit.py +4 -4
- tinybird/tb_cli_modules/token.py +1 -2
- tinybird/tb_cli_modules/workspace.py +11 -13
- tinybird/tornado_template.py +227 -83
- tinybird/utils/__init__.py +0 -0
- tinybird/utils/bools.py +19 -0
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/METADATA +15 -1
- tinybird_cli-6.5.8.dev0.dist-info/RECORD +45 -0
- tinybird_cli-6.5.6.dev0.dist-info/RECORD +0 -43
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/WHEEL +0 -0
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/entry_points.txt +0 -0
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/top_level.txt +0 -0
tinybird/sql_template.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import ast
|
|
2
|
-
import builtins
|
|
3
2
|
import linecache
|
|
4
3
|
import logging
|
|
5
4
|
import re
|
|
@@ -18,7 +17,7 @@ from tinybird.context import (
|
|
|
18
17
|
)
|
|
19
18
|
|
|
20
19
|
from .datatypes import testers
|
|
21
|
-
from .tornado_template import VALID_CUSTOM_FUNCTION_NAMES, SecurityException, Template
|
|
20
|
+
from .tornado_template import SAFE_BUILTINS, VALID_CUSTOM_FUNCTION_NAMES, SecurityException, Template
|
|
22
21
|
|
|
23
22
|
VALID_ACTIVATE_FEATURES = frozenset(["analyzer", "parallel_replicas", "optimize_aggregation_in_order"])
|
|
24
23
|
|
|
@@ -84,6 +83,7 @@ class SQLTemplateException(ValueError):
|
|
|
84
83
|
JOB_TIMESTAMP_PARAM = "job_timestamp"
|
|
85
84
|
DEFAULT_PARAM_NAMES = ["format", "q"]
|
|
86
85
|
RESERVED_PARAM_NAMES = [
|
|
86
|
+
"__tb__csv_header",
|
|
87
87
|
"__tb__semver",
|
|
88
88
|
"debug_source_tables",
|
|
89
89
|
"debug",
|
|
@@ -142,8 +142,7 @@ def transform_type(
|
|
|
142
142
|
if x is None:
|
|
143
143
|
if defined:
|
|
144
144
|
raise SQLTemplateException(REQUIRED_PARAM_NOT_DEFINED, documentation="/cli/advanced-templates.html")
|
|
145
|
-
|
|
146
|
-
return None
|
|
145
|
+
return None
|
|
147
146
|
if tester == "String":
|
|
148
147
|
if x is not None:
|
|
149
148
|
return transform(x)
|
|
@@ -167,12 +166,11 @@ def _and(*args, **kwargs):
|
|
|
167
166
|
tk = k.rsplit("__", 1)
|
|
168
167
|
if len(tk) == 1:
|
|
169
168
|
return "="
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
169
|
+
if tk[1] in operands:
|
|
170
|
+
return operands[tk[1]]
|
|
171
|
+
raise SQLTemplateException(
|
|
172
|
+
f"operand {tk[1]} not supported", documentation="/cli/advanced-templates.html#sql_and"
|
|
173
|
+
)
|
|
176
174
|
|
|
177
175
|
return Expression(
|
|
178
176
|
" and ".join([f"{_name(k)} {_op(k)} {expression_wrapper(v, k)}" for k, v in kwargs.items() if v is not None])
|
|
@@ -277,8 +275,7 @@ def columns(x, default=None, fn=None):
|
|
|
277
275
|
|
|
278
276
|
if fn:
|
|
279
277
|
return Expression(",".join(f"{fn}({str(column(c, c))}) as {c}" for c in _columns))
|
|
280
|
-
|
|
281
|
-
return Expression(",".join(str(column(c, c)) for c in _columns))
|
|
278
|
+
return Expression(",".join(str(column(c, c)) for c in _columns))
|
|
282
279
|
|
|
283
280
|
|
|
284
281
|
def column(x, default=None):
|
|
@@ -368,11 +365,10 @@ def boolean(x, default=None):
|
|
|
368
365
|
if default is None:
|
|
369
366
|
return 0
|
|
370
367
|
return boolean(default)
|
|
371
|
-
|
|
368
|
+
if isinstance(x, Placeholder):
|
|
372
369
|
return boolean(default)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return 0
|
|
370
|
+
if isinstance(x, str) and (x == "0" or x.lower() == "false"):
|
|
371
|
+
return 0
|
|
376
372
|
|
|
377
373
|
return int(bool(x))
|
|
378
374
|
|
|
@@ -403,8 +399,7 @@ def array_type(types):
|
|
|
403
399
|
raise SQLTemplateException(
|
|
404
400
|
REQUIRED_PARAM_NOT_DEFINED, documentation="/cli/advanced-templates.html"
|
|
405
401
|
)
|
|
406
|
-
|
|
407
|
-
return None
|
|
402
|
+
return None
|
|
408
403
|
values = []
|
|
409
404
|
list_values = x if type(x) == list else x.split(",") # noqa: E721
|
|
410
405
|
for i, t in enumerate(list_values):
|
|
@@ -428,22 +423,29 @@ def array_type(types):
|
|
|
428
423
|
return _f
|
|
429
424
|
|
|
430
425
|
|
|
431
|
-
def sql_unescape(x, what=""):
|
|
426
|
+
def sql_unescape(x: str, what: str = "") -> Expression:
|
|
432
427
|
"""
|
|
433
428
|
unescapes specific characters in a string. It allows to allow some
|
|
434
429
|
special characters to be used, for example in like condictionals
|
|
435
430
|
|
|
436
431
|
{{sql_unescape(String(like_filter), '%')}}
|
|
437
432
|
|
|
433
|
+
`what` must be given and must not contain quotes or backslashes: unescaping
|
|
434
|
+
those would let the caller close the string literal, so they stay escaped.
|
|
438
435
|
|
|
439
436
|
>>> sql_unescape('testing%', '%')
|
|
440
437
|
"'testing%'"
|
|
441
438
|
>>> sql_unescape('testing%', '$')
|
|
442
439
|
"'testing\\\\%'"
|
|
443
440
|
>>> sql_unescape('testing"')
|
|
444
|
-
'\\'testing"\\''
|
|
441
|
+
'\\'testing\\\\"\\''
|
|
442
|
+
>>> sql_unescape("x' OR 1=1", "'")
|
|
443
|
+
"'x\\\\' OR 1=1'"
|
|
445
444
|
"""
|
|
446
|
-
|
|
445
|
+
escaped = sqlescape_for_string_expression(x)
|
|
446
|
+
if what and not any(c in what for c in "'\"\\"):
|
|
447
|
+
escaped = escaped.replace(f"\\{what}", what)
|
|
448
|
+
return Expression("'" + escaped + "'")
|
|
447
449
|
|
|
448
450
|
|
|
449
451
|
def split_to_array(x, default="", separator: str = ","):
|
|
@@ -744,12 +746,11 @@ def __date_diff(
|
|
|
744
746
|
|
|
745
747
|
if unit == "days":
|
|
746
748
|
return int(diff / 86400)
|
|
747
|
-
|
|
749
|
+
if unit == "hours":
|
|
748
750
|
return int(diff / 3600)
|
|
749
|
-
|
|
751
|
+
if unit == "minutes":
|
|
750
752
|
return int(diff / 60)
|
|
751
|
-
|
|
752
|
-
return int(diff)
|
|
753
|
+
return int(diff)
|
|
753
754
|
except Exception:
|
|
754
755
|
if none_if_error:
|
|
755
756
|
return None
|
|
@@ -1341,16 +1342,18 @@ sqlescape_for_string_expression = sqlescape_generator(
|
|
|
1341
1342
|
)
|
|
1342
1343
|
|
|
1343
1344
|
|
|
1344
|
-
def escape_single_quote_str(s):
|
|
1345
|
-
|
|
1345
|
+
def escape_single_quote_str(s: str) -> str:
|
|
1346
|
+
# ClickHouse string literals treat backslash as an escape char, so it must be doubled too:
|
|
1347
|
+
# otherwise a trailing backslash escapes the closing quote and the following input becomes SQL
|
|
1348
|
+
return "'" + s.replace("\\", "\\\\").replace("'", "''") + "'"
|
|
1346
1349
|
|
|
1347
1350
|
|
|
1348
1351
|
def expression_wrapper(x, name, escape_arrays: bool = False):
|
|
1349
1352
|
if type(x) in (unicode_type, bytes, str):
|
|
1350
1353
|
return "'" + sqlescape_for_string_expression(x) + "'"
|
|
1351
|
-
|
|
1354
|
+
if isinstance(x, Placeholder):
|
|
1352
1355
|
return "'__no_value__'"
|
|
1353
|
-
|
|
1356
|
+
if isinstance(x, Comment):
|
|
1354
1357
|
return "-- {x} \n"
|
|
1355
1358
|
if x is None:
|
|
1356
1359
|
truncated_name = name[:20] + "..." if len(name) > 20 else name
|
|
@@ -1361,10 +1364,7 @@ def expression_wrapper(x, name, escape_arrays: bool = False):
|
|
|
1361
1364
|
logging.warning(f"expression_wrapper -> list :{x}:")
|
|
1362
1365
|
|
|
1363
1366
|
try:
|
|
1364
|
-
|
|
1365
|
-
f"[{','.join(escape_single_quote_str(item) if isinstance(item, str) else str(item) for item in x)}]"
|
|
1366
|
-
)
|
|
1367
|
-
return result
|
|
1367
|
+
return f"[{','.join(escape_single_quote_str(item) if isinstance(item, str) else str(item) for item in x)}]"
|
|
1368
1368
|
except Exception as e:
|
|
1369
1369
|
logging.error(f"Error escaping array: {e}")
|
|
1370
1370
|
return x
|
|
@@ -1383,7 +1383,7 @@ _namespace = {
|
|
|
1383
1383
|
# they raise a pretty non understandable error but if someone
|
|
1384
1384
|
# is using them they know what they are trying to do
|
|
1385
1385
|
# read https://anee.me/escaping-python-jails-849c65cf306e on how to escape from python jails
|
|
1386
|
-
"
|
|
1386
|
+
"__builtins__": SAFE_BUILTINS,
|
|
1387
1387
|
"__import__": {},
|
|
1388
1388
|
"__debug__": {},
|
|
1389
1389
|
"__doc__": {},
|
|
@@ -1396,10 +1396,13 @@ _namespace = {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
1398
|
|
|
1399
|
-
|
|
1399
|
+
# Built from the builtins templates can actually resolve: names missing from SAFE_BUILTINS (`type`, `dir`,
|
|
1400
|
+
# `property`, `object`, ...) can only be template parameters, so they must be discovered as such to get a
|
|
1401
|
+
# value (None or a test-mode placeholder) instead of raising NameError.
|
|
1402
|
+
reserved_vars = {"_tt_tmp", "_tt_append", "isinstance", "str", "error", "custom_error", *SAFE_BUILTINS}
|
|
1400
1403
|
for p in DEFAULT_PARAM_NAMES: # we handle these in an specific manner
|
|
1401
1404
|
reserved_vars.discard(p) # `format` is part of builtins
|
|
1402
|
-
# Allow 'id'
|
|
1405
|
+
# Allow 'id' as a template parameter name despite being a builtin - https://gitlab.com/tinybird/analytics/-/issues/19119
|
|
1403
1406
|
reserved_vars.discard("id")
|
|
1404
1407
|
error_vars = ["error", "custom_error"]
|
|
1405
1408
|
|
|
@@ -1425,20 +1428,18 @@ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
|
|
|
1425
1428
|
# secret available: Always use workspace secret regardless of test mode
|
|
1426
1429
|
template_execution_results.add_ch_param(x)
|
|
1427
1430
|
return Symbol("{" + sqlescape(x) + ": String}")
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
f"Cannot access secret '{x}'. Check the secret exists in the Workspace and the token has the required scope."
|
|
1441
|
-
)
|
|
1431
|
+
# secret not available: Check test mode and defaults
|
|
1432
|
+
is_test_mode = TB_SECRET_IN_TEST_MODE in template_execution_results
|
|
1433
|
+
if default is not None:
|
|
1434
|
+
# Use provided default value
|
|
1435
|
+
return default
|
|
1436
|
+
if is_test_mode:
|
|
1437
|
+
# In test mode without default - return placeholder
|
|
1438
|
+
return Symbol("{" + sqlescape(x) + ": String}")
|
|
1439
|
+
# Not in test mode, no secret, no default - raise error
|
|
1440
|
+
raise SQLTemplateException(
|
|
1441
|
+
f"Cannot access secret '{x}'. Check the secret exists in the Workspace and the token has the required scope."
|
|
1442
|
+
)
|
|
1442
1443
|
except Exception:
|
|
1443
1444
|
raise SQLTemplateException(
|
|
1444
1445
|
f"Cannot access secret '{x}'. Check the secret exists in the Workspace and the token has the required scope."
|
|
@@ -1478,8 +1479,7 @@ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
|
|
|
1478
1479
|
return Expression(f"-- disable {feature}\n")
|
|
1479
1480
|
|
|
1480
1481
|
namespace.update(_namespace)
|
|
1481
|
-
#
|
|
1482
|
-
# We need to do the override here because if we modify the _namespace, we would filter out parameters from the users
|
|
1482
|
+
# Default it here, not in _namespace, so it stays visible to variable extraction.
|
|
1483
1483
|
namespace.update({"id": None})
|
|
1484
1484
|
namespace.update(kwargs)
|
|
1485
1485
|
namespace.update(
|
|
@@ -1488,6 +1488,8 @@ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
|
|
|
1488
1488
|
# the generated source code.
|
|
1489
1489
|
"__name__": self.name.replace(".", "_"),
|
|
1490
1490
|
"__loader__": ObjectDict(get_source=lambda name: self.code),
|
|
1491
|
+
# Set again after kwargs so request parameters can't override it
|
|
1492
|
+
"__builtins__": SAFE_BUILTINS,
|
|
1491
1493
|
"max_threads": set_max_threads,
|
|
1492
1494
|
"tb_secret": set_tb_secret,
|
|
1493
1495
|
"tb_var": set_tb_secret,
|
|
@@ -1522,14 +1524,12 @@ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
|
|
|
1522
1524
|
|
|
1523
1525
|
if line:
|
|
1524
1526
|
raise SQLTemplateException(f"{message.strip()} line {line[0]}")
|
|
1525
|
-
|
|
1526
|
-
raise SQLTemplateException(f"{message.strip()}")
|
|
1527
|
+
raise SQLTemplateException(f"{message.strip()}")
|
|
1527
1528
|
except Exception as e:
|
|
1528
1529
|
if isinstance(e, SQLTemplateException):
|
|
1529
1530
|
raise e
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
raise ValueError(str(e))
|
|
1531
|
+
logging.exception(f"Error on unbound local error: {e}")
|
|
1532
|
+
raise ValueError(str(e))
|
|
1533
1533
|
except TypeError as e:
|
|
1534
1534
|
error = str(e)
|
|
1535
1535
|
if "not supported between instances of 'Placeholder' and " in str(e):
|
|
@@ -1829,7 +1829,7 @@ def get_var_names(t: Template):
|
|
|
1829
1829
|
>>> [v['name'] for v in get_var_names(Template("{% for name in secret_names %}{{tb_secret(name)}}{% end %}"))]
|
|
1830
1830
|
['secret_names', 'name', 'tb_secret', 'name']
|
|
1831
1831
|
|
|
1832
|
-
Type casting with date function in elif chain
|
|
1832
|
+
Type casting with date function in elif chain:
|
|
1833
1833
|
>>> t = Template("{% if kind == 'int' %}{{Int32(val, 0)}}{% elif kind == 'date' %}{{DateTime(val)}}{% elif kind == 'diff' %}{{date_diff_in_days(val, now)}}{% end %}")
|
|
1834
1834
|
>>> [v['name'] for v in get_var_names(t)]
|
|
1835
1835
|
['kind', 'Int32', 'val', 'kind', 'DateTime', 'val', 'kind', 'date_diff_in_days', 'val', 'now']
|
|
@@ -1960,29 +1960,27 @@ def get_var_data(content, node_id=None):
|
|
|
1960
1960
|
# ast.Constant nodes, so .value holds str/bytes/int/float/bool/None.
|
|
1961
1961
|
if type(x) == ast.Constant: # noqa: E721
|
|
1962
1962
|
return x.value
|
|
1963
|
-
|
|
1963
|
+
if type(x) == ast.Name: # noqa: E721
|
|
1964
1964
|
return x.id
|
|
1965
|
-
|
|
1965
|
+
if type(x) == ast.List: # noqa: E721
|
|
1966
1966
|
# List can hold different types
|
|
1967
1967
|
return _get_list_var_data(x)
|
|
1968
|
-
|
|
1968
|
+
if type(x) == ast.BinOp: # noqa: E721
|
|
1969
1969
|
# in this case there could be several variables
|
|
1970
1970
|
# if that's the case the left one is the main
|
|
1971
1971
|
r = node_to_value(x.left)
|
|
1972
1972
|
if not r:
|
|
1973
1973
|
r = node_to_value(x.right)
|
|
1974
1974
|
return r
|
|
1975
|
-
|
|
1975
|
+
if type(x) == ast.UnaryOp and type(x.operand) == ast.Constant: # noqa: E721
|
|
1976
1976
|
if type(x.op) == ast.USub: # noqa: E721
|
|
1977
1977
|
return x.operand.value * -1
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
# don't let this ruin the parsing
|
|
1985
|
-
pass
|
|
1978
|
+
return x.operand.value
|
|
1979
|
+
try:
|
|
1980
|
+
return x.id
|
|
1981
|
+
except Exception:
|
|
1982
|
+
# don't let this ruin the parsing
|
|
1983
|
+
pass
|
|
1986
1984
|
return None
|
|
1987
1985
|
|
|
1988
1986
|
def _get_list_var_data(x):
|
|
@@ -1994,7 +1992,7 @@ def get_var_data(content, node_id=None):
|
|
|
1994
1992
|
# ast.Bytes/ast.Str/ast.Num/ast.NameConstant (removed in 3.14).
|
|
1995
1993
|
if type(first_elem) == ast.Constant: # noqa: E721
|
|
1996
1994
|
return [elem.value for elem in x.elts]
|
|
1997
|
-
|
|
1995
|
+
if type(first_elem) == ast.Name: # noqa: E721
|
|
1998
1996
|
return [elem.id for elem in x.elts]
|
|
1999
1997
|
|
|
2000
1998
|
return []
|
|
@@ -2013,8 +2011,7 @@ def get_var_data(content, node_id=None):
|
|
|
2013
2011
|
# to just `ast.parse(content)` once we confirm all environments use patched Python versions.
|
|
2014
2012
|
def parse_content(content, retries=0):
|
|
2015
2013
|
try:
|
|
2016
|
-
|
|
2017
|
-
return parsed
|
|
2014
|
+
return ast.parse(content)
|
|
2018
2015
|
except Exception as e:
|
|
2019
2016
|
if "AST constructor recursion depth mismatch" not in str(e):
|
|
2020
2017
|
raise e
|
|
@@ -2042,8 +2039,7 @@ def get_var_data(content, node_id=None):
|
|
|
2042
2039
|
if type(x) == ast.Call: # noqa: E721
|
|
2043
2040
|
# Nested calls are traversed via `ast.iter_child_nodes` below.
|
|
2044
2041
|
continue
|
|
2045
|
-
|
|
2046
|
-
args.append(node_to_value(x))
|
|
2042
|
+
args.append(node_to_value(x))
|
|
2047
2043
|
|
|
2048
2044
|
kwargs = {}
|
|
2049
2045
|
for x in node.keywords:
|
|
@@ -2385,7 +2381,24 @@ def wrap_vars(t, escape_arrays: bool = False):
|
|
|
2385
2381
|
return var
|
|
2386
2382
|
|
|
2387
2383
|
|
|
2388
|
-
def
|
|
2384
|
+
def _constant_table_name(node: ast.expr) -> Optional[str]:
|
|
2385
|
+
"""Value of a constant table() argument: a literal or a concatenation of string literals."""
|
|
2386
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
|
2387
|
+
return str(node.value)
|
|
2388
|
+
return _string_literal(node)
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
def _string_literal(node: ast.expr) -> Optional[str]:
|
|
2392
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
2393
|
+
return node.value
|
|
2394
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
2395
|
+
left, right = _string_literal(node.left), _string_literal(node.right)
|
|
2396
|
+
if left is not None and right is not None:
|
|
2397
|
+
return left + right
|
|
2398
|
+
return None
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
def get_used_tables_in_template(sql: str) -> List[str]:
|
|
2389
2402
|
"""
|
|
2390
2403
|
>>> get_used_tables_in_template("select * from {{table('test')}}")
|
|
2391
2404
|
['test']
|
|
@@ -2399,30 +2412,42 @@ def get_used_tables_in_template(sql):
|
|
|
2399
2412
|
[]
|
|
2400
2413
|
>>> get_used_tables_in_template("select * from {{table('my.test')}}, {{table('another.one')}}")
|
|
2401
2414
|
['my.test', 'another.one']
|
|
2415
|
+
|
|
2416
|
+
Only constant table names can be known without rendering, so anything else is skipped
|
|
2417
|
+
>>> get_used_tables_in_template("select * from {{table(tbl)}}, {{TABLE('b')}}, {{table('my.' + 'c')}}")
|
|
2418
|
+
['b', 'my.c']
|
|
2402
2419
|
"""
|
|
2420
|
+
return list(_get_used_tables_in_template_cached(sql))
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
# Building the Template dominates the cost and the result only depends on the SQL, which deployments and
|
|
2424
|
+
# /v0/sql pipelines scan repeatedly
|
|
2425
|
+
@lru_cache(maxsize=2**10)
|
|
2426
|
+
def _get_used_tables_in_template_cached(sql: str) -> Tuple[str, ...]:
|
|
2403
2427
|
try:
|
|
2404
2428
|
t = Template(sql)
|
|
2405
2429
|
|
|
2406
2430
|
def _n(chunks, tables):
|
|
2407
2431
|
for x in chunks:
|
|
2408
2432
|
if type(x).__name__ == "_Expression":
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2433
|
+
# Read the names from the AST instead of executing the expression: this runs outside
|
|
2434
|
+
# generate(), without the restricted builtins, on every pipe, query and deployment.
|
|
2435
|
+
for node in ast.walk(ast.parse(x.expression)):
|
|
2436
|
+
if (
|
|
2437
|
+
isinstance(node, ast.Call)
|
|
2438
|
+
and isinstance(node.func, ast.Name)
|
|
2439
|
+
and node.func.id in ("table", "TABLE")
|
|
2440
|
+
and node.args
|
|
2441
|
+
):
|
|
2442
|
+
name = _constant_table_name(node.args[0])
|
|
2443
|
+
if name is not None:
|
|
2444
|
+
tables.append(name)
|
|
2420
2445
|
elif type(x).__name__ == "_ControlBlock":
|
|
2421
2446
|
_n(x.body.chunks, tables)
|
|
2422
2447
|
|
|
2423
|
-
tables = []
|
|
2448
|
+
tables: List[str] = []
|
|
2424
2449
|
_n(t.file.body.chunks, tables)
|
|
2425
|
-
return tables
|
|
2450
|
+
return tuple(tables)
|
|
2426
2451
|
except SecurityException as e:
|
|
2427
2452
|
raise SQLTemplateException(e)
|
|
2428
2453
|
|
|
@@ -2531,8 +2556,7 @@ def format_SQLTemplateException_message(e: SQLTemplateException, vars_and_types:
|
|
|
2531
2556
|
f"{REQUIRED_PARAM_NOT_DEFINED}. Check the parameters {join_with_different_last_separator(vars_with_default_none)}. Please provide a value or set a default value in the pipe code.",
|
|
2532
2557
|
e.documentation,
|
|
2533
2558
|
)
|
|
2534
|
-
|
|
2535
|
-
raise e
|
|
2559
|
+
raise e
|
|
2536
2560
|
|
|
2537
2561
|
|
|
2538
2562
|
def render_sql_template(
|
|
@@ -2893,6 +2917,13 @@ def render_sql_template(
|
|
|
2893
2917
|
documentation="/cli/advanced-templates.html",
|
|
2894
2918
|
)
|
|
2895
2919
|
raise SQLTemplateException(str(e), documentation="/cli/advanced-templates.html")
|
|
2920
|
+
except KeyError as e:
|
|
2921
|
+
key = e.args[0] if e.args else "unknown"
|
|
2922
|
+
raise SQLTemplateException(
|
|
2923
|
+
f"Missing key '{key}' while rendering the template. "
|
|
2924
|
+
"Ensure every object accessed in the template has this key.",
|
|
2925
|
+
documentation="/cli/advanced-templates.html",
|
|
2926
|
+
)
|
|
2896
2927
|
except IndexError as e:
|
|
2897
2928
|
# This happens when trying to access string indices on empty strings
|
|
2898
2929
|
if "string index out of range" in str(e):
|
|
@@ -2906,12 +2937,12 @@ def render_sql_template(
|
|
|
2906
2937
|
# https://gitlab.com/tinybird/analytics/-/issues/943
|
|
2907
2938
|
if "length" in v and not v["length"]:
|
|
2908
2939
|
raise SQLTemplateException("length cannot be used as a variable name or as a function inside of a template")
|
|
2909
|
-
|
|
2940
|
+
if "missing 1 required positional argument" in str(e):
|
|
2910
2941
|
raise SQLTemplateException(
|
|
2911
2942
|
"one of the transform type functions is missing an argument",
|
|
2912
2943
|
documentation="/cli/advanced-templates.html#transform-types-functions",
|
|
2913
2944
|
)
|
|
2914
|
-
|
|
2945
|
+
if "not callable" in str(e) or "unhashable type" in str(e):
|
|
2915
2946
|
raise SQLTemplateException(
|
|
2916
2947
|
"wrong syntax, you might be using a not valid function inside a control block",
|
|
2917
2948
|
documentation="/cli/advanced-templates.html",
|
|
@@ -3056,7 +3087,7 @@ def render_template_with_secrets(
|
|
|
3056
3087
|
)
|
|
3057
3088
|
return '""'
|
|
3058
3089
|
return value
|
|
3059
|
-
|
|
3090
|
+
if default is not None:
|
|
3060
3091
|
if isinstance(default, str) and len(default) == 0:
|
|
3061
3092
|
if empty_secret_raises:
|
|
3062
3093
|
raise SQLTemplateException(
|
|
@@ -3064,10 +3095,9 @@ def render_template_with_secrets(
|
|
|
3064
3095
|
)
|
|
3065
3096
|
return '""'
|
|
3066
3097
|
return default
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
)
|
|
3098
|
+
raise SQLTemplateException(
|
|
3099
|
+
f"Cannot access secret '{secret_name}'. Check the secret exists in the Workspace and the token has the required scope."
|
|
3100
|
+
)
|
|
3071
3101
|
|
|
3072
3102
|
# Create the template
|
|
3073
3103
|
t = Template(content, name=name, autoescape=None)
|
tinybird/sql_template_fmt.py
CHANGED
|
@@ -33,8 +33,7 @@ class TBLine(Line):
|
|
|
33
33
|
Tinybird => This is overriden from the base Line because we want SQL inside a template to be indented
|
|
34
34
|
https://github.com/tconbeer/sqlfmt/blob/c11775b92d8a45f0e91d871b81a88a894d620bec/src/sqlfmt/line.py#L92
|
|
35
35
|
"""
|
|
36
|
-
|
|
37
|
-
return prefix
|
|
36
|
+
return INDENT * (self.depth[0] + self.depth[1])
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
def from_nodes(
|
|
@@ -106,8 +105,7 @@ def _format_jinja_node(self, node: Node, max_length: int) -> bool:
|
|
|
106
105
|
|
|
107
106
|
return tag.is_blackened
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
return False
|
|
108
|
+
return False
|
|
111
109
|
|
|
112
110
|
|
|
113
111
|
# Some monkey patching
|
|
@@ -158,8 +156,7 @@ class TinybirdNodeManager(NodeManager):
|
|
|
158
156
|
TokenType.SET_OPERATOR,
|
|
159
157
|
):
|
|
160
158
|
return " ".join(token.token.split())
|
|
161
|
-
|
|
162
|
-
return token.token
|
|
159
|
+
return token.token
|
|
163
160
|
|
|
164
161
|
|
|
165
162
|
class TinybirdDialect(ClickHouse):
|
|
@@ -217,8 +214,8 @@ class TinybirdDialect(ClickHouse):
|
|
|
217
214
|
],
|
|
218
215
|
}
|
|
219
216
|
|
|
220
|
-
for section in override_rules:
|
|
221
|
-
for rule in
|
|
217
|
+
for section, rules in override_rules.items():
|
|
218
|
+
for rule in rules:
|
|
222
219
|
for rr in self.RULES[section]:
|
|
223
220
|
if rr.name == rule.name:
|
|
224
221
|
self.RULES[section].remove(rr)
|
|
@@ -230,12 +227,11 @@ class TinybirdDialect(ClickHouse):
|
|
|
230
227
|
Creates and returns an analyzer that uses the Dialect's rules for lexing
|
|
231
228
|
Custom NodeManager for Tinybird
|
|
232
229
|
"""
|
|
233
|
-
|
|
230
|
+
return Analyzer(
|
|
234
231
|
line_length=line_length,
|
|
235
232
|
rules=self.get_rules(),
|
|
236
233
|
node_manager=TinybirdNodeManager(self.case_sensitive_names, self.lower_keywords),
|
|
237
234
|
)
|
|
238
|
-
return analyzer
|
|
239
235
|
|
|
240
236
|
|
|
241
237
|
@dataclass
|
|
@@ -253,11 +249,10 @@ class TBMode(Mode):
|
|
|
253
249
|
def _calc_str(self) -> str:
|
|
254
250
|
if self.is_multiline:
|
|
255
251
|
return self.token.token + "\n"
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return marker + " " + comment_text + "\n"
|
|
252
|
+
marker, comment_text = self._comment_parts()
|
|
253
|
+
if comment_text == "":
|
|
254
|
+
return marker + "\n"
|
|
255
|
+
return marker + " " + comment_text + "\n"
|
|
261
256
|
|
|
262
257
|
|
|
263
258
|
Comment._calc_str = property(_calc_str)
|