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.
Files changed (48) hide show
  1. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/PKG-INFO +6 -1
  2. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/__cli__.py +2 -2
  3. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/context.py +0 -1
  4. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/datafile_common.py +4 -0
  5. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql_template.py +90 -84
  6. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql_toolset.py +209 -29
  7. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/PKG-INFO +6 -1
  8. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/setup.cfg +0 -0
  9. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/ch_utils/constants.py +0 -0
  10. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/ch_utils/engine.py +0 -0
  11. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/check_pypi.py +0 -0
  12. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/client.py +0 -0
  13. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/config.py +0 -0
  14. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/connectors.py +0 -0
  15. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/datatypes.py +0 -0
  16. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/feedback_manager.py +0 -0
  17. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/git_settings.py +0 -0
  18. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql.py +0 -0
  19. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/sql_template_fmt.py +0 -0
  20. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/syncasync.py +0 -0
  21. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli.py +0 -0
  22. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
  23. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
  24. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
  25. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
  26. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/common.py +0 -0
  27. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/config.py +0 -0
  28. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
  29. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/datasource.py +0 -0
  30. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  31. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
  32. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/job.py +0 -0
  33. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
  34. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
  35. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
  36. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  37. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/test.py +0 -0
  38. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  39. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  40. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/token.py +0 -0
  41. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/workspace.py +0 -0
  42. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  43. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird/tornado_template.py +0 -0
  44. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  45. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  46. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/entry_points.txt +0 -0
  47. {tinybird_cli-5.22.2.dev0 → tinybird_cli-5.22.3.dev0}/tinybird_cli.egg-info/requires.txt +0 -0
  48. {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.2.dev0
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.2.dev0'
8
- __revision__ = 'e1b50a6'
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
- # calculate parents for each node for later checks
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.parent = node
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
- @lru_cache(maxsize=2**15)
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
- try:
177
- _function_allow_list = list() if function_allow_list is None else list(function_allow_list)
178
- _function_deny_list = list() if function_deny_list is None else list(function_deny_list)
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
- tables: List[Tuple[str, str, str]] = chquery.tables(
181
- sql,
182
- default_database=default_database,
183
- function_allow_list=_function_allow_list,
184
- function_deny_list=_function_deny_list,
185
- )
186
- if not table_functions:
187
- return [(t[0], t[1], "") for t in tables if t[0] or t[1]]
188
- return tables
189
- except ValueError as e:
190
- if raising:
191
- msg = str(e)
192
- if "is restricted. Contact support@tinybird.co" in msg:
193
- raise InvalidFunction(msg=msg) from e
194
- elif "Unknown function tb_secret" in msg:
195
- raise InvalidFunction(msg="Unknown function tb_secret. Usage: {{tb_secret('secret_name')}}") from e
196
- elif "Unknown function tb_var" in msg:
197
- raise InvalidFunction(msg="Unknown function tb_var. Usage: {{tb_var('var_name')}}") from e
198
- raise
199
- return [(default_database, sql, "")]
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, None, output_one_line=output_one_line, timestamp=timestamp, function_allow_list=hashable_list
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, None, output_one_line=output_one_line, timestamp=timestamp, function_allow_list=hashable_list
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, None, output_one_line=output_one_line, timestamp=timestamp, function_allow_list=hashable_list
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.2.dev0
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