Skip to content

Commit cba7b00

Browse files
committed
Merge main, split sql_expr/window_sql_expr, fix mixed metric/window HAVING, remove leaked multistep tests
2 parents 46e024c + 765ae8a commit cba7b00

18 files changed

Lines changed: 1750 additions & 1028 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sidemantic"
3-
version = "0.8.3"
3+
version = "0.8.4"
44
description = "Universal semantic layer - import from Cube, dbt, LookML, Hex, and more"
55
readme = "README.md"
66
license = {file = "LICENSE"}

sidemantic-schema.json

Lines changed: 177 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,19 @@
228228
"Metric": {
229229
"description": "Measure definition - supports simple aggregations and complex metric types.\n\nMeasures can be:\n- Simple aggregations: SUM(amount), COUNT(*), AVG(price)\n- Ratios: revenue / order_count\n- Derived formulas: (revenue - cost) / revenue\n- Cumulative: running totals, period-to-date\n- Time comparisons: YoY, MoM growth\n- Conversion funnels: signup -> purchase rate\n\nAuto-registers as a graph-level metric with the current semantic layer context if available.",
230230
"properties": {
231+
"activity_event": {
232+
"anyOf": [
233+
{
234+
"type": "string"
235+
},
236+
{
237+
"type": "null"
238+
}
239+
],
240+
"default": null,
241+
"description": "SQL filter for activity event (default: any event, e.g., \"event IS NOT NULL\")",
242+
"title": "Activity Event"
243+
},
231244
"agg": {
232245
"anyOf": [
233246
{
@@ -298,6 +311,19 @@
298311
"description": "Comparison calculation (default: percent_change)",
299312
"title": "Calculation"
300313
},
314+
"cohort_event": {
315+
"anyOf": [
316+
{
317+
"type": "string"
318+
},
319+
{
320+
"type": "null"
321+
}
322+
],
323+
"default": null,
324+
"description": "SQL filter for cohort-defining event (e.g., \"event = 'install'\")",
325+
"title": "Cohort Event"
326+
},
301327
"comparison_type": {
302328
"anyOf": [
303329
{
@@ -566,12 +592,43 @@
566592
"description": "Time offset for denominator (e.g., '1 month')",
567593
"title": "Offset Window"
568594
},
595+
"periods": {
596+
"anyOf": [
597+
{
598+
"type": "integer"
599+
},
600+
{
601+
"type": "null"
602+
}
603+
],
604+
"default": null,
605+
"description": "Number of retention periods to compute (e.g., 28 for 28-day)",
606+
"title": "Periods"
607+
},
569608
"public": {
570609
"default": true,
571610
"description": "Whether metric is visible in API/UI",
572611
"title": "Public",
573612
"type": "boolean"
574613
},
614+
"retention_granularity": {
615+
"anyOf": [
616+
{
617+
"enum": [
618+
"day",
619+
"week",
620+
"month"
621+
],
622+
"type": "string"
623+
},
624+
{
625+
"type": "null"
626+
}
627+
],
628+
"default": null,
629+
"description": "Time granularity for retention periods (day, week, month)",
630+
"title": "Retention Granularity"
631+
},
575632
"sql": {
576633
"anyOf": [
577634
{
@@ -606,7 +663,8 @@
606663
"derived",
607664
"cumulative",
608665
"time_comparison",
609-
"conversion"
666+
"conversion",
667+
"retention"
610668
],
611669
"type": "string"
612670
},
@@ -1161,6 +1219,19 @@
11611219
"items": {
11621220
"description": "Measure definition - supports simple aggregations and complex metric types.\n\nMeasures can be:\n- Simple aggregations: SUM(amount), COUNT(*), AVG(price)\n- Ratios: revenue / order_count\n- Derived formulas: (revenue - cost) / revenue\n- Cumulative: running totals, period-to-date\n- Time comparisons: YoY, MoM growth\n- Conversion funnels: signup -> purchase rate\n\nAuto-registers as a graph-level metric with the current semantic layer context if available.",
11631221
"properties": {
1222+
"activity_event": {
1223+
"anyOf": [
1224+
{
1225+
"type": "string"
1226+
},
1227+
{
1228+
"type": "null"
1229+
}
1230+
],
1231+
"default": null,
1232+
"description": "SQL filter for activity event (default: any event, e.g., \"event IS NOT NULL\")",
1233+
"title": "Activity Event"
1234+
},
11641235
"agg": {
11651236
"anyOf": [
11661237
{
@@ -1231,6 +1302,19 @@
12311302
"description": "Comparison calculation (default: percent_change)",
12321303
"title": "Calculation"
12331304
},
1305+
"cohort_event": {
1306+
"anyOf": [
1307+
{
1308+
"type": "string"
1309+
},
1310+
{
1311+
"type": "null"
1312+
}
1313+
],
1314+
"default": null,
1315+
"description": "SQL filter for cohort-defining event (e.g., \"event = 'install'\")",
1316+
"title": "Cohort Event"
1317+
},
12341318
"comparison_type": {
12351319
"anyOf": [
12361320
{
@@ -1499,12 +1583,43 @@
14991583
"description": "Time offset for denominator (e.g., '1 month')",
15001584
"title": "Offset Window"
15011585
},
1586+
"periods": {
1587+
"anyOf": [
1588+
{
1589+
"type": "integer"
1590+
},
1591+
{
1592+
"type": "null"
1593+
}
1594+
],
1595+
"default": null,
1596+
"description": "Number of retention periods to compute (e.g., 28 for 28-day)",
1597+
"title": "Periods"
1598+
},
15021599
"public": {
15031600
"default": true,
15041601
"description": "Whether metric is visible in API/UI",
15051602
"title": "Public",
15061603
"type": "boolean"
15071604
},
1605+
"retention_granularity": {
1606+
"anyOf": [
1607+
{
1608+
"enum": [
1609+
"day",
1610+
"week",
1611+
"month"
1612+
],
1613+
"type": "string"
1614+
},
1615+
{
1616+
"type": "null"
1617+
}
1618+
],
1619+
"default": null,
1620+
"description": "Time granularity for retention periods (day, week, month)",
1621+
"title": "Retention Granularity"
1622+
},
15081623
"sql": {
15091624
"anyOf": [
15101625
{
@@ -1539,7 +1654,8 @@
15391654
"derived",
15401655
"cumulative",
15411656
"time_comparison",
1542-
"conversion"
1657+
"conversion",
1658+
"retention"
15431659
],
15441660
"type": "string"
15451661
},
@@ -1857,6 +1973,19 @@
18571973
"Metric": {
18581974
"description": "Measure definition - supports simple aggregations and complex metric types.\n\nMeasures can be:\n- Simple aggregations: SUM(amount), COUNT(*), AVG(price)\n- Ratios: revenue / order_count\n- Derived formulas: (revenue - cost) / revenue\n- Cumulative: running totals, period-to-date\n- Time comparisons: YoY, MoM growth\n- Conversion funnels: signup -> purchase rate\n\nAuto-registers as a graph-level metric with the current semantic layer context if available.",
18591975
"properties": {
1976+
"activity_event": {
1977+
"anyOf": [
1978+
{
1979+
"type": "string"
1980+
},
1981+
{
1982+
"type": "null"
1983+
}
1984+
],
1985+
"default": null,
1986+
"description": "SQL filter for activity event (default: any event, e.g., \"event IS NOT NULL\")",
1987+
"title": "Activity Event"
1988+
},
18601989
"agg": {
18611990
"anyOf": [
18621991
{
@@ -1927,6 +2056,19 @@
19272056
"description": "Comparison calculation (default: percent_change)",
19282057
"title": "Calculation"
19292058
},
2059+
"cohort_event": {
2060+
"anyOf": [
2061+
{
2062+
"type": "string"
2063+
},
2064+
{
2065+
"type": "null"
2066+
}
2067+
],
2068+
"default": null,
2069+
"description": "SQL filter for cohort-defining event (e.g., \"event = 'install'\")",
2070+
"title": "Cohort Event"
2071+
},
19302072
"comparison_type": {
19312073
"anyOf": [
19322074
{
@@ -2195,12 +2337,43 @@
21952337
"description": "Time offset for denominator (e.g., '1 month')",
21962338
"title": "Offset Window"
21972339
},
2340+
"periods": {
2341+
"anyOf": [
2342+
{
2343+
"type": "integer"
2344+
},
2345+
{
2346+
"type": "null"
2347+
}
2348+
],
2349+
"default": null,
2350+
"description": "Number of retention periods to compute (e.g., 28 for 28-day)",
2351+
"title": "Periods"
2352+
},
21982353
"public": {
21992354
"default": true,
22002355
"description": "Whether metric is visible in API/UI",
22012356
"title": "Public",
22022357
"type": "boolean"
22032358
},
2359+
"retention_granularity": {
2360+
"anyOf": [
2361+
{
2362+
"enum": [
2363+
"day",
2364+
"week",
2365+
"month"
2366+
],
2367+
"type": "string"
2368+
},
2369+
{
2370+
"type": "null"
2371+
}
2372+
],
2373+
"default": null,
2374+
"description": "Time granularity for retention periods (day, week, month)",
2375+
"title": "Retention Granularity"
2376+
},
22042377
"sql": {
22052378
"anyOf": [
22062379
{
@@ -2235,7 +2408,8 @@
22352408
"derived",
22362409
"cumulative",
22372410
"time_comparison",
2238-
"conversion"
2411+
"conversion",
2412+
"retention"
22392413
],
22402414
"type": "string"
22412415
},

sidemantic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import TYPE_CHECKING
44

5-
__version__ = "0.8.3"
5+
__version__ = "0.8.4"
66

77
from sidemantic.core.dimension import Dimension
88
from sidemantic.core.metric import Metric

sidemantic/adapters/sidemantic.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ def _parse_model(self, model_def: dict) -> Model | None:
298298
conversion_event=measure_def.get("conversion_event"),
299299
conversion_window=measure_def.get("conversion_window"),
300300
offset_window=measure_def.get("offset_window"),
301+
# Retention parameters
302+
cohort_event=measure_def.get("cohort_event"),
303+
activity_event=measure_def.get("activity_event"),
304+
periods=measure_def.get("periods"),
305+
retention_granularity=(measure_def.get("retention_granularity") or measure_def.get("granularity"))
306+
if measure_def.get("type") == "retention"
307+
else None,
301308
# Cumulative/window parameters
302309
window=measure_def.get("window"),
303310
grain_to_date=measure_def.get("grain_to_date"),
@@ -414,6 +421,12 @@ def _parse_metric(self, metric_def: dict) -> Metric | None:
414421
conversion_event=metric_def.get("conversion_event"),
415422
conversion_window=metric_def.get("conversion_window"),
416423
offset_window=metric_def.get("offset_window"),
424+
cohort_event=metric_def.get("cohort_event"),
425+
activity_event=metric_def.get("activity_event"),
426+
periods=metric_def.get("periods"),
427+
retention_granularity=(metric_def.get("retention_granularity") or metric_def.get("granularity"))
428+
if metric_type == "retention"
429+
else None,
417430
window=metric_def.get("window"),
418431
grain_to_date=metric_def.get("grain_to_date"),
419432
window_expression=metric_def.get("window_expression"),
@@ -577,6 +590,15 @@ def _export_model(self, model: Model) -> dict:
577590
measure_def["conversion_window"] = measure.conversion_window
578591
if measure.offset_window:
579592
measure_def["offset_window"] = measure.offset_window
593+
# Retention parameters
594+
if measure.cohort_event:
595+
measure_def["cohort_event"] = measure.cohort_event
596+
if measure.activity_event:
597+
measure_def["activity_event"] = measure.activity_event
598+
if measure.periods is not None:
599+
measure_def["periods"] = measure.periods
600+
if measure.retention_granularity:
601+
measure_def["retention_granularity"] = measure.retention_granularity
580602
# Cumulative/window parameters
581603
if measure.window:
582604
measure_def["window"] = measure.window
@@ -658,6 +680,14 @@ def _export_metric(self, measure: Metric, graph) -> dict:
658680
result["conversion_window"] = measure.conversion_window
659681
if measure.offset_window:
660682
result["offset_window"] = measure.offset_window
683+
if measure.cohort_event:
684+
result["cohort_event"] = measure.cohort_event
685+
if measure.activity_event:
686+
result["activity_event"] = measure.activity_event
687+
if measure.periods is not None:
688+
result["periods"] = measure.periods
689+
if measure.retention_granularity:
690+
result["retention_granularity"] = measure.retention_granularity
661691
if measure.sql:
662692
result["sql"] = measure.sql
663693
# Auto-detect and export dependencies for derived measures

sidemantic/cli.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import typer
66

77
from sidemantic import SemanticLayer, __version__, load_from_directory
8-
from sidemantic.config import SidemanticConfig, build_connection_string, find_config, load_config
8+
from sidemantic.config import SidemanticConfig, build_connection_string, find_config, get_init_sql, load_config
99

1010

1111
def version_callback(value: bool):
@@ -348,6 +348,7 @@ def query(
348348
try:
349349
# Build connection string from args or config
350350
connection_str = None
351+
init_sql = None
351352
if connection:
352353
# Explicit --connection arg provided
353354
connection_str = connection
@@ -357,6 +358,7 @@ def query(
357358
elif _loaded_config and _loaded_config.connection:
358359
# Use connection from config
359360
connection_str = build_connection_string(_loaded_config)
361+
init_sql = get_init_sql(_loaded_config)
360362
else:
361363
# Try to find database file in data/
362364
data_dir = models / "data"
@@ -369,7 +371,9 @@ def query(
369371
preagg_db = _loaded_config.preagg_database if _loaded_config else None
370372
preagg_sch = _loaded_config.preagg_schema if _loaded_config else None
371373
if connection_str:
372-
layer = SemanticLayer(connection=connection_str, preagg_database=preagg_db, preagg_schema=preagg_sch)
374+
layer = SemanticLayer(
375+
connection=connection_str, preagg_database=preagg_db, preagg_schema=preagg_sch, init_sql=init_sql
376+
)
373377
else:
374378
layer = SemanticLayer(preagg_database=preagg_db, preagg_schema=preagg_sch)
375379
load_from_directory(layer, str(models))

0 commit comments

Comments
 (0)