Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6213aab
fix: route filter/expression strings to the table's ibis flavor
hussainsultan Jul 18, 2026
4c959d4
fix(query): time-range/grain soundness and input validation
hussainsultan Jul 18, 2026
749d465
fix: measure redefinition eviction and workable bool-guard remediation
hussainsultan Jul 18, 2026
ff94f1e
fix: dimension shadowing a raw column no longer corrupts filters/meas…
hussainsultan Jul 18, 2026
9c1076a
fix: raise when pre-aggregation cannot attach requested columns
hussainsultan Jul 18, 2026
89bd6c0
test: regression suite for round-4 soundness findings
hussainsultan Jul 18, 2026
7863f72
fix: always restrict join_many measure legs to join participants
hussainsultan Jul 18, 2026
ca03d25
fix(calc): compile partitioned/ordered windows over inline base reduc…
hussainsultan Jul 18, 2026
8bb453e
fix(nest): compile nest= aggregations through the semantic model
hussainsultan Jul 18, 2026
7be9ca7
test: assert specific backend error for uncoerced partial dates
hussainsultan Jul 18, 2026
e277287
test(nest): pin inner order_by support gained in the rebase onto main
hussainsultan Jul 18, 2026
d57aba9
fix
hussainsultan Jul 18, 2026
7cad8e0
wip
hussainsultan Jul 20, 2026
c9f34c0
fix: restrict semantic joins to left joins
hussainsultan Jul 20, 2026
e0ec671
fix
hussainsultan Jul 20, 2026
8d4994f
fix test without xorq dependency
hussainsultan Jul 20, 2026
185c8b1
Fix nested docs build and null-safe joins
hussainsultan Jul 20, 2026
ce53660
test: add adversarial semantic model stress coverage
hussainsultan Jul 20, 2026
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
2 changes: 1 addition & 1 deletion docs/md/doc/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ result = (

## Key Takeaways

- **Composition via Joins**: Use `join_many()`, `join_one()`, or `join()` to compose models
- **Composition via Joins**: Use `join_many()`, `join_one()`, or `join_cross()` to compose models
- **Additive**: Each join adds dimensions and measures from the joined table
- **Table Prefixes**: Dimensions/measures are prefixed with table names (`flights.`, `carriers.`, `aircraft.`)
- **No Limit**: Compose as many models as needed for your analysis
Expand Down
38 changes: 34 additions & 4 deletions docs/md/doc/nested-subtotals.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,16 @@ result = (
.aggregate(
year_order_count=lambda t: t.order_count.sum(),
year_total_sales=lambda t: t.total_sales.sum(),
nest={"by_month": lambda t: t.group_by(["created_month", "order_count", "total_sales"]).order_by("created_month")}
nest={
"by_month": lambda t: (
t.group_by("created_month")
.aggregate(
order_count=lambda t: t.order_count.sum(),
total_sales=lambda t: t.total_sales.sum(),
)
.order_by("created_month")
)
}
)
.order_by("created_year")
)
Expand Down Expand Up @@ -109,7 +118,17 @@ result = (
.aggregate(
year_order_count=lambda t: t.order_count.sum(),
year_total_sales=lambda t: t.total_sales.sum(),
nest={"by_status": lambda t: t.group_by(["status", "order_count", "total_sales", "avg_price"]).order_by(xo.desc("total_sales"))}
nest={
"by_status": lambda t: (
t.group_by("status")
.aggregate(
order_count=lambda t: t.order_count.sum(),
total_sales=lambda t: t.total_sales.sum(),
avg_price=lambda t: t.avg_price.mean(),
)
.order_by(lambda t: t.total_sales.desc())
)
}
)
.order_by("created_year")
)
Expand Down Expand Up @@ -138,7 +157,14 @@ monthly_with_status = (
.aggregate(
month_order_count=lambda t: t.order_count.sum(),
month_total_sales=lambda t: t.total_sales.sum(),
nest={"by_status": lambda t: t.group_by(["status", "order_count", "total_sales"])}
nest={
"by_status": lambda t: (
t.group_by("status").aggregate(
order_count=lambda t: t.order_count.sum(),
total_sales=lambda t: t.total_sales.sum(),
)
)
}
)
)

Expand All @@ -149,7 +175,11 @@ result = (
.aggregate(
year_order_count=lambda t: t.month_order_count.sum(),
year_total_sales=lambda t: t.month_total_sales.sum(),
nest={"by_month": lambda t: t.group_by(["created_month", "month_order_count", "month_total_sales", "by_status"]).order_by("created_month")}
nest={
"by_month": lambda t: t.group_by(
"created_month", "month_order_count", "month_total_sales", "by_status"
)
}
)
.order_by("created_year")
.limit(3)
Expand Down
10 changes: 6 additions & 4 deletions docs/md/doc/query-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,19 +297,21 @@ result = (

**How it works:**
- The `nest` parameter accepts a dictionary: `{"column_name": lambda t: ...}`
- The lambda specifies which columns to collect using `.group_by()` or `.select()`
- A bare `.group_by()` in the lambda specifies which row fields to collect
- Results in an array of structs column named `flights`

You can also use `.select()` to specify which columns to nest:
To nest selected row-level fields, use the bare `group_by(...)` form. In a
`nest` lambda this selects the struct fields to collect; it does not collapse
duplicate source rows:

```query_nest_select
# Nest specific columns
# Nest specific row-level fields
result = (
flights_st
.group_by("carrier")
.aggregate(
"flight_count",
nest={"routes": lambda t: t.select("origin", "distance", "duration")}
nest={"routes": lambda t: t.group_by("origin", "distance", "duration")}
)
)
```
Expand Down
30 changes: 7 additions & 23 deletions docs/md/doc/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ Methods for composing semantic tables through joins.
join_many(
other: SemanticTable,
on: Callable,
how: str = "left",
name: str = None
) -> SemanticTable
```
Expand All @@ -179,6 +180,7 @@ One-to-many relationship join (LEFT JOIN). Use when the left table can match mul
|-----------|------|-------------|
| `other` | `SemanticTable` | The semantic table to join with |
| `on` | `Callable` | Lambda function defining the join condition |
| `how` | `str` | Join type; only `'left'` is supported |
| `name` | `str` | Optional name for the joined table reference |

**Example:**
Expand All @@ -196,11 +198,13 @@ flights_st = flights_st.join_many(
join_one(
other: SemanticTable,
on: Callable,
how: str = "left",
name: str = None
) -> SemanticTable
```

One-to-one relationship join (INNER JOIN). Use when each row in the left table matches exactly one row in the right table.
One-to-one relationship join (LEFT JOIN). Use when each left row can match at
most one row in the right table. Only `how="left"` is supported.

**Example:**
```python
Expand All @@ -221,26 +225,6 @@ join_cross(

Cross join (CARTESIAN PRODUCT). Creates all possible combinations of rows from both tables.

### join()

```python
join(
other: SemanticTable,
on: Callable,
how: str = "inner",
name: str = None
) -> SemanticTable
```

Custom join with flexible join type. Supports 'inner', 'left', 'right', 'outer', and 'cross'.

| Parameter | Type | Description |
|-----------|------|-------------|
| `other` | `SemanticTable` | The semantic table to join with |
| `on` | `Callable` | Lambda function defining the join condition |
| `how` | `str` | Join type: 'inner', 'left', 'right', 'outer', or 'cross' |
| `name` | `str` | Optional name for the joined table reference |

## Query Methods

Methods for querying and transforming semantic tables.
Expand Down Expand Up @@ -369,7 +353,7 @@ Create nested data structures within aggregations.
aggregate(
*measures,
nest={
"nested_column": lambda t: t.group_by([...]) | t.select(...)
"nested_column": lambda t: t.group_by(...)
}
)
```
Expand Down Expand Up @@ -531,7 +515,7 @@ model_name:
join_name:
model: model_reference
on: join_condition
how: join_type # left, inner, right, outer, cross
how: left # Optional; non-cross semantic joins are always left joins
```

#### Expression Syntax
Expand Down
32 changes: 10 additions & 22 deletions docs/md/doc/semantic-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,9 @@ After joining, all dimensions and measures from both tables are available. Each

### join_one() - One-to-One Relationships

Use `join_one()` when rows have a unique matching relationship (INNER JOIN).
Use `join_one()` when rows have a unique matching relationship. Like all
non-cross semantic joins, it uses a LEFT JOIN so unmatched left rows remain
visible to measures.

```python
# Many flights → one carrier (each flight has exactly one carrier)
Expand Down Expand Up @@ -330,34 +332,20 @@ Use `join_cross()` to create every possible combination of rows from both tables
all_combinations = flights_st.join_cross(carriers)
```

### join() - Custom Join Conditions
### Requiring a Match

Use `join()` for complex join conditions or specific SQL join types.
`join_one()` and `join_many()` only support `how="left"`. When a query should
require a match, make the row removal explicit with a filter on a non-nullable
field from the right table:

```python
# LEFT JOIN with custom condition
flights_with_carriers = flights_st.join(
flights_matched = flights_st.join_one(
carriers,
lambda f, c: f.carrier == c.code,
how="left"
)

# INNER JOIN
flights_matched = flights_st.join(
carriers,
lambda f, c: f.carrier == c.code,
how="inner"
)

# Complex conditions
date_range_join = flights_st.join(
promotions,
lambda f, p: (f.date >= p.start_date) & (f.date <= p.end_date),
how="left"
)
).filter(lambda t: t["carriers.name"].notnull())
```

**Supported join types:** `"inner"`, `"left"`, `"right"`, `"outer"`, `"cross"`
Use `join_cross()` for Cartesian products.

## Next Steps

Expand Down
2 changes: 1 addition & 1 deletion docs/md/prompts/build/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ flights_with_carriers = flights_st.join_many(
)
```

### join_one() - One-to-One (INNER JOIN)
### join_one() - One-to-One (LEFT JOIN)

```python
# Each flight has exactly one carrier
Expand Down
2 changes: 1 addition & 1 deletion docs/md/skills/claude-code/bsl-model-builder/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ flights_with_carriers = flights_st.join_many(
)
```

### join_one() - One-to-One (INNER JOIN)
### join_one() - One-to-One (LEFT JOIN)

```python
# Each flight has exactly one carrier
Expand Down
2 changes: 1 addition & 1 deletion docs/md/skills/codex/bsl-model-builder.codex
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ flights_with_carriers = flights_st.join_many(
)
```

### join_one() - One-to-One (INNER JOIN)
### join_one() - One-to-One (LEFT JOIN)

```python
# Each flight has exactly one carrier
Expand Down
2 changes: 1 addition & 1 deletion docs/md/skills/cursor/bsl-model-builder.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ flights_with_carriers = flights_st.join_many(
)
```

### join_one() - One-to-One (INNER JOIN)
### join_one() - One-to-One (LEFT JOIN)

```python
# Each flight has exactly one carrier
Expand Down
25 changes: 23 additions & 2 deletions src/boring_semantic_layer/agents/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@
from boring_semantic_layer.utils import safe_eval


def _models_ibis_module(models: dict) -> Any:
"""Return the ibis module matching the models' table flavor.

Model tables are converted to xorq's vendored ibis at construction when
xorq is installed; query strings evaluated against them must build
literals/expressions with the same flavor (``ibis.literal``,
``ibis.cases``, ...) or comparisons silently mis-compose.
"""
from boring_semantic_layer.nested_compile import get_ibis_module

for model in models.values():
table = getattr(model, "table", None)
if table is not None:
return get_ibis_module(table)
return ibis


@cache
def _get_md_dir() -> Path:
"""Get the directory containing markdown documentation files.
Expand Down Expand Up @@ -282,14 +299,18 @@ def _query_model(
chart_format: str | None = None,
chart_spec: dict | None = None,
) -> str:
from ibis import _
from returns.result import Failure, Success

# Extract model name for error context
model_name = self._extract_model_name(query)

try:
result = safe_eval(query, context={**self.models, "ibis": ibis, "_": _})
# Match the models' ibis flavor so agent-built literals
# (ibis.literal, ibis.cases, ...) compose with the tables.
ibis_module = _models_ibis_module(self.models)
result = safe_eval(
query, context={**self.models, "ibis": ibis_module, "_": ibis_module._}
)
if isinstance(result, Failure):
raise result.failure()
query_result = result.unwrap() if isinstance(result, Success) else result
Expand Down
4 changes: 2 additions & 2 deletions src/boring_semantic_layer/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def join_one(
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 - "left", "inner", "right", or "outer" (default: "left")
how: Join type. Only ``"left"`` is supported.

Returns:
Joined SemanticModel
Expand All @@ -85,7 +85,7 @@ def join_many(
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 - "inner", "left", "right", or "outer" (default: "left")
how: Join type. Only ``"left"`` is supported.

Returns:
Joined SemanticModel
Expand Down
Loading
Loading