The purpose of this vignette is to walk through how expressions like _.id.mean() are converted into SQL.
This process involves 3 parts
SQL translation functions, e.g. taking column “id” and producing the SQL “ROUND(id)”.
SQL translation from a symbolic call
Converting method calls like _.id.round(2) to round(_.id, 2)
Looking up SQL translators (e.g. for “mean” function call)
Handling SQL partitions, like in OVER clauses
Using sqlalchemy select statement for convenience
Throughout this vignette, we’ll use a select statement object from sqlalchemy, so we can conveniently access its columns as needed.
from sqlalchemy import sqlcol_names = ['id', 'x', 'y']sel = sql.select([sql.column(x) for x in col_names])print(sel)print(type(sel.columns))print(sel.columns)
SELECT id, x, y
<class 'sqlalchemy.sql.base.ImmutableColumnCollection'>
['id', 'x', 'y']
Translator functions
A SQL translator function takes…
a first argument that is a sqlalchemy Column
(optional) additional arguments for the translation
<class 'siuba.sql.translate.AggOver'>
avg(x) OVER ()
Notice that this window expression has an empty over clause. This clause needs to be able to include any variables we’ve grouped the data by.
Siuba handles this by implementing a set_over method on these custom sqlalchemy Over clauses, which takes grouping and ordering variables as arguments.
The section above discusses how SQL translators are functions that take a sqlalchemy column, and return a SQL expression. However, when using siuba we often have expressions like…
mutate(data, x = _.y.round(2))
In this case, before we can even use a SQL translator, we need to…
find the name and arguments of the method being called
find the column it is being called on
This is done by using the CallTreeLocal class to analyze the tree of operations for each expression.
from siuba.siu import Lazy, CallTreeLocal, Call, strip_symbolicfrom siuba import __.y.round(2)
This is the same result as when we called the SQL translator for mean manually! In that section we also showed that we can set group information, so that it takes an average within each group.
In this case it’s easy to set group information to the Over clause. However, an additional challenge is when it’s part of a larger expression…
<sqlalchemy.sql.elements.BinaryExpression object at 0x11889ada0>
Handling partitions
While the first section showed how siuba’s custom Over clauses can add grouping info to a translation, it is missing one key detail: expressions that generate Over clauses, like _.id.mean(), can be part of larger expressions. For example _.id.mean() + 1.
In this case, if we look at the call tree for that expression, the top operation is the addition…
when the piece that needs grouping info is not easily accessible? The answer is by using a tree visitor, which steps down every black rectangle in the call tree shown above, from top to bottom.
Full example
Below, we copy the code from the call shaping section..
col, windows, window_cte = track_call_windows( func_call3, sel.columns, group_by = ['x', 'y'], order_by = [],# note that this is optional, and results in window_cte being a# copy of this select that contains the window clauses window_cte = sel.select() )print(col)print(windows)print(window_cte)
avg(id) OVER (PARTITION BY x, y) + :param_1
[<sqlalchemy.sql.elements.Label object at 0x11889ec50>, <sqlalchemy.sql.elements.Label object at 0x11889e1d0>]
SELECT id, x, y, avg(id) OVER (PARTITION BY x, y) AS win1, avg(id) OVER (PARTITION BY x, y) + :param_1 AS win2
FROM (SELECT id, x, y)