tinybird 0.0.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tinybird might be problematic. Click here for more details.

Files changed (45) hide show
  1. tinybird/__cli__.py +8 -0
  2. tinybird/ch_utils/constants.py +244 -0
  3. tinybird/ch_utils/engine.py +855 -0
  4. tinybird/check_pypi.py +25 -0
  5. tinybird/client.py +1281 -0
  6. tinybird/config.py +117 -0
  7. tinybird/connectors.py +428 -0
  8. tinybird/context.py +23 -0
  9. tinybird/datafile.py +5589 -0
  10. tinybird/datatypes.py +434 -0
  11. tinybird/feedback_manager.py +1022 -0
  12. tinybird/git_settings.py +145 -0
  13. tinybird/sql.py +865 -0
  14. tinybird/sql_template.py +2343 -0
  15. tinybird/sql_template_fmt.py +281 -0
  16. tinybird/sql_toolset.py +350 -0
  17. tinybird/syncasync.py +682 -0
  18. tinybird/tb_cli.py +25 -0
  19. tinybird/tb_cli_modules/auth.py +252 -0
  20. tinybird/tb_cli_modules/branch.py +1043 -0
  21. tinybird/tb_cli_modules/cicd.py +434 -0
  22. tinybird/tb_cli_modules/cli.py +1571 -0
  23. tinybird/tb_cli_modules/common.py +2082 -0
  24. tinybird/tb_cli_modules/config.py +344 -0
  25. tinybird/tb_cli_modules/connection.py +803 -0
  26. tinybird/tb_cli_modules/datasource.py +900 -0
  27. tinybird/tb_cli_modules/exceptions.py +91 -0
  28. tinybird/tb_cli_modules/fmt.py +91 -0
  29. tinybird/tb_cli_modules/job.py +85 -0
  30. tinybird/tb_cli_modules/pipe.py +858 -0
  31. tinybird/tb_cli_modules/regions.py +9 -0
  32. tinybird/tb_cli_modules/tag.py +100 -0
  33. tinybird/tb_cli_modules/telemetry.py +310 -0
  34. tinybird/tb_cli_modules/test.py +107 -0
  35. tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
  36. tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
  37. tinybird/tb_cli_modules/token.py +349 -0
  38. tinybird/tb_cli_modules/workspace.py +269 -0
  39. tinybird/tb_cli_modules/workspace_members.py +212 -0
  40. tinybird/tornado_template.py +1194 -0
  41. tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
  42. tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
  43. tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
  44. tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
  45. tinybird-0.0.1.dev0.dist-info/top_level.txt +4 -0
