Skip to content

Commit e1c69d0

Browse files
authored
Toby/revert types (#2261)
* Revert "chore: cleanup" This reverts commit 4e86d5b. * Revert "Feat: Typed macros 2 (#2109)" This reverts commit f450e4a.
1 parent b08f9d4 commit e1c69d0

File tree

6 files changed

+19
-425
lines changed

6 files changed

+19
-425
lines changed

docs/concepts/macros/sqlmesh_macros.md

Lines changed: 0 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,116 +1044,6 @@ def some_macro(evaluator):
10441044
...
10451045
```
10461046

1047-
## Typed Macros
1048-
1049-
Typed macros in SQLMesh bring the power of type hints from Python, enhancing readability, maintainability, and usability of your SQL macros. These macros enable developers to specify expected types for arguments, making the macros more intuitive and less error-prone.
1050-
1051-
### Benefits of Typed Macros
1052-
1053-
1. **Improved Readability**: By specifying types, the intent of the macro is clearer to other developers or future you.
1054-
2. **Reduced Boilerplate**: No need for manual type conversion within the macro function, allowing you to focus on the core logic.
1055-
3. **Enhanced Autocompletion**: IDEs can provide better autocompletion and documentation based on the specified types.
1056-
1057-
### Defining a Typed Macro
1058-
1059-
Typed macros in SQLMesh use Python's type hints. Here's a simple example of a typed macro that repeats a string a given number of times:
1060-
1061-
```python linenums="1"
1062-
from sqlmesh import macro
1063-
1064-
@macro()
1065-
def repeat_string(evaluator, text: str, count: int) -> str:
1066-
return text * count
1067-
```
1068-
1069-
Usage in SQLMesh:
1070-
1071-
```sql linenums="1"
1072-
SELECT
1073-
@repeat_string('SQLMesh ', 3) as repeated_string
1074-
FROM some_table;
1075-
```
1076-
1077-
This macro takes two arguments: `text` of type `str` and `count` of type `int`, and it returns a string. Without type hints, the inputs to the macro would have been two `exp.Literal` objects you would have had to convert to strings and integers manually.
1078-
1079-
### Supported Types
1080-
1081-
SQLMesh supports common Python types for typed macros including:
1082-
1083-
- `str`
1084-
- `int`
1085-
- `float`
1086-
- `bool`
1087-
- `List[T]` - where `T` is any supported type including sqlglot expressions
1088-
- `Tuple[T]` - where `T` is any supported type including sqlglot expressions
1089-
- `Union[T1, T2, ...]` - where `T1`, `T2`, etc. are any supported types including sqlglot expressions
1090-
1091-
We also support SQLGlot expressions as type hints, allowing you to ensure inputs are coerced to the desired SQL AST node your intending on working with. Some useful examples include:
1092-
1093-
- `exp.Table`
1094-
- `exp.Column`
1095-
- `exp.Literal`
1096-
- `exp.Identifier`
1097-
1098-
While these might be obvious examples, you can effectively coerce an input into _any_ SQLGlot expression type, which can be useful for more complex macros. When coercing to more complex types, you will almost certainly need to pass a string literal since expression to expression coercion is limited. When a string literal is passed to a macro that hints at a SQLGlot expression, the string will be parsed using SQLGlot and coerced to the correct type. Failure to coerce to the correct type will result in the original expression being passed to the macro and a warning being logged for the user to address as-needed.
1099-
1100-
```python linenums="1"
1101-
@macro()
1102-
def stamped(evaluator, query: exp.Select) -> exp.Subquery:
1103-
return query.select(exp.Literal.string(str(datetime.now())).as_("stamp")).subquery()
1104-
1105-
# Coercing to a complex node like `exp.Select` works as expected given a string literal input
1106-
# SELECT * FROM @stamped('SELECT a, b, c')
1107-
```
1108-
1109-
When coercion fails, there will always be a warning logged but we will not crash. We believe the macro should be flexible by default, meaning the default behavior is preserved if we cannot coerce. Give that, the user can express whatever level of additional checks they want. For example, if you would like to raise an error when the coercion fails, you can use an `assert` statement. For example:
1110-
1111-
```python linenums="1"
1112-
@macro()
1113-
def my_macro(evaluator, table: exp.Table) -> exp.Column:
1114-
assert isinstance(table, exp.Table)
1115-
table.set("catalog", "dev")
1116-
return table
1117-
1118-
# Works
1119-
# SELECT * FROM @my_macro('some.table')
1120-
# SELECT * FROM @my_macro(some.table)
1121-
1122-
# Raises an error thanks to the users inclusion of the assert, otherwise would pass through the string literal and log a warning
1123-
# SELECT * FROM @my_macro('SELECT 1 + 1')
1124-
```
1125-
1126-
In using assert this way, you still get the benefits of reducing/removing the boilerplate needed to coerce types; but you **also** get guarantees about the type of the input. This is a useful pattern and is user-defined, so you can use it as you see fit. It ultimately allows you to keep the macro definition clean and focused on the core business logic.
1127-
1128-
### Advanced Typed Macros
1129-
1130-
You can create more complex macros using advanced Python features like generics. For example, a macro that accepts a list of integers and returns their sum:
1131-
1132-
```python linenums="1"
1133-
from typing import List
1134-
from sqlmesh import macro
1135-
1136-
@macro()
1137-
def sum_integers(evaluator, numbers: List[int]) -> int:
1138-
return sum(numbers)
1139-
```
1140-
1141-
Usage in SQLMesh:
1142-
1143-
```sql linenums="1"
1144-
SELECT
1145-
@sum_integers([1, 2, 3, 4, 5]) as total
1146-
FROM some_table;
1147-
```
1148-
1149-
Generics can be nested and are resolved recursively allowing for fairly robust type hinting.
1150-
1151-
See examples of the coercion function in action in the test suite [here](../../../tests/core/test_macros.py).
1152-
1153-
### Conclusion
1154-
1155-
Typed macros in SQLMesh not only enhance the development experience by making macros more readable and easier to use but also contribute to more robust and maintainable code. By leveraging Python's type hinting system, developers can create powerful and intuitive macros for their SQL queries, further bridging the gap between SQL and Python.
1156-
11571047
## Mixing macro systems
11581048

11591049
SQLMesh supports both SQLMesh and [Jinja](./jinja_macros.md) macro systems. We strongly recommend using only one system in a model - if both are present, they may fail or behave in unintuitive ways.

examples/sushi/macros/macros.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from macros.utils import between # type: ignore
2-
from sqlglot import exp
32

43
from sqlmesh import macro
54

65

76
@macro()
8-
def incremental_by_ds(evaluator, column: exp.Column):
7+
def incremental_by_ds(evaluator, column):
98
return between(evaluator, column, evaluator.locals["start_date"], evaluator.locals["end_date"])

sqlmesh/core/macros.py

Lines changed: 3 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
from __future__ import annotations
22

3-
import inspect
4-
import logging
53
import typing as t
64
from enum import Enum
7-
from functools import reduce, wraps
5+
from functools import reduce
86
from string import Template
97

108
import sqlglot
119
from jinja2 import Environment
12-
from sqlglot import Generator, exp, parse_one
10+
from sqlglot import Generator, exp
1311
from sqlglot.executor.env import ENV
1412
from sqlglot.executor.python import Python
1513
from sqlglot.helper import csv, ensure_collection
1614
from sqlglot.schema import MappingSchema
1715

1816
from sqlmesh.core.dialect import (
1917
SQLMESH_MACRO_PREFIX,
20-
Dialect,
2118
MacroDef,
2219
MacroFunc,
2320
MacroSQL,
@@ -36,8 +33,6 @@
3633
from sqlmesh.core.engine_adapter import EngineAdapter
3734
from sqlmesh.core.snapshot import Snapshot
3835

39-
logger = logging.getLogger(__name__)
40-
4136

4237
class RuntimeStage(Enum):
4338
LOADING = "loading"
@@ -354,75 +349,6 @@ def engine_adapter(self) -> EngineAdapter:
354349
)
355350
return self.locals["engine_adapter"]
356351

357-
def _coerce(self, expr: exp.Expression, typ: t.Any, strict: bool = False) -> t.Any:
358-
"""Coerces the given expression to the specified type on a best-effort basis."""
359-
base_err_msg = f"Failed to coerce expression '{expr}' to type '{typ}'."
360-
try:
361-
if typ is None or typ is t.Any:
362-
return expr
363-
base = t.get_origin(typ) or typ
364-
# We need to handle t.Union first since we cannot use isinstance with it
365-
if base is t.Union:
366-
for branch in t.get_args(typ):
367-
try:
368-
return self._coerce(expr, branch, True)
369-
except Exception:
370-
pass
371-
raise SQLMeshError(base_err_msg)
372-
if isinstance(expr, base):
373-
return expr
374-
if issubclass(base, exp.Expression):
375-
d = Dialect.get_or_raise(self.dialect)
376-
into = base if base in d.parser().EXPRESSION_PARSERS else None
377-
if into is None:
378-
if isinstance(expr, exp.Literal):
379-
coerced = parse_one(expr.this)
380-
else:
381-
raise SQLMeshError(
382-
f"{base_err_msg} Coercion to {base} requires a literal expression."
383-
)
384-
else:
385-
coerced = parse_one(
386-
expr.this if isinstance(expr, exp.Literal) else expr.sql(), into=into
387-
)
388-
if isinstance(coerced, base):
389-
return coerced
390-
raise SQLMeshError(base_err_msg)
391-
392-
if base in (int, float, str) and isinstance(expr, exp.Literal):
393-
return base(expr.this)
394-
if base is bool and isinstance(expr, exp.Boolean):
395-
return expr.this
396-
if base is str and isinstance(expr, exp.Expression):
397-
return expr.sql(self.dialect)
398-
if base is tuple and isinstance(expr, (exp.Tuple, exp.Array)):
399-
generic = t.get_args(typ)
400-
if not generic:
401-
return tuple(expr.expressions)
402-
if generic[-1] is ...:
403-
return tuple(self._coerce(expr, generic[0]) for expr in expr.expressions)
404-
elif len(generic) == len(expr.expressions):
405-
return tuple(
406-
self._coerce(expr, generic[i]) for i, expr in enumerate(expr.expressions)
407-
)
408-
raise SQLMeshError(f"{base_err_msg} Expected {len(generic)} items.")
409-
if base is list and isinstance(expr, (exp.Array, exp.Tuple)):
410-
generic = t.get_args(typ)
411-
if not generic:
412-
return expr.expressions
413-
return [self._coerce(expr, generic[0]) for expr in expr.expressions]
414-
raise SQLMeshError(base_err_msg)
415-
except Exception:
416-
if strict:
417-
raise
418-
logger.warning(
419-
"Coercion of expression '%s' to type '%s' failed. Using non coerced expression.",
420-
expr,
421-
typ,
422-
exc_info=True,
423-
)
424-
return expr
425-
426352

427353
class macro(registry_decorator):
428354
"""Specifies a function is a macro and registers it the global MACROS registry.
@@ -446,31 +372,7 @@ def add_one(evaluator: MacroEvaluator, column: exp.Literal) -> exp.Add:
446372
def __call__(
447373
self, func: t.Callable[..., DECORATOR_RETURN_TYPE]
448374
) -> t.Callable[..., DECORATOR_RETURN_TYPE]:
449-
@wraps(func)
450-
def _typed_func(
451-
evaluator: MacroEvaluator, *args_: t.Any, **kwargs_: t.Any
452-
) -> DECORATOR_RETURN_TYPE:
453-
spec = inspect.getfullargspec(func)
454-
annotations = t.get_type_hints(func)
455-
kwargs = inspect.getcallargs(func, evaluator, *args_, **kwargs_)
456-
for param, value in kwargs.items():
457-
coercible_type = annotations.get(param)
458-
if not coercible_type:
459-
continue
460-
kwargs[param] = evaluator._coerce(value, coercible_type)
461-
args = [kwargs.pop(k) for k in spec.args if k in kwargs]
462-
if spec.varargs:
463-
args.extend(kwargs.pop(spec.varargs, []))
464-
return func(*args, **kwargs)
465-
466-
try:
467-
annotated = any(t.get_type_hints(func).keys() - {"return"})
468-
except TypeError:
469-
annotated = False
470-
471-
wrapper = super().__call__(
472-
func if not annotated else t.cast(t.Callable[..., DECORATOR_RETURN_TYPE], _typed_func)
473-
)
375+
wrapper = super().__call__(func)
474376

475377
# This is useful to identify macros at runtime
476378
setattr(wrapper, "__sqlmesh_macro__", True)

sqlmesh/utils/metaprogramming.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ def normalize_source(obj: t.Any) -> str:
215215
# remove function return type annotation
216216
if isinstance(node, ast.FunctionDef):
217217
node.returns = None
218+
elif isinstance(node, ast.arg):
219+
node.annotation = None
218220

219221
return to_source(root_node).strip()
220222

@@ -276,8 +278,7 @@ def walk(obj: t.Any) -> None:
276278
if name not in env:
277279
# We only need to add the undecorated code of @macro() functions in env, which
278280
# is accessible through the `__wrapped__` attribute added by functools.wraps
279-
# We account for the case where the function is wrapped multiple times too
280-
env[name] = inspect.unwrap(obj) if hasattr(obj, "__sqlmesh_macro__") else obj
281+
env[name] = obj.__wrapped__ if getattr(obj, "__sqlmesh_macro__", None) else obj
281282

282283
if obj_module and _is_relative_to(obj_module.__file__, path):
283284
walk(env[name])

0 commit comments

Comments
 (0)