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.
- tinybird/__cli__.py +8 -0
- tinybird/ch_utils/constants.py +244 -0
- tinybird/ch_utils/engine.py +855 -0
- tinybird/check_pypi.py +25 -0
- tinybird/client.py +1281 -0
- tinybird/config.py +117 -0
- tinybird/connectors.py +428 -0
- tinybird/context.py +23 -0
- tinybird/datafile.py +5589 -0
- tinybird/datatypes.py +434 -0
- tinybird/feedback_manager.py +1022 -0
- tinybird/git_settings.py +145 -0
- tinybird/sql.py +865 -0
- tinybird/sql_template.py +2343 -0
- tinybird/sql_template_fmt.py +281 -0
- tinybird/sql_toolset.py +350 -0
- tinybird/syncasync.py +682 -0
- tinybird/tb_cli.py +25 -0
- tinybird/tb_cli_modules/auth.py +252 -0
- tinybird/tb_cli_modules/branch.py +1043 -0
- tinybird/tb_cli_modules/cicd.py +434 -0
- tinybird/tb_cli_modules/cli.py +1571 -0
- tinybird/tb_cli_modules/common.py +2082 -0
- tinybird/tb_cli_modules/config.py +344 -0
- tinybird/tb_cli_modules/connection.py +803 -0
- tinybird/tb_cli_modules/datasource.py +900 -0
- tinybird/tb_cli_modules/exceptions.py +91 -0
- tinybird/tb_cli_modules/fmt.py +91 -0
- tinybird/tb_cli_modules/job.py +85 -0
- tinybird/tb_cli_modules/pipe.py +858 -0
- tinybird/tb_cli_modules/regions.py +9 -0
- tinybird/tb_cli_modules/tag.py +100 -0
- tinybird/tb_cli_modules/telemetry.py +310 -0
- tinybird/tb_cli_modules/test.py +107 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
- tinybird/tb_cli_modules/token.py +349 -0
- tinybird/tb_cli_modules/workspace.py +269 -0
- tinybird/tb_cli_modules/workspace_members.py +212 -0
- tinybird/tornado_template.py +1194 -0
- tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
- tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
- tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
- tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- tinybird-0.0.1.dev0.dist-info/top_level.txt +4 -0
tinybird/sql_template.py
ADDED
|
@@ -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
|