tinybird 0.0.1.dev291__py3-none-any.whl → 1.0.5__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.
Files changed (76) hide show
  1. tinybird/ch_utils/constants.py +5 -0
  2. tinybird/connectors.py +1 -7
  3. tinybird/context.py +3 -3
  4. tinybird/datafile/common.py +10 -8
  5. tinybird/datafile/parse_pipe.py +2 -2
  6. tinybird/feedback_manager.py +3 -0
  7. tinybird/prompts.py +1 -0
  8. tinybird/service_datasources.py +223 -0
  9. tinybird/sql_template.py +26 -11
  10. tinybird/sql_template_fmt.py +14 -4
  11. tinybird/tb/__cli__.py +2 -2
  12. tinybird/tb/cli.py +1 -0
  13. tinybird/tb/client.py +104 -26
  14. tinybird/tb/config.py +24 -0
  15. tinybird/tb/modules/agent/agent.py +103 -67
  16. tinybird/tb/modules/agent/banner.py +15 -15
  17. tinybird/tb/modules/agent/explore_agent.py +5 -0
  18. tinybird/tb/modules/agent/mock_agent.py +5 -1
  19. tinybird/tb/modules/agent/models.py +6 -2
  20. tinybird/tb/modules/agent/prompts.py +49 -2
  21. tinybird/tb/modules/agent/tools/deploy.py +1 -1
  22. tinybird/tb/modules/agent/tools/execute_query.py +15 -18
  23. tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
  24. tinybird/tb/modules/agent/tools/run_command.py +9 -0
  25. tinybird/tb/modules/agent/utils.py +38 -48
  26. tinybird/tb/modules/branch.py +150 -0
  27. tinybird/tb/modules/build.py +58 -13
  28. tinybird/tb/modules/build_common.py +209 -25
  29. tinybird/tb/modules/cli.py +129 -16
  30. tinybird/tb/modules/common.py +172 -146
  31. tinybird/tb/modules/connection.py +125 -194
  32. tinybird/tb/modules/connection_kafka.py +382 -0
  33. tinybird/tb/modules/copy.py +3 -1
  34. tinybird/tb/modules/create.py +83 -150
  35. tinybird/tb/modules/datafile/build.py +27 -38
  36. tinybird/tb/modules/datafile/build_datasource.py +21 -25
  37. tinybird/tb/modules/datafile/diff.py +1 -1
  38. tinybird/tb/modules/datafile/format_pipe.py +46 -7
  39. tinybird/tb/modules/datafile/playground.py +59 -68
  40. tinybird/tb/modules/datafile/pull.py +2 -3
  41. tinybird/tb/modules/datasource.py +477 -308
  42. tinybird/tb/modules/deployment.py +2 -0
  43. tinybird/tb/modules/deployment_common.py +84 -44
  44. tinybird/tb/modules/deprecations.py +4 -4
  45. tinybird/tb/modules/dev_server.py +33 -12
  46. tinybird/tb/modules/exceptions.py +14 -0
  47. tinybird/tb/modules/feedback_manager.py +1 -1
  48. tinybird/tb/modules/info.py +69 -12
  49. tinybird/tb/modules/infra.py +4 -5
  50. tinybird/tb/modules/job_common.py +15 -0
  51. tinybird/tb/modules/local.py +143 -23
  52. tinybird/tb/modules/local_common.py +347 -19
  53. tinybird/tb/modules/local_logs.py +209 -0
  54. tinybird/tb/modules/login.py +21 -2
  55. tinybird/tb/modules/login_common.py +254 -12
  56. tinybird/tb/modules/mock.py +5 -54
  57. tinybird/tb/modules/mock_common.py +0 -54
  58. tinybird/tb/modules/open.py +10 -5
  59. tinybird/tb/modules/project.py +14 -5
  60. tinybird/tb/modules/shell.py +15 -7
  61. tinybird/tb/modules/sink.py +3 -1
  62. tinybird/tb/modules/telemetry.py +11 -3
  63. tinybird/tb/modules/test.py +13 -9
  64. tinybird/tb/modules/test_common.py +13 -87
  65. tinybird/tb/modules/tinyunit/tinyunit.py +0 -14
  66. tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -6
  67. tinybird/tb/modules/watch.py +5 -3
  68. tinybird/tb_cli_modules/common.py +2 -2
  69. tinybird/tb_cli_modules/telemetry.py +1 -1
  70. tinybird/tornado_template.py +6 -7
  71. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/METADATA +32 -6
  72. tinybird-1.0.5.dist-info/RECORD +132 -0
  73. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
  74. tinybird-0.0.1.dev291.dist-info/RECORD +0 -128
  75. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
  76. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