@@ -0,0 +1,2343 @@
1
+ import ast
2
+ import builtins
3
+ import linecache
4
+ import logging
5
+ import re
6
+ from datetime import datetime
7
+ from functools import lru_cache
8
+ from io import StringIO
9
+ from json import loads
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ from tornado import escape
13
+ from tornado.util import ObjectDict, exec_in, unicode_type
14
+
15
+ from tinybird.context import ff_preprocess_parameters_circuit_breaker, ff_split_to_array_escape
16
+
17
+ from .datatypes import testers
18
+ from .tornado_template import VALID_CUSTOM_FUNCTION_NAMES, SecurityException, Template
19
+
20
+ TB_SECRET_IN_TEST_MODE = "tb_secret_dont_raise"
21
+ TB_SECRET_PREFIX = "tb_secret_"
22
+ CH_PARAM_PREFIX = "param_"
23
+ REQUIRED_PARAM_NOT_DEFINED = "Required parameter is not defined"
24
+
25
+
26
+ def secret_template_key(secret_name: str) -> str:
27
+ return f"{TB_SECRET_PREFIX}{secret_name}"
28
+
29
+
30
+ def is_secret_template_key(key: str) -> bool:
31
+ return key.startswith(TB_SECRET_PREFIX)
32
+
33
+
34
+ class TemplateExecutionResults(dict):
35
+ def __init__(self, *args, **kwargs):
36
+ super().__init__(*args, **kwargs)
37
+ self.template_params = set()
38
+ self.ch_params = set()
39
+
40
+ def add_template_param(self, param: str):
41
+ self.template_params.add(param)
42
+
43
+ def add_ch_param(self, param: str):
44
+ self.ch_params.add(param)
45
+
46
+ def update_all(self, other: "TemplateExecutionResults"):
47
+ self.update(other)
48
+ self.ch_params.update(other.ch_params)
49
+ self.template_params.update(other.template_params)
50
+
51
+
52
+ class SQLTemplateCustomError(Exception):
53
+ def __init__(self, err, code=400):
54
+ self.code = code
55
+ self.err = err
56
+ super().__init__(err)
57
+
58
+
59
+ class SQLTemplateException(ValueError):
60
+ def __init__(self, err, documentation=None):
61
+ self.documentation = documentation
62
+ super().__init__(f"Template Syntax Error: {str(err)}")
63
+
64
+
65
+ # t = Template(""" SELECT * from test where lon between {{Float32(lon1, 0)}} and {{Float32(lon2, 0)}} """)
66
+ # names = get_var_names(t)
67
+ # print(generate(t, **{x: '' for x in names}))
68
+
69
+ # t = Template(""" SELECT * from test where lon between {{lon1}} and {{lon2}} """)
70
+ # names = get_var_names(t)
71
+ # replace_vars_smart(t)
72
+ # print(generate(t, **{x: '' for x in names}))
73
+
74
+
75
+ DEFAULT_PARAM_NAMES = ["format", "q"]
76
+ RESERVED_PARAM_NAMES = [
77
+ "__tb__semver",
78
+ "debug_source_tables",
79
+ "debug",
80
+ "explain",
81
+ "finalize_aggregations",
82
+ "output_format_json_quote_64bit_integers",
83
+ "output_format_json_quote_denormals",
84
+ "output_format_parquet_string_as_string",
85
+ "pipeline",
86
+ "playground",
87
+ "q",
88
+ "query_id",
89
+ "release_replacements",
90
+ "tag",
91
+ "template_parameters",
92
+ "token",
93
+ ]
94
+
95
+ parameter_types = [
96
+ "String",
97
+ "Boolean",
98
+ "DateTime64",
99
+ "DateTime",
100
+ "Date",
101
+ "Float32",
102
+ "Float64",
103
+ "Int8",
104
+ "Int16",
105
+ "Int32",
106
+ "Int64",
107
+ "Int128",
108
+ "Int256",
109
+ "UInt8",
110
+ "UInt16",
111
+ "UInt32",
112
+ "UInt64",
113
+ "UInt128",
114
+ "UInt256",
115
+ "Array",
116
+ "JSON",
117
+ ]
118
+
119
+
120
+ def transform_type(
121
+ tester, transform, placeholder=None, required=None, description=None, enum=None, example=None, format=None
122
+ ):
123
+ def _f(x, default=None, defined=True, required=None, description=None, enum=None, example=None, format=None):
124
+ if isinstance(x, Placeholder):
125
+ if default:
126
+ x = default
127
+ else:
128
+ x = placeholder
129
+ elif x is None:
130
+ x = default
131
+ if x is None:
132
+ if defined:
133
+ raise SQLTemplateException(REQUIRED_PARAM_NOT_DEFINED, documentation="/cli/advanced-templates.html")
134
+ else:
135
+ return None
136
+ if tester == "String":
137
+ if x is not None:
138
+ return transform(x)
139
+ elif testers[tester](str(x)):
140
+ return transform(x)
141
+ raise SQLTemplateException(
142
+ f"Error validating '{x}' to type {tester}", documentation="/cli/advanced-templates.html"
143
+ )
144
+
145
+ return _f
146
+
147
+
148
+ def _and(*args, **kwargs):
149
+ operands = {"in": "in", "not_in": "not in", "gt": ">", "lt": "<", "gte": ">=", "lte": "<="}
150
+
151
+ def _name(k):
152
+ tk = k.rsplit("__", 1)
153
+ return tk[0]
154
+
155
+ def _op(k):
156
+ tk = k.rsplit("__", 1)
157
+ if len(tk) == 1:
158
+ return "="
159
+ else:
160
+ if tk[1] in operands:
161
+ return operands[tk[1]]
162
+ raise SQLTemplateException(
163
+ f"operand {tk[1]} not supported", documentation="/cli/advanced-templates.html#sql_and"
164
+ )
165
+
166
+ return Expression(
167
+ " and ".join([f"{_name(k)} {_op(k)} {expression_wrapper(v, k)}" for k, v in kwargs.items() if v is not None])
168
+ )
169
+
170
+
171
+ def error(s, code=400):
172
+ raise ValueError(s)
173
+
174
+
175
+ def custom_error(s, code=400):
176
+ raise SQLTemplateCustomError(s, code)
177
+
178
+
179
+ class Expression(str):
180
+ pass
181
+
182
+
183
+ class Comment:
184
+ def __init__(self, s):
185
+ self.text = s
186
+
187
+ def __str__(self):
188
+ return self.text
189
+
190
+
191
+ class Placeholder:
192
+ def __init__(self, name=None, line=None):
193
+ self.name = name if name else "__no_value__"
194
+ self.line = line or "unknown"
195
+
196
+ def __str__(self):
197
+ return "__no_value__"
198
+
199
+ def __getitem__(self, i):
200
+ if i > 2:
201
+ raise IndexError()
202
+ return Placeholder()
203
+
204
+ def __add__(self, s):
205
+ return Placeholder()
206
+
207
+ def __call__(self, *args, **kwargs):
208
+ raise SQLTemplateException(
209
+ f"'{self.name}' is not a valid function, line {self.line}", documentation="/cli/advanced-templates.html"
210
+ )
211
+
212
+ def split(self, ch):
213
+ return [Placeholder(), Placeholder()]
214
+
215
+ def startswith(self, c):
216
+ return False
217
+
218
+
219
+ class Symbol:
220
+ def __init__(self, x):
221
+ self.x = x
222
+
223
+ def __str__(self):
224
+ return self.x
225
+
226
+
227
+ class Integer(int):
228
+ def __new__(self, value, type):
229
+ return int.__new__(self, value)
230
+
231
+ def __init__(self, value, type):
232
+ int.__init__(value)
233
+ self.type = type
234
+
235
+ def __str__(self):
236
+ return f"to{self.type}('{int(self)}')"
237
+
238
+
239
+ class Float(float):
240
+ def __new__(self, value, type):
241
+ return float.__new__(self, value)
242
+
243
+ def __init__(self, value, type):
244
+ float.__init__(value)
245
+ self.type = type
246
+
247
+ def __str__(self):
248
+ return f"to{self.type}('{float(self)}')"
249
+
250
+
251
+ def columns(x, default=None, fn=None):
252
+ if x is None or isinstance(x, Placeholder):
253
+ if default is None:
254
+ raise SQLTemplateException(
255
+ "Missing columns() default value, use `columns(column_names, 'default_column_name')`",
256
+ documentation="/cli/advanced-templates.html#columns",
257
+ )
258
+ x = default
259
+
260
+ try:
261
+ _columns = [c.strip() for c in x.split(",")]
262
+ except AttributeError:
263
+ raise SQLTemplateException(
264
+ "The 'columns' function expects a String not an Array", documentation="/cli/advanced-templates.html#columns"
265
+ )
266
+
267
+ if fn:
268
+ return Expression(",".join(f"{fn}({str(column(c, c))}) as {c}" for c in _columns))
269
+ else:
270
+ return Expression(",".join(str(column(c, c)) for c in _columns))
271
+
272
+
273
+ def column(x, default=None):
274
+ if x is None or isinstance(x, Placeholder):
275
+ if default is None:
276
+ raise SQLTemplateException(
277
+ "Missing column() default value, use `column(column_name, 'default_column_name')`",
278
+ documentation="/cli/advanced-templates.html#column",
279
+ )
280
+ x = default
281
+ return Symbol("`" + sqlescape(x) + "`")
282
+
283
+
284
+ def symbol(x, quote="`"):
285
+ if isinstance(x, Placeholder):
286
+ return Symbol("`placeholder`")
287
+ return Symbol(quote + sqlescape(x) + quote)
288
+
289
+
290
+ def table(x, quote="`"):
291
+ if isinstance(x, Placeholder):
292
+ return Symbol("placeholder")
293
+ return Symbol(sqlescape(x))
294
+
295
+
296
+ # ClickHouse does not have a boolean type. Docs suggest to use 1/0:
297
+ #
298
+ # https://clickhouse.com/docs/en/sql-reference/data-types/boolean/
299
+ #
300
+ def boolean(x, default=None):
301
+ """
302
+ >>> boolean(True)
303
+ 1
304
+ >>> boolean(False)
305
+ 0
306
+ >>> boolean('TRUE')
307
+ 1
308
+ >>> boolean('FALSE')
309
+ 0
310
+ >>> boolean('true')
311
+ 1
312
+ >>> boolean('false')
313
+ 0
314
+ >>> boolean(1)
315
+ 1
316
+ >>> boolean(0)
317
+ 0
318
+ >>> boolean('1')
319
+ 1
320
+ >>> boolean('0')
321
+ 0
322
+ >>> boolean(None)
323
+ 0
324
+ >>> boolean(None, default=True)
325
+ 1
326
+ >>> boolean(None, default=False)
327
+ 0
328
+ >>> boolean(None, default='TRUE')
329
+ 1
330
+ >>> boolean(None, default='FALSE')
331
+ 0
332
+ >>> boolean(Placeholder())
333
+ 0
334
+ >>> boolean(Placeholder(), default=True)
335
+ 1
336
+ >>> boolean(Placeholder(), default=False)
337
+ 0
338
+ >>> boolean(Placeholder(), default='TRUE')
339
+ 1
340
+ >>> boolean(Placeholder(), default='FALSE')
341
+ 0
342
+ """
343
+ if x is None:
344
+ if default is None:
345
+ return 0
346
+ return boolean(default)
347
+ elif isinstance(x, Placeholder):
348
+ return boolean(default)
349
+ elif isinstance(x, str):
350
+ if x == "0" or x.lower() == "false":
351
+ return 0
352
+
353
+ return int(bool(x))
354
+
355
+
356
+ def defined(x=None):
357
+ if isinstance(x, Placeholder) or x is None:
358
+ return False
359
+ return True
360
+
361
+
362
+ def array_type(types):
363
+ def _f(
364
+ x, _type=None, default=None, defined=True, required=None, description=None, enum=None, example=None, format=None
365
+ ):
366
+ try:
367
+ if isinstance(x, Placeholder):
368
+ if default:
369
+ x = default
370
+ else:
371
+ if _type and _type in types:
372
+ x = ",".join(map(str, [types[_type](x) for _ in range(2)]))
373
+ else:
374
+ x = ",".join([f"__no_value__{i}" for i in range(2)])
375
+ elif x is None:
376
+ x = default
377
+ if x is None:
378
+ if defined:
379
+ raise SQLTemplateException(
380
+ REQUIRED_PARAM_NOT_DEFINED, documentation="/cli/advanced-templates.html"
381
+ )
382
+ else:
383
+ return None
384
+ values = []
385
+ list_values = x if type(x) == list else x.split(",") # noqa: E721
386
+ for i, t in enumerate(list_values):
387
+ if _type in testers:
388
+ if testers[_type](str(t)):
389
+ values.append(expression_wrapper(types[_type](t), str(t)))
390
+ else:
391
+ raise SQLTemplateException(
392
+ f"Error validating {x}[{i}]({t}) to type {_type}",
393
+ documentation="/cli/advanced-templates.html",
394
+ )
395
+ else:
396
+ values.append(expression_wrapper(types.get(_type, lambda x: x)(t), str(t)))
397
+ return Expression(f"[{','.join(map(str, values))}]")
398
+ except AttributeError as e:
399
+ logging.warning(f"AttributeError on Array: {e}")
400
+ raise SQLTemplateException(
401
+ "transform type function Array is not well defined", documentation="/cli/advanced-templates.html"
402
+ )
403
+
404
+ return _f
405
+
406
+
407
+ def sql_unescape(x, what=""):
408
+ """
409
+ unescapes specific characters in a string. It allows to allow some
410
+ special characters to be used, for example in like condictionals
411
+
412
+ {{sql_unescape(String(like_filter), '%')}}
413
+
414
+
415
+ >>> sql_unescape('testing%', '%')
416
+ "'testing%'"
417
+ >>> sql_unescape('testing%', '$')
418
+ "'testing\\\\%'"
419
+ """
420
+ return Expression("'" + sqlescape(x).replace(f"\\{what}", what) + "'")
421
+
422
+
423
+ def split_to_array(x, default="", separator: str = ","):
424
+ try:
425
+ if isinstance(x, Placeholder) or x is None:
426
+ x = default
427
+ return [s.strip() for s in x.split(separator)]
428
+ except AttributeError as e:
429
+ logging.warning(f"warning on split_to_array: {str(e)}")
430
+ raise SQLTemplateException(
431
+ "First argument of split_to_array has to be a value that can be split to a list of elements, but found a PlaceHolder with no value instead",
432
+ documentation="/cli/advanced-templates.html#split_to_array",
433
+ )
434
+
435
+
436
+ def enumerate_with_last(arr):
437
+ """
438
+ >>> enumerate_with_last([1, 2])
439
+ [(False, 1), (True, 2)]
440
+ >>> enumerate_with_last([1])
441
+ [(True, 1)]
442
+ """
443
+ arr_len = len(arr)
444
+ return [(arr_len == i + 1, x) for i, x in enumerate(arr)]
445
+
446
+
447
+ def string_type(x, default=None):
448
+ if isinstance(x, Placeholder):
449
+ if default:
450
+ x = default
451
+ else:
452
+ x = "__no_value__"
453
+ return x
454
+
455
+
456
+ def day_diff(d0, d1, default=None):
457
+ """
458
+ >>> day_diff('2019-01-01', '2019-01-01')
459
+ 0
460
+ >>> day_diff('2019-01-01', '2019-01-02')
461
+ 1
462
+ >>> day_diff('2019-01-02', '2019-01-01')
463
+ 1
464
+ >>> day_diff('2019-01-02', '2019-02-01')
465
+ 30
466
+ >>> day_diff('2019-02-01', '2019-01-02')
467
+ 30
468
+ >>> day_diff(Placeholder(), Placeholder())
469
+ 0
470
+ >>> day_diff(Placeholder(), '')
471
+ 0
472
+ """
473
+ try:
474
+ return date_diff_in_days(d0, d1, date_format="%Y-%m-%d")
475
+ except Exception:
476
+ raise SQLTemplateException(
477
+ "invalid date format in function `day_diff`, it must be ISO format date YYYY-MM-DD, e.g. 2018-09-26. For other fotmats, try `date_diff_in_days`",
478
+ documentation="/cli/advanced-templates.html#date_diff_in_days",
479
+ )
480
+
481
+
482
+ def date_diff_in_days(
483
+ d0: Union[Placeholder, str],
484
+ d1: Union[Placeholder, str],
485
+ date_format: str = "%Y-%m-%d",
486
+ default=None,
487
+ backup_date_format=None,
488
+ none_if_error=False,
489
+ ):
490
+ """
491
+ >>> date_diff_in_days('2019-01-01', '2019-01-01')
492
+ 0
493
+ >>> date_diff_in_days('2019-01-01', '2019-01-02')
494
+ 1
495
+ >>> date_diff_in_days('2019-01-02', '2019-01-01')
496
+ 1
497
+ >>> date_diff_in_days('2019-01-02', '2019-02-01')
498
+ 30
499
+ >>> date_diff_in_days('2019-02-01 20:00:00', '2019-01-02 20:00:00', date_format="%Y-%m-%d %H:%M:%S")
500
+ 30
501
+ >>> date_diff_in_days('2019-02-01', '2019-01-02')
502
+ 30
503
+ >>> date_diff_in_days(Placeholder(), Placeholder())
504
+ 0
505
+ >>> date_diff_in_days(Placeholder(), '')
506
+ 0
507
+ >>> date_diff_in_days('2019-01-01', '2019/01/01', backup_date_format='%Y/%m/%d')
508
+ 0
509
+ >>> date_diff_in_days('2019-01-01', '2019/01/04', backup_date_format='%Y/%m/%d')
510
+ 3
511
+ >>> date_diff_in_days('2019/01/04', '2019-01-01', backup_date_format='%Y/%m/%d')
512
+ 3
513
+ >>> date_diff_in_days('2019-02-01T20:00:00z', '2019-02-15', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d')
514
+ 13
515
+ >>> date_diff_in_days('2019-02-01 20:00:00', '2019-02-15', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
516
+ True
517
+ >>> date_diff_in_days('2019-01-01', '2019-00-02', none_if_error=True) is None
518
+ True
519
+ >>> date_diff_in_days('2019-01-01 00:00:00', '2019-01-02 00:00:00', none_if_error=True) is None
520
+ True
521
+ """
522
+ if isinstance(d0, Placeholder) or isinstance(d1, Placeholder):
523
+ if default:
524
+ return default
525
+ return 0
526
+ try:
527
+ return __date_diff(d0, d1, date_format, backup_date_format, "days", none_if_error)
528
+ except Exception:
529
+ raise SQLTemplateException(
530
+ "invalid date format in function `date_diff_in_days`, it must be ISO format date YYYY-MM-DD, e.g. 2018-09-26",
531
+ documentation="/cli/advanced-templates.html#date_diff_in_days",
532
+ )
533
+
534
+
535
+ def date_diff_in_hours(
536
+ d0: Union[Placeholder, str],
537
+ d1: Union[Placeholder, str],
538
+ date_format: str = "%Y-%m-%d %H:%M:%S",
539
+ default=None,
540
+ backup_date_format=None,
541
+ none_if_error=False,
542
+ ):
543
+ """
544
+ >>> date_diff_in_hours('2022-12-19T18:42:23.521Z', '2022-12-19T18:42:23.521Z', date_format='%Y-%m-%dT%H:%M:%S.%fz')
545
+ 0
546
+ >>> date_diff_in_hours('2022-12-19T20:43:22Z', '2022-12-19T18:42:23Z','%Y-%m-%dT%H:%M:%Sz')
547
+ 2
548
+ >>> date_diff_in_hours('2022-12-14 18:42:22', '2022-12-19 18:42:22')
549
+ 120
550
+ >>> date_diff_in_hours('2022-12-19 18:42:23.521', '2022-12-19 18:42:24.521','%Y-%m-%d %H:%M:%S.%f')
551
+ 0
552
+ >>> date_diff_in_hours(Placeholder(), Placeholder())
553
+ 0
554
+ >>> date_diff_in_hours(Placeholder(), '')
555
+ 0
556
+ >>> date_diff_in_hours('2022-12-19T03:22:12.102Z', '2022-12-19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d')
557
+ 3
558
+ >>> date_diff_in_hours('2022-12-19', '2022-12-19', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d')
559
+ 0
560
+ >>> date_diff_in_hours('2022-12-19', '2022-12-18', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d')
561
+ 24
562
+ >>> date_diff_in_hours('2022-12-19', '2022-12-19 02:01:00', backup_date_format='%Y-%m-%d')
563
+ 2
564
+ >>> date_diff_in_hours('2022-25-19T00:00:03.521Z', '2022-12-19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
565
+ True
566
+ >>> date_diff_in_hours('2022-25-19 00:00:03', '2022-12-19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
567
+ True
568
+ >>> date_diff_in_hours('2022-12-19', '2022-25-19', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
569
+ True
570
+ >>> date_diff_in_hours('2022-12-19', '2022-25-19 00:01:00', backup_date_format='%Y-%m-%d', none_if_error=True) is None
571
+ True
572
+ >>> date_diff_in_hours('2022-12-32 18:42:22', '2022-12-19 18:42:22', none_if_error=True) is None
573
+ True
574
+ >>> date_diff_in_hours('2022-12-18T18:42:22Z', '2022-12-19T18:42:22Z', none_if_error=True) is None
575
+ True
576
+ """
577
+ if isinstance(d0, Placeholder) or isinstance(d1, Placeholder):
578
+ if default:
579
+ return default
580
+ return 0
581
+ try:
582
+ return __date_diff(d0, d1, date_format, backup_date_format, "hours", none_if_error)
583
+ except Exception:
584
+ raise SQLTemplateException(
585
+ "invalid date_format in function `date_diff_in_hours`, defaults to YYYY-MM-DD hh:mm:ss. Or %Y-%m-%d %H:%M:%S [.ssssss]Z, e.g. ms: 2022-12-19T18:42:22.591Z s:2022-12-19T18:42:22Z",
586
+ documentation="/cli/advanced-templates.html#date_diff_in_hours",
587
+ )
588
+
589
+
590
+ def date_diff_in_minutes(
591
+ d0: Union[Placeholder, str],
592
+ d1: Union[Placeholder, str],
593
+ date_format: str = "%Y-%m-%d %H:%M:%S",
594
+ default=None,
595
+ backup_date_format=None,
596
+ none_if_error=False,
597
+ ):
598
+ """
599
+ >>> date_diff_in_minutes('2022-12-19T18:42:23.521Z', '2022-12-19T18:42:23.521Z', date_format='%Y-%m-%dT%H:%M:%S.%fz')
600
+ 0
601
+ >>> date_diff_in_minutes('2022-12-19T18:43:22Z', '2022-12-19T18:42:23Z','%Y-%m-%dT%H:%M:%Sz')
602
+ 0
603
+ >>> date_diff_in_minutes('2022-12-14 18:42:22', '2022-12-19 18:42:22')
604
+ 7200
605
+ >>> date_diff_in_minutes('2022-12-19 18:42:23.521', '2022-12-19 18:42:24.521','%Y-%m-%d %H:%M:%S.%f')
606
+ 0
607
+ >>> date_diff_in_minutes(Placeholder(), Placeholder())
608
+ 0
609
+ >>> date_diff_in_minutes(Placeholder(), '')
610
+ 0
611
+ >>> date_diff_in_minutes('2022-12-19T03:22:12.102Z', '2022-12-19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d')
612
+ 202
613
+ >>> date_diff_in_minutes('2022-12-19', '2022-12-19', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d')
614
+ 0
615
+ >>> date_diff_in_minutes('2022-12-19', '2022-12-18', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d')
616
+ 1440
617
+ >>> date_diff_in_minutes('2022-12-19', '2022-12-19 00:01:00', backup_date_format='%Y-%m-%d')
618
+ 1
619
+ >>> date_diff_in_minutes('2022-25-19T00:00:03.521Z', '2022-12-19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
620
+ True
621
+ >>> date_diff_in_minutes('2022-12-19', '2022-25-19', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
622
+ True
623
+ >>> date_diff_in_minutes('2022-12-19', '2022-25-19 00:01:00', backup_date_format='%Y-%m-%d', none_if_error=True) is None
624
+ True
625
+ >>> date_diff_in_minutes('2022-25-19T00:00:03.521Z', '2022-12-19 00:23:12', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
626
+ True
627
+ >>> date_diff_in_minutes('2022-12-14 18:42:22', '2022/12/19 18:42:22', none_if_error=True) is None
628
+ True
629
+ >>> date_diff_in_minutes('2022-12-14 18:42:22', '2022/12/19 18:42:22', date_format='%Y/%m/%dT%H:%M:%S.%fz', none_if_error=True) is None
630
+ True
631
+ """
632
+ if isinstance(d0, Placeholder) or isinstance(d1, Placeholder):
633
+ if default:
634
+ return default
635
+ return 0
636
+ try:
637
+ return __date_diff(d0, d1, date_format, backup_date_format, "minutes", none_if_error)
638
+ except Exception:
639
+ raise SQLTemplateException(
640
+ "invalid date_format in function `date_diff_in_seconds`, defaults to YYYY-MM-DD hh:mm:ss. Or %Y-%m-%d %H:%M:%S [.ssssss]Z, e.g. ms: 2022-12-19T18:42:22.591Z s:2022-12-19T18:42:22Z",
641
+ documentation="/cli/advanced-templates.html#date_diff_in_minutes",
642
+ )
643
+
644
+
645
+ def date_diff_in_seconds(
646
+ d0: Union[Placeholder, str],
647
+ d1: Union[Placeholder, str],
648
+ date_format: str = "%Y-%m-%d %H:%M:%S",
649
+ default=None,
650
+ backup_date_format=None,
651
+ none_if_error=False,
652
+ ):
653
+ """
654
+ >>> date_diff_in_seconds('2022-12-19T18:42:23.521Z', '2022-12-19T18:42:23.521Z', date_format='%Y-%m-%dT%H:%M:%S.%fz')
655
+ 0
656
+ >>> date_diff_in_seconds('2022-12-19T18:42:22Z', '2022-12-19T18:42:23Z','%Y-%m-%dT%H:%M:%Sz')
657
+ 1
658
+ >>> date_diff_in_seconds('2022-12-19 18:42:22', '2022-12-19 18:43:22')
659
+ 60
660
+ >>> date_diff_in_seconds('2022-12-14 18:42:22', '2022-12-19 18:42:22')
661
+ 432000
662
+ >>> date_diff_in_seconds('2022-12-19T18:42:23.521Z', '2022-12-19T18:42:23.531Z','%Y-%m-%dT%H:%M:%S.%fz')
663
+ 0
664
+ >>> date_diff_in_seconds('2022-12-19 18:42:23.521', '2022-12-19 18:42:24.521','%Y-%m-%d %H:%M:%S.%f')
665
+ 1
666
+ >>> date_diff_in_seconds('2022-12-19T18:42:23.521Z', '2022-12-19T18:44:23.531Z','%Y-%m-%dT%H:%M:%S.%fz')
667
+ 120
668
+ >>> date_diff_in_seconds(Placeholder(), Placeholder())
669
+ 0
670
+ >>> date_diff_in_seconds(Placeholder(), '')
671
+ 0
672
+ >>> date_diff_in_seconds('2022-12-19T00:00:03.521Z', '2022-12-19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d')
673
+ 3
674
+ >>> date_diff_in_seconds('2022-12-19', '2022-12-19', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d')
675
+ 0
676
+ >>> date_diff_in_seconds('2022-12-19', '2022-12-19 00:01:00', backup_date_format='%Y-%m-%d')
677
+ 60
678
+ >>> date_diff_in_seconds('2022-25-19T00:00:03.521Z', '2022-12-19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
679
+ True
680
+ >>> date_diff_in_seconds('2022-12-19', '2022-25-19', '%Y-%m-%dT%H:%M:%Sz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
681
+ True
682
+ >>> date_diff_in_seconds('2022-12-19', '2022-25-19 00:01:00', backup_date_format='%Y-%m-%d', none_if_error=True) is None
683
+ True
684
+ >>> date_diff_in_seconds('2022-10-19T00:00:03.521Z', '2022/12/19', date_format='%Y-%m-%dT%H:%M:%S.%fz', backup_date_format='%Y-%m-%d', none_if_error=True) is None
685
+ True
686
+ >>> date_diff_in_seconds('2022-10-19 00:00:03', '2022-10-19 00:05:03', date_format='%Y-%m-%dT%H:%M:%S.%fz', none_if_error=True) is None
687
+ True
688
+ >>> date_diff_in_seconds('2022/12/19 00:00:03', '2022-10-19 00:05:03', none_if_error=True) is None
689
+ True
690
+ >>> date_diff_in_seconds('2022-25-19 00:00:03', '2022-10-19 00:05:03', none_if_error=True) is None
691
+ True
692
+ """
693
+ if isinstance(d0, Placeholder) or isinstance(d1, Placeholder):
694
+ if default:
695
+ return default
696
+ return 0
697
+ try:
698
+ return __date_diff(d0, d1, date_format, backup_date_format, "seconds", none_if_error)
699
+ except Exception:
700
+ raise SQLTemplateException(
701
+ "invalid date_format in function `date_diff_in_seconds`, defaults to YYYY-MM-DD hh:mm:ss. Or %Y-%m-%d %H:%M:%S [.ssssss]Z, e.g. ms: 2022-12-19T18:42:22.591Z s:2022-12-19T18:42:22Z",
702
+ documentation="/cli/advanced-templates.html#date_diff_in_seconds",
703
+ )
704
+
705
+
706
+ def __date_diff(
707
+ d0: Union[Placeholder, str],
708
+ d1: Union[Placeholder, str],
709
+ date_format: str = "%Y-%m-%d %H:%M:%S",
710
+ backup_date_format=None,
711
+ unit: str = "seconds",
712
+ none_if_error=False,
713
+ ):
714
+ try:
715
+ formatted_d0 = _parse_datetime(d0, date_format, backup_date_format)
716
+ formatted_d1 = _parse_datetime(d1, date_format, backup_date_format)
717
+ diff = abs(formatted_d1 - formatted_d0).total_seconds()
718
+
719
+ if unit == "days":
720
+ return int(diff / 86400)
721
+ elif unit == "hours":
722
+ return int(diff / 3600)
723
+ elif unit == "minutes":
724
+ return int(diff / 60)
725
+ else:
726
+ return int(diff)
727
+ except Exception:
728
+ if none_if_error:
729
+ return None
730
+
731
+ raise SQLTemplateException(
732
+ "invalid date_format in function date_diff_* function", documentation="/cli/advanced-templates.html"
733
+ )
734
+
735
+
736
+ def _parse_datetime(date_string, date_format, backup_date_format=None):
737
+ formats = [date_format]
738
+ if backup_date_format:
739
+ formats.append(backup_date_format)
740
+
741
+ for fmt in formats:
742
+ try:
743
+ return datetime.strptime(date_string, fmt)
744
+ except ValueError:
745
+ continue
746
+
747
+ raise SQLTemplateException(
748
+ "invalid date_format in function date_diff_* function", documentation="/cli/advanced-templates.html"
749
+ )
750
+
751
+
752
+ def json_type(x, default=None):
753
+ """
754
+ >>> json_type(None, '[]')
755
+ []
756
+ >>> json_type(None)
757
+ {}
758
+ >>> json_type('{"a": 1}')
759
+ {'a': 1}
760
+ >>> json_type('[{"a": 1}]')
761
+ [{'a': 1}]
762
+ >>> json_type({"a": 1})
763
+ {'a': 1}
764
+ >>> json_type([{"a": 1}])
765
+ [{'a': 1}]
766
+ """
767
+ if isinstance(x, Placeholder):
768
+ if default:
769
+ x = default
770
+ else:
771
+ x = "__no_value__"
772
+
773
+ try:
774
+ if x is None:
775
+ if isinstance(default, str):
776
+ x = default
777
+ else:
778
+ x = "{}"
779
+
780
+ value = "" # used for exception message
781
+ if isinstance(x, (str, bytes, bytearray)):
782
+ if len(x) > 16:
783
+ value = x[:16] + "..."
784
+ else:
785
+ value = x
786
+
787
+ parsed = loads(x)
788
+ x = parsed
789
+ except Exception as e:
790
+ msg = f"Error parsing JSON: '{value}' - {str(e)}"
791
+ raise SQLTemplateException(msg)
792
+
793
+ return x
794
+
795
+
796
+ function_list = {
797
+ "columns": columns,
798
+ "table": table,
799
+ "TABLE": table,
800
+ "error": error,
801
+ "custom_error": custom_error,
802
+ "sql_and": _and,
803
+ "defined": defined,
804
+ "column": column,
805
+ "enumerate_with_last": enumerate_with_last,
806
+ "split_to_array": split_to_array,
807
+ "day_diff": day_diff,
808
+ "date_diff_in_days": date_diff_in_days,
809
+ "date_diff_in_hours": date_diff_in_hours,
810
+ "date_diff_in_minutes": date_diff_in_minutes,
811
+ "date_diff_in_seconds": date_diff_in_seconds,
812
+ "sql_unescape": sql_unescape,
813
+ "JSON": json_type,
814
+ # 'enumerate': enumerate
815
+ }
816
+
817
+
818
+ def get_transform_types(placeholders=None):
819
+ if placeholders is None:
820
+ placeholders = {}
821
+ types = {
822
+ "bool": boolean,
823
+ "Boolean": boolean,
824
+ "DateTime": transform_type(
825
+ "DateTime",
826
+ str,
827
+ placeholders.get("DateTime", None),
828
+ required=None,
829
+ description=None,
830
+ enum=None,
831
+ example=None,
832
+ format=None,
833
+ ),
834
+ "DateTime64": transform_type(
835
+ "DateTime64",
836
+ str,
837
+ placeholders.get("DateTime64", None),
838
+ required=None,
839
+ description=None,
840
+ enum=None,
841
+ example=None,
842
+ format=None,
843
+ ),
844
+ "Date": transform_type(
845
+ "Date",
846
+ str,
847
+ placeholders.get("Date", None),
848
+ required=None,
849
+ description=None,
850
+ enum=None,
851
+ example=None,
852
+ format=None,
853
+ ),
854
+ "Float32": transform_type(
855
+ "Float32",
856
+ lambda x: Float(x, "Float32"),
857
+ placeholders.get("Float32", None),
858
+ required=None,
859
+ description=None,
860
+ enum=None,
861
+ example=None,
862
+ format=None,
863
+ ),
864
+ "Float64": transform_type(
865
+ "Float64",
866
+ lambda x: Float(x, "Float64"),
867
+ placeholders.get("Float64", None),
868
+ required=None,
869
+ description=None,
870
+ enum=None,
871
+ example=None,
872
+ format=None,
873
+ ),
874
+ "Int": transform_type(
875
+ "Int32",
876
+ int,
877
+ placeholders.get("Int", None),
878
+ required=None,
879
+ description=None,
880
+ enum=None,
881
+ example=None,
882
+ format=None,
883
+ ),
884
+ "Integer": transform_type(
885
+ "Int32",
886
+ int,
887
+ placeholders.get("Int32", None),
888
+ required=None,
889
+ description=None,
890
+ enum=None,
891
+ example=None,
892
+ format=None,
893
+ ),
894
+ "Int8": transform_type(
895
+ "Int8",
896
+ lambda x: Integer(x, "Int8"),
897
+ placeholders.get("Int8", None),
898
+ required=None,
899
+ description=None,
900
+ enum=None,
901
+ example=None,
902
+ format=None,
903
+ ),
904
+ "Int16": transform_type(
905
+ "Int16",
906
+ lambda x: Integer(x, "Int16"),
907
+ placeholders.get("Int16", None),
908
+ required=None,
909
+ description=None,
910
+ enum=None,
911
+ example=None,
912
+ format=None,
913
+ ),
914
+ "Int32": transform_type(
915
+ "Int32",
916
+ lambda x: Integer(x, "Int32"),
917
+ placeholders.get("Int32", None),
918
+ required=None,
919
+ description=None,
920
+ enum=None,
921
+ example=None,
922
+ format=None,
923
+ ),
924
+ "Int64": transform_type(
925
+ "Int64",
926
+ lambda x: Integer(x, "Int64"),
927
+ placeholders.get("Int64", None),
928
+ required=None,
929
+ description=None,
930
+ enum=None,
931
+ example=None,
932
+ format=None,
933
+ ),
934
+ "Int128": transform_type(
935
+ "Int128",
936
+ lambda x: Integer(x, "Int128"),
937
+ placeholders.get("Int128", None),
938
+ required=None,
939
+ description=None,
940
+ enum=None,
941
+ example=None,
942
+ format=None,
943
+ ),
944
+ "Int256": transform_type(
945
+ "Int256",
946
+ lambda x: Integer(x, "Int256"),
947
+ placeholders.get("Int256", None),
948
+ required=None,
949
+ description=None,
950
+ enum=None,
951
+ example=None,
952
+ format=None,
953
+ ),
954
+ "UInt8": transform_type(
955
+ "UInt8",
956
+ lambda x: Integer(x, "UInt8"),
957
+ placeholders.get("UInt8", None),
958
+ required=None,
959
+ description=None,
960
+ enum=None,
961
+ example=None,
962
+ format=None,
963
+ ),
964
+ "UInt16": transform_type(
965
+ "UInt16",
966
+ lambda x: Integer(x, "UInt16"),
967
+ placeholders.get("UInt16", None),
968
+ required=None,
969
+ description=None,
970
+ enum=None,
971
+ example=None,
972
+ format=None,
973
+ ),
974
+ "UInt32": transform_type(
975
+ "UInt32",
976
+ lambda x: Integer(x, "UInt32"),
977
+ placeholders.get("UInt32", None),
978
+ required=None,
979
+ description=None,
980
+ enum=None,
981
+ example=None,
982
+ format=None,
983
+ ),
984
+ "UInt64": transform_type(
985
+ "UInt64",
986
+ lambda x: Integer(x, "UInt64"),
987
+ placeholders.get("UInt64", None),
988
+ required=None,
989
+ description=None,
990
+ enum=None,
991
+ example=None,
992
+ format=None,
993
+ ),
994
+ "UInt128": transform_type(
995
+ "UInt128",
996
+ lambda x: Integer(x, "UInt128"),
997
+ placeholders.get("UInt128", None),
998
+ required=None,
999
+ description=None,
1000
+ enum=None,
1001
+ example=None,
1002
+ format=None,
1003
+ ),
1004
+ "UInt256": transform_type(
1005
+ "UInt256",
1006
+ lambda x: Integer(x, "UInt256"),
1007
+ placeholders.get("UInt256", None),
1008
+ required=None,
1009
+ description=None,
1010
+ enum=None,
1011
+ example=None,
1012
+ format=None,
1013
+ ),
1014
+ "Symbol": symbol,
1015
+ "Column": symbol,
1016
+ "String": transform_type(
1017
+ "String",
1018
+ str,
1019
+ placeholder="__no_value__",
1020
+ required=None,
1021
+ description=None,
1022
+ enum=None,
1023
+ example=None,
1024
+ format=None,
1025
+ ),
1026
+ "JSON": json_type,
1027
+ }
1028
+
1029
+ types["Array"] = array_type(
1030
+ {
1031
+ "bool": boolean,
1032
+ "Boolean": boolean,
1033
+ "DateTime": transform_type(
1034
+ "DateTime",
1035
+ str,
1036
+ placeholders.get("DateTime", None),
1037
+ required=None,
1038
+ description=None,
1039
+ enum=None,
1040
+ example=None,
1041
+ format=None,
1042
+ ),
1043
+ "DateTime64": transform_type(
1044
+ "DateTime64",
1045
+ str,
1046
+ placeholders.get("DateTime64", None),
1047
+ required=None,
1048
+ description=None,
1049
+ enum=None,
1050
+ example=None,
1051
+ format=None,
1052
+ ),
1053
+ "Date": transform_type(
1054
+ "Date",
1055
+ str,
1056
+ placeholders.get("Date", None),
1057
+ required=None,
1058
+ description=None,
1059
+ enum=None,
1060
+ example=None,
1061
+ format=None,
1062
+ ),
1063
+ "Float32": transform_type(
1064
+ "Float32",
1065
+ float,
1066
+ placeholders.get("Float32", None),
1067
+ required=None,
1068
+ description=None,
1069
+ enum=None,
1070
+ example=None,
1071
+ format=None,
1072
+ ),
1073
+ "Float64": transform_type(
1074
+ "Float64",
1075
+ float,
1076
+ placeholders.get("Float64", None),
1077
+ required=None,
1078
+ description=None,
1079
+ enum=None,
1080
+ example=None,
1081
+ format=None,
1082
+ ),
1083
+ "Int": transform_type(
1084
+ "Int32",
1085
+ int,
1086
+ placeholders.get("Int", None),
1087
+ required=None,
1088
+ description=None,
1089
+ enum=None,
1090
+ example=None,
1091
+ format=None,
1092
+ ),
1093
+ "Integer": transform_type(
1094
+ "Int32",
1095
+ int,
1096
+ placeholders.get("Int32", None),
1097
+ required=None,
1098
+ description=None,
1099
+ enum=None,
1100
+ example=None,
1101
+ format=None,
1102
+ ),
1103
+ "Int8": transform_type(
1104
+ "Int8",
1105
+ int,
1106
+ placeholders.get("Int8", None),
1107
+ required=None,
1108
+ description=None,
1109
+ enum=None,
1110
+ example=None,
1111
+ format=None,
1112
+ ),
1113
+ "Int16": transform_type(
1114
+ "Int16",
1115
+ int,
1116
+ placeholders.get("Int16", None),
1117
+ required=None,
1118
+ description=None,
1119
+ enum=None,
1120
+ example=None,
1121
+ format=None,
1122
+ ),
1123
+ "Int32": transform_type(
1124
+ "Int32",
1125
+ int,
1126
+ placeholders.get("Int32", None),
1127
+ required=None,
1128
+ description=None,
1129
+ enum=None,
1130
+ example=None,
1131
+ format=None,
1132
+ ),
1133
+ "Int64": transform_type(
1134
+ "Int64",
1135
+ int,
1136
+ placeholders.get("Int64", None),
1137
+ required=None,
1138
+ description=None,
1139
+ enum=None,
1140
+ example=None,
1141
+ format=None,
1142
+ ),
1143
+ "Int128": transform_type(
1144
+ "Int128",
1145
+ int,
1146
+ placeholders.get("Int128", None),
1147
+ required=None,
1148
+ description=None,
1149
+ enum=None,
1150
+ example=None,
1151
+ format=None,
1152
+ ),
1153
+ "Int256": transform_type(
1154
+ "Int256",
1155
+ int,
1156
+ placeholders.get("Int256", None),
1157
+ required=None,
1158
+ description=None,
1159
+ enum=None,
1160
+ example=None,
1161
+ format=None,
1162
+ ),
1163
+ "UInt8": transform_type(
1164
+ "UInt8",
1165
+ int,
1166
+ placeholders.get("UInt8", None),
1167
+ required=None,
1168
+ description=None,
1169
+ enum=None,
1170
+ example=None,
1171
+ format=None,
1172
+ ),
1173
+ "UInt16": transform_type(
1174
+ "UInt16",
1175
+ int,
1176
+ placeholders.get("UInt16", None),
1177
+ required=None,
1178
+ description=None,
1179
+ enum=None,
1180
+ example=None,
1181
+ format=None,
1182
+ ),
1183
+ "UInt32": transform_type(
1184
+ "UInt32",
1185
+ int,
1186
+ placeholders.get("UInt32", None),
1187
+ required=None,
1188
+ description=None,
1189
+ enum=None,
1190
+ example=None,
1191
+ format=None,
1192
+ ),
1193
+ "UInt64": transform_type(
1194
+ "UInt64",
1195
+ int,
1196
+ placeholders.get("UInt64", None),
1197
+ required=None,
1198
+ description=None,
1199
+ enum=None,
1200
+ example=None,
1201
+ format=None,
1202
+ ),
1203
+ "UInt128": transform_type(
1204
+ "UInt128",
1205
+ int,
1206
+ placeholders.get("UInt128", None),
1207
+ required=None,
1208
+ description=None,
1209
+ enum=None,
1210
+ example=None,
1211
+ format=None,
1212
+ ),
1213
+ "UInt256": transform_type(
1214
+ "UInt256",
1215
+ int,
1216
+ placeholders.get("UInt256", None),
1217
+ required=None,
1218
+ description=None,
1219
+ enum=None,
1220
+ example=None,
1221
+ format=None,
1222
+ ),
1223
+ "Symbol": symbol,
1224
+ "Column": symbol,
1225
+ "String": transform_type(
1226
+ "String",
1227
+ str,
1228
+ placeholder="__no_value__",
1229
+ required=None,
1230
+ description=None,
1231
+ enum=None,
1232
+ example=None,
1233
+ format=None,
1234
+ ),
1235
+ }
1236
+ )
1237
+
1238
+ types.update(function_list)
1239
+ return types
1240
+
1241
+
1242
+ type_fns = get_transform_types()
1243
+ type_fns_check = get_transform_types(
1244
+ {
1245
+ "DateTime64": "2019-01-01 00:00:00.000",
1246
+ "DateTime": "2019-01-01 00:00:00",
1247
+ "Date": "2019-01-01",
1248
+ "Float32": 0.0,
1249
+ "Float64": 0.0,
1250
+ "Int": 0,
1251
+ "Integer": 0,
1252
+ "UInt8": 0,
1253
+ "UInt16": 0,
1254
+ "UInt32": 0,
1255
+ "UInt64": 0,
1256
+ "UInt128": 0,
1257
+ "UInt256": 0,
1258
+ "Int8": 0,
1259
+ "Int16": 0,
1260
+ "Int32": 0,
1261
+ "Int64": 0,
1262
+ "Int128": 0,
1263
+ "Int256": 0,
1264
+ "Symbol": "symbol",
1265
+ "JSON": "{}",
1266
+ }
1267
+ )
1268
+
1269
+
1270
+ # from https://github.com/elouajib/sqlescapy/
1271
+ # MIT license
1272
+ def sqlescape_generator(translations):
1273
+ def sqlscape(str):
1274
+ return str.translate(str.maketrans(translations))
1275
+
1276
+ return sqlscape
1277
+
1278
+
1279
+ sqlescape = sqlescape_generator(
1280
+ {
1281
+ "\0": "\\0",
1282
+ "\r": "\\r",
1283
+ "\x08": "\\b",
1284
+ "\x09": "\\t",
1285
+ "\x1a": "\\z",
1286
+ "\n": "\\n",
1287
+ '"': "",
1288
+ "'": "\\'",
1289
+ "\\": "\\\\",
1290
+ "%": "\\%",
1291
+ "`": "\\`",
1292
+ }
1293
+ )
1294
+
1295
+ # sqlescape_for_string_expression is only meant to be used when escaping
1296
+ # string expressions (column=<string expression>) within SQL templates.
1297
+ # This version includes a specific translation on top of the ones in the
1298
+ # sqlescape above to escape double quotes (" will be translated into \")
1299
+ # instead of removing them.
1300
+ # It'll allow users to use parameter values with strings including double quotes.
1301
+ sqlescape_for_string_expression = sqlescape_generator(
1302
+ {
1303
+ "\0": "\\0",
1304
+ "\r": "\\r",
1305
+ "\x08": "\\b",
1306
+ "\x09": "\\t",
1307
+ "\x1a": "\\z",
1308
+ "\n": "\\n",
1309
+ '"': '\\"',
1310
+ "'": "\\'",
1311
+ "\\": "\\\\",
1312
+ "%": "\\%",
1313
+ "`": "\\`",
1314
+ }
1315
+ )
1316
+
1317
+
1318
+ def escape_single_quote_str(s):
1319
+ return "'" + s.replace("'", "''") + "'"
1320
+
1321
+
1322
+ def expression_wrapper(x, name, escape_arrays: bool = False):
1323
+ if type(x) in (unicode_type, bytes, str):
1324
+ return "'" + sqlescape_for_string_expression(x) + "'"
1325
+ elif isinstance(x, Placeholder):
1326
+ return "'__no_value__'"
1327
+ elif isinstance(x, Comment):
1328
+ return "-- {x} \n"
1329
+ if x is None:
1330
+ truncated_name = name[:20] + "..." if len(name) > 20 else name
1331
+ raise SQLTemplateException(
1332
+ f'expression "{truncated_name}" evaluated to null', documentation="/cli/advanced-templates.html"
1333
+ )
1334
+ if isinstance(x, list) and escape_arrays:
1335
+ logging.warning(f"expression_wrapper -> list :{x}:")
1336
+
1337
+ try:
1338
+ result = (
1339
+ f"[{','.join(escape_single_quote_str(item) if isinstance(item, str) else str(item) for item in x)}]"
1340
+ )
1341
+ return result
1342
+ except Exception as e:
1343
+ logging.error(f"Error escaping array: {e}")
1344
+ return x
1345
+
1346
+
1347
+ _namespace = {
1348
+ "column": column,
1349
+ "symbol": symbol,
1350
+ "error": error,
1351
+ "custom_error": custom_error,
1352
+ "_tt_utf8": escape.utf8, # for internal use
1353
+ "_tt_string_types": (unicode_type, bytes),
1354
+ "xhtml_escape": lambda x: x,
1355
+ "expression_wrapper": expression_wrapper,
1356
+ # disable __builtins__ and some other functions
1357
+ # they raise a pretty non understandable error but if someone
1358
+ # is using them they know what they are trying to do
1359
+ # read https://anee.me/escaping-python-jails-849c65cf306e on how to escape from python jails
1360
+ "__buildins__": {},
1361
+ "__import__": {},
1362
+ "__debug__": {},
1363
+ "__doc__": {},
1364
+ "__name__": {},
1365
+ "__package__": {},
1366
+ "open": None,
1367
+ "close": None,
1368
+ "print": None,
1369
+ "input": None,
1370
+ }
1371
+
1372
+
1373
+ reserved_vars = set(["_tt_tmp", "_tt_append", "isinstance", "str", "error", "custom_error", *list(vars(builtins))])
1374
+ for p in DEFAULT_PARAM_NAMES: # we handle these in an specific manner
1375
+ reserved_vars.discard(p) # `format` is part of builtins
1376
+ error_vars = ["error", "custom_error"]
1377
+
1378
+
1379
+ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
1380
+ """Generate this template with the given arguments."""
1381
+ namespace = {}
1382
+ template_execution_results = TemplateExecutionResults()
1383
+ for key in kwargs.get("tb_secrets", []):
1384
+ if is_secret_template_key(key):
1385
+ template_execution_results.add_template_param(key)
1386
+
1387
+ if TB_SECRET_IN_TEST_MODE in kwargs:
1388
+ template_execution_results[TB_SECRET_IN_TEST_MODE] = None
1389
+
1390
+ def set_tb_secret(x):
1391
+ try:
1392
+ key = secret_template_key(x)
1393
+ if key in template_execution_results.template_params:
1394
+ template_execution_results.add_ch_param(x)
1395
+ return Symbol("{" + sqlescape(x) + ": String}")
1396
+ else:
1397
+ is_test_mode = TB_SECRET_IN_TEST_MODE in template_execution_results
1398
+ if is_test_mode:
1399
+ return Symbol("{" + sqlescape(x) + ": String}")
1400
+ else:
1401
+ raise SQLTemplateException(
1402
+ f"Cannot access secret '{x}'. Check the secret exists in the Workspace and the token has the required scope."
1403
+ )
1404
+ except Exception:
1405
+ raise SQLTemplateException(
1406
+ f"Cannot access secret '{x}'. Check the secret exists in the Workspace and the token has the required scope."
1407
+ )
1408
+
1409
+ def set_max_threads(x):
1410
+ try:
1411
+ template_execution_results["max_threads"] = int(x)
1412
+ return Expression(f"-- max_threads {x}\n")
1413
+ except Exception:
1414
+ return Expression(f"-- max_threads: wrong argument {x}\n")
1415
+
1416
+ def set_backend_hint(hint):
1417
+ template_execution_results["backend_hint"] = str(hint)
1418
+ if hint is None or hint is False:
1419
+ template_execution_results["backend_hint"] = None
1420
+ return Expression(f"-- backend_hint {hint}\n")
1421
+
1422
+ def set_cache_ttl(ttl_expression):
1423
+ valid_ttl_expressions = ("5s", "1m", "5m", "30m", "1h")
1424
+ if ttl_expression not in valid_ttl_expressions:
1425
+ raise SQLTemplateException(f"Invalid TTL cache expression, valid expressions are {valid_ttl_expressions}")
1426
+ template_execution_results["cache_ttl"] = ttl_expression
1427
+ return Expression(f"-- cache_ttl {ttl_expression}\n")
1428
+
1429
+ def set_activate(feature):
1430
+ valid_features = ("analyzer", "parallel_replicas")
1431
+ if feature not in valid_features:
1432
+ raise SQLTemplateException(f"'{feature}' is not a valid 'activate' argument")
1433
+ template_execution_results["activate"] = feature
1434
+ return Expression(f"-- activate {feature}\n")
1435
+
1436
+ namespace.update(_namespace)
1437
+ namespace.update(kwargs)
1438
+ namespace.update(
1439
+ {
1440
+ # __name__ and __loader__ allow the traceback mechanism to find
1441
+ # the generated source code.
1442
+ "__name__": self.name.replace(".", "_"),
1443
+ "__loader__": ObjectDict(get_source=lambda name: self.code),
1444
+ "max_threads": set_max_threads,
1445
+ "tb_secret": set_tb_secret,
1446
+ "tb_var": set_tb_secret,
1447
+ "backend_hint": set_backend_hint,
1448
+ "cache_ttl": set_cache_ttl,
1449
+ "activate": set_activate,
1450
+ }
1451
+ )
1452
+
1453
+ exec_in(self.compiled, namespace)
1454
+ execute = namespace["_tt_execute"]
1455
+ # Clear the traceback module's cache of source data now that
1456
+ # we've generated a new template (mainly for this module's
1457
+ # unittests, where different tests reuse the same name).
1458
+ linecache.clearcache()
1459
+
1460
+ try:
1461
+ return execute().decode(), template_execution_results
1462
+ except SQLTemplateCustomError as e:
1463
+ raise e
1464
+ except UnboundLocalError as e:
1465
+ try:
1466
+ message = getattr(e, "msg", str(e)).split("(<string>.generated.py")[0].strip()
1467
+ text = getattr(e, "text", message)
1468
+ line = None
1469
+ try:
1470
+ line = re.findall(r"\<string\>:(\d*)", text)
1471
+ message = re.sub(r"\<string\>:(\d*)", "", message)
1472
+ except TypeError:
1473
+ pass
1474
+
1475
+ if line:
1476
+ raise SQLTemplateException(f"{message.strip()} line {line[0]}")
1477
+ else:
1478
+ raise SQLTemplateException(f"{message.strip()}")
1479
+ except Exception as e:
1480
+ if isinstance(e, SQLTemplateException):
1481
+ raise e
1482
+ else:
1483
+ logging.exception(f"Error on unbound local error: {e}")
1484
+ raise ValueError(str(e))
1485
+ except TypeError as e:
1486
+ error = str(e)
1487
+ if "not supported between instances of 'Placeholder' and " in str(e):
1488
+ error = f"{str(e)}. If you are using a dynamic parameter, you need to wrap it around a valid Data Type (e.g. Int8(placeholder))"
1489
+ raise ValueError(error)
1490
+ except Exception as e:
1491
+ if "x" in namespace and namespace["x"] and hasattr(namespace["x"], "line") and namespace["x"].line:
1492
+ line = namespace["x"].line
1493
+ raise ValueError(f"{e}, line {line}")
1494
+ raise e
1495
+
1496
+
1497
+ class CodeWriter:
1498
+ def __init__(self, file, template):
1499
+ self.file = file
1500
+ self.current_template = template
1501
+ self.apply_counter = 0
1502
+ self._indent = 0
1503
+
1504
+ def indent_size(self):
1505
+ return self._indent
1506
+
1507
+ def indent(self):
1508
+ class Indenter:
1509
+ def __enter__(_):
1510
+ self._indent += 1
1511
+ return self
1512
+
1513
+ def __exit__(_, *args):
1514
+ assert self._indent > 0
1515
+ self._indent -= 1
1516
+
1517
+ return Indenter()
1518
+
1519
+ def write_line(self, line, line_number, indent=None):
1520
+ if indent is None:
1521
+ indent = self._indent
1522
+ line_comment = " # %s:%d" % ("<generated>", line_number)
1523
+ print(" " * indent + line + line_comment, file=self.file)
1524
+
1525
+
1526
+ def get_var_names(t):
1527
+ try:
1528
+
1529
+ def _n(chunks, v):
1530
+ for x in chunks:
1531
+ line_number = x.line
1532
+ if type(x).__name__ == "_ChunkList":
1533
+ _n(x.chunks, v)
1534
+ elif type(x).__name__ == "_Expression":
1535
+ c = compile(x.expression, "<string>", "exec", dont_inherit=True)
1536
+ variable_names = [x for x in c.co_names if x not in _namespace and x not in reserved_vars]
1537
+ v += list(map(lambda variable: {"line": line_number, "name": variable}, variable_names))
1538
+ elif type(x).__name__ == "_ControlBlock":
1539
+ from io import StringIO
1540
+
1541
+ buffer = StringIO()
1542
+ writer = CodeWriter(buffer, t)
1543
+ x.generate(writer)
1544
+ c = compile(buffer.getvalue(), "<string>", "exec", dont_inherit=True)
1545
+ variable_names = [x for x in c.co_names if x not in _namespace and x not in reserved_vars]
1546
+ v += list(map(lambda variable: {"line": line_number, "name": variable}, variable_names))
1547
+ _n(x.body.chunks, v)
1548
+
1549
+ var = []
1550
+ _n(t.file.body.chunks, var)
1551
+ return var
1552
+ except SecurityException as e:
1553
+ raise SQLTemplateException(e)
1554
+
1555
+
1556
+ def get_var_data(content, node_id=None):
1557
+ def node_to_value(x):
1558
+ if type(x) in (ast.Bytes, ast.Str):
1559
+ return x.s
1560
+ elif type(x) == ast.Num: # noqa: E721
1561
+ return x.n
1562
+ elif type(x) == ast.NameConstant: # noqa: E721
1563
+ return x.value
1564
+ elif type(x) == ast.Name: # noqa: E721
1565
+ return x.id
1566
+ elif type(x) == ast.List: # noqa: E721
1567
+ # List can hold different types
1568
+ return _get_list_var_data(x)
1569
+ elif type(x) == ast.BinOp: # noqa: E721
1570
+ # in this case there could be several variables
1571
+ # if that's the case the left one is the main
1572
+ r = node_to_value(x.left)
1573
+ if not r:
1574
+ r = node_to_value(x.right)
1575
+ return r
1576
+ elif type(x) == ast.Constant: # noqa: E721
1577
+ return x.value
1578
+ elif type(x) == ast.UnaryOp and type(x.operand) == ast.Constant: # noqa: E721
1579
+ if type(x.op) == ast.USub: # noqa: E721
1580
+ return x.operand.value * -1
1581
+ else:
1582
+ return x.operand.value
1583
+ else:
1584
+ try:
1585
+ return x.id
1586
+ except Exception:
1587
+ # don't let this ruin the parsing
1588
+ pass
1589
+ return None
1590
+
1591
+ def _get_list_var_data(x):
1592
+ if not x.elts:
1593
+ return []
1594
+
1595
+ first_elem = x.elts[0]
1596
+ if type(first_elem) in (ast.Bytes, ast.Str):
1597
+ return [elem.s for elem in x.elts]
1598
+ elif type(first_elem) == ast.Num: # noqa: E721
1599
+ return [elem.n for elem in x.elts]
1600
+ elif type(first_elem) == ast.NameConstant or type(first_elem) == ast.Constant: # noqa: E721
1601
+ return [elem.value for elem in x.elts]
1602
+ elif type(first_elem) == ast.Name: # noqa: E721
1603
+ return [elem.id for elem in x.elts]
1604
+
1605
+ return []
1606
+
1607
+ def _w(parsed):
1608
+ vars = {}
1609
+ for node in ast.walk(parsed):
1610
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
1611
+ try:
1612
+ func = node.func.id
1613
+ # parse function args
1614
+ args = []
1615
+ for x in node.args:
1616
+ if type(x) == ast.Call: # noqa: E721
1617
+ vars.update(_w(x))
1618
+ else:
1619
+ args.append(node_to_value(x))
1620
+
1621
+ kwargs = {}
1622
+ for x in node.keywords:
1623
+ value = node_to_value(x.value)
1624
+ kwargs[x.arg] = value
1625
+ if x.arg == "default":
1626
+ kwargs["default"] = check_default_value(value)
1627
+ if func in VALID_CUSTOM_FUNCTION_NAMES:
1628
+ # Type definition here is set to 'String' because it comes from a
1629
+ # `defined(variable)` expression that does not contain any type hint.
1630
+ # It will be overriden in later definitions or left as is otherwise.
1631
+ # args[0] check is used to avoid adding unnamed parameters found in
1632
+ # templates like: `split_to_array('')`
1633
+ if len(args) and isinstance(args[0], list):
1634
+ raise ValueError(f'"{args[0]}" can not be used as a variable name')
1635
+ if len(args) > 0 and args[0] not in vars and args[0]:
1636
+ vars[args[0]] = {
1637
+ "type": "String",
1638
+ "default": None,
1639
+ "used_in": "function_call",
1640
+ }
1641
+ elif func == "Array":
1642
+ if "default" not in kwargs:
1643
+ default = kwargs.get("default", args[2] if len(args) > 2 and args[2] else None)
1644
+ kwargs["default"] = check_default_value(default)
1645
+ if len(args):
1646
+ if isinstance(args[0], list):
1647
+ raise ValueError(f'"{args[0]}" can not be used as a variable name')
1648
+ vars[args[0]] = {
1649
+ "type": f"Array({args[1]})" if len(args) > 1 else "Array(String)",
1650
+ **kwargs,
1651
+ }
1652
+ elif func in parameter_types:
1653
+ # avoid variable names to be None
1654
+ if len(args) and args[0] is not None:
1655
+ # if this is a cast use the function name to get the type
1656
+ if "default" not in kwargs:
1657
+ default = kwargs.get("default", args[1] if len(args) > 1 else None)
1658
+ kwargs["default"] = check_default_value(default)
1659
+ try:
1660
+ if isinstance(args[0], list):
1661
+ raise ValueError(f'"{args[0]}" can not be used as a variable name')
1662
+ vars[args[0]] = {"type": func, **kwargs}
1663
+ if "default" in kwargs:
1664
+ kwargs["default"] = check_default_value(kwargs["default"])
1665
+ except TypeError as e:
1666
+ logging.exception(f"pipe parsing problem {content} (node '{node_id}'): {e}")
1667
+ except ValueError:
1668
+ raise
1669
+ except Exception as e:
1670
+ # if we find a problem parsing, let the parsing continue
1671
+ logging.exception(f"pipe parsing problem {content} (node: '{node_id}'): {e}")
1672
+ elif isinstance(node, ast.Name):
1673
+ # when parent node is a call it means it's managed by the Call workflow (see above)
1674
+ is_cast = (
1675
+ isinstance(node.parent, ast.Call)
1676
+ and isinstance(node.parent.func, ast.Name)
1677
+ and node.parent.func.id in parameter_types
1678
+ )
1679
+ is_reserved_name = node.id in reserved_vars or node.id in function_list or node.id in _namespace
1680
+ if (not isinstance(node.parent, ast.Call) and not is_cast) and not is_reserved_name:
1681
+ vars[node.id] = {"type": "String", "default": None}
1682
+
1683
+ return vars
1684
+
1685
+ def check_default_value(value):
1686
+ if isinstance(value, int):
1687
+ MAX_SAFE_INTEGER = 9007199254740991
1688
+ if value > MAX_SAFE_INTEGER:
1689
+ return str(value)
1690
+ return value
1691
+
1692
+ def parse_content(content, retries=0):
1693
+ try:
1694
+ parsed = ast.parse(content)
1695
+ return parsed
1696
+ except Exception as e:
1697
+ if "AST constructor recursion depth mismatch" not in str(e):
1698
+ raise e
1699
+ retries += 1
1700
+ if retries > 3:
1701
+ raise e
1702
+ return parse_content(content, retries)
1703
+
1704
+ parsed = parse_content(content)
1705
+
1706
+ # calculate parents for each node for later checks
1707
+ for node in ast.walk(parsed):
1708
+ for child in ast.iter_child_nodes(node):
1709
+ child.parent = node
1710
+ vars = _w(parsed)
1711
+
1712
+ return [dict(name=k, **v) for k, v in vars.items()]
1713
+
1714
+
1715
+ def get_var_names_and_types(t, node_id=None):
1716
+ """
1717
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Float32(with_value, 0.0)}}"))
1718
+ [{'name': 'with_value', 'type': 'Float32', 'default': 0.0}]
1719
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Float32(with_value, -0.0)}}"))
1720
+ [{'name': 'with_value', 'type': 'Float32', 'default': -0.0}]
1721
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Int32(with_value, 0)}}"))
1722
+ [{'name': 'with_value', 'type': 'Int32', 'default': 0}]
1723
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Int32(with_value, -0)}}"))
1724
+ [{'name': 'with_value', 'type': 'Int32', 'default': 0}]
1725
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Float32(with_value, -0.1)}}"))
1726
+ [{'name': 'with_value', 'type': 'Float32', 'default': -0.1}]
1727
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Float32(with_value, 0.1)}}"))
1728
+ [{'name': 'with_value', 'type': 'Float32', 'default': 0.1}]
1729
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{String(d, 'test_1')}} AND value = {{Int8(v, 3)}}"))
1730
+ [{'name': 'd', 'type': 'String', 'default': 'test_1'}, {'name': 'v', 'type': 'Int8', 'default': 3}]
1731
+ >>> get_var_names_and_types(Template("select * from test {% if defined({{UInt64(number_variable)}}) %} where 1 {% end %}"))
1732
+ [{'name': 'number_variable', 'type': 'UInt64', 'default': None}]
1733
+ >>> get_var_names_and_types(Template("select * from test {% if defined(testing) and defined(testing2) %} where 1 {%end %}"))
1734
+ [{'name': 'testing', 'type': 'String', 'default': None, 'used_in': 'function_call'}, {'name': 'testing2', 'type': 'String', 'default': None, 'used_in': 'function_call'}]
1735
+ >>> get_var_names_and_types(Template("select * from test {% if defined({{UInt64(number_variable)}}) %} where 1 {% end %}"))
1736
+ [{'name': 'number_variable', 'type': 'UInt64', 'default': None}]
1737
+ >>> get_var_names_and_types(Template("select {{Array(cod_stock_source_type,'Int16', defined=False)}}"))
1738
+ [{'name': 'cod_stock_source_type', 'type': 'Array(Int16)', 'defined': False, 'default': None}]
1739
+ >>> get_var_names_and_types(Template("select {{Array(cod_stock_source_type, defined=False)}}"))
1740
+ [{'name': 'cod_stock_source_type', 'type': 'Array(String)', 'defined': False, 'default': None}]
1741
+ >>> get_var_names_and_types(Template("select {{cod_stock_source_type}}"))
1742
+ [{'name': 'cod_stock_source_type', 'type': 'String', 'default': None}]
1743
+ >>> get_var_names_and_types(Template("SELECT {{len([1] * 10**7)}}"))
1744
+ Traceback (most recent call last):
1745
+ ...
1746
+ tinybird.tornado_template.SecurityException: Invalid BinOp: Pow()
1747
+ >>> get_var_names_and_types(Template("select {{String(cod_stock_source_type, 'test')}}"))
1748
+ [{'name': 'cod_stock_source_type', 'type': 'String', 'default': 'test'}]
1749
+ >>> get_var_names_and_types(Template("select {{split_to_array(test)}}"))
1750
+ [{'name': 'test', 'type': 'String', 'default': None, 'used_in': 'function_call'}]
1751
+ >>> get_var_names_and_types(Template("select {{String(test + 'abcd', 'default_value')}}"))
1752
+ [{'name': 'test', 'type': 'String', 'default': None}]
1753
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{String(d, 'test_1', description='test', required=True)}} AND value = {{Int8(v, 3, format='number', example='1')}}"))
1754
+ [{'name': 'd', 'type': 'String', 'description': 'test', 'required': True, 'default': 'test_1'}, {'name': 'v', 'type': 'Int8', 'format': 'number', 'example': '1', 'default': 3}]
1755
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{String(d, default='test_1', description='test')}}"))
1756
+ [{'name': 'd', 'type': 'String', 'default': 'test_1', 'description': 'test'}]
1757
+ >>> get_var_names_and_types(Template("select {{Array(cod_stock_source_type, 'Int16', default='1', defined=False)}}"))
1758
+ [{'name': 'cod_stock_source_type', 'type': 'Array(Int16)', 'default': '1', 'defined': False}]
1759
+ >>> get_var_names_and_types(Template('select {{symbol(split_to_array(attr, "amount_net")[0] + "_intermediate" )}}'))
1760
+ [{'name': 'attr', 'type': 'String', 'default': None, 'used_in': 'function_call'}]
1761
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Float32(with_value, 0.1)}} AND description = {{Float32(zero, 0)}} AND value = {{Float32(no_default)}}"))
1762
+ [{'name': 'with_value', 'type': 'Float32', 'default': 0.1}, {'name': 'zero', 'type': 'Float32', 'default': 0}, {'name': 'no_default', 'type': 'Float32', 'default': None}]
1763
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Float32(with_value, -0.1)}} AND description = {{Float32(zero, 0)}} AND value = {{Float32(no_default)}}"))
1764
+ [{'name': 'with_value', 'type': 'Float32', 'default': -0.1}, {'name': 'zero', 'type': 'Float32', 'default': 0}, {'name': 'no_default', 'type': 'Float32', 'default': None}]
1765
+ >>> get_var_names_and_types(Template('''SELECT * FROM abcd WHERE hotel_id <> 0 {% if defined(date_from) %} AND script_created_at > {{DateTime(date_from, '2020-09-09 10:10:10', description="This is a description", required=True)(date_from, '2020-09-09', description="Filter script alert creation date", required=False)}} {% end %}'''))
1766
+ [{'name': 'date_from', 'type': 'DateTime', 'description': 'This is a description', 'required': True, 'default': '2020-09-09 10:10:10'}, {'name': 'date_from', 'type': 'DateTime', 'description': 'This is a description', 'required': True, 'default': '2020-09-09 10:10:10'}]
1767
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE symbol = {{Int128(symbol_id, 11111, description='Symbol Id', required=True)}} AND user = {{Int256(user_id, 3555, description='User Id')}}"))
1768
+ [{'name': 'symbol_id', 'type': 'Int128', 'description': 'Symbol Id', 'required': True, 'default': 11111}, {'name': 'user_id', 'type': 'Int256', 'description': 'User Id', 'default': 3555}]
1769
+ >>> get_var_names_and_types(Template("SELECT now() > {{DateTime64(timestamp, '2020-09-09 10:10:10.000')}}"))
1770
+ [{'name': 'timestamp', 'type': 'DateTime64', 'default': '2020-09-09 10:10:10.000'}]
1771
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE symbol = {{Int64(symbol_id, 9223372036854775807)}}"))
1772
+ [{'name': 'symbol_id', 'type': 'Int64', 'default': '9223372036854775807'}]
1773
+ """
1774
+ try:
1775
+
1776
+ def _n(chunks, v):
1777
+ for x in chunks:
1778
+ if type(x).__name__ == "_ChunkList":
1779
+ _n(x.chunks, v)
1780
+ elif type(x).__name__ == "_Expression":
1781
+ var_data = get_var_data(x.expression, node_id=node_id)
1782
+ if var_data:
1783
+ v += var_data
1784
+ elif type(x).__name__ == "_ControlBlock":
1785
+ buffer = StringIO()
1786
+ writer = CodeWriter(buffer, t)
1787
+ x.generate(writer)
1788
+ var_data = get_var_data(buffer.getvalue(), node_id=node_id)
1789
+ if var_data:
1790
+ v += var_data
1791
+ _n(x.body.chunks, v)
1792
+
1793
+ var = []
1794
+ _n(t.file.body.chunks, var)
1795
+ return var
1796
+ except SecurityException as e:
1797
+ raise SQLTemplateException(e)
1798
+
1799
+
1800
+ @lru_cache(maxsize=256)
1801
+ def get_var_names_and_types_cached(t: Template):
1802
+ return get_var_names_and_types(t)
1803
+
1804
+
1805
+ def wrap_vars(t, escape_arrays: bool = False):
1806
+ def _n(chunks, v):
1807
+ for x in chunks:
1808
+ if type(x).__name__ == "_ChunkList":
1809
+ _n(x.chunks, v)
1810
+ elif type(x).__name__ == "_Expression":
1811
+ x.expression = (
1812
+ "expression_wrapper("
1813
+ + x.expression
1814
+ + ',"""'
1815
+ + x.expression.replace('"', '\\"')
1816
+ + '""",escape_arrays='
1817
+ + str(escape_arrays)
1818
+ + ")"
1819
+ )
1820
+ elif type(x).__name__ == "_ControlBlock":
1821
+ _n(x.body.chunks, v)
1822
+
1823
+ var: List[Any] = []
1824
+ _n(t.file.body.chunks, var)
1825
+ t.code = t._generate_python(t.loader)
1826
+ try:
1827
+ t.compiled = compile(
1828
+ escape.to_unicode(t.code), "%s.generated.py" % t.name.replace(".", "_"), "exec", dont_inherit=True
1829
+ )
1830
+ except Exception:
1831
+ # formatted_code = _format_code(t.code).rstrip()
1832
+ # app_log.error("%s code:\n%s", t.name, formatted_code)
1833
+ raise
1834
+
1835
+ return var
1836
+
1837
+
1838
+ def get_used_tables_in_template(sql):
1839
+ """
1840
+ >>> get_used_tables_in_template("select * from {{table('test')}}")
1841
+ ['test']
1842
+ >>> get_used_tables_in_template("select * from {%if x %}{{table('test')}}{%else%}{{table('test2')}}{%end%}")
1843
+ ['test', 'test2']
1844
+ >>> get_used_tables_in_template("select * from {{table('my.test')}}")
1845
+ ['my.test']
1846
+ >>> get_used_tables_in_template("select * from {{table('my.test')}}, another_table")
1847
+ ['my.test']
1848
+ >>> get_used_tables_in_template("select * from another_table")
1849
+ []
1850
+ >>> get_used_tables_in_template("select * from {{table('my.test')}}, {{table('another.one')}}")
1851
+ ['my.test', 'another.one']
1852
+ """
1853
+ try:
1854
+ t = Template(sql)
1855
+
1856
+ def _n(chunks, tables):
1857
+ for x in chunks:
1858
+ if type(x).__name__ == "_Expression":
1859
+ c = compile(x.expression, "<string>", "exec", dont_inherit=True)
1860
+ v = [x.lower() for x in c.co_names if x not in _namespace and x not in reserved_vars]
1861
+ if "table" in v:
1862
+
1863
+ def _t(*args, **kwargs):
1864
+ return str(args[0])
1865
+
1866
+ n = {"table": _t, "TABLE": _t}
1867
+ e = "_tt_tmp = %s" % x.expression
1868
+ exec_in(e, n)
1869
+ tables += [n["_tt_tmp"]]
1870
+ elif type(x).__name__ == "_ControlBlock":
1871
+ _n(x.body.chunks, tables)
1872
+
1873
+ tables = []
1874
+ _n(t.file.body.chunks, tables)
1875
+ return tables
1876
+ except SecurityException as e:
1877
+ raise SQLTemplateException(e)
1878
+
1879
+
1880
+ @lru_cache(maxsize=2**13)
1881
+ def get_template_and_variables(sql: str, name: Optional[str], escape_arrays: bool = False):
1882
+ """
1883
+ Generates a Template and does all the processes necessary. As the object and template variables are cached
1884
+ it is important to NOT MODIFY THESE OBJECTS.
1885
+ Neither render_sql_template() or generate() modify them, so neither should you
1886
+ """
1887
+ variable_warnings = []
1888
+
1889
+ try:
1890
+ t = Template(sql, name)
1891
+ template_variables = get_var_names(t)
1892
+
1893
+ for variable in template_variables:
1894
+ if variable["name"] in DEFAULT_PARAM_NAMES:
1895
+ name = variable["name"]
1896
+ line = variable["line"]
1897
+ raise ValueError(f'"{name}" can not be used as a variable name, line {line}')
1898
+ if variable["name"] in RESERVED_PARAM_NAMES:
1899
+ variable_warnings.append(variable["name"])
1900
+
1901
+ wrap_vars(t, escape_arrays=escape_arrays)
1902
+
1903
+ return t, template_variables, variable_warnings
1904
+ except SecurityException as e:
1905
+ raise SQLTemplateException(e)
1906
+
1907
+
1908
+ def preprocess_variables(variables: dict, template_variables_with_types: List[dict]):
1909
+ """
1910
+ >>> preprocess_variables({"test": '24'}, [{"name": "test", "type": "Int32", "default": None}])
1911
+ {}
1912
+ >>> preprocess_variables({"test": "1,2"}, [{"name": "test", "type": "Array(String)", "default": None}])
1913
+ {'test': ['1', '2']}
1914
+ >>> preprocess_variables({"test": ['1', '2']}, [{"name": "test", "type": "Array(String)", "default": None}])
1915
+ {'test': ['1', '2']}
1916
+ >>> preprocess_variables({"test": [1,2]}, [{"name": "test", "type": "Array(String)", "default": None}])
1917
+ {'test': ['1', '2']}
1918
+ >>> preprocess_variables({"test": "1,2,3"}, [{"name": "test", "type": "Array(Int32)", "default": None}])
1919
+ {'test': [1, 2, 3]}
1920
+ >>> preprocess_variables({"test": "1,2,msg"}, [{"name": "test", "type": "Array(Int32)", "default": None}])
1921
+ {}
1922
+ """
1923
+ processed_variables = {}
1924
+ for variable, value in variables.items():
1925
+ try:
1926
+ template_vars = [t_var for t_var in template_variables_with_types if t_var["name"] == variable] or None
1927
+ if template_vars is None or value is None:
1928
+ continue
1929
+
1930
+ t_var = template_vars[0]
1931
+ var_type = t_var.get("type")
1932
+ if var_type is None:
1933
+ continue
1934
+
1935
+ # For now, we only preprocess Array types
1936
+ match = re.match(r"Array\((\w+)\)", var_type)
1937
+ if match is None:
1938
+ continue
1939
+
1940
+ array_type = match.group(1)
1941
+ array_fn = type_fns.get("Array")
1942
+ parsed_exp = array_fn(value, array_type)
1943
+ processed_variables[variable] = ast.literal_eval(parsed_exp)
1944
+ except Exception:
1945
+ continue
1946
+
1947
+ return processed_variables
1948
+
1949
+
1950
+ def format_SQLTemplateException_message(e: SQLTemplateException, vars_and_types: Optional[dict] = None):
1951
+ def join_with_different_last_separator(items, separator=", ", last_separator=" and "):
1952
+ if not items:
1953
+ return ""
1954
+ if len(items) == 1:
1955
+ return items[0]
1956
+
1957
+ result = separator.join(items[:-1])
1958
+ return result + last_separator + items[-1]
1959
+
1960
+ message = str(e)
1961
+ var_names = ""
1962
+
1963
+ try:
1964
+ if REQUIRED_PARAM_NOT_DEFINED in message and vars_and_types:
1965
+ vars_with_default_none = []
1966
+ for item in vars_and_types:
1967
+ if (
1968
+ item.get("default") is None
1969
+ and item.get("used_in", None) is None
1970
+ and item.get("name") not in vars_with_default_none
1971
+ ):
1972
+ vars_with_default_none.append(item["name"])
1973
+
1974
+ var_names = join_with_different_last_separator(vars_with_default_none)
1975
+ except Exception:
1976
+ pass
1977
+
1978
+ if var_names:
1979
+ raise SQLTemplateException(
1980
+ f"{REQUIRED_PARAM_NOT_DEFINED}. Check the parameters {join_with_different_last_separator(vars_with_default_none)}. Please provide a value or set a default value in the pipe code.",
1981
+ e.documentation,
1982
+ )
1983
+ else:
1984
+ raise e
1985
+
1986
+
1987
+ def render_sql_template(
1988
+ sql: str,
1989
+ variables: Optional[dict] = None,
1990
+ secrets: Optional[List[str]] = None,
1991
+ test_mode: bool = False,
1992
+ name: Optional[str] = None,
1993
+ local_variables: Optional[dict] = None,
1994
+ ) -> Tuple[str, TemplateExecutionResults, list]:
1995
+ """
1996
+ >>> render_sql_template("select * from table where f = {{Float32(foo)}}", { 'foo': -1 })
1997
+ ("select * from table where f = toFloat32('-1.0')", {}, [])
1998
+ >>> render_sql_template("{% if defined(open) %}ERROR{% else %}YEAH!{% end %}")
1999
+ ('YEAH!', {}, [])
2000
+ >>> render_sql_template("{% if defined(close) %}ERROR{% else %}YEAH!{% end %}")
2001
+ ('YEAH!', {}, [])
2002
+ >>> render_sql_template("{% if defined(input) %}ERROR{% else %}YEAH!{% end %}")
2003
+ ('YEAH!', {}, [])
2004
+ >>> render_sql_template("{% if defined(print) %}ERROR{% else %}YEAH!{% end %}")
2005
+ ('YEAH!', {}, [])
2006
+ >>> render_sql_template("select * from table where str = {{foo}}", { 'foo': 'test' })
2007
+ ("select * from table where str = 'test'", {}, [])
2008
+ >>> render_sql_template("select * from table where f = {{foo}}", { 'foo': 1.0 })
2009
+ ('select * from table where f = 1.0', {}, [])
2010
+ >>> render_sql_template("select {{Boolean(foo)}} from table", { 'foo': True })
2011
+ ('select 1 from table', {}, [])
2012
+ >>> render_sql_template("select {{Boolean(foo)}} from table", { 'foo': False })
2013
+ ('select 0 from table', {}, [])
2014
+ >>> render_sql_template("select * from table where f = {{Float32(foo)}}", { 'foo': 1 })
2015
+ ("select * from table where f = toFloat32('1.0')", {}, [])
2016
+ >>> render_sql_template("select * from table where f = {{foo}}", { 'foo': "';drop table users;" })
2017
+ ("select * from table where f = '\\\\';drop table users;'", {}, [])
2018
+ >>> render_sql_template("select * from {{symbol(foo)}}", { 'foo': 'table-name' })
2019
+ ('select * from `table-name`', {}, [])
2020
+ >>> render_sql_template("select * from {{symbol(foo)}}", { 'foo': '"table-name"' })
2021
+ ('select * from `table-name`', {}, [])
2022
+ >>> render_sql_template("select * from {{table(foo)}}", { 'foo': '"table-name"' })
2023
+ ('select * from table-name', {}, [])
2024
+ >>> render_sql_template("select * from {{Int32(foo)}}", { 'foo': 'non_int' })
2025
+ Traceback (most recent call last):
2026
+ ...
2027
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Error validating 'non_int' to type Int32
2028
+ >>> render_sql_template("select * from table where f = {{Float32(foo)}}", test_mode=True)
2029
+ ("select * from table where f = toFloat32('0.0')", {}, [])
2030
+ >>> render_sql_template("SELECT * FROM query_log__dev where a = {{test}}", test_mode=True)
2031
+ ("SELECT * FROM query_log__dev where a = '__no_value__'", {}, [])
2032
+ >>> render_sql_template("SELECT {{test}}", {'token':'testing'})
2033
+ Traceback (most recent call last):
2034
+ ...
2035
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: expression "test" evaluated to null
2036
+ >>> render_sql_template("SELECT {{testisasuperlongthingandwedontwanttoreturnthefullthing}}", {'token':'testing'})
2037
+ Traceback (most recent call last):
2038
+ ...
2039
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: expression "testisasuperlongthin..." evaluated to null
2040
+ >>> render_sql_template("SELECT {{ Array(embedding, 'Float32') }}", {'token':'testing', 'embedding': '1,2,3,4, null'})
2041
+ Traceback (most recent call last):
2042
+ ...
2043
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Error validating 1,2,3,4, null[4]( null) to type Float32
2044
+ >>> render_sql_template('{% if test %}SELECT 1{% else %} select 2 {% end %}')
2045
+ (' select 2 ', {}, [])
2046
+ >>> render_sql_template('{% if Int32(test, 1) %}SELECT 1{% else %} select 2 {% end %}')
2047
+ ('SELECT 1', {}, [])
2048
+ >>> render_sql_template('{% for v in test %}SELECT {{v}} {% end %}',test_mode=True)
2049
+ ("SELECT '__no_value__' SELECT '__no_value__' SELECT '__no_value__' ", {}, [])
2050
+ >>> render_sql_template("select {{Int32(foo, 1)}}", test_mode=True)
2051
+ ("select toInt32('1')", {}, [])
2052
+ >>> render_sql_template("SELECT count() c FROM test_table where a > {{Float32(myvar)}} {% if defined(my_condition) %} and c = Int32({{my_condition}}){% end %}", {'myvar': 1.0})
2053
+ ("SELECT count() c FROM test_table where a > toFloat32('1.0') ", {}, [])
2054
+ >>> render_sql_template("SELECT count() c FROM where {{sql_and(a=a, b=b)}}", {'a': '1', 'b': '2'})
2055
+ ("SELECT count() c FROM where a = '1' and b = '2'", {}, [])
2056
+ >>> render_sql_template("SELECT count() c FROM where {{sql_and(a=a, b=b)}}", {'b': '2'})
2057
+ ("SELECT count() c FROM where b = '2'", {}, [])
2058
+ >>> render_sql_template("SELECT count() c FROM where {{sql_and(a=Int(a, defined=False), b=Int(b, defined=False))}}", {'b': '2'})
2059
+ ('SELECT count() c FROM where b = 2', {}, [])
2060
+ >>> render_sql_template("SELECT count() c FROM where {{sql_and(a__in=Array(a), b=b)}}", {'a': 'a,b,c','b': '2'})
2061
+ ("SELECT count() c FROM where a in ['a','b','c'] and b = '2'", {}, [])
2062
+ >>> render_sql_template("SELECT count() c FROM where {{sql_and(a__not_in=Array(a), b=b)}}", {'a': 'a,b,c','b': '2'})
2063
+ ("SELECT count() c FROM where a not in ['a','b','c'] and b = '2'", {}, [])
2064
+ >>> render_sql_template("SELECT c FROM where a > {{Date(start)}}", test_mode=True)
2065
+ ("SELECT c FROM where a > '2019-01-01'", {}, [])
2066
+ >>> render_sql_template("SELECT c FROM where a > {{DateTime(start)}}", test_mode=True)
2067
+ ("SELECT c FROM where a > '2019-01-01 00:00:00'", {}, [])
2068
+ >>> render_sql_template("SELECT c FROM where a > {{DateTime(start)}}", {'start': '2018-09-07 23:55:00'})
2069
+ ("SELECT c FROM where a > '2018-09-07 23:55:00'", {}, [])
2070
+ >>> render_sql_template('SELECT * FROM tracker {% if defined(start) %} {{DateTime(start)}} and {{DateTime(end)}} {% end %}', {'start': '2019-08-01 00:00:00', 'end': '2019-08-02 00:00:00'})
2071
+ ("SELECT * FROM tracker '2019-08-01 00:00:00' and '2019-08-02 00:00:00' ", {}, [])
2072
+ >>> render_sql_template('SELECT * from test limit {{Int(limit)}}', test_mode=True)
2073
+ ('SELECT * from test limit 0', {}, [])
2074
+ >>> render_sql_template('SELECT {{symbol(attr)}} from test', test_mode=True)
2075
+ ('SELECT `placeholder` from test', {}, [])
2076
+ >>> render_sql_template('SELECT {{Array(foo)}}', {'foo': 'a,b,c,d'})
2077
+ ("SELECT ['a','b','c','d']", {}, [])
2078
+ >>> render_sql_template("SELECT {{Array(foo, 'Int32')}}", {'foo': '1,2,3,4'})
2079
+ ('SELECT [1,2,3,4]', {}, [])
2080
+ >>> render_sql_template("SELECT {{Array(foo, 'Int32')}}", test_mode=True)
2081
+ ('SELECT [0,0]', {}, [])
2082
+ >>> render_sql_template("SELECT {{Array(foo)}}", test_mode=True)
2083
+ ("SELECT ['__no_value__0','__no_value__1']", {}, [])
2084
+ >>> render_sql_template("{{max_threads(2)}} SELECT 1")
2085
+ ('-- max_threads 2\\n SELECT 1', {'max_threads': 2}, [])
2086
+ >>> render_sql_template("SELECT {{String(foo)}}", test_mode=True)
2087
+ ("SELECT '__no_value__'", {}, [])
2088
+ >>> render_sql_template("SELECT {{String(foo, 'test')}}", test_mode=True)
2089
+ ("SELECT 'test'", {}, [])
2090
+ >>> render_sql_template("SELECT {{String(foo, 'test')}}", {'foo': 'tt'})
2091
+ ("SELECT 'tt'", {}, [])
2092
+ >>> render_sql_template("SELECT {{String(format, 'test')}}", {'format': 'tt'})
2093
+ Traceback (most recent call last):
2094
+ ...
2095
+ ValueError: "format" can not be used as a variable name, line 1
2096
+ >>> render_sql_template("SELECT {{format}}", {'format': 'tt'})
2097
+ Traceback (most recent call last):
2098
+ ...
2099
+ ValueError: "format" can not be used as a variable name, line 1
2100
+ >>> render_sql_template("SELECT {{String(q, 'test')}}", {'q': 'tt'})
2101
+ Traceback (most recent call last):
2102
+ ...
2103
+ ValueError: "q" can not be used as a variable name, line 1
2104
+ >>> render_sql_template("SELECT {{column(agg)}}", {})
2105
+ Traceback (most recent call last):
2106
+ ...
2107
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Missing column() default value, use `column(column_name, 'default_column_name')`
2108
+ >>> render_sql_template("SELECT {{column(agg)}}", {'agg': 'foo'})
2109
+ ('SELECT `foo`', {}, [])
2110
+ >>> render_sql_template("SELECT {{column(agg)}}", {'agg': '"foo"'})
2111
+ ('SELECT `foo`', {}, [])
2112
+ >>> render_sql_template('{% if not defined(test) %}error("This is an error"){% end %}', {})
2113
+ ('error("This is an error")', {}, [])
2114
+ >>> render_sql_template('{% if not defined(test) %}custom_error({error: "This is an error"}){% end %}', {})
2115
+ ('custom_error({error: "This is an error"})', {}, [])
2116
+ >>> render_sql_template("SELECT {{String(foo + 'abcd')}}", test_mode=True)
2117
+ ("SELECT '__no_value__'", {}, [])
2118
+ >>> render_sql_template("SELECT {{columns(agg)}}", {})
2119
+ Traceback (most recent call last):
2120
+ ...
2121
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Missing columns() default value, use `columns(column_names, 'default_column_name')`
2122
+ >>> render_sql_template("SELECT {{columns(agg, 'a,b,c')}} FROM table", {})
2123
+ ('SELECT `a`,`b`,`c` FROM table', {}, [])
2124
+ >>> render_sql_template("SELECT {{columns(agg, 'a,b,c')}} FROM table", {'agg': 'foo'})
2125
+ ('SELECT `foo` FROM table', {}, [])
2126
+ >>> render_sql_template("SELECT {{columns('a,b,c')}} FROM table", {})
2127
+ ('SELECT `a`,`b`,`c` FROM table', {}, [])
2128
+ >>> render_sql_template("% {% if whatever(passenger_count) %}{% end %}", test_mode=True)
2129
+ Traceback (most recent call last):
2130
+ ...
2131
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: 'whatever' is not a valid function, line 1
2132
+ >>> render_sql_template("% {% if defined((passenger_count) %}{% end %}", test_mode=True)
2133
+ Traceback (most recent call last):
2134
+ ...
2135
+ SyntaxError: invalid syntax
2136
+ >>> render_sql_template("SELECT * FROM dim_fecha_evento where foo like {{sql_unescape(String(pepe), '%')}}", {"pepe": 'raul_el_bueno_is_the_best_%'})
2137
+ ("SELECT * FROM dim_fecha_evento where foo like 'raul_el_bueno_is_the_best_%'", {}, [])
2138
+ >>> render_sql_template("SELECT * FROM table WHERE field={{String(field_filter)}}", {"field_filter": 'action."test run"'})
2139
+ ('SELECT * FROM table WHERE field=\\'action.\\\\"test run\\\\"\\'', {}, [])
2140
+ >>> render_sql_template("SELECT {{Int128(foo)}} as x, {{Int128(bar)}} as y", {'foo': -170141183460469231731687303715884105728, 'bar': 170141183460469231731687303715884105727})
2141
+ ("SELECT toInt128('-170141183460469231731687303715884105728') as x, toInt128('170141183460469231731687303715884105727') as y", {}, [])
2142
+ >>> render_sql_template("SELECT {{Int256(foo)}} as x, {{Int256(bar)}} as y", {'foo': -57896044618658097711785492504343953926634992332820282019728792003956564819968, 'bar': 57896044618658097711785492504343953926634992332820282019728792003956564819967})
2143
+ ("SELECT toInt256('-57896044618658097711785492504343953926634992332820282019728792003956564819968') as x, toInt256('57896044618658097711785492504343953926634992332820282019728792003956564819967') as y", {}, [])
2144
+ >>> render_sql_template('% SELECT * FROM {% import os %}{{ os.popen("whoami").read() }}')
2145
+ Traceback (most recent call last):
2146
+ ...
2147
+ tinybird.tornado_template.ParseError: import is forbidden at <string>:1
2148
+ >>> render_sql_template('% SELECT * FROM {% import os %}{{ os.popen("ls").read() }}')
2149
+ Traceback (most recent call last):
2150
+ ...
2151
+ tinybird.tornado_template.ParseError: import is forbidden at <string>:1
2152
+ >>> render_sql_template('% SELECT * FROM {% import os %}{{ os.popen("cat etc/passwd").read() }}')
2153
+ Traceback (most recent call last):
2154
+ ...
2155
+ tinybird.tornado_template.ParseError: import is forbidden at <string>:1
2156
+ >>> render_sql_template('% SELECT * FROM {% from os import popen %}{{ popen("cat etc/passwd").read() }}')
2157
+ Traceback (most recent call last):
2158
+ ...
2159
+ tinybird.tornado_template.ParseError: import is forbidden at <string>:1
2160
+ >>> render_sql_template('% SELECT {{len([1] * 10**7)}}')
2161
+ Traceback (most recent call last):
2162
+ ...
2163
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Invalid BinOp: Pow()
2164
+ >>> render_sql_template("% SELECT {{Array(click_selector, 'String', 'pre,pro')}}")
2165
+ ("% SELECT ['pre','pro']", {}, [])
2166
+ >>> render_sql_template("% SELECT {{Array(click_selector, 'String', 'pre,pro')}}", {'click_selector': 'hi,hello'})
2167
+ ("% SELECT ['hi','hello']", {}, [])
2168
+ >>> render_sql_template("% SELECT now() > {{DateTime64(variable, '2020-09-09 10:10:10.000')}}", {})
2169
+ ("% SELECT now() > '2020-09-09 10:10:10.000'", {}, [])
2170
+ >>> render_sql_template("% SELECT {% if defined(x) %} x, 1", {})
2171
+ Traceback (most recent call last):
2172
+ ...
2173
+ tinybird.tornado_template.UnClosedIfError: Missing {% end %} block for if at line 1
2174
+ >>> render_sql_template("% SELECT * FROM employees WHERE 0 {% for kv in JSON(payload) %} OR department = {{kv['dp']}} {% end %}")
2175
+ ('% SELECT * FROM employees WHERE 0 ', {}, [])
2176
+ >>> render_sql_template("% SELECT * FROM employees WHERE 0 {% for kv in JSON(payload, '[{\\"dp\\":\\"Sales\\"}]') %} OR department = {{kv['dp']}} {% end %}")
2177
+ ("% SELECT * FROM employees WHERE 0 OR department = 'Sales' ", {}, [])
2178
+ >>> render_sql_template("% SELECT * FROM employees WHERE 0 {% for kv in JSON(payload) %} OR department = {{kv['dp']}} {% end %}", { 'payload': '[{"dp":"Design"},{"dp":"Marketing"}]'})
2179
+ ("% SELECT * FROM employees WHERE 0 OR department = 'Design' OR department = 'Marketing' ", {}, [])
2180
+ >>> render_sql_template("% {% for kv in JSON(payload) %} department = {{kv['dp']}} {% end %}", test_mode=True)
2181
+ Traceback (most recent call last):
2182
+ ...
2183
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Error parsing JSON: '__no_value__' - Expecting value: line 1 column 1 (char 0)
2184
+ >>> render_sql_template("% {% for kv in JSON(payload, '') %} department = {{kv['dp']}} {% end %}")
2185
+ Traceback (most recent call last):
2186
+ ...
2187
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Error parsing JSON: '' - Expecting value: line 1 column 1 (char 0)
2188
+ >>> render_sql_template("% {% if defined(test) %}{% set _groupByCSV = ','.join(test) %} SELECT test as aa, {{Array(test, 'String')}} as test, {{_groupByCSV}} as a {% end %}", {"test": "1,2"})
2189
+ ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {}, [])
2190
+ >>> render_sql_template("% {% if defined(test) %}{% set _groupByCSV = ','.join(test) %} SELECT test as aa, {{Array(test, 'String')}} as test, {{_groupByCSV}} as a {% end %}", {"test": ["1","2"]})
2191
+ ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {}, [])
2192
+ >>> render_sql_template("% {% if defined(test) %}{% set _total = sum(test) %} SELECT test as aa, {{Array(test, 'Int32')}} as test, {{_total}} as a {% end %}", {"test": "1,2"})
2193
+ ('% SELECT test as aa, [1,2] as test, 3 as a ', {}, [])
2194
+ >>> render_sql_template("% {% if defined(test) %}{% set _groupByCSV = ','.join(test) %} SELECT test as aa, {{Array(test, 'String')}} as test, {{_groupByCSV}} as a {% end %}", {"test": ["1","2"]})
2195
+ ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {}, [])
2196
+ >>> render_sql_template("% SELECT {% if defined(x) %} x, 1")
2197
+ Traceback (most recent call last):
2198
+ ...
2199
+ tinybird.tornado_template.UnClosedIfError: Missing {% end %} block for if at line 1
2200
+ >>> render_sql_template("select * from table where str = {{pipeline}}", { 'pipeline': 'test' })
2201
+ ("select * from table where str = 'test'", {}, ['pipeline'])
2202
+ >>> render_sql_template("select * from table where str = {{tb_secret('test')}}", secrets = [ 'tb_secret_test' ])
2203
+ ('select * from table where str = {test: String}', {}, [])
2204
+ >>> render_sql_template("select * from table where str = {{tb_var('test')}}", secrets = [ 'tb_secret_test' ])
2205
+ ('select * from table where str = {test: String}', {}, [])
2206
+ >>> render_sql_template("select * from table where str = {{tb_secret('test')}}", variables = { 'test': '1234' })
2207
+ Traceback (most recent call last):
2208
+ ...
2209
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Cannot access secret 'test'. Check the secret exists in the Workspace and the token has the required scope.
2210
+ >>> render_sql_template("select * from table where str = {{tb_secret('test')}}", test_mode=True)
2211
+ ('select * from table where str = {test: String}', {}, [])
2212
+ >>> render_sql_template("select * from table where str = {{tb_secret('test')}}", secrets = [ 'tb_secret_test' ], test_mode=True)
2213
+ ('select * from table where str = {test: String}', {}, [])
2214
+ >>> render_sql_template("select * from table where str = {{tb_secret('test')}}", secrets = [ 'tb_secret_test2' ])
2215
+ Traceback (most recent call last):
2216
+ ...
2217
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Cannot access secret 'test'. Check the secret exists in the Workspace and the token has the required scope.
2218
+ >>> render_sql_template("select * from table where str = {{String(test)}} and category = {{String(category, 'shirts')}} and color = {{ Int32(color)}}", test_mode=False)
2219
+ Traceback (most recent call last):
2220
+ ...
2221
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Required parameter is not defined. Check the parameters test and color. Please provide a value or set a default value in the pipe code.
2222
+ >>> render_sql_template("select columns(cols, 'salary') from table where str = {{String(test)}}", test_mode=False)
2223
+ Traceback (most recent call last):
2224
+ ...
2225
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Required parameter is not defined. Check the parameters test. Please provide a value or set a default value in the pipe code.
2226
+ """
2227
+ escape_split_to_array = ff_split_to_array_escape.get(False)
2228
+ bypass_preprocess_variables = ff_preprocess_parameters_circuit_breaker.get(False)
2229
+
2230
+ t, template_variables, variable_warnings = get_template_and_variables(
2231
+ sql, name, escape_arrays=escape_split_to_array
2232
+ )
2233
+ template_variables_with_types = get_var_names_and_types_cached(t)
2234
+
2235
+ if not bypass_preprocess_variables and variables is not None:
2236
+ processed_variables = preprocess_variables(variables, template_variables_with_types)
2237
+ variables.update(processed_variables)
2238
+
2239
+ if test_mode:
2240
+
2241
+ def dummy(*args, **kwargs):
2242
+ return Comment("error launched")
2243
+
2244
+ v: dict = {x["name"]: Placeholder(x["name"], x["line"]) for x in template_variables}
2245
+ is_tb_secret = any([s for s in template_variables if s["name"] == "tb_secret" or s["name"] == "tb_var"])
2246
+
2247
+ if variables:
2248
+ v.update(variables)
2249
+
2250
+ if secrets:
2251
+ v.update({"tb_secrets": secrets})
2252
+
2253
+ if is_tb_secret:
2254
+ v.update({TB_SECRET_IN_TEST_MODE: None})
2255
+
2256
+ v.update(type_fns_check)
2257
+ v.update(
2258
+ {
2259
+ # disable error throws on check
2260
+ "error": dummy,
2261
+ "custom_error": dummy,
2262
+ }
2263
+ )
2264
+
2265
+ if local_variables:
2266
+ v.update(local_variables)
2267
+
2268
+ else:
2269
+ v = {x["name"]: None for x in template_variables}
2270
+ if variables:
2271
+ v.update(variables)
2272
+
2273
+ if secrets:
2274
+ v.update({"tb_secrets": secrets})
2275
+
2276
+ v.update(type_fns)
2277
+
2278
+ if local_variables:
2279
+ v.update(local_variables)
2280
+
2281
+ try:
2282
+ sql, template_execution_results = generate(t, **v)
2283
+ try:
2284
+ if TB_SECRET_IN_TEST_MODE in template_execution_results:
2285
+ del template_execution_results[TB_SECRET_IN_TEST_MODE]
2286
+ except Exception:
2287
+ pass
2288
+ return sql, template_execution_results, variable_warnings
2289
+ except NameError as e:
2290
+ raise SQLTemplateException(e, documentation="/cli/advanced-templates.html#defined")
2291
+ except SQLTemplateException as e:
2292
+ format_SQLTemplateException_message(e, vars_and_types=template_variables_with_types)
2293
+ raise
2294
+ except Exception as e:
2295
+ # errors might vary here, we need to support as much as possible
2296
+ # https://gitlab.com/tinybird/analytics/-/issues/943
2297
+ if "length" in v and not v["length"]:
2298
+ raise SQLTemplateException("length cannot be used as a variable name or as a function inside of a template")
2299
+ elif "missing 1 required positional argument" in str(e):
2300
+ raise SQLTemplateException(
2301
+ "one of the transform type functions is missing an argument",
2302
+ documentation="/cli/advanced-templates.html#transform-types-functions",
2303
+ )
2304
+ elif "not callable" in str(e) or "unhashable type" in str(e):
2305
+ raise SQLTemplateException(
2306
+ "wrong syntax, you might be using a not valid function inside a control block",
2307
+ documentation="/cli/advanced-templates.html",
2308
+ )
2309
+ raise e
2310
+
2311
+
2312
+ def extract_variables_from_sql(sql: str, params: List[Dict[str, Any]]) -> Dict[str, Any]:
2313
+ sql = sql[1:] if sql[0] == "%" else sql
2314
+ defaults = {}
2315
+ mock_data = {}
2316
+ try:
2317
+ for param in params:
2318
+ mock_data[param["name"]] = "__NO__VALUE__DEFINED__"
2319
+ # Initialize a dictionary to track variables
2320
+ variable_tracker = {}
2321
+
2322
+ # Wrapper function to capture variable assignments
2323
+ def capture_variable(name, value):
2324
+ variable_tracker[name] = value
2325
+ return value
2326
+
2327
+ # Modify the template by adding capture hooks
2328
+ tracked_template_string = sql
2329
+ for var_name in mock_data.keys():
2330
+ tracked_template_string += f"{{% set __ = capture_variable('{var_name}', {var_name}) %}}"
2331
+
2332
+ # Define the modified template with tracking
2333
+ template = Template(tracked_template_string)
2334
+ type_fns = get_transform_types()
2335
+ template.generate(**mock_data, **type_fns, capture_variable=capture_variable)
2336
+ for var_name, value in variable_tracker.items():
2337
+ if value != "__NO__VALUE__DEFINED__":
2338
+ defaults[var_name] = value
2339
+ except Exception as e:
2340
+ logging.error(f"Error extracting variables from sql: {e}")
2341
+ return {}
2342
+
2343
+ return defaults