tinybird-cli 5.20.1.dev0__tar.gz → 5.20.1.dev1__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.20.1.dev0 → tinybird_cli-5.20.1.dev1}/PKG-INFO +7 -1
  2. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/__cli__.py +2 -2
  3. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/ch_utils/engine.py +5 -2
  4. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/datafile.py +2 -3
  5. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/feedback_manager.py +1 -1
  6. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/sql_template.py +3 -3
  7. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/sql_toolset.py +79 -0
  8. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/connection.py +1 -1
  9. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/fmt.py +1 -1
  10. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +1 -1
  11. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird_cli.egg-info/PKG-INFO +7 -1
  12. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/setup.cfg +0 -0
  13. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/ch_utils/constants.py +0 -0
  14. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/check_pypi.py +0 -0
  15. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/client.py +0 -0
  16. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/config.py +0 -0
  17. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/connectors.py +0 -0
  18. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/context.py +0 -0
  19. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/datatypes.py +0 -0
  20. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/git_settings.py +0 -0
  21. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/sql.py +0 -0
  22. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/sql_template_fmt.py +0 -0
  23. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/syncasync.py +0 -0
  24. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli.py +0 -0
  25. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/auth.py +0 -0
  26. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/branch.py +0 -0
  27. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/cicd.py +0 -0
  28. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/cli.py +0 -0
  29. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/common.py +0 -0
  30. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/config.py +0 -0
  31. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/datasource.py +0 -0
  32. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/exceptions.py +0 -0
  33. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/tag.py +0 -0
  37. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/telemetry.py +0 -0
  38. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/test.py +0 -0
  39. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  40. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/token.py +0 -0
  41. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/workspace.py +0 -0
  42. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  43. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird/tornado_template.py +0 -0
  44. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  45. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  46. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird_cli.egg-info/entry_points.txt +0 -0
  47. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/tinybird_cli.egg-info/requires.txt +0 -0
  48. {tinybird_cli-5.20.1.dev0 → tinybird_cli-5.20.1.dev1}/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.20.1.dev0
3
+ Version: 5.20.1.dev1
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.20.1.dev1
65
+ ***********
66
+
67
+ - `Added` When trying to push a materialized view with an unoptimized join, the CLI will now warn the user.
68
+
64
69
  5.19.0
65
70
  ***********
66
71
 
@@ -198,6 +203,7 @@ Changelog
198
203
  - `Added` `tb tag` commands to manage tags from the CLI.
199
204
  - `Added` support to `TAGS` in `tb fmt`.
200
205
  - `Added` support to `TAGS` in `tb pull` and `tb push`. Allows tagging resources for filtering in the UI.
206
+ - `Changed` Improved message when creating a DynamoDB connection
201
207
 
202
208
  5.7.0
203
209
  **********
@@ -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.20.1.dev0'
8
- __revision__ = '4f9495f'
7
+ __version__ = '5.20.1.dev1'
8
+ __revision__ = '6ec3b53'
@@ -146,10 +146,9 @@ class TableDetails:
146
146
  def is_replacing_engine(self) -> bool:
147
147
  if self.engine:
148
148
  engine_lower = self.engine.lower()
149
- is_aggregating = "aggregatingmergetree" in engine_lower
150
149
  is_replacing = "replacingmergetree" in engine_lower
151
150
  is_collapsing = "collapsingmergetree" in engine_lower
152
- return is_aggregating or is_replacing or is_collapsing
151
+ return is_replacing or is_collapsing
153
152
  return False
154
153
 
155
154
  def diff_ttl(self, new_ttl: str) -> bool:
@@ -169,6 +168,10 @@ class TableDetails:
169
168
  @property
170
169
  def sorting_key(self) -> Optional[str]:
171
170
  _sorting_key = self.details.get("sorting_key", None)
171
+ # TODO: This should use ENABLED_ENGINES to guess if the sorting key is required or not
172
+ # Also checking this and raising an error in a getter is a bit of an anti-pattern,
173
+ # a data source could have a "wrong" sorting key and we won't be able to even show it in the API.
174
+ # All these checks be performed only on creation time.
172
175
  if self.is_replacing_engine() and not _sorting_key:
173
176
  raise ValueError(f"SORTING_KEY must be defined for the {self.engine} engine")
174
177
  if self.is_mergetree_family():
@@ -1872,8 +1872,7 @@ def drop_token(url: str) -> str:
1872
1872
  parsed = urlparse(url)
1873
1873
  qs = parse_qs(parsed.query)
1874
1874
  qs_simplify = {k: v[0] for k, v in qs.items()} # change several arguments to single one
1875
- if "token" in qs_simplify:
1876
- del qs_simplify["token"]
1875
+ qs_simplify.pop("token", None)
1877
1876
  return f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{urlencode(qs_simplify)}"
1878
1877
 
1879
1878
 
