boring-semantic-layer 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boring_semantic_layer-0.0.1/.codespell.ignore-words +0 -0
- boring_semantic_layer-0.0.1/.envrc +5 -0
- boring_semantic_layer-0.0.1/.envrc.user +1 -0
- boring_semantic_layer-0.0.1/.gitignore +11 -0
- boring_semantic_layer-0.0.1/.gitignore.template +5 -0
- boring_semantic_layer-0.0.1/.pre-commit-config.yaml +33 -0
- boring_semantic_layer-0.0.1/PKG-INFO +495 -0
- boring_semantic_layer-0.0.1/README.md +481 -0
- boring_semantic_layer-0.0.1/examples/basic_example.py +71 -0
- boring_semantic_layer-0.0.1/examples/example_flight_query.py +44 -0
- boring_semantic_layer-0.0.1/examples/example_flight_semantic_model.py +53 -0
- boring_semantic_layer-0.0.1/examples/example_query_join_many.py +79 -0
- boring_semantic_layer-0.0.1/examples/materialize_example.py +45 -0
- boring_semantic_layer-0.0.1/examples/mcp_example.py +237 -0
- boring_semantic_layer-0.0.1/flake.lock +99 -0
- boring_semantic_layer-0.0.1/flake.nix +360 -0
- boring_semantic_layer-0.0.1/pyproject.toml +34 -0
- boring_semantic_layer-0.0.1/requirements-dev.txt +275 -0
- boring_semantic_layer-0.0.1/src/boring_semantic_layer/__init__.py +2 -0
- boring_semantic_layer-0.0.1/src/boring_semantic_layer/semantic_model.py +985 -0
- boring_semantic_layer-0.0.1/tests/test_join_factories.py +121 -0
- boring_semantic_layer-0.0.1/tests/test_malloy_style_joins.py +141 -0
- boring_semantic_layer-0.0.1/tests/test_materialize.py +122 -0
- boring_semantic_layer-0.0.1/tests/test_semantic_model.py +887 -0
- boring_semantic_layer-0.0.1/tt.md +0 -0
- boring_semantic_layer-0.0.1/uv.lock +1550 -0
|
File without changes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
if ! has nix_direnv_version || ! nix_direnv_version 2.4.0; then
|
|
2
|
+
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.4.0/direnvrc" "sha256-XQzUAvL6pysIJnRJyR7uVpmUSZfc7LSgWQwq/4mBr1U="
|
|
3
|
+
fi
|
|
4
|
+
source_env_if_exists .envrc.secrets
|
|
5
|
+
source_env_if_exists .envrc.user
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
use flake .#editable
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
ci:
|
|
2
|
+
autofix_commit_msg: "style: auto fixes from pre-commit.ci hooks"
|
|
3
|
+
autofix_prs: false
|
|
4
|
+
autoupdate_commit_msg: "chore(deps): pre-commit.ci autoupdate"
|
|
5
|
+
skip:
|
|
6
|
+
- ruff
|
|
7
|
+
default_stages:
|
|
8
|
+
- pre-commit
|
|
9
|
+
repos:
|
|
10
|
+
- repo: https://github.com/codespell-project/codespell
|
|
11
|
+
rev: v2.4.1
|
|
12
|
+
hooks:
|
|
13
|
+
- id: codespell
|
|
14
|
+
additional_dependencies:
|
|
15
|
+
- tomli
|
|
16
|
+
args: [--ignore-words=.codespell.ignore-words]
|
|
17
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
18
|
+
# Ruff version.
|
|
19
|
+
rev: v0.11.13
|
|
20
|
+
hooks:
|
|
21
|
+
# Run the linter.
|
|
22
|
+
- id: ruff
|
|
23
|
+
args: ["--output-format=full", "--fix", "src", "examples", "tests"]
|
|
24
|
+
# Run the formatter.
|
|
25
|
+
- id: ruff-format
|
|
26
|
+
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
27
|
+
# uv version.
|
|
28
|
+
rev: 0.7.13
|
|
29
|
+
hooks:
|
|
30
|
+
# Update the uv lockfile
|
|
31
|
+
- id: uv-lock
|
|
32
|
+
- id: uv-export
|
|
33
|
+
args: ["--frozen", "--no-hashes", "--no-emit-project", "--all-groups", "--all-extras", "--output-file=requirements-dev.txt"]
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boring-semantic-layer
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A boring semantic layer built with ibis
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: ibis-framework>=10.6.0
|
|
7
|
+
Requires-Dist: urllib3>=2.2.3
|
|
8
|
+
Provides-Extra: examples
|
|
9
|
+
Requires-Dist: duckdb>=1.3.1; extra == 'examples'
|
|
10
|
+
Requires-Dist: xorq>=2.2.0; extra == 'examples'
|
|
11
|
+
Provides-Extra: xorq
|
|
12
|
+
Requires-Dist: xorq>=2.2.0; extra == 'xorq'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Boring Semantic Layer (BSL)
|
|
16
|
+
|
|
17
|
+
The Boring Semantic Layer (BSL) is a lightweight semantic layer based on [Ibis](https://ibis-project.org/).
|
|
18
|
+
|
|
19
|
+
**Key Features:**
|
|
20
|
+
- **Lightweight**: `pip install boring-semantic-layer`
|
|
21
|
+
- **Ibis-powered**: Built on top of [Ibis](https://ibis-project.org/), supporting any database engine that Ibis integrates with (DuckDB, Snowflake, BigQuery, PostgreSQL, and more)
|
|
22
|
+
- **MCP-friendly**: Perfect for connecting Large Language Models to structured data sources
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
*This project is a joint effort by [xorq-labs](https://github.com/xorq-labs) and [boringdata](https://www.boringdata.io/).*
|
|
26
|
+
|
|
27
|
+
We welcome feedback and contributions!
|
|
28
|
+
|
|
29
|
+
# Quick Example
|
|
30
|
+
|
|
31
|
+
**1. Define your ibis input table**
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import ibis
|
|
35
|
+
|
|
36
|
+
flights_tbl = ibis.table(
|
|
37
|
+
name="flights",
|
|
38
|
+
schema={"origin": "string", "carrier": "string"}
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**2. Define a semantic model**
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from boring_semantic_layer import SemanticModel
|
|
46
|
+
|
|
47
|
+
flights_sm = SemanticModel(
|
|
48
|
+
table=flights_tbl,
|
|
49
|
+
dimensions={"origin": lambda t: t.origin},
|
|
50
|
+
measures={"flight_count": lambda t: t.count()}
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**3. Query it**
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
flights_sm.query(
|
|
58
|
+
dimensions=["origin"],
|
|
59
|
+
measures=["flight_count"]
|
|
60
|
+
).execute()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Example output (dataframe):**
|
|
64
|
+
|
|
65
|
+
| origin | flight_count |
|
|
66
|
+
|--------|--------------|
|
|
67
|
+
| JFK | 3689 |
|
|
68
|
+
| LGA | 2941 |
|
|
69
|
+
| ... | ... |
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
-----
|
|
73
|
+
|
|
74
|
+
## Table of Contents
|
|
75
|
+
|
|
76
|
+
- [Installation](#installation)
|
|
77
|
+
- [Get Started](#get-started)
|
|
78
|
+
1. [Get Sample Data](#1-get-sample-data)
|
|
79
|
+
2. [Build a Semantic Model](#2-build-a-semantic-model)
|
|
80
|
+
3. [Query a Semantic Model](#3-query-a-semantic-model)
|
|
81
|
+
- [Features](#features)
|
|
82
|
+
- [Filters](#filters)
|
|
83
|
+
- [Ibis Expression](#ibis-expression)
|
|
84
|
+
- [JSON-based (MCP & LLM friendly)](#json-based-mcp-llm-friendly)
|
|
85
|
+
- [Time-Based Dimensions and Queries](#time-based-dimensions-and-queries)
|
|
86
|
+
- [Joins Across Semantic Models](#joins-across-semantic-models)
|
|
87
|
+
- [Classic SQL Joins](#classic-sql-joins)
|
|
88
|
+
- [join_one](#join_one)
|
|
89
|
+
- [join_many](#join_many)
|
|
90
|
+
- [join_cross](#join_cross)
|
|
91
|
+
- [Reference](#reference)
|
|
92
|
+
- [SemanticModel](#semanticmodel)
|
|
93
|
+
- [Query (SemanticModel.query / QueryExpr)](#query-semanticmodelquery--queryexpr)
|
|
94
|
+
|
|
95
|
+
-----
|
|
96
|
+
|
|
97
|
+
## Installation
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install boring-semantic-layer
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
-----
|
|
104
|
+
|
|
105
|
+
## Get Started
|
|
106
|
+
|
|
107
|
+
### 1. Get Sample Data
|
|
108
|
+
|
|
109
|
+
We'll use a public flight dataset from the [Malloy Samples repository](https://github.com/malloydata/malloy-samples/tree/main/data).
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git clone https://github.com/malloydata/malloy-samples
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 2. Build a Semantic Model
|
|
116
|
+
|
|
117
|
+
Define your data source and create a semantic model that describes your data in terms of dimensions and measures.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import ibis
|
|
121
|
+
from boring_semantic_layer import SemanticModel
|
|
122
|
+
|
|
123
|
+
# Connect to your database (here, DuckDB in-memory for demo)
|
|
124
|
+
con = ibis.duckdb.connect(":memory:")
|
|
125
|
+
flights_tbl = con.read_parquet("malloy-samples/data/flights.parquet")
|
|
126
|
+
|
|
127
|
+
# Define the semantic model
|
|
128
|
+
flights_sm = SemanticModel(
|
|
129
|
+
name="flights",
|
|
130
|
+
table=flights_tbl,
|
|
131
|
+
dimensions={
|
|
132
|
+
'origin': lambda t: t.origin,
|
|
133
|
+
'destination': lambda t: t.dest,
|
|
134
|
+
'year': lambda t: t.year
|
|
135
|
+
},
|
|
136
|
+
measures={
|
|
137
|
+
'total_flights': lambda t: t.count(),
|
|
138
|
+
'total_distance': lambda t: t.distance.sum(),
|
|
139
|
+
'avg_distance': lambda t: t.distance.mean(),
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **Dimensions** are attributes to group or filter by (e.g., origin, destination).
|
|
145
|
+
- **Measures** are aggregations or calculations (e.g., total flights, average distance).
|
|
146
|
+
|
|
147
|
+
All dimensions and measures are defined as Ibis expressions.
|
|
148
|
+
|
|
149
|
+
Ibis expressions are Python functions that represent database operations.
|
|
150
|
+
|
|
151
|
+
They allow you to write database queries using familiar Python syntax while Ibis handles the translation to optimized SQL for your specific database backend (like DuckDB, PostgreSQL, BigQuery, etc.).
|
|
152
|
+
|
|
153
|
+
For example, in our semantic model:
|
|
154
|
+
|
|
155
|
+
- `lambda t: t.origin` is an Ibis expression that references the "origin" column
|
|
156
|
+
- `lambda t: t.count()` is an Ibis expression that counts rows
|
|
157
|
+
- `lambda t: t.distance.mean()` is an Ibis expression that calculates the average distance
|
|
158
|
+
|
|
159
|
+
The `t` parameter represents the table, and you can chain operations like `t.origin.upper()` or `t.dep_delay > 0` to create complex expressions. Ibis ensures these expressions are translated to efficient SQL queries.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### 3. Query a Semantic Model
|
|
164
|
+
|
|
165
|
+
Use your semantic model to run queries—selecting dimensions, measures, and applying filters or limits.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
flights_sm.query(
|
|
169
|
+
dimensions=['origin'],
|
|
170
|
+
measures=['total_flights', 'avg_distance'],
|
|
171
|
+
limit=10
|
|
172
|
+
).execute()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Example output:
|
|
176
|
+
|
|
177
|
+
| origin | total_flights | avg_distance |
|
|
178
|
+
|--------|---------------|--------------|
|
|
179
|
+
| JFK | 3689 | 1047.71 |
|
|
180
|
+
| PHL | 7708 | 1044.97 |
|
|
181
|
+
| ... | ... | ... |
|
|
182
|
+
|
|
183
|
+
-----
|
|
184
|
+
|
|
185
|
+
## Features
|
|
186
|
+
|
|
187
|
+
### Filters
|
|
188
|
+
|
|
189
|
+
#### Ibis Expression
|
|
190
|
+
|
|
191
|
+
The `query` method can filter data using raw Ibis expressions for full flexibility.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
flights_sm.query(
|
|
195
|
+
dimensions=['origin'],
|
|
196
|
+
measures=['total_flights'],
|
|
197
|
+
filters=[
|
|
198
|
+
lambda t: t.origin == 'JFK'
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
| origin | total_flights |
|
|
205
|
+
|--------|---------------|
|
|
206
|
+
| JFK | 3689 |
|
|
207
|
+
|
|
208
|
+
#### JSON-based (MCP & LLM friendly)
|
|
209
|
+
|
|
210
|
+
A format that's easy to serialize, good for dynamic queries or LLM integration.
|
|
211
|
+
```python
|
|
212
|
+
flights_sm.query(
|
|
213
|
+
dimensions=['origin'],
|
|
214
|
+
measures=['total_flights'],
|
|
215
|
+
filters=[
|
|
216
|
+
{
|
|
217
|
+
'operator': 'AND',
|
|
218
|
+
'conditions': [
|
|
219
|
+
{'field': 'origin', 'operator': 'in', 'values': ['JFK', 'LGA', 'PHL']},
|
|
220
|
+
{'field': 'total_flights', 'operator': '>', 'value': 5000}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
).execute()
|
|
225
|
+
```
|
|
226
|
+
BSL supports the following operators: `=`, `!=`, `>`, `>=`, `in`, `not in`, `like`, `not like`, `is null`, `is not null`, `AND`, `OR`
|
|
227
|
+
|
|
228
|
+
### Time-Based Dimensions and Queries
|
|
229
|
+
|
|
230
|
+
BSL has built-in support for flexible time-based analysis.
|
|
231
|
+
|
|
232
|
+
To use it, define a `time_dimension` in your `SemanticModel` that points to a timestamp column.
|
|
233
|
+
|
|
234
|
+
You can also set `smallest_time_grain` to prevent incorrect time aggregations.
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
flights_sm_with_time = SemanticModel(
|
|
238
|
+
name="flights_timed",
|
|
239
|
+
table=flights_tbl,
|
|
240
|
+
dimensions={
|
|
241
|
+
'origin': lambda t: t.origin,
|
|
242
|
+
'destination': lambda t: t.dest,
|
|
243
|
+
'year': lambda t: t.year,
|
|
244
|
+
},
|
|
245
|
+
measures={
|
|
246
|
+
'total_flights': lambda t: t.count(),
|
|
247
|
+
},
|
|
248
|
+
time_dimension='dep_time', # The column containing timestamps. Crucial for time-based queries.
|
|
249
|
+
smallest_time_grain='TIME_GRAIN_SECOND' # Optional: sets the lowest granularity (e.g., DAY, MONTH).
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# With the time dimension defined, you can query using a specific time range and grain.
|
|
253
|
+
query_time_based_df = flights_sm_with_time.query(
|
|
254
|
+
dims=['origin'],
|
|
255
|
+
measures=['total_flights'],
|
|
256
|
+
time_range={'start': '2013-01-01', 'end': '2013-01-31'},
|
|
257
|
+
time_grain='TIME_GRAIN_DAY' # Use specific TIME_GRAIN constants
|
|
258
|
+
).execute()
|
|
259
|
+
|
|
260
|
+
print(query_time_based_df)
|
|
261
|
+
```
|
|
262
|
+
Example output:
|
|
263
|
+
|
|
264
|
+
| origin | arr_time | flight_count |
|
|
265
|
+
|--------|------------|--------------|
|
|
266
|
+
| PHL | 2013-01-01 | 5 |
|
|
267
|
+
| CLE | 2013-01-01 | 5 |
|
|
268
|
+
| DFW | 2013-01-01 | 7 |
|
|
269
|
+
| DFW | 2013-01-02 | 9 |
|
|
270
|
+
| DFW | 2013-01-03 | 13 |
|
|
271
|
+
|
|
272
|
+
### Joins Across Semantic Models
|
|
273
|
+
|
|
274
|
+
BSL allows you to join multiple `SemanticModel` instances to enrich your data. Joins are defined in the `joins` parameter of a `SemanticModel`.
|
|
275
|
+
|
|
276
|
+
There are four main ways to define joins:
|
|
277
|
+
|
|
278
|
+
#### Classic SQL Joins
|
|
279
|
+
|
|
280
|
+
For full control, you can create a `Join` object directly, specifying the join condition with an `on` lambda function and the join type with `how` (e.g., `'inner'`, `'left'`).
|
|
281
|
+
|
|
282
|
+
First, let's define two semantic models: one for flights and one for carriers.
|
|
283
|
+
|
|
284
|
+
The flight model resulting from a join with the carriers model:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from boring_semantic_layer import Join, SemanticModel
|
|
288
|
+
import ibis
|
|
289
|
+
import os
|
|
290
|
+
|
|
291
|
+
# Assume `con` is an existing Ibis connection from the Quickstart example.
|
|
292
|
+
con = ibis.duckdb.connect(":memory:")
|
|
293
|
+
|
|
294
|
+
# Load the required tables from the sample data
|
|
295
|
+
flights_tbl = con.read_parquet("malloy-samples/data/flights.parquet")
|
|
296
|
+
carriers_tbl = con.read_parquet("malloy-samples/data/carriers.parquet")
|
|
297
|
+
|
|
298
|
+
# First, define the 'carriers' semantic model to join with.
|
|
299
|
+
carriers_sm = SemanticModel(
|
|
300
|
+
name="carriers",
|
|
301
|
+
table=carriers_tbl,
|
|
302
|
+
dimensions={
|
|
303
|
+
"code": lambda t: t.code,
|
|
304
|
+
"name": lambda t: t.name,
|
|
305
|
+
"nickname": lambda t: t.nickname,
|
|
306
|
+
},
|
|
307
|
+
measures={
|
|
308
|
+
"carrier_count": lambda t: t.count(),
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Now, define the 'flights' semantic model with a join to 'carriers'
|
|
313
|
+
flight_sm = SemanticModel(
|
|
314
|
+
name="flights",
|
|
315
|
+
table=flights_tbl,
|
|
316
|
+
dimensions={
|
|
317
|
+
"origin": lambda t: t.origin,
|
|
318
|
+
"destination": lambda t: t.destination,
|
|
319
|
+
"carrier": lambda t: t.carrier, # This is the join key
|
|
320
|
+
},
|
|
321
|
+
measures={
|
|
322
|
+
"flight_count": lambda t: t.count(),
|
|
323
|
+
},
|
|
324
|
+
joins={
|
|
325
|
+
"carriers": Join(
|
|
326
|
+
model=carriers_sm,
|
|
327
|
+
on=lambda left, right: left.carrier == right.code,
|
|
328
|
+
),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Querying across the joined models to get flight counts by carrier name
|
|
333
|
+
query_joined_df = flight_sm.query(
|
|
334
|
+
dims=['carriers.name', 'origin'],
|
|
335
|
+
measures=['flight_count'],
|
|
336
|
+
limit=10
|
|
337
|
+
).execute()
|
|
338
|
+
```
|
|
339
|
+
| carriers_name | origin | flight_count |
|
|
340
|
+
|---------------------------|--------|--------------|
|
|
341
|
+
| Delta Air Lines | MDT | 235 |
|
|
342
|
+
| Delta Air Lines | ATL | 8419 |
|
|
343
|
+
| Comair (Delta Connections)| ATL | 239 |
|
|
344
|
+
| American Airlines | DFW | 8742 |
|
|
345
|
+
| American Eagle Airlines | JFK | 418 |
|
|
346
|
+
|
|
347
|
+
#### join_one
|
|
348
|
+
|
|
349
|
+
For common join patterns, BSL provides helper class methods inspired by [Malloy](https://docs.malloydata.dev/documentation/language/join): `Join.one`, `Join.many`, and `Join.cross`.
|
|
350
|
+
|
|
351
|
+
These simplify joins based on primary/foreign key relationships.
|
|
352
|
+
|
|
353
|
+
To use them, first define a `primary_key` on the model you are joining to. The primary key should be one of the model's dimensions.
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
carriers_pk_sm = SemanticModel(
|
|
357
|
+
name="carriers",
|
|
358
|
+
table=con.read_parquet("malloy-samples/data/carriers.parquet"),
|
|
359
|
+
primary_key="code",
|
|
360
|
+
dimensions={
|
|
361
|
+
'code': lambda t: t.code,
|
|
362
|
+
'name': lambda t: t.name
|
|
363
|
+
},
|
|
364
|
+
measures={'carrier_count': lambda t: t.count()}
|
|
365
|
+
)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Now, you can use `Join.one` in the `flights` model to link to `carriers_pk_sm`. The `with_` parameter specifies the foreign key on the `flights` model.
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from boring_semantic_layer import Join
|
|
372
|
+
|
|
373
|
+
flights_with_join_one_sm = SemanticModel(
|
|
374
|
+
name="flights",
|
|
375
|
+
table=flights_tbl,
|
|
376
|
+
dimensions={'origin': lambda t: t.origin},
|
|
377
|
+
measures={'flight_count': lambda t: t.count()},
|
|
378
|
+
joins={
|
|
379
|
+
"carriers": Join.one(
|
|
380
|
+
alias="carriers",
|
|
381
|
+
model=carriers_pk_sm,
|
|
382
|
+
with_=lambda t: t.carrier
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- **`Join.one(alias, model, with_)`**: Use for one-to-one or many-to-one relationships. It joins where the foreign key specified in `with_` matches the `primary_key` of the joined `model`.
|
|
389
|
+
|
|
390
|
+
#### join_many
|
|
391
|
+
|
|
392
|
+
- **`Join.many(alias, model, with_)`**: Similar to `Join.one`, but semantically represents a one-to-many relationship.
|
|
393
|
+
|
|
394
|
+
#### join_cross
|
|
395
|
+
|
|
396
|
+
- **`Join.cross(alias, model)`**: Creates a cross product, joining every row from the left model with every row of the right `model`.
|
|
397
|
+
|
|
398
|
+
Querying remains the same—just reference the joined fields using the alias.
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
flights_with_join_one_sm.query(
|
|
402
|
+
dimensions=["carriers.name"],
|
|
403
|
+
measures=["flight_count"],
|
|
404
|
+
limit=5
|
|
405
|
+
).execute()
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Example output:
|
|
409
|
+
|
|
410
|
+
| carriers_name | flight_count |
|
|
411
|
+
|-------------------------|--------------|
|
|
412
|
+
| Delta Air Lines | 10000 |
|
|
413
|
+
| American Airlines | 9000 |
|
|
414
|
+
| United Airlines | 8500 |
|
|
415
|
+
| Southwest Airlines | 8000 |
|
|
416
|
+
| JetBlue Airways | 7500 |
|
|
417
|
+
|
|
418
|
+
## Reference
|
|
419
|
+
|
|
420
|
+
### SemanticModel
|
|
421
|
+
|
|
422
|
+
| Field | Type | Required | Allowed Values / Notes |
|
|
423
|
+
|----------------------|-------------------------------------------|----------|------------------------------------------------------------------------------------------------------------|
|
|
424
|
+
| `table` | Ibis table expression | Yes | Any Ibis table or view |
|
|
425
|
+
| `dimensions` | dict[str, callable] | Yes | Keys: dimension names; Values: functions mapping table → column |
|
|
426
|
+
| `measures` | dict[str, callable] | Yes | Keys: measure names; Values: functions mapping table → aggregation |
|
|
427
|
+
| `joins` | dict[str, Join] | No | Keys: join alias; Values: `Join` object (see below) |
|
|
428
|
+
| `primary_key` | str | No | Name of the primary key dimension (required for certain join types) |
|
|
429
|
+
| `name` | str | No | Optional model name (inferred from table if omitted) |
|
|
430
|
+
| `time_dimension` | str | No | Name of the column to use as the time dimension |
|
|
431
|
+
| `smallest_time_grain`| str | No | One of:<br>`TIME_GRAIN_SECOND`, `TIME_GRAIN_MINUTE`, `TIME_GRAIN_HOUR`, `TIME_GRAIN_DAY`,<br>`TIME_GRAIN_WEEK`, `TIME_GRAIN_MONTH`, `TIME_GRAIN_QUARTER`, `TIME_GRAIN_YEAR` |
|
|
432
|
+
|
|
433
|
+
#### Join object (for `joins`)
|
|
434
|
+
- Use `Join.one(alias, model, with_)` for one-to-one/many-to-one
|
|
435
|
+
- Use `Join.many(alias, model, with_)` for one-to-many
|
|
436
|
+
- Use `Join.cross(alias, model)` for cross join
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
### Query (SemanticModel.query / QueryExpr)
|
|
441
|
+
|
|
442
|
+
| Parameter | Type | Required | Allowed Values / Notes |
|
|
443
|
+
|----------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------|
|
|
444
|
+
| `dimensions` | list[str] | No | List of dimension names (can include joined fields, e.g. `"carriers.name"`) |
|
|
445
|
+
| `measures` | list[str] | No | List of measure names (can include joined fields) |
|
|
446
|
+
| `filters` | list[dict/str/callable] or dict/str/callable | No | See below for filter formats and operators |
|
|
447
|
+
| `order_by` | list[tuple[str, str]] | No | List of (field, direction) tuples, e.g. `[("avg_delay", "desc")]` |
|
|
448
|
+
| `limit` | int | No | Maximum number of rows to return |
|
|
449
|
+
| `time_range` | dict with `start` and `end` (ISO 8601 strings) | No | Example: `{'start': '2024-01-01', 'end': '2024-12-31'}` |
|
|
450
|
+
| `time_grain` | str | No | One of:<br>`TIME_GRAIN_SECOND`, `TIME_GRAIN_MINUTE`, `TIME_GRAIN_HOUR`, `TIME_GRAIN_DAY`,<br>`TIME_GRAIN_WEEK`, `TIME_GRAIN_MONTH`, `TIME_GRAIN_QUARTER`, `TIME_GRAIN_YEAR` |
|
|
451
|
+
|
|
452
|
+
#### Filters
|
|
453
|
+
|
|
454
|
+
- **Simple filter (dict):**
|
|
455
|
+
```python
|
|
456
|
+
{"field": "origin", "operator": "=", "value": "JFK"}
|
|
457
|
+
```
|
|
458
|
+
- **Compound filter (dict):**
|
|
459
|
+
```python
|
|
460
|
+
{
|
|
461
|
+
"operator": "AND",
|
|
462
|
+
"conditions": [
|
|
463
|
+
{"field": "origin", "operator": "in", "values": ["JFK", "LGA"]},
|
|
464
|
+
{"field": "year", "operator": ">", "value": 2010}
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
- **Callable:** `lambda t: t.origin == 'JFK'`
|
|
469
|
+
- **String:** `"_.origin == 'JFK'"`
|
|
470
|
+
|
|
471
|
+
**Supported operators:** `=`, `!=`, `>`, `>=`, `<`, `<=`, `in`, `not in`, `like`, `not like`, `is null`, `is not null`, `AND`, `OR`
|
|
472
|
+
|
|
473
|
+
#### Example
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
flights_sm.query(
|
|
477
|
+
dimensions=['origin', 'year'],
|
|
478
|
+
measures=['total_flights'],
|
|
479
|
+
filters=[
|
|
480
|
+
{"field": "origin", "operator": "in", "values": ["JFK", "LGA"]},
|
|
481
|
+
{"field": "year", "operator": ">", "value": 2010}
|
|
482
|
+
],
|
|
483
|
+
order_by=[('total_flights', 'desc')],
|
|
484
|
+
limit=10,
|
|
485
|
+
time_range={'start': '2015-01-01', 'end': '2015-12-31'},
|
|
486
|
+
time_grain='TIME_GRAIN_MONTH'
|
|
487
|
+
)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Example output:
|
|
491
|
+
|
|
492
|
+
| origin | year | total_flights |
|
|
493
|
+
|--------|------|---------------|
|
|
494
|
+
| JFK | 2015 | 350 |
|
|
495
|
+
| LGA | 2015 | 300 |
|