Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/boring_semantic_layer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@
Semantic API layer on top of external ibis.
"""

# Import convert and format to register dispatch handlers for semantic operations
from . import (
convert, # noqa: F401
format, # noqa: F401
)
# Import format to register repr dispatch handlers for semantic operations
from . import format # noqa: F401

# Main API exports
from .api import (
Expand Down
44 changes: 44 additions & 0 deletions src/boring_semantic_layer/_xorq.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from __future__ import annotations

import ibis as _plain_ibis

try:
import xorq.api as api
from xorq.api import selectors
Expand Down Expand Up @@ -266,4 +268,46 @@ def walk_nodes(*args, **kwargs):
"to_node",
"types",
"walk_nodes",
"get_ibis_module",
"null_safe_equal",
]


def _unwrap_table_proxy(obj):
for _ in range(8):
if not type(obj).__module__.startswith("boring_semantic_layer"):
return obj
inner = getattr(obj, "_t", None)
if inner is None:
resolver = getattr(obj, "_resolver", None)
inner = getattr(resolver, "_t", None) if resolver is not None else None
if inner is None:
return obj
obj = inner
return obj


def get_ibis_module(table):
"""Return the ibis module that built ``table`` (regular vs xorq-vendored).

BSL coexists with both flavors of ibis. Picking the right module avoids
cross-flavor literal/struct construction errors. Filter and dimension
callables receive resolver proxies rather than the table itself, so
unwrap those first — otherwise flavor detection would report plain ibis
for a xorq-backed table.
"""
table = _unwrap_table_proxy(table)
if type(table).__module__.startswith("xorq.vendor.ibis"):
return ibis
return _plain_ibis


def null_safe_equal(left, right):
"""Return equality that also matches two NULL values.

xorq/DataFusion currently misplans multiple ``identical_to`` join
predicates by folding an integer key into a boolean ``AND``. Expressing
the same semantics with ordinary equality and explicit NULL checks keeps
multi-key joins portable across the plain-ibis and xorq backends.
"""
return (left == right) | (left.isnull() & right.isnull())
2 changes: 1 addition & 1 deletion src/boring_semantic_layer/agents/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
import ibis
from langchain_core.tools import ToolException

from boring_semantic_layer import from_yaml
from boring_semantic_layer.agents.utils.chart_handler import generate_chart_with_data
from boring_semantic_layer.agents.utils.prompts import load_prompt
from boring_semantic_layer.utils import safe_eval
from boring_semantic_layer.yaml import from_yaml


def _models_ibis_module(models: dict) -> Any:
Expand Down
164 changes: 2 additions & 162 deletions src/boring_semantic_layer/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@

from __future__ import annotations

from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING, Any
from collections.abc import Callable
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ibis.common.deferred import Deferred
from ibis.expr import types as ir

from .expr import SemanticModel
Expand Down Expand Up @@ -44,165 +43,6 @@ def to_semantic_table(
)


def join_one(
left: SemanticModel,
other: SemanticModel,
on: Callable[[Any, Any], ir.BooleanValue] | str | Deferred | Sequence[str | Deferred],
how: str = "left",
) -> SemanticModel:
"""Join two semantic tables with a one-to-one relationship (left outer join).

Args:
left: Left semantic table
other: Right semantic table
on: Join predicate. Accepts a lambda ``(left, right) -> bool``, a column
name string, a Deferred ``_.col``, or a list of strings/Deferred for
compound equi-joins.
how: Join type. Only ``"left"`` is supported.

Returns:
Joined SemanticModel

Examples:
>>> join_one(orders, customers, on="customer_id")
>>> join_one(orders, customers, on=_.customer_id)
>>> join_one(orders, customers, on=lambda o, c: o.customer_id == c.customer_id)
"""
return left.join_one(other, on, how)


def join_many(
left: SemanticModel,
other: SemanticModel,
on: Callable[[Any, Any], ir.BooleanValue] | str | Deferred | Sequence[str | Deferred],
how: str = "left",
) -> SemanticModel:
"""Join two semantic tables with a one-to-many relationship.

Args:
left: Left semantic table
other: Right semantic table
on: Join predicate. Accepts a lambda ``(left, right) -> bool``, a column
name string, a Deferred ``_.col``, or a list of strings/Deferred for
compound equi-joins.
how: Join type. Only ``"left"`` is supported.

Returns:
Joined SemanticModel

Examples:
>>> join_many(customer, orders, on="customer_id")
>>> join_many(customer, orders, on=_.customer_id)
>>> join_many(customer, orders, on=lambda c, o: c.customer_id == o.customer_id)
"""
return left.join_many(other, on, how)


def join_cross(left: SemanticModel, other: SemanticModel) -> SemanticModel:
"""Cross join (Cartesian product) two semantic tables.

Args:
left: Left semantic table
other: Right semantic table

Returns:
Joined SemanticModel (Cartesian product of all rows)

Examples:
>>> join_cross(table_a, table_b) # All combinations of rows
"""
return left.join_cross(other)


def filter_(
table: SemanticModel,
predicate: Callable[[ir.Table], ir.BooleanValue],
) -> SemanticModel:
"""Filter a semantic table by a predicate.

Args:
table: Semantic table to filter
predicate: Function that takes a table and returns a boolean expression

Returns:
Filtered SemanticModel
"""
return table.filter(predicate)


def group_by_(table: SemanticModel, *dims: str | Deferred) -> SemanticModel:
"""Group a semantic table by dimensions.

Args:
table: Semantic table to group
*dims: Dimension names to group by

Returns:
Grouped SemanticModel
"""
return table.group_by(*dims)


def aggregate_(
table: SemanticModel,
*measure_names: str | Callable | Deferred,
**aliased,
) -> SemanticModel:
"""Aggregate measures in a semantic table.

Args:
table: Semantic table to aggregate
*measure_names: Names of measures to aggregate
**aliased: Aliased measure aggregations

Returns:
Aggregated SemanticModel
"""
return table.aggregate(*measure_names, **aliased)


def mutate_(
table: SemanticModel,
**kwargs: Callable[[ir.Table], ir.Value],
) -> SemanticModel:
"""Add computed columns to a semantic table.

Args:
table: Semantic table to mutate
**kwargs: Named column expressions (xorq vendored ibis expressions)

Returns:
Mutated SemanticModel
"""
return table.mutate(**kwargs)


def order_by_(table: SemanticModel, *keys: str | ir.Value) -> SemanticModel:
"""Order a semantic table by keys.

Args:
table: Semantic table to order
*keys: Column names or expressions to order by

Returns:
Ordered SemanticModel
"""
return table.order_by(*keys)


def limit_(table: SemanticModel, n: int) -> SemanticModel:
"""Limit the number of rows in a semantic table.

Args:
table: Semantic table to limit
n: Maximum number of rows

Returns:
Limited SemanticModel
"""
return table.limit(n)


def entity_dimension(
expr: Callable[[ir.Table], ir.Value],
description: str | None = None,
Expand Down
Loading
Loading