refactor (Expr): add generics to make Expr properties type safe (POC proposal)#7599
refactor (Expr): add generics to make Expr properties type safe (POC proposal)#7599OutSquareCapital wants to merge 22 commits into
Expr properties type safe (POC proposal)#7599Conversation
…er::annotate_types::TypeAnnotator::_get_unpivot_column_types`
| if isinstance(first, exp.PivotAlias) and (alias_node := first.args.get("alias")): | ||
| new_types[field_col.name] = alias_node.type | ||
| in_src = first.this | ||
| in_src: exp.PivotAlias | t.Any | None = first.this |
There was a problem hiding this comment.
This is needed, otherwise mypy fail. Since in_src can be Any if first is not an exp::PivotAlias, using this union was the strictest solution. We could also use directly Any
| UTINYINT_MAX: t.ClassVar = 255 | ||
|
|
||
| COMPLEMENT_COMPARISONS: t.ClassVar = { | ||
| COMPLEMENT_COMPARISONS: t.ClassVar[dict[type[exp.Expr], type[exp.Expr]]] = { |
There was a problem hiding this comment.
Will cause mypy fail if not explicit
|
this looks really complicated, what exactly is the value with it, i'm not sure i want a complicated generic type system |
Currently, having
About the complexity, may I ask what you find complex and that could be simplified? Basically this is just an additional intermediate class |
| # NEXT_DAY: (target_dow - current_dow + 6) % 7 + 1 | ||
| days_offset = exp.paren(target_dow - isodow_call + 6, copy=False) % 7 + 1 | ||
| date_with_offset = date_expr + exp.Interval(this=days_offset, unit=exp.var("DAY")) | ||
| date_with_offset: exp.Binary = date_expr + exp.Interval( |
There was a problem hiding this comment.
Needed to avoid mypy failure, since date_with_offset is an implicit union, and mypy don't handle this very well. Better to simply use it as the parent class.
| unit = expression.args.get("unit") | ||
| modifier = f"'{modifier} {unit.name}'" if unit else f"'{modifier}'" | ||
| return self.func("DATE", expression.this, modifier) | ||
| modifier_name = modifier.name if modifier.is_string else self.sql(modifier) |
There was a problem hiding this comment.
Needed to avoid mypy failure. modifier first is an Expr then it's a str. This make both the code hard to read (personal opinion) and in any case isn't accepted by mypy nor basedpyright.
…r than a too narrow `Query`
…ore::ExprTyped`, use `expressions::query::Selectable` instead
…t::this`, we can't statically type it as of now.
… transforms modules (tobymao#7579) * refactor: improve planner typing annotations * refactor: improve planner, schema, and serde annotations Co-authored-by: Copilot <copilot@github.com> * refactor: added lazy annotations import to time module Co-authored-by: Copilot <copilot@github.com> * refactor: improved transforms annotations Co-authored-by: Copilot <copilot@github.com> * fix: make `StackVal` `type alias compatible with python 3.9 Co-authored-by: Copilot <copilot@github.com> * fix: since ast tree is mutated in `transforms::eliminate_qualify`, we indeed need to collect the Iterator in a list first Co-authored-by: Copilot <copilot@github.com> * fix: mypc is buggy with `object::__module__` access, so we can't narrow to a precise type the `Expr` path in serde::dump` * refactor: revert `type` -> `isinstance` usage in `serde::dump` function body * refactor: revert `nodes` list type in `serde::load` * refactor: ignore `node` type in `serde::load` body to avoid errors * refactor: Apply suggestions from code review Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> * fix: use `Any` for the key type of the trie mapping in `time::format_time` * refactor: move comment of joins_ons in `transforms::eliminate_join_marks` above the line * refactor: change the function body of `trnasforms::move_schema_columns_to_partitioned_by` into something more type safe * fix: revert instance checks in `serde::dump` * fix: revert type hint of `node` in `serde::load` * refactor: make the `_sql_handler` variable in `transforms::preprocess::_to_sql` a Protocol --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com>
| is_lateral = isinstance(join_expr, exp.Lateral) | ||
|
|
||
| unnest = join_expr.this if is_lateral else join_expr | ||
| unnest_join = join_expr.this if is_lateral else join_expr |
There was a problem hiding this comment.
Needed to avoid mypy issues.
sqlglot\transforms.py:351: error: Incompatible types in assignment (expression has type "Any | Expr", variable has type "Unnest") [assignment]Same issue than in generators::sqlite::dateadd_sql, first an Unnest, then an Expr.
|
After some toughts, decided to close this for the time being. My uni exams are in 1 month, and I won't have the time to handle a massive change like this one until they are finished. Furthermore, the downside of "no contract enforcement" is too fragile, as the multiple failed CI runs can show. Finally, continuing on the "small incremental changes" for a while can be beneficial, as this could make a major shift like this one with less frictions, as more "local issues" may be resolved until then. |
|
@OutSquareCapital sounds good, thanks for the heads up. Good luck with your exams! |
Abstract
After +10 typing PR's, I realize now that the best ROI going forward would be to improve the typing of the
Exprclass in itself as much as possible, rather than handling each call sites like I did in this PR.This is WIP, but I already put this here so we can discuss if that solution is a good one, or if another one would be preferrable.
1) Motivation
Currently, having
Expr::{this, expression}completely untyped withAnyas return types has multiple problems:.this.expression.thiscalls, with zero autocompletion# type: ignorecomments at each call site if you use a strict type checkerExprproperties, which is a LOT more work than if the definition site become type safe by designexpressions::core::Alias::thisis across the code assumed to always be an Expr, but in the tests they can be None. Orexpressions::functions::Cast::this, also assumed to be an Expr, but ingenerators::postgres::cast_sqlwe give it a str. Thus, it's hard to know when or why we should assume a given return type.2) Implementation
The basic idea is to introduce a new class,
expressions::core::ExprTyped[ET, EE], where the generic typeETrepresent the return type ofExpr::this, andEEthe return type ofExpr::expression.This class only herit from
Expression, consider it an intermediate step.On the long run,
Expressioncould become generic in itself, which would avoid this step.2.1) Changes
All the "actual" changes lives in the
expressionsfolder.Two additionnal TypeVar, and the new
ExprTypedclass are the only new code.Otherwise, all other changes are
Expressionheritage replaced byExprTyped.The other impacted files outside of this folder are minor required changes to make mypy happy, and are explained in this PR comments.
The changes are quite large, but it's just to check if this could work at a large scale.
I think that a first, small PR that focuses on the most impactful classes with precises types would be better for review than this massive chunk. Later on, more complex classes, like the ones for which there are runtime incoherences, could be implemented one by one.
2.2) Pros and cons
2.2.1) Pros
Exprsubclasses would. This is only a new class, and a change of heritage.2.2.2) Cons
argsattribute will be able to adress thisExprclasses hierarchy -> one more intermediate classExprType[str, None]is str and None at the definition site.Expr::args::gettyping. If that could be done, it would instantly solve a LOT of casting, implicit Any, and strict type checker errors.2.2) Alternative solutions
IMO, the best solution would be a total refactor with concrete attributes for each Expr subclass, and no more
argsat all. But this is a massive, breaking one. I'm not opposed to try it tough. I'm just not sure how much you guys would be interested in it. But it would solve a lot of the typing problems by design.Another option would be to use overrides for each property. This would result in the same outcome, but with like 10x the boilerplate.
3) Impact
For the
expressions::core::Betweenclass:We now benefit from correct type inference when accessing the properties:
This clean up 550 errors from strict basedpyright, and as of now for the branch, can make fully type safe some nested
.this.expression.thisusages, which is a huge win IMO.This also revealed a few unsafe code usage, i.e patterns where unsafe access to
Expr.this.some_expr_attributeis done for example, accross the code, but then at some point in the tests or the dialect specific implementations we deal with aExpr::thisthat don't return anExpr.Note that in those cases, it wasn't possible to make the
Exprsubclass herit fromExprTyped.A few examples from the
expressionsfolder:query::{Query, CTE}string::{Left, Right}functions::Castcore::Alias4) Disclaimer
I came up with the architectural solution, naming, etc...
I asked Claude to "implement" the ExprTyped generic classes for each child class that could be determined by him statically. Then if mypy passes, to me it's valid. Otherwise, I either fixed the callers sites when it was pure typing considerations, or dropped the implementation to avoid too many changes