@@ -258,4 +258,9 @@ VALID_QUERY_FORMATS = (
258
258
  "RowBinaryWithNamesAndTypes",
259
259
  "TabSeparated",
260
260
  "JSONCompactEachRowWithNamesAndTypes",
261
+ "TabSeparatedWithNamesAndTypes",
262
+ "JSONCompactEachRow",
263
+ "JSONCompact",
264
+ "JSONStringsEachRowWithProgress",
265
+ "ODBCDriver2",
261
266
  )
tinybird/connectors.py CHANGED
@@ -369,13 +369,7 @@ class Snowflake(Connector):
369
369
  the_type = "String"
370
370
  if t.startswith("NUMBER"):
371
371
  the_type = "Int32"
372
- if (
373
- t.startswith("FLOAT")
374
- or t.startswith("DOUBLE")
375
- or t.startswith("REAL")
376
- or t.startswith("NUMERIC")
377
- or t.startswith("DECIMAL")
378
- ):
372
+ if t.startswith(("FLOAT", "DOUBLE", "REAL", "NUMERIC", "DECIMAL")):
379
373
  the_type = "Float32"
380
374
  if t == "DATE":
381
375
  the_type = "Date"
tinybird/context.py CHANGED
@@ -3,15 +3,15 @@ from typing import TYPE_CHECKING
3
3
 
4
4
  # Avoid circular import error
5
5
  if TYPE_CHECKING:
6
- from tinybird.user import User
6
+ from hfi.hfi_workspace_data import HfiWorkspaceData
7
+
7
8
 
8
9
  workspace_id: ContextVar[str] = ContextVar("workspace_id")
9
- workspace: ContextVar["User"] = ContextVar("workspace")
10
+ hfi_workspace_data: ContextVar["HfiWorkspaceData"] = ContextVar("hfi_workspace_data")
10
11
  table_id: ContextVar[str] = ContextVar("table_id")
11
12
  hfi_frequency: ContextVar[float] = ContextVar("hfi_frequency")
12
13
  hfi_frequency_gatherer: ContextVar[float] = ContextVar("hfi_frequency_gatherer")
13
14
  use_gatherer: ContextVar[bool] = ContextVar("use_gatherer")
14
- allow_gatherer_fallback: ContextVar[bool] = ContextVar("allow_gatherer_fallback")
15
15
  gatherer_allow_s3_backup_on_user_errors: ContextVar[bool] = ContextVar("gatherer_allow_s3_backup_on_user_errors")
16
16
  disable_template_security_validation: ContextVar[bool] = ContextVar("disable_template_security_validation")
17
17
  origin: ContextVar[str] = ContextVar("origin")
@@ -630,7 +630,10 @@ class Datafile:
630
630
  def _validate_single_column(self, col_name: str, column_info: Dict[str, Any]) -> None:
631
631
  """Validate a single column for use in sorting keys."""
632
632
  col_type = column_info.get("type", "").lower()
633
- is_nullable = column_info.get("nullable", False)
633
+
634
+ # we need to check any presence of Nullable in the column type
635
+ is_nullable = column_info.get("nullable", False) or "Nullable(" in column_info.get("type", "")
636
+
634
637
  if is_nullable:
