Skip to content

Commit 574c6de

Browse files
cpsievertclaude
andauthored
feat: Add Snowflake Semantic Views support (#200)
* feat: Add Snowflake Semantic Views support Add automatic detection and context injection for Snowflake Semantic Views when a Snowflake connection is provided. This helps LLMs generate correct queries using the SEMANTIC_VIEW() function with certified business metrics. Changes: - Add SnowflakeSource class (Python and R) that extends SQLAlchemySource/DBISource - Discover semantic views via SHOW SEMANTIC VIEWS at initialization - Retrieve DDL definitions via GET_DDL('SEMANTIC_VIEW', ...) - Include semantic view context in schema output - Add SEMANTIC_VIEW() syntax reference to system prompt - Add Snowflake-specific SQL tips (QUALIFY, LATERAL FLATTEN, time travel) - Graceful fallback when no semantic views exist or discovery fails Based on the Snowflake skill from posit-dev/databot#278. Co-Authored-By: Claude Opus 4.5 <[email protected]> * `air format` (GitHub Actions) * fix: Address Snowflake Semantic Views code review concerns - Fix SQL injection risk by escaping single quotes in view names - Add discover_semantic_views parameter for lazy initialization - Remove error swallowing, let errors propagate for debugging - Add debug logging when no semantic views are found - Move logger placement to top of file (Python) - Add defensive dialect check in normalize_data_source (Python) - Add comprehensive unit tests for both Python and R Co-Authored-By: Claude Opus 4.5 <[email protected]> * `air format` (GitHub Actions) * refactor: Extract Snowflake semantic views into adapter pattern Introduces a Protocol-based adapter pattern for Snowflake semantic view discovery that works with both SQLAlchemy and Ibis backends: - Add _snowflake.py with RawSQLExecutor Protocol, executor implementations (SQLAlchemyExecutor, IbisExecutor), and standalone discovery functions - Add _snowflake_sources.py with SnowflakeSource and new SnowflakeIbisSource - Update normalize_data_source() to route Ibis Snowflake backends - Maintain backwards-compatible imports from _datasource.py This enables semantic view support for Ibis connections to Snowflake, not just SQLAlchemy connections. Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore: Remove backwards-compat re-exports from _datasource.py Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Move Snowflake semantic view discovery into base classes Instead of separate SnowflakeSource and SnowflakeIbisSource classes, SQLAlchemySource and IbisSource now auto-detect Snowflake backends and discover semantic views during initialization. Changes: - SQLAlchemySource/IbisSource check dialect/backend name for "snowflake" - Discovery can be disabled via QUERYCHAT_DISABLE_SEMANTIC_VIEWS env var - Removed _snowflake_sources.py (no longer needed) - Simplified normalize_data_source() - no Snowflake-specific routing - Updated tests to verify new architecture Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Move SEMANTIC_VIEW_SYNTAX to shared prompt files Extract inline syntax documentation to `prompts/semantic-view-syntax.md` in both R and Python packages, making it language-agnostic and easier to maintain. Co-Authored-By: Claude Opus 4.5 <[email protected]> * `air format` (GitHub Actions) * Update pkg-py/src/querychat/_querychat_base.py * refactor: Extract Snowflake semantic views into adapter pattern - Replace Protocol/class-based OOP with functional approach using backend_type: Literal["sqlalchemy", "ibis"] discriminator - Move env var check (QUERYCHAT_DISABLE_SEMANTIC_VIEWS) into discover_semantic_views() for early exit - Move semantic view discovery from __init__ to get_schema() for lazy initialization - Remove SemanticViewMixin in favor of direct function calls - Update tests to verify new lazy discovery behavior Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore: Remove is_snowflake template variable Snowflake SQL Tips section isn't necessary for semantic views support. Removed the {{#is_snowflake}} block from prompt templates and the is_snowflake variable from system prompt code in both R and Python. Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Use isinstance() checks instead of type parameter Replace backend_type: Literal["sqlalchemy", "ibis"] parameter with isinstance(backend, sqlalchemy.Engine) checks for cleaner API. Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore: Remove _semantic_views class attribute declarations Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Merge SnowflakeSource semantic view logic into DBISource Move semantic view discovery from separate SnowflakeSource class into DBISource to match Python implementation: - Add semantic_views field and has_semantic_views() method to DBISource - Move is_snowflake_connection(), discover_semantic_views_impl(), get_semantic_view_ddl(), format_semantic_views_section() to DBISource.R - Add QUERYCHAT_DISABLE_SEMANTIC_VIEWS env var support for R - Delete SnowflakeSource.R - Update normalize_data_source() to use DBISource for all DBI connections - Update QueryChatSystemPrompt to check DBISource for semantic views Co-Authored-By: Claude Opus 4.5 <[email protected]> * (local) * chore: Minimize docstrings and remove redundant comments - Use single-line docstrings for internal functions - Remove redundant inline comments Co-Authored-By: Claude Opus 4.5 <[email protected]> * `air format` (GitHub Actions) * `devtools::document()` (GitHub Actions) * refactor: Remove _semantic_views attribute and has_semantic_views property - Simplify get_schema() to discover semantic views inline without storing - Detect semantic views in system prompt by checking schema string - Remove unnecessary state management Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Consolidate semantic view information into single prompt section Move semantic view DDLs out of <database_schema> into a dedicated <semantic_views> section, placing all semantic view info in one cohesive location in the prompt template. Structure is now: - General explanation (why semantic views matter) - Query syntax documentation - <semantic_views> tag with table-specific DDL definitions Changes: - Remove semantic view info from get_schema() output - Add get_semantic_view_ddls() method to return just DDL content - Add semantic_view_ddls template variable to prompt context - Update prompt.md to include <semantic_views> tag after syntax docs - Rename format_semantic_views_section to format_semantic_view_ddls Co-Authored-By: Claude Opus 4.5 <[email protected]> * `air format` (GitHub Actions) * `devtools::document()` (GitHub Actions) * refactor: Add semantic view methods to DataSource base class - Add has_semantic_views() and get_semantic_view_ddls() to DataSource with default implementations returning False and empty string - Remove hasattr check in _system_prompt.py since methods now exist on base class - Revert superfluous formatting changes from ruff Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Move semantic view discovery into get_semantic_view_ddls Extract discovery logic into _ensure_semantic_views_discovered() method called from has_semantic_views() and get_semantic_view_ddls(). This: - Removes discovery from get_schema() as requested - Eliminates the type: ignore by ensuring _semantic_views is always a list after discovery Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Remove has_semantic_views(), improve prompt text - Remove has_semantic_views() method from DataSource classes - Use truthy check on get_semantic_view_ddls() instead - Update prompt text with improved explanation and real-world example that better illustrates why semantic views matter Co-Authored-By: Claude Opus 4.5 <[email protected]> * `devtools::document()` (GitHub Actions) * refactor: Simplify semantic views, adjust prompt structure - Change "### Semantic Views" to "## Semantic Views" (top-level section) - Move "use SEMANTIC_VIEW() instead of raw SQL" into prompt.md - Adjust header levels in semantic-view-syntax.md accordingly - Remove _semantic_views attribute from datasource classes - compute directly in get_semantic_view_ddls() without caching Co-Authored-By: Claude Opus 4.5 <[email protected]> * Apply suggestions from code review * refactor: Restructure semantic views into dedicated directory - Move semantic view prompts to prompts/semantic-views/ directory - prompt.md: Contains IMPORTANT notice and real-world example - syntax.md: Contains SEMANTIC_VIEW() query syntax reference - Rename get_semantic_view_ddls() to get_semantic_views_section() which now returns the complete assembled section - Simplify main prompt.md to use single {{{semantic_views}}} placeholder - Remove type ignore in _snowflake.py by using itertuples instead of to_dict(orient="records") - Update IMPORTANT paragraph to include SEMANTIC_VIEW() instruction inline per PR feedback - Update tests to reflect new method names and structure Co-Authored-By: Claude Opus 4.5 <[email protected]> * `air format` (GitHub Actions) * `devtools::document()` (GitHub Actions) * style: Revert formatting changes in DBISource.R Minimize diff by reverting air format changes that were not part of the semantic views feature. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: Use raw_sql() for Ibis backends to support SHOW commands The `backend.sql()` method in Ibis parses queries with sqlglot, which doesn't support Snowflake commands like `SHOW SEMANTIC VIEWS`. Switch to using `backend.raw_sql()` which executes queries without parsing. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: Address PR feedback and fix pyright error - Remove comment about raw_sql() per PR feedback - Add type ignore for raw_sql() to fix pyright error (method exists on concrete backends but not typed on SQLBackend base class) - Remove unused PROMPTS_DIR constant per PR feedback Co-Authored-By: Claude Opus 4.5 <[email protected]> * Add brand.yml website dependency * refactor: Rename semantic views methods for clarity - Rename get_semantic_views_section() to get_semantic_views_description() on DataSource classes (clearer intent) - Rename get_semantic_views_section() to format_semantic_views() in _snowflake.py / DBISource.R (matches other format_* functions) - Update tests to use new method names Co-Authored-By: Claude Opus 4.5 <[email protected]> * `devtools::document()` (GitHub Actions) * style: Revert formatting changes and _shiny.py modification Minimize diff by reverting auto-formatter changes that were not part of the semantic views feature. Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs: Add Snowflake Semantic Views to CHANGELOG and NEWS Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Update docstrings and revert formatting changes - Update get_semantic_views_description() docstrings to clarify purpose - Revert list comprehension formatting to match original style Co-Authored-By: Claude Opus 4.5 <[email protected]> * `devtools::document()` (GitHub Actions) * style: Revert list comprehension formatting to original style Co-Authored-By: Claude Opus 4.5 <[email protected]> * style: Revert unnecessary R formatting changes Minimize diff by reverting air format changes that were not part of the semantic views feature. Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore: Remove accidentally committed files --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: cpsievert <[email protected]>
1 parent f3a4360 commit 574c6de

File tree

19 files changed

+1139
-9
lines changed

19 files changed

+1139
-9
lines changed

.github/workflows/docs-r-pkgdown.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848

4949
- uses: r-lib/actions/setup-r-dependencies@v2
5050
with:
51-
extra-packages: any::pkgdown, local::.
51+
extra-packages: any::pkgdown, any::brand.yml, local::.
5252
needs: website
5353
working-directory: pkg-r
5454

pkg-py/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### New features
11+
12+
* Added support for Snowflake Semantic Views. When connected to Snowflake (via SQLAlchemy or Ibis), querychat automatically discovers available Semantic Views and includes their definitions in the system prompt. This helps the LLM generate correct queries using the `SEMANTIC_VIEW()` table function with certified business metrics and dimensions. (#200)
13+
814
## [0.5.1] - 2026-01-23
915

1016
### New features

pkg-py/src/querychat/_datasource.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
from sqlalchemy.sql import sqltypes
1212

1313
from ._df_compat import read_sql
14+
from ._snowflake import (
15+
discover_semantic_views,
16+
format_semantic_views,
17+
)
1418
from ._utils import as_narwhals, check_query
1519

1620
if TYPE_CHECKING:
@@ -179,6 +183,10 @@ def cleanup(self) -> None:
179183
180184
"""
181185

186+
def get_semantic_views_description(self) -> str:
187+
"""Get information about semantic views (if any) for the system prompt."""
188+
return ""
189+
182190

183191
class DataFrameSource(DataSource[IntoDataFrameT]):
184192
"""A DataSource implementation that wraps a DataFrame using DuckDB."""
@@ -489,6 +497,13 @@ def get_schema(self, *, categorical_threshold: int) -> str:
489497
self._add_column_stats(columns, categorical_threshold)
490498
return format_schema(self.table_name, columns)
491499

500+
def get_semantic_views_description(self) -> str:
501+
"""Get information about semantic views (if any) for the system prompt."""
502+
if self._engine.dialect.name.lower() != "snowflake":
503+
return ""
504+
views = discover_semantic_views(self._engine)
505+
return format_semantic_views(views)
506+
492507
@staticmethod
493508
def _make_column_meta(name: str, sa_type: sqltypes.TypeEngine) -> ColumnMeta:
494509
"""Create ColumnMeta from SQLAlchemy type."""
@@ -895,8 +910,7 @@ def _add_column_stats(
895910

896911
# Find text columns that qualify as categorical
897912
categorical_cols = [
898-
col
899-
for col in columns
913+
col for col in columns
900914
if col.kind == "text"
901915
and (nunique := stats.get(f"{col.name}__nunique"))
902916
and nunique <= categorical_threshold
@@ -960,6 +974,13 @@ def get_schema(self, *, categorical_threshold: int) -> str:
960974
self._add_column_stats(columns, self._table, categorical_threshold)
961975
return format_schema(self.table_name, columns)
962976

977+
def get_semantic_views_description(self) -> str:
978+
"""Get information about semantic views (if any) for the system prompt."""
979+
if self._backend.name.lower() != "snowflake":
980+
return ""
981+
views = discover_semantic_views(self._backend)
982+
return format_semantic_views(views)
983+
963984
@staticmethod
964985
def _make_column_meta(name: str, dtype: IbisDataType) -> ColumnMeta:
965986
"""Create ColumnMeta from an ibis dtype."""
@@ -1018,8 +1039,7 @@ def _add_column_stats(
10181039
col.max_val = stats.get(f"{col.name}__max")
10191040

10201041
categorical_cols = [
1021-
col
1022-
for col in columns
1042+
col for col in columns
10231043
if col.kind == "text"
10241044
and (nunique := stats.get(f"{col.name}__nunique"))
10251045
and nunique <= categorical_threshold

pkg-py/src/querychat/_snowflake.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
Snowflake-specific utilities for semantic view discovery.
3+
4+
This module provides functions for discovering Snowflake Semantic Views,
5+
supporting both SQLAlchemy engines and Ibis backends via isinstance() checks.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
import os
12+
from dataclasses import dataclass
13+
from typing import TYPE_CHECKING, Any
14+
15+
import sqlalchemy
16+
17+
if TYPE_CHECKING:
18+
from ibis.backends.sql import SQLBackend
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
@dataclass
24+
class SemanticViewInfo:
25+
"""Metadata for a Snowflake Semantic View."""
26+
27+
name: str
28+
"""Fully qualified name (database.schema.view_name)."""
29+
30+
ddl: str
31+
"""The DDL definition from GET_DDL()."""
32+
33+
34+
def execute_raw_sql(
35+
query: str,
36+
backend: sqlalchemy.Engine | SQLBackend,
37+
) -> list[dict[str, Any]]:
38+
"""Execute raw SQL and return results as list of row dicts."""
39+
if isinstance(backend, sqlalchemy.Engine):
40+
with backend.connect() as conn:
41+
result = conn.execute(sqlalchemy.text(query))
42+
keys = list(result.keys())
43+
return [dict(zip(keys, row, strict=False)) for row in result.fetchall()]
44+
else:
45+
with backend.raw_sql(query) as cursor: # type: ignore[union-attr]
46+
columns = [desc[0] for desc in cursor.description]
47+
return [dict(zip(columns, row, strict=False)) for row in cursor.fetchall()]
48+
49+
50+
def discover_semantic_views(
51+
backend: sqlalchemy.Engine | SQLBackend,
52+
) -> list[SemanticViewInfo]:
53+
"""Discover semantic views in the current schema."""
54+
if os.environ.get("QUERYCHAT_DISABLE_SEMANTIC_VIEWS"):
55+
return []
56+
57+
rows = execute_raw_sql("SHOW SEMANTIC VIEWS", backend)
58+
59+
if not rows:
60+
logger.debug("No semantic views found in current schema")
61+
return []
62+
63+
views: list[SemanticViewInfo] = []
64+
for row in rows:
65+
db = row.get("database_name")
66+
schema = row.get("schema_name")
67+
name = row.get("name")
68+
69+
if not name:
70+
continue
71+
72+
fq_name = f"{db}.{schema}.{name}"
73+
ddl = get_semantic_view_ddl(backend, fq_name)
74+
if ddl:
75+
views.append(SemanticViewInfo(name=fq_name, ddl=ddl))
76+
77+
return views
78+
79+
80+
def get_semantic_view_ddl(
81+
backend: sqlalchemy.Engine | SQLBackend,
82+
fq_name: str,
83+
) -> str | None:
84+
"""Get DDL for a semantic view by fully qualified name."""
85+
safe_name = fq_name.replace("'", "''")
86+
rows = execute_raw_sql(f"SELECT GET_DDL('SEMANTIC_VIEW', '{safe_name}')", backend)
87+
if rows:
88+
return str(next(iter(rows[0].values())))
89+
return None
90+
91+
92+
def format_semantic_view_ddls(semantic_views: list[SemanticViewInfo]) -> str:
93+
"""Format just the DDL definitions for semantic views."""
94+
lines: list[str] = []
95+
96+
for sv in semantic_views:
97+
lines.append(f"### Semantic View: `{sv.name}`")
98+
lines.append("")
99+
lines.append("```sql")
100+
lines.append(sv.ddl)
101+
lines.append("```")
102+
lines.append("")
103+
104+
return "\n".join(lines)
105+
106+
107+
def format_semantic_views(semantic_views: list[SemanticViewInfo]) -> str:
108+
"""Build the complete semantic views section for the prompt."""
109+
if not semantic_views:
110+
return ""
111+
112+
from importlib.resources import files
113+
114+
prompts = files("querychat.prompts.semantic-views")
115+
prompt_text = (prompts / "prompt.md").read_text()
116+
syntax_text = (prompts / "syntax.md").read_text()
117+
ddls_text = format_semantic_view_ddls(semantic_views)
118+
119+
return f"""{prompt_text}
120+
121+
{syntax_text}
122+
123+
<semantic_views>
124+
{ddls_text}
125+
</semantic_views>
126+
"""

pkg-py/src/querychat/_system_prompt.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ def render(self, tools: tuple[TOOL_GROUPS, ...] | None) -> str:
6565
Fully rendered system prompt string
6666
6767
"""
68-
is_duck_db = self.data_source.get_db_type().lower() == "duckdb"
68+
db_type = self.data_source.get_db_type()
69+
is_duck_db = db_type.lower() == "duckdb"
6970

7071
context = {
71-
"db_type": self.data_source.get_db_type(),
72+
"db_type": db_type,
7273
"is_duck_db": is_duck_db,
74+
"semantic_views": self.data_source.get_semantic_views_description(),
7375
"schema": self.schema,
7476
"data_description": self.data_description,
7577
"extra_instructions": self.extra_instructions,

pkg-py/src/querychat/prompts/prompt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ quantile_cont(salary, 0.5)
7171
```
7272

7373
{{/is_duck_db}}
74+
{{{semantic_views}}}
7475
## Your Capabilities
7576

7677
You can handle these types of requests:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Semantic Views
2+
3+
**IMPORTANT**: This database has Semantic Views available. Semantic Views provide a curated layer over raw data with pre-defined metrics, dimensions, and relationships. They encode business logic and calculation rules that ensure consistent, accurate results. When a Semantic View covers the data you need, prefer it over raw tables to benefit from these certified definitions (that is, use the `SEMANTIC_VIEW()` table function where appropriate when generating SQL).
4+
5+
**Real-world example**: A legacy ERP database had a revenue column (`X_AMT`) with hidden business rules—only status code 90 transactions count as realized revenue, and a discount factor (`ADJ_FCTR`) must be applied. Querying raw tables for "external customer revenue" returned **$184B**. The same query using the semantic model's certified `NET_REVENUE` metric returned **$84.5B**—the correct answer. The raw query was **2x+ too high** because it ignored discounts and included invalid transaction codes.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
### SEMANTIC_VIEW() Query Syntax
2+
3+
#### Basic Syntax
4+
5+
```sql
6+
SELECT * FROM SEMANTIC_VIEW(
7+
{view_name}
8+
METRICS {logical_table}.{metric_name}
9+
DIMENSIONS {logical_table}.{dimension_name}
10+
[WHERE {dimension} = 'value'] -- Optional: pre-aggregation filter
11+
)
12+
[WHERE {column} = 'value'] -- Optional: post-aggregation filter
13+
```
14+
15+
#### Key Rules
16+
17+
1. **Use `SEMANTIC_VIEW()` function** - Not direct SELECT FROM the view
18+
2. **No GROUP BY needed** - Semantic layer handles aggregation via DIMENSIONS
19+
3. **No JOINs needed within model** - Relationships are pre-defined
20+
4. **No aggregate functions needed** - Metrics are pre-aggregated
21+
5. **Use DDL-defined names** - Metrics and dimensions must match the DDL exactly
22+
23+
#### WHERE Clause: Inside vs Outside
24+
25+
- **Inside** (pre-aggregation): Filters base data BEFORE metrics are computed
26+
- **Outside** (post-aggregation): Filters results AFTER metrics are computed
27+
28+
```sql
29+
-- Pre-aggregation: only include 'EXT' accounts in the calculation
30+
SELECT * FROM SEMANTIC_VIEW(
31+
MODEL_NAME
32+
METRICS T_DATA.NET_REVENUE
33+
DIMENSIONS REF_ENTITIES.ACC_TYPE_CD
34+
WHERE REF_ENTITIES.ACC_TYPE_CD = 'EXT'
35+
)
36+
37+
-- Post-aggregation: compute all, then filter results
38+
SELECT * FROM SEMANTIC_VIEW(
39+
MODEL_NAME
40+
METRICS T_DATA.NET_REVENUE
41+
DIMENSIONS REF_ENTITIES.ACC_TYPE_CD
42+
)
43+
WHERE NET_REVENUE > 1000000
44+
```
45+
46+
#### Common Patterns
47+
48+
**Single metric (total):**
49+
```sql
50+
SELECT * FROM SEMANTIC_VIEW(MODEL_NAME METRICS T_DATA.NET_REVENUE)
51+
```
52+
53+
**Metric by dimension:**
54+
```sql
55+
SELECT * FROM SEMANTIC_VIEW(
56+
MODEL_NAME
57+
METRICS T_DATA.NET_REVENUE
58+
DIMENSIONS REF_ENTITIES.ACC_TYPE_CD
59+
)
60+
```
61+
62+
**Multiple metrics and dimensions:**
63+
```sql
64+
SELECT * FROM SEMANTIC_VIEW(
65+
MODEL_NAME
66+
METRICS T_DATA.NET_REVENUE, T_DATA.GROSS_REVENUE
67+
DIMENSIONS REF_ENTITIES.ACC_TYPE_CD, T_DATA.LOG_DT
68+
)
69+
ORDER BY LOG_DT ASC
70+
```
71+
72+
**Time series:**
73+
```sql
74+
SELECT * FROM SEMANTIC_VIEW(
75+
MODEL_NAME
76+
METRICS T_DATA.NET_REVENUE
77+
DIMENSIONS T_DATA.LOG_DT
78+
)
79+
ORDER BY LOG_DT ASC
80+
```
81+
82+
**Join results with other data:**
83+
```sql
84+
SELECT sv.*, lookup.category_name
85+
FROM SEMANTIC_VIEW(
86+
MODEL_NAME
87+
METRICS T_DATA.NET_REVENUE
88+
DIMENSIONS REF_ENTITIES.ACC_TYPE_CD
89+
) AS sv
90+
JOIN category_lookup AS lookup ON sv.ACC_TYPE_CD = lookup.code
91+
```
92+
93+
#### Troubleshooting
94+
95+
- **"Invalid identifier"**: Verify metric/dimension names match exactly what's in the DDL
96+
- **Syntax error**: Use SEMANTIC_VIEW() function, GROUP BY isn't needed

0 commit comments

Comments
 (0)