Skip to content

Commit

Permalink
feat: add unbind method to expressions
Browse files Browse the repository at this point in the history
Calling `unbind()` on an expression returns an equivalent expression but
with all references to backend-specific tables, e.g. `AlchemyTable`,
`PandasTable`... translated into `UnboundTable`.

Should help with serialization of expressions and ease execution of
expressions across backends.

Resolves #4536
  • Loading branch information
gforsyth authored and cpcloud committed Nov 16, 2022
1 parent 8eed1c9 commit 4b91b0b
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 0 deletions.
16 changes: 16 additions & 0 deletions ibis/backends/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,19 @@ def test_limit_chain(alltypes, expr_fn):
expr = expr_fn(alltypes)
result = expr.execute()
assert len(result) == 5


@pytest.mark.parametrize(
"expr_fn",
[
param(lambda t: t, id="alltypes table"),
param(lambda t: t.join(t.view(), t.id == t.view().int_col), id="self join"),
],
)
def test_unbind(alltypes, expr_fn):
expr = expr_fn(alltypes)
assert expr.unbind() != expr
assert expr.unbind().schema() == expr.schema()

assert "Unbound" not in repr(expr)
assert "Unbound" in repr(expr.unbind())
32 changes: 32 additions & 0 deletions ibis/expr/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,38 @@ def fn(node):
return substitute(fn, node)


def substitute_unbound(node):
"""Rewrite the input expression by replacing any table expressions with an
equivalent unbound table."""
assert isinstance(node, ops.Node), type(node)

def fn(node):
if isinstance(node, ops.DatabaseTable):
return ops.UnboundTable(name=node.name, schema=node.schema)
elif isinstance(node, ops.TableColumn):
# For table column references, in the event that we're on top of a
# projection, we need to check whether the ref comes from the base
# table schema or is a derived field. If we've projected out of
# something other than a physical table, then lifting should not
# occur
table = node.table

if isinstance(table, ops.Selection):
for val in table.selections:
if isinstance(val, ops.PhysicalTable) and node.name in val.schema:
return ops.TableColumn(val, node.name)
elif isinstance(node, ops.Join):
return node.__class__(
substitute_unbound(node.left),
substitute_unbound(node.right),
map(substitute_unbound, node.predicates),
)
# keep looking for nodes to substitute
return g.proceed

return substitute(fn, node)


def get_mutation_exprs(exprs: list[ir.Expr], table: ir.Table) -> list[ir.Expr | None]:
"""Given the list of exprs and the underlying table of a mutation op,
return the exprs to use to instantiate the mutation."""
Expand Down
7 changes: 7 additions & 0 deletions ibis/expr/types/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,13 @@ def to_pyarrow(
self, params=params, limit=limit, **kwargs
)

def unbind(self) -> ir.Table:
"""Return equivalent expression built on `UnboundTable` instead of
backend-specific table objects."""
from ibis.expr.analysis import substitute_unbound

return substitute_unbound(self.op()).to_expr()


unnamed = UnnamedMarker()

Expand Down

0 comments on commit 4b91b0b

Please sign in to comment.