tinybird-cli 5.22.2.dev0__tar.gz → 5.22.3.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.
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/PKG-INFO +6 -1
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/__cli__.py +2 -2
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/context.py +0 -1
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/datafile_common.py +4 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql_template.py +90 -84
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql_toolset.py +209 -29
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/PKG-INFO +6 -1
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/setup.cfg +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/ch_utils/constants.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/ch_utils/engine.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/check_pypi.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/client.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/config.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/connectors.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/datatypes.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/feedback_manager.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/git_settings.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql_template_fmt.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/syncasync.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/common.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/datasource.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/token.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tornado_template.py +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/SOURCES.txt +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/entry_points.txt +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/requires.txt +0 -0
- {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.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: 5.22.
|
|
3
|
+
Version: 5.22.3.dev0
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/cli
|
|
6
6
|
Author: Tinybird
|
|
@@ -61,6 +61,11 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
61
61
|
Changelog
|
|
62
62
|
----------
|
|
63
63
|
|
|
64
|
+
5.22.2
|
|
65
|
+
***********
|
|
66
|
+
|
|
67
|
+
- `Fixed` tokens in existing datasource files not being updated when using `tb push -f`
|
|
68
|
+
|
|
64
69
|
5.22.1
|
|
65
70
|
***********
|
|
66
71
|
|
|
@@ -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__ = '5.22.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '5.22.3.dev0'
|
|
8
|
+
__revision__ = '35eaae2'
|
|
@@ -11,7 +11,6 @@ hfi_workspace_data: ContextVar["HfiWorkspaceData"] = ContextVar("hfi_workspace_d
|
|
|
11
11
|
table_id: ContextVar[str] = ContextVar("table_id")
|
|
12
12
|
hfi_frequency: ContextVar[float] = ContextVar("hfi_frequency")
|
|
13
13
|
hfi_frequency_gatherer: ContextVar[float] = ContextVar("hfi_frequency_gatherer")
|
|
14
|
-
use_gatherer: ContextVar[bool] = ContextVar("use_gatherer")
|
|
15
14
|
gatherer_allow_s3_backup_on_user_errors: ContextVar[bool] = ContextVar("gatherer_allow_s3_backup_on_user_errors")
|
|
16
15
|
disable_template_security_validation: ContextVar[bool] = ContextVar("disable_template_security_validation")
|
|
17
16
|
origin: ContextVar[str] = ContextVar("origin")
|
|
@@ -3515,6 +3515,10 @@ async def new_ds(
|
|
|
3515
3515
|
except Exception as e:
|
|
3516
3516
|
promote_error_message = str(e)
|
|
3517
3517
|
|
|
3518
|
+
# Update token scopes when force-pushing an existing datasource
|
|
3519
|
+
if ds.get("tokens"):
|
|
3520
|
+
await manage_tokens()
|
|
3521
|
+
|
|
3518
3522
|
if alter_response and make_changes:
|
|
3519
3523
|
# alter operation finished
|
|
3520
3524
|
pass
|
|
@@ -3,6 +3,7 @@ import builtins
|
|
|
3
3
|
import linecache
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
|
+
from collections import deque
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from functools import lru_cache
|
|
8
9
|
from io import StringIO
|
|
@@ -1580,8 +1581,6 @@ def get_var_names(t):
|
|
|
1580
1581
|
variable_names = [x for x in c.co_names if x not in _namespace and x not in reserved_vars]
|
|
1581
1582
|
v += list(map(lambda variable: {"line": line_number, "name": variable}, variable_names))
|
|
1582
1583
|
elif type(x).__name__ == "_ControlBlock":
|
|
1583
|
-
from io import StringIO
|
|
1584
|
-
|
|
1585
1584
|
buffer = StringIO()
|
|
1586
1585
|
writer = CodeWriter(buffer, t)
|
|
1587
1586
|
x.generate(writer)
|
|
@@ -1598,6 +1597,11 @@ def get_var_names(t):
|
|
|
1598
1597
|
|
|
1599
1598
|
|
|
1600
1599
|
def get_var_data(content, node_id=None):
|
|
1600
|
+
"""Extract variable data from a template expression.
|
|
1601
|
+
|
|
1602
|
+
Optimized to use a single AST traversal instead of two separate walks.
|
|
1603
|
+
"""
|
|
1604
|
+
|
|
1601
1605
|
def node_to_value(x):
|
|
1602
1606
|
if type(x) in (ast.Bytes, ast.Str):
|
|
1603
1607
|
return x.s
|
|
@@ -1648,84 +1652,6 @@ def get_var_data(content, node_id=None):
|
|
|
1648
1652
|
|
|
1649
1653
|
return []
|
|
1650
1654
|
|
|
1651
|
-
def _w(parsed):
|
|
1652
|
-
vars = {}
|
|
1653
|
-
for node in ast.walk(parsed):
|
|
1654
|
-
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
1655
|
-
try:
|
|
1656
|
-
func = node.func.id
|
|
1657
|
-
# parse function args
|
|
1658
|
-
args = []
|
|
1659
|
-
for x in node.args:
|
|
1660
|
-
if type(x) == ast.Call: # noqa: E721
|
|
1661
|
-
vars.update(_w(x))
|
|
1662
|
-
else:
|
|
1663
|
-
args.append(node_to_value(x))
|
|
1664
|
-
|
|
1665
|
-
kwargs = {}
|
|
1666
|
-
for x in node.keywords:
|
|
1667
|
-
value = node_to_value(x.value)
|
|
1668
|
-
kwargs[x.arg] = value
|
|
1669
|
-
if x.arg == "default":
|
|
1670
|
-
kwargs["default"] = check_default_value(value)
|
|
1671
|
-
if func in VALID_CUSTOM_FUNCTION_NAMES:
|
|
1672
|
-
# Type definition here is set to 'String' because it comes from a
|
|
1673
|
-
# `defined(variable)` expression that does not contain any type hint.
|
|
1674
|
-
# It will be overriden in later definitions or left as is otherwise.
|
|
1675
|
-
# args[0] check is used to avoid adding unnamed parameters found in
|
|
1676
|
-
# templates like: `split_to_array('')`
|
|
1677
|
-
if args and isinstance(args[0], list):
|
|
1678
|
-
raise ValueError(f'"{args[0]}" can not be used as a variable name')
|
|
1679
|
-
if len(args) > 0 and args[0] not in vars and args[0]:
|
|
1680
|
-
vars[args[0]] = {
|
|
1681
|
-
"type": "String",
|
|
1682
|
-
"default": None,
|
|
1683
|
-
"used_in": "function_call",
|
|
1684
|
-
}
|
|
1685
|
-
elif func == "Array":
|
|
1686
|
-
if "default" not in kwargs:
|
|
1687
|
-
default = kwargs.get("default", args[2] if len(args) > 2 and args[2] else None)
|
|
1688
|
-
kwargs["default"] = check_default_value(default)
|
|
1689
|
-
if args:
|
|
1690
|
-
if isinstance(args[0], list):
|
|
1691
|
-
raise ValueError(f'"{args[0]}" can not be used as a variable name')
|
|
1692
|
-
vars[args[0]] = {
|
|
1693
|
-
"type": f"Array({args[1]})" if len(args) > 1 else "Array(String)",
|
|
1694
|
-
**kwargs,
|
|
1695
|
-
}
|
|
1696
|
-
elif func in parameter_types:
|
|
1697
|
-
# avoid variable names to be None
|
|
1698
|
-
if args and args[0] is not None:
|
|
1699
|
-
# if this is a cast use the function name to get the type
|
|
1700
|
-
if "default" not in kwargs:
|
|
1701
|
-
default = kwargs.get("default", args[1] if len(args) > 1 else None)
|
|
1702
|
-
kwargs["default"] = check_default_value(default)
|
|
1703
|
-
try:
|
|
1704
|
-
if isinstance(args[0], list):
|
|
1705
|
-
raise ValueError(f'"{args[0]}" can not be used as a variable name')
|
|
1706
|
-
vars[args[0]] = {"type": func, **kwargs}
|
|
1707
|
-
if "default" in kwargs:
|
|
1708
|
-
kwargs["default"] = check_default_value(kwargs["default"])
|
|
1709
|
-
except TypeError as e:
|
|
1710
|
-
logging.exception(f"pipe parsing problem {content} (node '{node_id}'): {e}")
|
|
1711
|
-
except ValueError:
|
|
1712
|
-
raise
|
|
1713
|
-
except Exception as e:
|
|
1714
|
-
# if we find a problem parsing, let the parsing continue
|
|
1715
|
-
logging.exception(f"pipe parsing problem {content} (node: '{node_id}'): {e}")
|
|
1716
|
-
elif isinstance(node, ast.Name):
|
|
1717
|
-
# when parent node is a call it means it's managed by the Call workflow (see above)
|
|
1718
|
-
is_cast = (
|
|
1719
|
-
isinstance(node.parent, ast.Call)
|
|
1720
|
-
and isinstance(node.parent.func, ast.Name)
|
|
1721
|
-
and node.parent.func.id in parameter_types
|
|
1722
|
-
)
|
|
1723
|
-
is_reserved_name = node.id in reserved_vars or node.id in function_list or node.id in _namespace
|
|
1724
|
-
if (not isinstance(node.parent, ast.Call) and not is_cast) and not is_reserved_name:
|
|
1725
|
-
vars[node.id] = {"type": "String", "default": None}
|
|
1726
|
-
|
|
1727
|
-
return vars
|
|
1728
|
-
|
|
1729
1655
|
def check_default_value(value):
|
|
1730
1656
|
if isinstance(value, int):
|
|
1731
1657
|
MAX_SAFE_INTEGER = 9007199254740991
|
|
@@ -1746,12 +1672,90 @@ def get_var_data(content, node_id=None):
|
|
|
1746
1672
|
return parse_content(content, retries)
|
|
1747
1673
|
|
|
1748
1674
|
parsed = parse_content(content)
|
|
1675
|
+
vars = {}
|
|
1676
|
+
|
|
1677
|
+
# Single pass: traverse AST while tracking parent references
|
|
1678
|
+
# Use FIFO order so children are visited in source order (matching ast.walk)
|
|
1679
|
+
# because first-seen wins for duplicates like defined(a) and defined(a, ...) later.
|
|
1680
|
+
queue = deque([(parsed, None)]) # (node, parent)
|
|
1681
|
+
while queue:
|
|
1682
|
+
node, parent = queue.popleft()
|
|
1683
|
+
|
|
1684
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
1685
|
+
try:
|
|
1686
|
+
func = node.func.id
|
|
1687
|
+
# parse function args
|
|
1688
|
+
args = []
|
|
1689
|
+
for x in node.args:
|
|
1690
|
+
if type(x) == ast.Call: # noqa: E721
|
|
1691
|
+
# Nested calls are traversed via `ast.iter_child_nodes` below.
|
|
1692
|
+
continue
|
|
1693
|
+
else:
|
|
1694
|
+
args.append(node_to_value(x))
|
|
1695
|
+
|
|
1696
|
+
kwargs = {}
|
|
1697
|
+
for x in node.keywords:
|
|
1698
|
+
value = node_to_value(x.value)
|
|
1699
|
+
kwargs[x.arg] = value
|
|
1700
|
+
if x.arg == "default":
|
|
1701
|
+
kwargs["default"] = check_default_value(value)
|
|
1702
|
+
if func in VALID_CUSTOM_FUNCTION_NAMES:
|
|
1703
|
+
# Type definition here is set to 'String' because it comes from a
|
|
1704
|
+
# `defined(variable)` expression that does not contain any type hint.
|
|
1705
|
+
# It will be overriden in later definitions or left as is otherwise.
|
|
1706
|
+
# args[0] check is used to avoid adding unnamed parameters found in
|
|
1707
|
+
# templates like: `split_to_array('')`
|
|
1708
|
+
if args and isinstance(args[0], list):
|
|
1709
|
+
raise ValueError(f'"{args[0]}" can not be used as a variable name')
|
|
1710
|
+
if len(args) > 0 and args[0] not in vars and args[0]:
|
|
1711
|
+
vars[args[0]] = {
|
|
1712
|
+
"type": "String",
|
|
1713
|
+
"default": None,
|
|
1714
|
+
"used_in": "function_call",
|
|
1715
|
+
}
|
|
1716
|
+
elif func == "Array":
|
|
1717
|
+
if "default" not in kwargs:
|
|
1718
|
+
default = kwargs.get("default", args[2] if len(args) > 2 and args[2] else None)
|
|
1719
|
+
kwargs["default"] = check_default_value(default)
|
|
1720
|
+
if args:
|
|
1721
|
+
if isinstance(args[0], list):
|
|
1722
|
+
raise ValueError(f'"{args[0]}" can not be used as a variable name')
|
|
1723
|
+
vars[args[0]] = {
|
|
1724
|
+
"type": f"Array({args[1]})" if len(args) > 1 else "Array(String)",
|
|
1725
|
+
**kwargs,
|
|
1726
|
+
}
|
|
1727
|
+
elif func in parameter_types:
|
|
1728
|
+
# avoid variable names to be None
|
|
1729
|
+
if args and args[0] is not None:
|
|
1730
|
+
# if this is a cast use the function name to get the type
|
|
1731
|
+
if "default" not in kwargs:
|
|
1732
|
+
default = kwargs.get("default", args[1] if len(args) > 1 else None)
|
|
1733
|
+
kwargs["default"] = check_default_value(default)
|
|
1734
|
+
try:
|
|
1735
|
+
if isinstance(args[0], list):
|
|
1736
|
+
raise ValueError(f'"{args[0]}" can not be used as a variable name')
|
|
1737
|
+
vars[args[0]] = {"type": func, **kwargs}
|
|
1738
|
+
if "default" in kwargs:
|
|
1739
|
+
kwargs["default"] = check_default_value(kwargs["default"])
|
|
1740
|
+
except TypeError as e:
|
|
1741
|
+
logging.exception(f"pipe parsing problem {content} (node '{node_id}'): {e}")
|
|
1742
|
+
except ValueError:
|
|
1743
|
+
raise
|
|
1744
|
+
except Exception as e:
|
|
1745
|
+
# if we find a problem parsing, let the parsing continue
|
|
1746
|
+
logging.exception(f"pipe parsing problem {content} (node: '{node_id}'): {e}")
|
|
1747
|
+
elif isinstance(node, ast.Name):
|
|
1748
|
+
# when parent node is a call it means it's managed by the Call workflow (see above)
|
|
1749
|
+
is_cast = (
|
|
1750
|
+
isinstance(parent, ast.Call) and isinstance(parent.func, ast.Name) and parent.func.id in parameter_types
|
|
1751
|
+
)
|
|
1752
|
+
is_reserved_name = node.id in reserved_vars or node.id in function_list or node.id in _namespace
|
|
1753
|
+
if (not isinstance(parent, ast.Call) and not is_cast) and not is_reserved_name:
|
|
1754
|
+
vars[node.id] = {"type": "String", "default": None}
|
|
1749
1755
|
|
|
1750
|
-
|
|
1751
|
-
for node in ast.walk(parsed):
|
|
1756
|
+
# Add children preserving source order so downstream precedence matches templates
|
|
1752
1757
|
for child in ast.iter_child_nodes(node):
|
|
1753
|
-
child
|
|
1754
|
-
vars = _w(parsed)
|
|
1758
|
+
queue.append((child, node))
|
|
1755
1759
|
|
|
1756
1760
|
return [dict(name=k, **v) for k, v in vars.items()]
|
|
1757
1761
|
|
|
@@ -1812,6 +1816,8 @@ def get_var_names_and_types(t, node_id=None):
|
|
|
1812
1816
|
[{'name': 'symbol_id', 'type': 'Int128', 'description': 'Symbol Id', 'required': True, 'default': 11111}, {'name': 'user_id', 'type': 'Int256', 'description': 'User Id', 'default': 3555}]
|
|
1813
1817
|
>>> get_var_names_and_types(Template("SELECT now() > {{DateTime64(timestamp, '2020-09-09 10:10:10.000')}}"))
|
|
1814
1818
|
[{'name': 'timestamp', 'type': 'DateTime64', 'default': '2020-09-09 10:10:10.000'}]
|
|
1819
|
+
>>> get_var_names_and_types(Template("select {{Int32(dup, 1)}} {{String(dup, 'last')}}"))
|
|
1820
|
+
[{'name': 'dup', 'type': 'Int32', 'default': 1}, {'name': 'dup', 'type': 'String', 'default': 'last'}]
|
|
1815
1821
|
>>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE symbol = {{Int64(symbol_id, 9223372036854775807)}}"))
|
|
1816
1822
|
[{'name': 'symbol_id', 'type': 'Int64', 'default': '9223372036854775807'}]
|
|
1817
1823
|
"""
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
|
+
import threading
|
|
4
5
|
from collections import defaultdict
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from functools import lru_cache
|
|
7
8
|
from typing import FrozenSet, List, Optional, Set, Tuple, Union
|
|
8
9
|
|
|
9
10
|
from chtoolset import query as chquery
|
|
11
|
+
from lru import LRU
|
|
10
12
|
from toposort import toposort
|
|
11
13
|
|
|
12
14
|
from tinybird.ch_utils.constants import COPY_ENABLED_TABLE_FUNCTIONS, ENABLED_TABLE_FUNCTIONS
|
|
@@ -144,7 +146,135 @@ def format_where_for_mutation_command(where_clause: str) -> str:
|
|
|
144
146
|
return f"DELETE WHERE {quoted_condition[1:-1]}"
|
|
145
147
|
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
# Functions that take table/dictionary names as string literal arguments.
|
|
150
|
+
# Normalizing these would cause incorrect cache hits since different table names
|
|
151
|
+
# would map to the same cache key.
|
|
152
|
+
# See: https://clickhouse.com/docs/en/sql-reference/functions/other-functions#joinget
|
|
153
|
+
# https://clickhouse.com/docs/en/sql-reference/functions/ext-dict-functions
|
|
154
|
+
# https://clickhouse.com/docs/en/sql-reference/table-functions/cluster
|
|
155
|
+
# https://clickhouse.com/docs/en/sql-reference/table-functions/remote
|
|
156
|
+
_FUNCTIONS_WITH_TABLE_NAME_ARGS = re.compile(
|
|
157
|
+
r"\b(?:joinGet|joinGetOrNull|dictGet\w*|dictHas|dictIsIn|hasColumnInTable|remote|cluster|clusterAllReplicas)\s*\(",
|
|
158
|
+
re.IGNORECASE,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _normalize_sql_for_cache(sql: str) -> str:
|
|
163
|
+
"""Normalize SQL for cache key purposes.
|
|
164
|
+
|
|
165
|
+
Uses normalize_query_keep_names which replaces literal values with placeholders
|
|
166
|
+
while preserving table/column names, so queries with the same structure share
|
|
167
|
+
cache entries.
|
|
168
|
+
|
|
169
|
+
However, some functions like joinGet(), dictGet*, remote(), cluster(), and
|
|
170
|
+
clusterAllReplicas() take table/dictionary names as arguments. Normalizing these
|
|
171
|
+
would incorrectly map different tables to the same cache key, so we fall back to
|
|
172
|
+
using the original SQL for such queries.
|
|
173
|
+
|
|
174
|
+
>>> _normalize_sql_for_cache("SELECT * FROM events WHERE id = 'alice'")
|
|
175
|
+
'SELECT * FROM events WHERE id = ?'
|
|
176
|
+
>>> _normalize_sql_for_cache("SELECT * FROM events WHERE id = 123 AND name = 'bob'")
|
|
177
|
+
'SELECT * FROM events WHERE id = ? AND name = ?'
|
|
178
|
+
>>> _normalize_sql_for_cache("SELECT * FROM events")
|
|
179
|
+
'SELECT * FROM events'
|
|
180
|
+
>>> _normalize_sql_for_cache("SELECT joinGet('my_table', 'col', id) FROM t")
|
|
181
|
+
"SELECT joinGet('my_table', 'col', id) FROM t"
|
|
182
|
+
>>> _normalize_sql_for_cache("SELECT dictGet('my_dict', 'value', id) FROM t")
|
|
183
|
+
"SELECT dictGet('my_dict', 'value', id) FROM t"
|
|
184
|
+
>>> _normalize_sql_for_cache("SELECT * FROM remote('host', db, table)")
|
|
185
|
+
"SELECT * FROM remote('host', db, table)"
|
|
186
|
+
>>> _normalize_sql_for_cache("SELECT * FROM cluster('cluster_name', db, table)")
|
|
187
|
+
"SELECT * FROM cluster('cluster_name', db, table)"
|
|
188
|
+
>>> _normalize_sql_for_cache("not valid sql at all")
|
|
189
|
+
'not valid sql at all'
|
|
190
|
+
"""
|
|
191
|
+
# Skip normalization for queries with functions that have table names as arguments
|
|
192
|
+
if _FUNCTIONS_WITH_TABLE_NAME_ARGS.search(sql):
|
|
193
|
+
return sql
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
return chquery.normalize_query_keep_names(sql)
|
|
197
|
+
except Exception:
|
|
198
|
+
return sql
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Cache for sql_get_used_tables using normalized SQL as key.
|
|
202
|
+
# Uses lru-dict (C extension) for a fast LRU implementation.
|
|
203
|
+
_sql_get_used_tables_cache: LRU = LRU(2**15)
|
|
204
|
+
_sql_get_used_tables_cache_lock = threading.Lock()
|
|
205
|
+
_sql_get_used_tables_cache_hits = 0
|
|
206
|
+
_sql_get_used_tables_cache_misses = 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def sql_get_used_tables_cache_info() -> dict:
|
|
210
|
+
"""Return cache statistics for sql_get_used_tables."""
|
|
211
|
+
with _sql_get_used_tables_cache_lock:
|
|
212
|
+
return {
|
|
213
|
+
"hits": _sql_get_used_tables_cache_hits,
|
|
214
|
+
"misses": _sql_get_used_tables_cache_misses,
|
|
215
|
+
"size": len(_sql_get_used_tables_cache),
|
|
216
|
+
"maxsize": _sql_get_used_tables_cache.get_size(),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def sql_get_used_tables_cache_clear() -> None:
|
|
221
|
+
"""Clear the sql_get_used_tables cache."""
|
|
222
|
+
global _sql_get_used_tables_cache_hits, _sql_get_used_tables_cache_misses
|
|
223
|
+
with _sql_get_used_tables_cache_lock:
|
|
224
|
+
_sql_get_used_tables_cache.clear()
|
|
225
|
+
_sql_get_used_tables_cache_hits = 0
|
|
226
|
+
_sql_get_used_tables_cache_misses = 0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _sql_get_used_tables_impl(
|
|
230
|
+
sql: str,
|
|
231
|
+
raising: bool,
|
|
232
|
+
default_database: str,
|
|
233
|
+
table_functions: bool,
|
|
234
|
+
function_allow_list: Optional[FrozenSet[str]],
|
|
235
|
+
function_deny_list: Optional[FrozenSet[str]],
|
|
236
|
+
settings_allow_list: Optional[FrozenSet[str]],
|
|
237
|
+
settings_deny_list: Optional[FrozenSet[str]],
|
|
238
|
+
) -> tuple[List[Tuple[str, str, str]], bool]:
|
|
239
|
+
"""Extract tables from SQL (uncached implementation).
|
|
240
|
+
|
|
241
|
+
Returns a tuple of (result, cacheable) where cacheable indicates whether the
|
|
242
|
+
result is safe to store in the cache.
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
_function_allow_list = list() if function_allow_list is None else list(function_allow_list)
|
|
246
|
+
_function_deny_list = list() if function_deny_list is None else list(function_deny_list)
|
|
247
|
+
_settings_allow_list = list() if settings_allow_list is None else list(settings_allow_list)
|
|
248
|
+
_settings_deny_list = list() if settings_deny_list is None else list(settings_deny_list)
|
|
249
|
+
|
|
250
|
+
tables: List[Tuple[str, str, str]] = chquery.tables(
|
|
251
|
+
sql,
|
|
252
|
+
default_database=default_database,
|
|
253
|
+
function_allow_list=_function_allow_list,
|
|
254
|
+
function_deny_list=_function_deny_list,
|
|
255
|
+
query_settings_allow_list=_settings_allow_list,
|
|
256
|
+
query_settings_deny_list=_settings_deny_list,
|
|
257
|
+
)
|
|
258
|
+
if not table_functions:
|
|
259
|
+
tables = [(t[0], t[1], "") for t in tables if t[0] or t[1]]
|
|
260
|
+
|
|
261
|
+
return tables, True
|
|
262
|
+
except ValueError as e:
|
|
263
|
+
if raising:
|
|
264
|
+
msg = str(e)
|
|
265
|
+
if "is restricted. Contact support@tinybird.co" in msg:
|
|
266
|
+
raise InvalidFunction(msg=msg) from e
|
|
267
|
+
elif "Unknown function tb_secret" in msg:
|
|
268
|
+
raise InvalidFunction(msg="Unknown function tb_secret. Usage: {{tb_secret('secret_name')}}") from e
|
|
269
|
+
elif "Unknown function tb_var" in msg:
|
|
270
|
+
raise InvalidFunction(msg="Unknown function tb_var. Usage: {{tb_var('var_name')}}") from e
|
|
271
|
+
raise
|
|
272
|
+
# Do not cache this fallback result: the returned sql string can contain
|
|
273
|
+
# sensitive literal values, and the normalized cache key could collide
|
|
274
|
+
# across different queries.
|
|
275
|
+
return [(default_database, sql, "")], False
|
|
276
|
+
|
|
277
|
+
|
|
148
278
|
def sql_get_used_tables_cached(
|
|
149
279
|
sql: str,
|
|
150
280
|
raising: bool = False,
|
|
@@ -152,10 +282,17 @@ def sql_get_used_tables_cached(
|
|
|
152
282
|
table_functions: bool = True,
|
|
153
283
|
function_allow_list: Optional[FrozenSet[str]] = None,
|
|
154
284
|
function_deny_list: Optional[FrozenSet[str]] = None,
|
|
285
|
+
settings_allow_list: Optional[FrozenSet[str]] = None,
|
|
286
|
+
settings_deny_list: Optional[FrozenSet[str]] = None,
|
|
155
287
|
) -> List[Tuple[str, str, str]]:
|
|
156
288
|
"""More like: get used sql names
|
|
157
289
|
|
|
158
290
|
Returns a list of tuples: (database_or_namespace, table_name, table_func).
|
|
291
|
+
|
|
292
|
+
Uses normalized SQL as cache key to improve hit ratio for templated queries.
|
|
293
|
+
The normalization replaces literal values with placeholders while preserving
|
|
294
|
+
table/column names, so queries with the same structure share cache entries.
|
|
295
|
+
|
|
159
296
|
>>> sql_get_used_tables("SELECT 1 FROM the_table")
|
|
160
297
|
[('', 'the_table', '')]
|
|
161
298
|
>>> sql_get_used_tables("SELECT 1 FROM the_database.the_table")
|
|
@@ -173,30 +310,47 @@ def sql_get_used_tables_cached(
|
|
|
173
310
|
>>> sql_get_used_tables("SELECT * FROM `d_d3926a`.`t_976af08ec4b547419e729c63e754b17b`", table_functions=False)
|
|
174
311
|
[('d_d3926a', 't_976af08ec4b547419e729c63e754b17b', '')]
|
|
175
312
|
"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
313
|
+
global _sql_get_used_tables_cache_hits, _sql_get_used_tables_cache_misses
|
|
314
|
+
|
|
315
|
+
# Build cache key using normalized SQL
|
|
316
|
+
normalized_sql = _normalize_sql_for_cache(sql)
|
|
317
|
+
cache_key = (
|
|
318
|
+
normalized_sql,
|
|
319
|
+
raising,
|
|
320
|
+
default_database,
|
|
321
|
+
table_functions,
|
|
322
|
+
function_allow_list,
|
|
323
|
+
function_deny_list,
|
|
324
|
+
settings_allow_list,
|
|
325
|
+
settings_deny_list,
|
|
326
|
+
)
|
|
179
327
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
328
|
+
# Single lookup with hit/miss tracking
|
|
329
|
+
with _sql_get_used_tables_cache_lock:
|
|
330
|
+
cached_value = _sql_get_used_tables_cache.get(cache_key)
|
|
331
|
+
if cached_value is not None:
|
|
332
|
+
_sql_get_used_tables_cache_hits += 1
|
|
333
|
+
return cached_value
|
|
334
|
+
_sql_get_used_tables_cache_misses += 1
|
|
335
|
+
|
|
336
|
+
# Compute outside lock to avoid blocking other threads
|
|
337
|
+
result, cacheable = _sql_get_used_tables_impl(
|
|
338
|
+
sql,
|
|
339
|
+
raising,
|
|
340
|
+
default_database,
|
|
341
|
+
table_functions,
|
|
342
|
+
function_allow_list,
|
|
343
|
+
function_deny_list,
|
|
344
|
+
settings_allow_list,
|
|
345
|
+
settings_deny_list,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if cacheable:
|
|
349
|
+
# Store result in cache
|
|
350
|
+
with _sql_get_used_tables_cache_lock:
|
|
351
|
+
_sql_get_used_tables_cache.setdefault(cache_key, result)
|
|
352
|
+
|
|
353
|
+
return result
|
|
200
354
|
|
|
201
355
|
|
|
202
356
|
def sql_get_used_tables(
|
|
@@ -206,6 +360,8 @@ def sql_get_used_tables(
|
|
|
206
360
|
table_functions: bool = True,
|
|
207
361
|
function_allow_list: Optional[FrozenSet[str]] = None,
|
|
208
362
|
function_deny_list: Optional[FrozenSet[str]] = None,
|
|
363
|
+
settings_allow_list: Optional[FrozenSet[str]] = None,
|
|
364
|
+
settings_deny_list: Optional[FrozenSet[str]] = None,
|
|
209
365
|
) -> List[Tuple[str, str, str]]:
|
|
210
366
|
"""More like: get used sql names
|
|
211
367
|
|
|
@@ -213,7 +369,8 @@ def sql_get_used_tables(
|
|
|
213
369
|
"""
|
|
214
370
|
function_allow_hashable_list = frozenset() if function_allow_list is None else function_allow_list
|
|
215
371
|
function_deny_hashable_list = frozenset() if function_deny_list is None else function_deny_list
|
|
216
|
-
|
|
372
|
+
settings_allow_hashable_list = frozenset() if settings_allow_list is None else settings_allow_list
|
|
373
|
+
settings_deny_hashable_list = frozenset() if settings_deny_list is None else settings_deny_list
|
|
217
374
|
return copy.copy(
|
|
218
375
|
sql_get_used_tables_cached(
|
|
219
376
|
sql,
|
|
@@ -222,6 +379,8 @@ def sql_get_used_tables(
|
|
|
222
379
|
table_functions,
|
|
223
380
|
function_allow_list=function_allow_hashable_list,
|
|
224
381
|
function_deny_list=function_deny_hashable_list,
|
|
382
|
+
settings_allow_list=settings_allow_hashable_list,
|
|
383
|
+
settings_deny_list=settings_deny_hashable_list,
|
|
225
384
|
)
|
|
226
385
|
)
|
|
227
386
|
|
|
@@ -309,16 +468,18 @@ def replace_tables_chquery_cached(
|
|
|
309
468
|
output_one_line: bool = False,
|
|
310
469
|
timestamp: Optional[datetime] = None,
|
|
311
470
|
function_allow_list: Optional[FrozenSet[str]] = None,
|
|
471
|
+
settings_allow_list: Optional[FrozenSet[str]] = None,
|
|
312
472
|
) -> str:
|
|
313
473
|
replacements = dict(sorted_replacements) if sorted_replacements else {}
|
|
314
474
|
_function_allow_list = list() if function_allow_list is None else list(function_allow_list)
|
|
315
|
-
|
|
475
|
+
_settings_allow_list = list() if settings_allow_list is None else list(settings_allow_list)
|
|
316
476
|
return chquery.replace_tables(
|
|
317
477
|
sql,
|
|
318
478
|
replacements,
|
|
319
479
|
default_database=default_database,
|
|
320
480
|
one_line=output_one_line,
|
|
321
481
|
function_allow_list=_function_allow_list,
|
|
482
|
+
query_settings_allow_list=_settings_allow_list,
|
|
322
483
|
)
|
|
323
484
|
|
|
324
485
|
|
|
@@ -332,6 +493,7 @@ def replace_tables(
|
|
|
332
493
|
output_one_line: bool = False,
|
|
333
494
|
timestamp: Optional[datetime] = None,
|
|
334
495
|
function_allow_list: Optional[FrozenSet[str]] = None,
|
|
496
|
+
settings_allow_list: Optional[FrozenSet[str]] = None,
|
|
335
497
|
original_replacements: Optional[dict] = None,
|
|
336
498
|
) -> str:
|
|
337
499
|
"""
|
|
@@ -340,10 +502,16 @@ def replace_tables(
|
|
|
340
502
|
It also validates the sql to verify it's valid and doesn't use unknown or prohibited functions
|
|
341
503
|
"""
|
|
342
504
|
hashable_list = frozenset() if function_allow_list is None else function_allow_list
|
|
505
|
+
hashable_settings_list = frozenset() if settings_allow_list is None else settings_allow_list
|
|
343
506
|
if not replacements:
|
|
344
507
|
# Always call replace_tables to do validation and formatting
|
|
345
508
|
return replace_tables_chquery_cached(
|
|
346
|
-
sql,
|
|
509
|
+
sql,
|
|
510
|
+
None,
|
|
511
|
+
output_one_line=output_one_line,
|
|
512
|
+
timestamp=timestamp,
|
|
513
|
+
function_allow_list=hashable_list,
|
|
514
|
+
settings_allow_list=hashable_settings_list,
|
|
347
515
|
)
|
|
348
516
|
|
|
349
517
|
_replaced_with = set()
|
|
@@ -366,6 +534,7 @@ def replace_tables(
|
|
|
366
534
|
raising=True,
|
|
367
535
|
table_functions=check_functions,
|
|
368
536
|
function_allow_list=function_allow_list,
|
|
537
|
+
settings_allow_list=settings_allow_list,
|
|
369
538
|
)
|
|
370
539
|
seen_tables = set()
|
|
371
540
|
table: Union[Tuple[str, str], Tuple[str, str, str]]
|
|
@@ -411,7 +580,12 @@ def replace_tables(
|
|
|
411
580
|
|
|
412
581
|
if not deps_sorted:
|
|
413
582
|
return replace_tables_chquery_cached(
|
|
414
|
-
sql,
|
|
583
|
+
sql,
|
|
584
|
+
None,
|
|
585
|
+
output_one_line=output_one_line,
|
|
586
|
+
timestamp=timestamp,
|
|
587
|
+
function_allow_list=hashable_list,
|
|
588
|
+
settings_allow_list=hashable_settings_list,
|
|
415
589
|
)
|
|
416
590
|
|
|
417
591
|
for current_deps in deps_sorted:
|
|
@@ -450,10 +624,16 @@ def replace_tables(
|
|
|
450
624
|
output_one_line=output_one_line,
|
|
451
625
|
timestamp=timestamp,
|
|
452
626
|
function_allow_list=hashable_list,
|
|
627
|
+
settings_allow_list=hashable_settings_list,
|
|
453
628
|
)
|
|
454
629
|
else:
|
|
455
630
|
sql = replace_tables_chquery_cached(
|
|
456
|
-
sql,
|
|
631
|
+
sql,
|
|
632
|
+
None,
|
|
633
|
+
output_one_line=output_one_line,
|
|
634
|
+
timestamp=timestamp,
|
|
635
|
+
function_allow_list=hashable_list,
|
|
636
|
+
settings_allow_list=hashable_settings_list,
|
|
457
637
|
)
|
|
458
638
|
|
|
459
639
|
# Fix for empty database names in JOINs - remove empty backticks like ``.table_name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird_cli
|
|
3
|
-
Version: 5.22.
|
|
3
|
+
Version: 5.22.3.dev0
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/cli
|
|
6
6
|
Author: Tinybird
|
|
@@ -61,6 +61,11 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
61
61
|
Changelog
|
|
62
62
|
----------
|
|
63
63
|
|
|
64
|
+
5.22.2
|
|
65
|
+
***********
|
|
66
|
+
|
|
67
|
+
- `Fixed` tokens in existing datasource files not being updated when using `tb push -f`
|
|
68
|
+
|
|
64
69
|
5.22.1
|
|
65
70
|
***********
|
|
66
71
|
|
|
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
|
|
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
|
|
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_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/workspace_members.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|