|
7 | 7 |
|
8 | 8 | import pytest |
9 | 9 |
|
| 10 | +from sidemantic import Dimension, Metric, Model, Relationship, SemanticLayer |
| 11 | + |
10 | 12 | # Mark all tests in this module as integration tests |
11 | 13 | pytestmark = [ |
12 | 14 | pytest.mark.integration, |
|
22 | 24 |
|
23 | 25 |
|
24 | 26 | @pytest.fixture(scope="module") |
25 | | -def bigquery_adapter(): |
26 | | - """Create BigQuery adapter connected to emulator.""" |
27 | | - from sidemantic.db.bigquery import BigQueryAdapter |
28 | | - |
29 | | - # For emulator, we need to set BIGQUERY_EMULATOR_HOST |
| 27 | +def bigquery_layer(): |
| 28 | + """Create SemanticLayer with BigQuery connection.""" |
| 29 | + # Set emulator host |
30 | 30 | emulator_host = os.getenv("BIGQUERY_EMULATOR_HOST", "localhost:9050") |
31 | 31 | os.environ["BIGQUERY_EMULATOR_HOST"] = emulator_host |
32 | 32 |
|
33 | | - adapter = BigQueryAdapter(project_id=BIGQUERY_PROJECT, dataset_id=BIGQUERY_DATASET) |
34 | | - yield adapter |
35 | | - adapter.close() |
| 33 | + # Create layer with BigQuery URL |
| 34 | + layer = SemanticLayer(connection=f"bigquery://{BIGQUERY_PROJECT}/{BIGQUERY_DATASET}") |
| 35 | + yield layer |
| 36 | + layer.adapter.close() |
| 37 | + |
| 38 | + |
| 39 | +def test_semantic_layer_basic_metric(bigquery_layer): |
| 40 | + """Test basic metric query with SemanticLayer.""" |
| 41 | + # Define a model using a CTE as table |
| 42 | + orders = Model( |
| 43 | + name="orders", |
| 44 | + table="(SELECT 1 as order_id, 100.0 as amount UNION ALL SELECT 2, 200.0 UNION ALL SELECT 3, 150.0)", |
| 45 | + primary_key="order_id", |
| 46 | + metrics=[ |
| 47 | + Metric(name="total_revenue", agg="sum", sql="amount"), |
| 48 | + Metric(name="avg_revenue", agg="avg", sql="amount"), |
| 49 | + Metric(name="order_count", agg="count", sql="order_id"), |
| 50 | + ], |
| 51 | + ) |
| 52 | + bigquery_layer.add_model(orders) |
36 | 53 |
|
| 54 | + # Test sum |
| 55 | + result = bigquery_layer.query(metrics=["orders.total_revenue"]) |
| 56 | + row = result.fetchone() |
| 57 | + assert row[0] == 450.0 |
| 58 | + |
| 59 | + # Test avg |
| 60 | + result = bigquery_layer.query(metrics=["orders.avg_revenue"]) |
| 61 | + row = result.fetchone() |
| 62 | + assert row[0] == 150.0 |
37 | 63 |
|
38 | | -def test_bigquery_adapter_execute(bigquery_adapter): |
39 | | - """Test basic query execution.""" |
40 | | - result = bigquery_adapter.execute("SELECT 1 as x, 2 as y") |
| 64 | + # Test count |
| 65 | + result = bigquery_layer.query(metrics=["orders.order_count"]) |
41 | 66 | row = result.fetchone() |
42 | | - assert row == (1, 2) |
43 | | - |
44 | | - |
45 | | -def test_bigquery_adapter_aggregations(bigquery_adapter): |
46 | | - """Test aggregations in queries.""" |
47 | | - result = bigquery_adapter.execute(""" |
48 | | - SELECT |
49 | | - SUM(x) as total, |
50 | | - AVG(x) as average, |
51 | | - MAX(x) as maximum, |
52 | | - MIN(x) as minimum, |
53 | | - COUNT(*) as count |
54 | | - FROM (SELECT 1 as x UNION ALL SELECT 2 UNION ALL SELECT 3) |
55 | | - """) |
| 67 | + assert row[0] == 3 |
| 68 | + |
| 69 | + |
| 70 | +def test_semantic_layer_dimension_grouping(bigquery_layer): |
| 71 | + """Test querying with dimensions and grouping.""" |
| 72 | + sales = Model( |
| 73 | + name="sales", |
| 74 | + table="""( |
| 75 | + SELECT 'Electronics' as category, 'US' as region, 100 as amount UNION ALL |
| 76 | + SELECT 'Electronics', 'EU', 150 UNION ALL |
| 77 | + SELECT 'Clothing', 'US', 200 UNION ALL |
| 78 | + SELECT 'Clothing', 'EU', 250 |
| 79 | + )""", |
| 80 | + primary_key="category", |
| 81 | + dimensions=[ |
| 82 | + Dimension(name="category", type="categorical"), |
| 83 | + Dimension(name="region", type="categorical"), |
| 84 | + ], |
| 85 | + metrics=[Metric(name="total_sales", agg="sum", sql="amount")], |
| 86 | + ) |
| 87 | + bigquery_layer.add_model(sales) |
| 88 | + |
| 89 | + # Group by one dimension |
| 90 | + result = bigquery_layer.query(metrics=["sales.total_sales"], dimensions=["sales.category"]) |
| 91 | + rows = result.fetchall() |
| 92 | + cols = [desc[0] for desc in result.description] |
| 93 | + results = [dict(zip(cols, row)) for row in rows] |
| 94 | + results_dict = {r["category"]: r["total_sales"] for r in results} |
| 95 | + |
| 96 | + assert results_dict["Electronics"] == 250 |
| 97 | + assert results_dict["Clothing"] == 450 |
| 98 | + |
| 99 | + |
| 100 | +def test_semantic_layer_joins(bigquery_layer): |
| 101 | + """Test joins between models.""" |
| 102 | + customers_join = Model( |
| 103 | + name="customers_join", |
| 104 | + table="""( |
| 105 | + SELECT 1 as customer_id, 'Alice' as name, 'US' as region UNION ALL |
| 106 | + SELECT 2, 'Bob', 'EU' |
| 107 | + )""", |
| 108 | + primary_key="customer_id", |
| 109 | + dimensions=[Dimension(name="region", type="categorical")], |
| 110 | + ) |
| 111 | + |
| 112 | + orders_join = Model( |
| 113 | + name="orders_join", |
| 114 | + table="""( |
| 115 | + SELECT 1 as order_id, 1 as customer_id, 100.0 as amount UNION ALL |
| 116 | + SELECT 2, 1, 150.0 UNION ALL |
| 117 | + SELECT 3, 2, 200.0 |
| 118 | + )""", |
| 119 | + primary_key="order_id", |
| 120 | + metrics=[Metric(name="total_revenue", agg="sum", sql="amount")], |
| 121 | + relationships=[Relationship(name="customers_join", type="many_to_one", foreign_key="customer_id")], |
| 122 | + ) |
| 123 | + |
| 124 | + bigquery_layer.add_model(customers_join) |
| 125 | + bigquery_layer.add_model(orders_join) |
| 126 | + |
| 127 | + # Query with join |
| 128 | + result = bigquery_layer.query(metrics=["orders_join.total_revenue"], dimensions=["customers_join.region"]) |
| 129 | + rows = result.fetchall() |
| 130 | + cols = [desc[0] for desc in result.description] |
| 131 | + results_dict = {dict(zip(cols, row))["region"]: dict(zip(cols, row))["total_revenue"] for row in rows} |
| 132 | + |
| 133 | + assert results_dict["US"] == 250.0 |
| 134 | + assert results_dict["EU"] == 200.0 |
| 135 | + |
| 136 | + |
| 137 | +def test_semantic_layer_multiple_metrics(bigquery_layer): |
| 138 | + """Test multiple metrics in one query.""" |
| 139 | + products = Model( |
| 140 | + name="products", |
| 141 | + table="""( |
| 142 | + SELECT 1 as product_id, 100 as price, 5 as quantity UNION ALL |
| 143 | + SELECT 2, 200, 3 UNION ALL |
| 144 | + SELECT 3, 150, 7 |
| 145 | + )""", |
| 146 | + primary_key="product_id", |
| 147 | + metrics=[ |
| 148 | + Metric(name="total_price", agg="sum", sql="price"), |
| 149 | + Metric(name="avg_price", agg="avg", sql="price"), |
| 150 | + Metric(name="total_quantity", agg="sum", sql="quantity"), |
| 151 | + Metric(name="product_count", agg="count", sql="product_id"), |
| 152 | + ], |
| 153 | + ) |
| 154 | + bigquery_layer.add_model(products) |
| 155 | + |
| 156 | + result = bigquery_layer.query( |
| 157 | + metrics=["products.total_price", "products.avg_price", "products.total_quantity", "products.product_count"] |
| 158 | + ) |
56 | 159 | row = result.fetchone() |
57 | 160 | cols = [desc[0] for desc in result.description] |
58 | 161 | row_dict = dict(zip(cols, row)) |
59 | 162 |
|
60 | | - assert row_dict["total"] == 6 |
61 | | - assert row_dict["average"] == 2.0 |
62 | | - assert row_dict["maximum"] == 3 |
63 | | - assert row_dict["minimum"] == 1 |
64 | | - assert row_dict["count"] == 3 |
| 163 | + assert row_dict["total_price"] == 450 |
| 164 | + assert row_dict["avg_price"] == 150.0 |
| 165 | + assert row_dict["total_quantity"] == 15 |
| 166 | + assert row_dict["product_count"] == 3 |
| 167 | + |
| 168 | + |
| 169 | +def test_semantic_layer_filters(bigquery_layer): |
| 170 | + """Test filtering with WHERE clause.""" |
| 171 | + inventory = Model( |
| 172 | + name="inventory", |
| 173 | + table="""( |
| 174 | + SELECT 1 as item_id, 'A' as category, 100 as quantity UNION ALL |
| 175 | + SELECT 2, 'A', 200 UNION ALL |
| 176 | + SELECT 3, 'B', 150 UNION ALL |
| 177 | + SELECT 4, 'B', 250 |
| 178 | + )""", |
| 179 | + primary_key="item_id", |
| 180 | + dimensions=[Dimension(name="category", type="categorical")], |
| 181 | + metrics=[Metric(name="total_quantity", agg="sum", sql="quantity")], |
| 182 | + ) |
| 183 | + bigquery_layer.add_model(inventory) |
| 184 | + |
| 185 | + # Test with filter in SQL |
| 186 | + result = bigquery_layer.query( |
| 187 | + metrics=["inventory.total_quantity"], |
| 188 | + dimensions=["inventory.category"], |
| 189 | + filters=["inventory.category = 'A'"], |
| 190 | + ) |
| 191 | + rows = result.fetchall() |
| 192 | + assert len(rows) == 1 |
| 193 | + assert rows[0][1] == 300 # Total for category A |
| 194 | + |
| 195 | + |
| 196 | +def test_semantic_layer_sql_generation(bigquery_layer): |
| 197 | + """Test SQL generation without executing.""" |
| 198 | + metrics_model = Model( |
| 199 | + name="metrics_sql", |
| 200 | + table="(SELECT 1 as id, 100 as value)", |
| 201 | + primary_key="id", |
| 202 | + metrics=[Metric(name="total", agg="sum", sql="value")], |
| 203 | + ) |
| 204 | + bigquery_layer.add_model(metrics_model) |
| 205 | + |
| 206 | + sql = bigquery_layer.compile(metrics=["metrics_sql.total"]) |
| 207 | + assert "SELECT" in sql.upper() |
| 208 | + assert "SUM" in sql.upper() |
| 209 | + assert bigquery_layer.dialect == "bigquery" |
0 commit comments