635
638
  raise DatafileValidationError(
636
639
  f"Sorting key contains nullable column '{col_name}'. Nullable columns cannot be used in sorting keys."
@@ -2048,11 +2051,7 @@ def parse(
2048
2051
  lexer = list(sa)
2049
2052
  if lexer:
2050
2053
  cmd, args = lexer[0], lexer[1:]
2051
- if (
2052
- parser_state.multiline
2053
- and cmd.lower() in cmds
2054
- and not (line.startswith(" ") or line.startswith("\t"))
2055
- ):
2054
+ if parser_state.multiline and cmd.lower() in cmds and not line.startswith((" ", "\t")):
2056
2055
  cmds[parser_state.command](
2057
2056
  parser_state.multiline_string,
2058
2057
  lineno=lineno,
@@ -2128,6 +2127,9 @@ def parse(
2128
2127
  return ParseResult(datafile=doc, warnings=warnings)
2129
2128
 
2130
2129
 
2130
+ # TODO: This class is duplicated in tinybird/datafile_common.py with a slightly different
2131
+ # _REPLACEMENTS tuple. The duplication happened during the CLI/server code split (commit
2132
+ # f86d02cdd7). Consider extracting shared code into a common module that both files can import.
2131
2133
  class ImportReplacements:
2132
2134
  _REPLACEMENTS: Tuple[Tuple[str, str, Optional[str]], ...] = (
2133
2135
  ("import_strategy", "mode", "replace"),
@@ -2147,7 +2149,7 @@ class ImportReplacements:
2147
2149
  return [x[0] for x in ImportReplacements._REPLACEMENTS]
2148
2150
 
2149
2151
  @staticmethod
2150
- def get_api_param_for_datafile_param(connector_service: str, key: str) -> Tuple[Optional[str], Optional[str]]:
2152
+ def get_api_param_for_datafile_param(key: str) -> Tuple[Optional[str], Optional[str]]:
2151
2153
  """Returns the API parameter name and default value for a given
2152
2154
  datafile parameter.
2153
2155
  """
@@ -2505,7 +2507,7 @@ def is_file_a_datasource(filename: str) -> bool:
2505
2507
 
2506
2508
  for line in lines:
2507
2509
  trimmed_line = line.strip().lower()
2508
- if trimmed_line.startswith("schema") or trimmed_line.startswith("engine"):
2510
+ if trimmed_line.startswith(("schema", "engine")):
2509
2511
  return True
2510
2512
 
2511
2513
  return False
@@ -11,7 +11,7 @@ from tinybird.datafile.common import (
11
11
  parse,
12
12
  )
13
13
  from tinybird.datafile.exceptions import IncludeFileNotFoundException, ParseException
14
- from tinybird.sql_template import get_template_and_variables, render_sql_template, secret_template_key
14
+ from tinybird.sql_template import get_template_and_variables, render_sql_template
15
15
  from tinybird.tb.modules.feedback_manager import FeedbackManager
16
16
  from tinybird.tornado_template import UnClosedIfError
17
17
 
@@ -54,7 +54,7 @@ def parse_pipe(
54
54
  if sql.strip()[0] == "%":
55
55
  secrets_list: Optional[List[str]] = None
56
56
  if secrets:
57
- secrets_list = [f"{secret_template_key(secret)}" for secret in secrets.keys()]
57
+ secrets_list = list(secrets.keys())
58
58
  # Setting test_mode=True to ignore errors on required parameters and
59
59
  # secrets_in_test_mode=False to raise errors on missing secrets
60
60
  sql, _, variable_warnings = render_sql_template(
@@ -506,6 +506,9 @@ Ready? """
506
506
  prompt_init_git_release_force = prompt_message(
507
507
  "You are going to manually update workspace commit reference manually, this is just for special occasions. Do you want to update current commit reference '{current_commit}' to '{new_commit}'?"
508
508
  )
509
+ prompt_init_git_release_new = prompt_message(
510
+ "This workspace does not have any release yet. Do you want to create one with commit '{commit}'? This will enable 'tb deploy' to work."
511
+ )
509
512
 
510
513
  warning_exchange = warning_message(
511
514
  "Warning: Do you want to exchange Data Source {datasource_a} by Data Source {datasource_b}?"
tinybird/prompts.py CHANGED
@@ -776,6 +776,7 @@ datasource_instructions = """
776
776
  - Use always json paths to define the schema. Example: `user_id` String `json:$.user_id`,
777
777
  - Array columns are supported with a special syntax. Example: `items` Array(String) `json:$.items[:]`
778
778
  - If the datasource is using an S3 or GCS connection, they need to set IMPORT_CONNECTION_NAME, IMPORT_BUCKET_URI and IMPORT_SCHEDULE (GCS @on-demand only, S3 supports @auto too)
779
+ - If the datasource is using a Kafka connection, they need to set KAFKA_CONNECTION_NAME as the name of the .connection file, KAFKA_TOPIC topic_name and KAFKA_GROUP_ID as the group id for the datasource
779
780
  - Unless the user asks for them, do not include ENGINE_PARTITION_KEY and ENGINE_PRIMARY_KEY.
780
781
  - DateTime64 type without precision is not supported. Use DateTime64(3) instead.
781
782
  </datasource_file_instructions>
@@ -5,6 +5,7 @@ This module provides access to predefined service datasources and their schemas
5
5
  for both Tinybird and Organization scopes.
6
6
  """
7
7
 
8
+ from functools import lru_cache
8
9
  from typing import Any, Dict, List, Optional
9
10
 
10
11
 
@@ -428,6 +429,135 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
428
429
  {"name": "total_cpu_time_seconds", "type": "Float64"},
429
430
  ],
430
431
  },
432
+ {
433
+ "name": "tinybird.llm_usage",
434
+ "description": "LLM usage metrics from Tinybird AI features including token consumption, costs, and model usage for each request in the workspace.",
435
+ "dateColumn": "start_time",
436
+ "engine": {
437
+ "engine": "MergeTree",
438
+ "sorting_key": "workspace_id, start_time, user_email, request_id",
439
+ "partition_key": "toYYYYMM(start_time)",
440
+ },
441
+ "columns": [
442
+ {"name": "start_time", "type": "DateTime"},
443
+ {"name": "end_time", "type": "DateTime"},
444
+ {"name": "organization_id", "type": "String"},
445
+ {"name": "organization_name", "type": "String"},
446
+ {"name": "workspace_id", "type": "String"},
447
+ {"name": "workspace_name", "type": "String"},
448
+ {"name": "user_email", "type": "String"},
449
+ {"name": "request_id", "type": "String"},
450
+ {"name": "prompt_tokens", "type": "UInt32"},
451
+ {"name": "completion_tokens", "type": "UInt32"},
452
+ {"name": "total_tokens", "type": "UInt32"},
453
+ {"name": "duration", "type": "Float32"},
454
+ {"name": "cost", "type": "Float32"},
455
+ {"name": "origin", "type": "String"},
456
+ {"name": "feature", "type": "String"},
457
+ ],
458
+ },
459
+ {
460
+ "name": "tinybird.llm_usage",
461
+ "description": "LLM usage metrics from Tinybird AI features including token consumption, costs, and model usage for each request in the workspace.",
462
+ "dateColumn": "start_time",
463
+ "engine": {
464
+ "engine": "MergeTree",
465
+ "sorting_key": "workspace_id, start_time, user_email, request_id",
466
+ "partition_key": "toYYYYMM(start_time)",
467
+ },
468
+ "columns": [
469
+ {"name": "start_time", "type": "DateTime"},
470
+ {"name": "end_time", "type": "DateTime"},
471
+ {"name": "organization_id", "type": "String"},
472
+ {"name": "organization_name", "type": "String"},
473
+ {"name": "workspace_id", "type": "String"},
474
+ {"name": "workspace_name", "type": "String"},
475
+ {"name": "user_email", "type": "String"},
476
+ {"name": "request_id", "type": "String"},
477
+ {"name": "prompt_tokens", "type": "UInt32"},
478
+ {"name": "completion_tokens", "type": "UInt32"},
479
+ {"name": "total_tokens", "type": "UInt32"},
480
+ {"name": "duration", "type": "Float32"},
481
+ {"name": "cost", "type": "Float32"},
482
+ {"name": "origin", "type": "String"},
483
+ {"name": "feature", "type": "String"},
484
+ ],
485
+ },
486
+ {
487
+ "name": "tinybird.query_metrics",
488
+ "description": "Query stats metrics from your workspace.",
489
+ "dateColumn": "event_time",
490
+ "engine": {
491
+ "engine": "ReplacingMergeTree",
492
+ "sorting_key": "event_time, organization_id, query_id",
493
+ "partition_key": "toStartOfDay(event_time)",
494
+ "ttl": "toDate(event_time) + INTERVAL 30 DAY",
495
+ },
496
+ "columns": [
497
+ {"name": "event_time", "type": "DateTime"},
498
+ {"name": "organization_id", "type": "String"},
499
+ {"name": "workspace_id", "type": "String"},
500
+ {"name": "query", "type": "String"},
501
+ {"name": "query_id", "type": "String"},
502
+ {"name": "query_type", "type": "String"},
503
+ {"name": "query_start_time", "type": "DateTime"},
504
+ {"name": "query_duration_ms", "type": "Int32"},
505
+ {"name": "pipe_id", "type": "String"},
506
+ {"name": "job_id", "type": "String"},
507
+ {"name": "job_kind", "type": "String"},
508
+ {"name": "read_rows", "type": "Int32"},
509
+ {"name": "read_bytes", "type": "Int32"},
510
+ {"name": "written_rows", "type": "Int32"},
511
+ {"name": "written_bytes", "type": "Int32"},
512
+ {"name": "memory_usage", "type": "Int32"},
513
+ {"name": "vcpu_time", "type": "Float32"},
514
+ {"name": "exception_code", "type": "Int32"},
515
+ {"name": "exception", "type": "String"},
516
+ ],
517
+ },
518
+ {
519
+ "name": "tinybird.vcpu_time",
520
+ "description": "vCPU time metrics from your workspace.",
521
+ "dateColumn": "second_slot",
522
+ "engine": {
523
+ "engine": "AggregatingMergeTree",
524
+ "sorting_key": "organization_id, second_slot",
525
+ "partition_key": "toStartOfDay(second_slot)",
526
+ "ttl": "toDate(second_slot) + INTERVAL 30 DAY",
527
+ },
528
+ "columns": [
529
+ {"name": "second_slot", "type": "DateTime"},
530
+ {"name": "organization_id", "type": "String"},
531
+ {"name": "vcpu_time", "type": "Float64"},
532
+ ],
533
+ },
534
+ {
535
+ "name": "tinybird.query_validator_log",
536
+ "description": "Log of failed queries executions in the next available ClickHouse version and their results.",
537
+ "dateColumn": "run_validation",
538
+ "engine": {
539
+ "engine": "MergeTree",
540
+ "sorting_key": "database, host, run_validation, query_hash",
541
+ "partition_key": "toYYYYMM(run_validation)",
542
+ },
543
+ "columns": [
544
+ {"name": "host", "type": "LowCardinality(String)"},
545
+ {"name": "version", "type": "LowCardinality(String)"},
546
+ {"name": "stable_version", "type": "LowCardinality(String)"},
547
+ {"name": "query_hash", "type": "UInt64"},
548
+ {"name": "query_last_execution", "type": "DateTime"},
549
+ {"name": "region", "type": "String"},
550
+ {"name": "workspace", "type": "String"},
551
+ {"name": "database", "type": "String"},
552
+ {"name": "pipe_name", "type": "String"},
553
+ {"name": "query_id", "type": "String"},
554
+ {"name": "query", "type": "String"},
555
+ {"name": "error_code", "type": "Int16"},
556
+ {"name": "error", "type": "String"},
557
+ {"name": "fix_suggestion", "type": "String"},
558
+ {"name": "run_validation", "type": "DateTime"},
559
+ ],
560
+ },
431
561
  ]
432
562
 
433
563
 
@@ -806,6 +936,22 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
806
936
  {"name": "active_minutes", "type": "Float64"},
807
937
  ],
808
938
  },
939
+ {
940
+ "name": "organization.shared_infra_active_seconds",
941
+ "description": "Contains information about vCPU active seconds consumption aggregated by second for all Organization workspaces. Only available for Developer and Enterprise plans in shared infrastructure.",
942
+ "dateColumn": "second",
943
+ "engine": {
944
+ "engine": "AggregatingMergeTree",
945
+ "sorting_key": "second_slot, organization_id",
946
+ "partition_key": "toYYYYMM(second_slot)",
947
+ },
948
+ "columns": [
949
+ {"name": "second_slot", "type": "DateTime"},
950
+ {"name": "organization_id", "type": "String"},
951
+ {"name": "organization_name", "type": "String"},
952
+ {"name": "cpu_time", "type": "Float64"},
953
+ ],
954
+ },
809
955
  {
810
956
  "name": "organization.shared_infra_qps_overages",
811
957
  "description": "Contains information about QPS consumption and overages aggregated by second for all Organization workspaces. Only available for Developer and Enterprise plans in shared infrastructure.",
@@ -881,9 +1027,86 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
881
1027
  {"name": "total_written_bytes", "type": "UInt64"},
882
1028
  ],
883
1029
  },
1030
+ {
1031
+ "name": "organization.llm_usage",
1032
+ "description": "LLM usage metrics from Tinybird AI features including token consumption, costs, and model usage for each request in the organization.",
1033
+ "dateColumn": "start_time",
1034
+ "engine": {
1035
+ "engine": "MergeTree",
1036
+ "sorting_key": "workspace_id, start_time, user_email, request_id",
1037
+ "partition_key": "toYYYYMM(start_time)",
1038
+ },
1039
+ "columns": [
1040
+ {"name": "start_time", "type": "DateTime"},
1041
+ {"name": "end_time", "type": "DateTime"},
1042
+ {"name": "organization_id", "type": "String"},
1043
+ {"name": "organization_name", "type": "String"},
1044
+ {"name": "workspace_id", "type": "String"},
1045
+ {"name": "workspace_name", "type": "String"},
1046
+ {"name": "user_email", "type": "String"},
1047
+ {"name": "request_id", "type": "String"},
1048
+ {"name": "prompt_tokens", "type": "UInt32"},
1049
+ {"name": "completion_tokens", "type": "UInt32"},
1050
+ {"name": "total_tokens", "type": "UInt32"},
1051
+ {"name": "duration", "type": "Float32"},
1052
+ {"name": "cost", "type": "Float32"},
1053
+ {"name": "origin", "type": "String"},
1054
+ {"name": "feature", "type": "String"},
1055
+ ],
1056
+ },
1057
+ {
1058
+ "name": "organization.query_metrics",
1059
+ "description": "Query stats metrics from your workspace.",
1060
+ "dateColumn": "event_time",
1061
+ "engine": {
1062
+ "engine": "ReplacingMergeTree",
1063
+ "sorting_key": "event_time, organization_id, query_id",
1064
+ "partition_key": "toStartOfDay(event_time)",
1065
+ "ttl": "toDate(event_time) + INTERVAL 30 DAY",
1066
+ },
1067
+ "columns": [
1068
+ {"name": "event_time", "type": "DateTime"},
1069
+ {"name": "organization_id", "type": "String"},
1070
+ {"name": "workspace_id", "type": "String"},
1071
+ {"name": "query", "type": "String"},
1072
+ {"name": "query_id", "type": "String"},
1073
+ {"name": "query_type", "type": "String"},
1074
+ {"name": "query_start_time", "type": "DateTime"},
1075
+ {"name": "query_duration_ms", "type": "Int32"},
1076
+ {"name": "pipe_id", "type": "String"},
1077
+ {"name": "job_id", "type": "String"},
1078
+ {"name": "job_kind", "type": "String"},
1079
+ {"name": "read_rows", "type": "Int32"},
1080
+ {"name": "read_bytes", "type": "Int32"},
1081
+ {"name": "written_rows", "type": "Int32"},
1082
+ {"name": "written_bytes", "type": "Int32"},
1083
+ {"name": "memory_usage", "type": "Int32"},
1084
+ {"name": "vcpu_time", "type": "Float32"},
1085
+ {"name": "exception_code", "type": "Int32"},
1086
+ {"name": "exception", "type": "String"},
1087
+ ],
1088
+ },
1089
+ {
1090
+ "name": "organization.vcpu_time",
1091
+ "description": "vCPU time metrics from your workspace.",
1092
+ "dateColumn": "second_slot",
1093
+ "engine": {
1094
+ "engine": "AggregatingMergeTree",
1095
+ "sorting_key": "organization_id, second_slot",
1096
+ "partition_key": "toStartOfDay(second_slot)",
1097
+ "ttl": "toDate(second_slot) + INTERVAL 30 DAY",
1098
+ },
1099
+ "columns": [
1100
+ {"name": "second_slot", "type": "DateTime"},
1101
+ {"name": "organization_id", "type": "String"},
1102
+ {"name": "workspace_id", "type": "String"},
1103
+ {"name": "vcpu_time", "type": "Float64"},
1104
+ ],
1105
+ },
884
1106
  ]
885
1107
 
886
1108
 
1109
+ @lru_cache(maxsize=1)
887
1110
  def get_service_datasources() -> List[Dict[str, Any]]:
888
1111
  """
889
1112
  Get the list of all Tinybird and Organization service datasources.
tinybird/sql_template.py CHANGED
@@ -384,14 +384,13 @@ def array_type(types):
384
384
  if isinstance(x, Placeholder):
385
385
  if default:
386
386
  x = default
387
- else:
388
- if _type and _type in types:
389
- if _type == "String":
390
- x = ""
391
- else:
392
- x = ",".join(map(str, [types[_type](x) for _ in range(2)]))
393
- else:
387
+ elif _type and _type in types:
388
+ if _type == "String":
394
389
  x = ""
390
+ else:
391
+ x = ",".join(map(str, [types[_type](x) for _ in range(2)]))
392
+ else:
393
+ x = ""
395
394
  elif x is None:
396
395
  x = default
397
396
  if x is None:
@@ -1405,8 +1404,11 @@ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
1405
1404
  namespace = {}
1406
1405
  template_execution_results = TemplateExecutionResults()
1407
1406
  for key in kwargs.get("tb_secrets", []):
1407
+ # Avoid double-prefixing if the key already has the tb_secret_ prefix
1408
1408
  if is_secret_template_key(key):
1409
1409
  template_execution_results.add_template_param(key)
1410
+ else:
1411
+ template_execution_results.add_template_param(secret_template_key(key))
1410
1412
 
1411
1413
  if TB_SECRET_IN_TEST_MODE in kwargs:
1412
1414
  template_execution_results[TB_SECRET_IN_TEST_MODE] = None
@@ -1415,15 +1417,20 @@ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
1415
1417
  try:
1416
1418
  key = secret_template_key(x)
1417
1419
  if key in template_execution_results.template_params:
1420
+ # secret available: Always use workspace secret regardless of test mode
1418
1421
  template_execution_results.add_ch_param(x)
1419
1422
  return Symbol("{" + sqlescape(x) + ": String}")
1420
1423
  else:
1424
+ # secret not available: Check test mode and defaults
1421
1425
  is_test_mode = TB_SECRET_IN_TEST_MODE in template_execution_results
1422
- if is_test_mode and default is None:
1423
- return Symbol("{" + sqlescape(x) + ": String}")
1424
- elif default is not None:
1426
+ if default is not None:
1427
+ # Use provided default value
1425
1428
  return default
1429
+ elif is_test_mode:
1430
+ # In test mode without default - return placeholder
1431
+ return Symbol("{" + sqlescape(x) + ": String}")
1426
1432
  else:
1433
+ # Not in test mode, no secret, no default - raise error
1427
1434
  raise SQLTemplateException(
1428
1435
  f"Cannot access secret '{x}'. Check the secret exists in the Workspace and the token has the required scope."
1429
1436
  )
@@ -1834,7 +1841,7 @@ def get_var_names_and_types(t, node_id=None):
1834
1841
  raise SQLTemplateException(e)
1835
1842
 
1836
1843
 
1837
- @lru_cache(maxsize=256)
1844
+ @lru_cache(maxsize=512)
1838
1845
  def get_var_names_and_types_cached(t: Template):
1839
1846
  return get_var_names_and_types(t)
1840
1847
 
@@ -2378,6 +2385,14 @@ def render_sql_template(
2378
2385
  documentation="/cli/advanced-templates.html",
2379
2386
  )
2380
2387
  raise SQLTemplateException(str(e), documentation="/cli/advanced-templates.html")
2388
+ except IndexError as e:
2389
+ # This happens when trying to access string indices on empty strings
2390
+ if "string index out of range" in str(e):
2391
+ raise SQLTemplateException(
2392
+ "String index out of range. Check that string parameters have values before accessing specific characters (e.g., param[0]). Provide default values or add length checks in your template.",
2393
+ documentation="/cli/advanced-templates.html",
2394
+ )
2395
+ raise SQLTemplateException(str(e), documentation="/cli/advanced-templates.html")
2381
2396
  except Exception as e:
2382
2397
  # errors might vary here, we need to support as much as possible
2383
2398
  # https://gitlab.com/tinybird/analytics/-/issues/943
@@ -203,13 +203,15 @@ class TinybirdDialect(ClickHouse):
203
203
  Rule(
204
204
  name="jinja_if_block_end",
205
205
  priority=203,
206
- pattern=group(r"\{%-?\s*end\s*-?%\}"),
206
+ # Accept both Tornado-style {% end %} and {% end if %}
207
+ pattern=group(r"\{%-?\s*end(\s+if)?\s*-?%\}"),
207
208
  action=actions.raise_sqlfmt_bracket_error,
208
209
  ),
209
210
  Rule(
210
211
  name="jinja_for_block_end",
211
212
  priority=211,
212
- pattern=group(r"\{%-?\s*end\s*-?%\}"),
213
+ # Accept both Tornado-style {% end %} and {% end for %}
214
+ pattern=group(r"\{%-?\s*end(\s+for)?\s*-?%\}"),
213
215
  action=actions.raise_sqlfmt_bracket_error,
214
216
  ),
215
217
  ],
@@ -261,7 +263,13 @@ def _calc_str(self) -> str:
261
263
  Comment._calc_str = property(_calc_str)
262
264
 
263
265
 
264
- def format_sql_template(sql: str, line_length: Optional[int] = None, lower_keywords: bool = False) -> str:
266
+ def format_sql_template(
267
+ sql: str,
268
+ line_length: Optional[int] = None,
269
+ lower_keywords: bool = False,
270
+ resource_name: Optional[str] = None,
271
+ resource_source: Optional[str] = None,
272
+ ) -> str:
265
273
  try:
266
274
  # https://github.com/tconbeer/sqlfmt/blob/c11775b92d8a45f0e91d871b81a88a894d620bec/src/sqlfmt/mode.py#L16-L29
267
275
  config: Dict[str, Any] = {
@@ -277,5 +285,7 @@ def format_sql_template(sql: str, line_length: Optional[int] = None, lower_keywo
277
285
  else api.format_string(sql, mode=mode).strip()
278
286
  )
279
287
  except Exception as e:
280
- logging.warning(f"sqlfmt error: {str(e)}")
288
+ resource_info = f" in '{resource_name}'" if resource_name else ""
289
+ source_info = f" ({resource_source})" if resource_source else ""
290
+ logging.warning(f"sqlfmt error{resource_info}{source_info}: {str(e)}")
281
291
  return sql
tinybird/tb/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '0.0.1.dev291'
8
- __revision__ = '87da698'
7
+ __version__ = '1.0.5'
8
+ __revision__ = '60ae688'
tinybird/tb/cli.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import tinybird.tb.modules.agent
2
+ import tinybird.tb.modules.branch
2
3
  import tinybird.tb.modules.build
3
4
  import tinybird.tb.modules.cli
4
5
  import tinybird.tb.modules.common