@@ -4849,7 +4848,7 @@ def format_include(file_parts: List[str], doc: Datafile, unroll_includes: bool =
4849
4848
  assert doc.raw is not None
4850
4849
 
4851
4850
  include = [line for line in doc.raw if "INCLUDE" in line and ".incl" in line]
4852
- if len(include):
4851
+ if include:
4853
4852
  file_parts.append(include[0])
4854
4853
  file_parts.append(DATAFILE_NEW_LINE)
4855
4854
  return file_parts
@@ -491,7 +491,7 @@ Ready? """
491
491
  """\n[2] Go to IAM > Policies. Create a new policy with the following permissions. Please, replace {replacements}:\n\n{access_policy}\n\n(The policy has been copied to your clipboard)\n\n"""
492
492
  )
493
493
  prompt_s3_iamrole_connection_policy_not_copied = prompt_message(
494
- """\n[2] Go to IAM > Policies. Create a new policy with the following permissions. Please, copy this policy and replace <bucket> with your bucket name:\n\n{access_policy}\n\n"""
494
+ """\n[2] Go to IAM > Policies. Create a new policy with the following permissions. Please, copy this policy and replace <bucket> with your bucket name and <table_name> with your table name:\n\n{access_policy}\n\n"""
495
495
  )
496
496
  prompt_s3_iamrole_connection_role = prompt_message(
497
497
  """\n[3] Go to IAM > Roles. Create a new IAM Role using the following custom trust policy and attach the access policy you just created in the previous step:\n\n{trust_policy}\n\n(The policy has been copied to your clipboard)\n\n"""
@@ -1657,7 +1657,7 @@ def get_var_data(content, node_id=None):
1657
1657
  # It will be overriden in later definitions or left as is otherwise.
1658
1658
  # args[0] check is used to avoid adding unnamed parameters found in
1659
1659
  # templates like: `split_to_array('')`
1660
- if len(args) and isinstance(args[0], list):
1660
+ if args and isinstance(args[0], list):
1661
1661
  raise ValueError(f'"{args[0]}" can not be used as a variable name')
1662
1662
  if len(args) > 0 and args[0] not in vars and args[0]:
1663
1663
  vars[args[0]] = {
@@ -1669,7 +1669,7 @@ def get_var_data(content, node_id=None):
1669
1669
  if "default" not in kwargs:
1670
1670
  default = kwargs.get("default", args[2] if len(args) > 2 and args[2] else None)
1671
1671
  kwargs["default"] = check_default_value(default)
1672
- if len(args):
1672
+ if args:
1673
1673
  if isinstance(args[0], list):
1674
1674
  raise ValueError(f'"{args[0]}" can not be used as a variable name')
1675
1675
  vars[args[0]] = {
@@ -1678,7 +1678,7 @@ def get_var_data(content, node_id=None):
1678
1678
  }
1679
1679
  elif func in parameter_types:
1680
1680
  # avoid variable names to be None
1681
- if len(args) and args[0] is not None:
1681
+ if args and args[0] is not None:
1682
1682
  # if this is a cast use the function name to get the type
1683
1683
  if "default" not in kwargs:
1684
1684
  default = kwargs.get("default", args[1] if len(args) > 1 else None)
@@ -1,5 +1,6 @@
1
1
  import copy
2
2
  import logging
3
+ import re
3
4
  from collections import defaultdict
4
5
  from datetime import datetime
5
6
  from functools import lru_cache
@@ -42,10 +43,88 @@ class InvalidResource(ValueError):
42
43
  self.table = table
43
44
 
44
45
 
46
+ class UnoptimizedJoinException(Exception):
47
+ def __init__(self, sql: str):
48
+ self.sql = sql
49
+ self.msg = f"Materialized node SQL contains a join that is not optimized: {sql}"
50
+ self.documentation = (
51
+ "/docs/work-with-data/optimization/opt201-fix-mistakes#understanding-the-materialized-join-issue"
52
+ )
53
+ super().__init__(self.msg)
54
+
55
+
56
+ ChQueryTable = Tuple[Optional[str], Optional[str], Optional[str]]
57
+
58
+
59
+ def get_left_table(sql: str, default_database: Optional[str] = None) -> ChQueryTable:
60
+ if default_database is None:
61
+ left_table = chquery.get_left_table(sql)
62
+ else:
63
+ left_table = chquery.get_left_table(sql, default_database=default_database)
64
+ return left_table
65
+
66
+
45
67
  def format_sql(sql: str) -> str:
46
68
  return chquery.format(sql)
47
69
 
48
70
 
71
+ def explain_plan(sql: str) -> str:
72
+ return chquery.explain_ast(sql)
73
+
74
+
75
+ def has_join(sql: str) -> bool:
76
+ return any(line.rstrip().startswith("TableJoin") for line in explain_plan(sql).split())
77
+
78
+
79
+ def has_unoptimized_join(sql: str, left_table: Optional[Union[Tuple[str, str], Tuple[str, str, str]]] = None) -> None:
80
+ """
81
+ Check if a SQL query contains an unoptimized join.
82
+ A join is considered optimized if the right table is filtered by the left table's data.
83
+
84
+ Args:
85
+ sql: The SQL query to check
86
+ left_table: Optional tuple of (database, table) for the left table
87
+
88
+ Raises:
89
+ UnoptimizedJoin: If an unoptimized join is found
90
+ """
91
+ # TODO: We should check that we are filtering the right table by the left table's data
92
+ # TODO: We should check if using EXPLAIN AST is better than using regex
93
+
94
+ number_of_joins = sum(1 for line in explain_plan(sql).split() if line.rstrip().startswith("TableJoin"))
95
+ if number_of_joins == 0:
96
+ return
97
+
98
+ if not left_table:
99
+ left_table = chquery.get_left_table(sql)
100
+ if not left_table:
101
+ return
102
+
103
+ # Find all JOIN clauses with subqueries
104
+ # This pattern matches anything between JOIN and ON/USING
105
+ join_pattern = r"(?:LEFT\s+|RIGHT\s+|INNER\s+|FULL\s+OUTER\s+)?JOIN\s*\((.*?)\)\s+(?:AS\s+\w+)?\s*(?:ON|USING)"
106
+
107
+ # Find all joins with subqueries
108
+ join_matches = list(re.finditer(join_pattern, sql, re.IGNORECASE | re.DOTALL))
109
+
110
+ if number_of_joins != len(join_matches):
111
+ logging.debug(f"number_of_joins: {number_of_joins}, join_matches: {join_matches}")
112
+ raise UnoptimizedJoinException(sql)
113
+
114
+ # If no joins with subqueries found, probably is an unoptimized join
115
+ if not join_matches:
116
+ raise UnoptimizedJoinException(sql)
117
+
118
+ # Check if the left table is referenced in the subquery
119
+ left_table_ref = f"{left_table[0]}.{left_table[1]}"
120
+
121
+ for match in join_matches:
122
+ subquery = match.group(1) # Get the captured subquery
123
+ logging.debug(f"subquery: {subquery} left_table_ref: {left_table_ref}")
124
+ if left_table_ref not in subquery:
125
+ raise UnoptimizedJoinException(sql)
126
+
127
+
49
128
  def format_where_for_mutation_command(where_clause: str) -> str:
50
129
  """
51
130
  >>> format_where_for_mutation_command("numnights = 99")
@@ -293,7 +293,7 @@ async def connection_create_snowflake(
293
293
  if not warehouse:
294
294
  warehouses = await client.get_snowflake_warehouses(account, username, password, role) or []
295
295
  warehouses_names = [w["name"] for w in warehouses]
296
- default_warehouse = warehouses_names[0] if len(warehouses_names) else ""
296
+ default_warehouse = warehouses_names[0] if warehouses_names else ""
297
297
  warehouse = click.prompt(
298
298
  "Warehouse (optional)",
299
299
  type=click.types.Choice(warehouses_names, case_sensitive=False),
@@ -83,7 +83,7 @@ async def fmt(
83
83
 
84
84
  click.echo(FeedbackManager.success_generated_local_file(file=filename))
85
85
 
86
- if len(failed):
86
+ if failed:
87
87
  click.echo(FeedbackManager.error_failed_to_format_files(number=len(failed)))
88
88
  for f in failed:
89
89
  click.echo(f"tb fmt {f} --yes")
@@ -317,7 +317,7 @@ def test_run_summary(results: List[TestSummaryResults], only_fail: bool = False,
317
317
  if test.error:
318
318
  click.secho(test.error, fg=test.status.color, bold=True, nl=True, err=True)
319
319
 
320
- if len(total_counts):
320
+ if total_counts:
321
321
  click.echo("\nTotals:")
322
322
  for key_status, value_total in total_counts.items():
323
323
  code_summary = f"Total {key_status.description}: {value_total}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird_cli
3
- Version: 5.20.1.dev0
3
+ Version: 5.20.1.dev1
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.20.1.dev1
65
+ ***********
66
+
67
+ - `Added` When trying to push a materialized view with an unoptimized join, the CLI will now warn the user.
68
+
64
69
  5.19.0
65
70
  ***********
66
71
 
@@ -198,6 +203,7 @@ Changelog
198
203
  - `Added` `tb tag` commands to manage tags from the CLI.
199
204
  - `Added` support to `TAGS` in `tb fmt`.
200
205
  - `Added` support to `TAGS` in `tb pull` and `tb push`. Allows tagging resources for filtering in the UI.
206
+ - `Changed` Improved message when creating a DynamoDB connection
201
207
 
202
208
  5.7.0
203
209
  **********