关于 omni-to-snowflake-semantic-view
此技能指导将 Omni 主题转换为 Snowflake Semantic View YAML 定义,适用于在 Snowflake 中建模 Omni 数据的用户,涵盖模型探索、字段与关系映射及 SQL 执行。
主要功能
- 01探索 Omni 模型与主题
- 02处理字段纳入和排除规则
- 03映射维度、度量和连接关系
- 04提供 Snowflake SQL 执行指引
适用场景
- 01将 Omni 主题转换为 Snowflake Semantic View
- 02创建视图前核对源表与连接关系
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ exploreomni/ omni-agent-skills --skill omni-to-snowflake-semantic-view安装后在 Claude Code 中输入/omni-to-snowflake-semantic-view确认已加载。
详细介绍
Omni → Snowflake Semantic View
先通过 API 探索 Omni 模型,再将其定义转换为 Snowflake Semantic View 格式,从而把 Omni 主题转换为 Snowflake Semantic View YAML 定义。
前置条件
# Verify the Omni CLI is installed — if not, ask the user to install it
# See: https://github.com/exploreomni/cli#readme
command -v omni >/dev/null || echo "ERROR: Omni CLI is not installed."
# Show available profiles and select the appropriate one
omni config show
# If multiple profiles exist, ask the user which to use, then switch:
omni config use <profile-name>
# Confirm the active profile is authenticated and inspect your permissions:
omni whoami whoami
身份验证:配置文件使用 API 密钥或 OAuth 进行身份验证。如果
whoami(或任何调用)返回 401,请转交处理——让用户运行! omni config login <profile>(OAuth 2.1 浏览器流程;会在浏览器中阻塞约 2 分钟)。在无头/CI 会话中不要自行运行config login(没有浏览器会导致超时);在本地交互式机器上则可以运行。有关配置文件设置(omni config init --auth oauth),以及使用--schema探索命令和请求正文格式,请参阅omni-api-conventions规则。
提示:使用
-o json强制输出结构化数据,以便以编程方式解析;使用-o human输出易读表格。默认值为auto(在 TTY 中输出人类可读格式,通过管道传递时输出 JSON)。
运行时环境检测
开始前,确定你正在使用的环境——这决定了在第 7 步中如何对 Snowflake 执行 SQL。
检测方法
检查是否具备内置的 Snowflake SQL 执行功能:
- Cortex Code(Snowflake Notebooks / Cortex Analyst):支持原生 SQL 执行,无需额外工具。
- Claude Code、Cursor 或任何外部 IDE/Agent:不支持原生 Snowflake 连接。必须使用以下某个 CLI 选项。
检测启发式方法
If the agent can execute a SQL statement like `SELECT CURRENT_USER()` directly without any shell command → you are in Cortex Code.
Otherwise → you are in an external environment.
如果不确定,请询问用户:“你是在 Snowflake(Cortex Code / Notebooks)中运行此流程,还是在 Claude Code 或 Cursor 等外部工具中运行?”
外部环境:Snowflake 连接选项
对于 Claude Code、Cursor 或任何基于终端的 Agent,请选择以下一种方式。按顺序检查是否可用:
选项 A — Snowflake CLI(snow)✅ 推荐
# Check if available
command -v snow
# Configure a connection (first time)
snow connection add
# Execute SQL
snow sql -q "SELECT CURRENT_USER();" --connection <connection_name>
设置默认连接,避免每次调用都重复指定 --connection:
snow connection set-default <connection_name>
选项 B — SnowSQL(经典 CLI)
# Check if available
command -v snowsql
# Execute SQL
snowsql -a <account> -u <user> -q "SELECT CURRENT_USER();"
选项 C — Python(snowflake-connector-python)
pip install snowflake-connector-python
import snowflake.connector
conn = snowflake.connector.connect(
account="<account>",
user="<user>",
password="<password>", # or use key-pair / SSO
warehouse="<warehouse>",
database="<database>",
schema="<schema>",
)
conn.cursor().execute("<SQL here>")
✋ 停止——在继续执行第 7 步之前,先与用户确认 Snowflake 连接方式。记录所用方式,以便第 7 步生成正确的执行命令。
工作流
第 1 步 — 收集需求
询问用户:
- 要转换哪个主题?
- Semantic View 应命名为什么?
- 应在 Snowflake 的哪个位置创建(数据库和架构)?
- 应向哪个角色授予 Semantic View 的访问权限?
⚠️ 停止——继续之前,确认以上四个答案。
第 2 步 — 探索 Omni 模型
2a. 查找模型 ID
omni models list --model-kind SHARED
找到共享模型并记下其 id。始终优先选择共享模型,而不是架构模型或工作簿模型。
2b. 获取主题文件
omni models yaml-get <modelId> --file-name <topic_name>.topic
2c. 获取关系文件
omni models yaml-get <modelId> --file-name relationships
2d. 获取主题中引用的每个视图文件
对于 base_view 和 joins 中的每个视图,获取其 YAML:
omni models yaml-get <modelId> --file-name <view_name>.view
如果视图以
omni_dbt_为前缀,请获取同样以omni_dbt_开头的文件(例如,omni_dbt_ecomm__order_items.view)。
第 3 步 — 识别表和联接
将视图名称映射到 Snowflake 表
主题文件中的 base_view 是主表。将视图名称转换为 Snowflake 表引用:
base_view / 联接值 | Snowflake 表 |
|---|---|
ecomm__order_items | ECOMM.ORDER_ITEMS |
omni_dbt_ecomm__order_items | ECOMM.ORDER_ITEMS(去掉 omni_dbt_ 前缀) |
__ 分隔符分别对应架构(左侧)和表名(右侧)。如果 Snowflake 中不存在该架构,则完全跳过该表。
阅读联接层级
主题中的 joins 参数使用缩进定义联接链——缩进在另一张表下方的表会联接到其父级:
joins:
user_order_facts: {} # skip — this is a derived CTE, not a physical table
ecomm__users: {} # joins to base_view (ORDER_ITEMS)
ecomm__inventory_items: # joins to base_view (ORDER_ITEMS)
ecomm__products: # joins to INVENTORY_ITEMS
demo__product_images: {} # joins to PRODUCTS
ecomm__distribution_centers: {} # joins to PRODUCTS
跳过任何作为派生表的视图(在 Omni 的 SQL 中定义的 CTE)。这些视图没有可供引用的 Snowflake 物理表。
主键
在每个视图文件中,找到 primary_key: true 的维度——它会成为 Semantic View 中 unique: true 的维度。
✋ 停止——继续之前,请与用户确认表列表。
第 4 步 — 确定字段列表
主题的 fields 参数用于控制 Semantic View 中包含哪些视图字段。
字段定位规则
| 语法 | 含义 |
|---|---|
(无 fields 参数) | 包含所有视图中的全部字段 |
all_views.* | 包含所有视图中的全部字段 |
view.* | 包含指定视图中的全部字段 |
tag:<value> | 包含带有此值标签的全部字段 |
view.field | 包含此特定字段 |
-view.field | 排除此特定字段 |
如何正确应用排除项
排除项(以 - 为前缀)必须在解析完所有包含项之后应用。处理流程如下:
- 从空的包含集合开始
- 按顺序处理
fields中的每一项:- 如果是包含规则(
view.*、view.field、tag:x)→ 将匹配的字段添加到集合中 - 如果是排除规则(
-view.field)→ 从集合中移除该字段,即使它已通过通配符添加
- 如果是包含规则(
- 最终集合就是要包含在 Semantic View 中的完整字段列表
示例:
fields:
- ecomm__order_items.* # include all order_items fields
- ecomm__users.country # include this one users field
- -ecomm__order_items.cost # remove cost — excluded even though * was used above
- -ecomm__order_items.raw_json # remove raw_json — same reason
结果:包含 order_items 中除 cost 和 raw_json 之外的所有字段,以及 users.country。
⚠️ 重要:
-排除项始终优先。无论通配符包含了哪些字段,都绝不要包含已明确排除的字段。
第 5 步 — 构建关系
使用第 3 步中的联接层级和第 2c 步获取的 relationships.yaml,将每个联接映射到 Snowflake Semantic View 关系。
relationships.yaml 中的每个条目如下所示:
- join_from_view: ecomm__order_items
join_to_view: ecomm__inventory_items
join_type: always_left
on_sql: ${ecomm__order_items.inventory_item_id} = ${ecomm__inventory_items.id}
relationship_type: assumed_many_to_one
on_sql 字段用于指明联接列。提取列名并填入输出中的 relationship_columns。
可用的关系参数:
| 参数 | 说明 |
|---|---|
join_from_view | 联接的源视图 |
join_to_view | 被联接的目标视图 |
join_type | SQL 联接类型(例如 always_left) |
on_sql | SQL 条件——从中提取列名 |
relationship_type | 基数(例如 assumed_many_to_one) |
reversible | 联接是否为双向 |
where_sql | 联接生效时的附加 WHERE 子句 |
第 6 步 — 映射维度和度量
对于已解析字段列表中的每个视图,将其 Omni 字段定义转换为 Semantic View 条目。
⚠️ 仅转换通过第 4 步包含/排除解析的字段。不要添加已排除的字段。
维度 → dimensions 或 time_dimensions
除非定义了 label,否则字段名将成为维度名。直接沿用 description 和 synonyms。
标准维度:
# Omni view YAML
city:
sql: '"CITY"'
label: City
description: Customer's city
# Semantic View output
- name: city
expr: CITY
description: Customer's city
data_type: TEXT
日期/时间戳维度 → 使用 time_dimensions:
# Omni
created_at:
sql: '"CREATED_AT"'
type: time
label: Created At
# Semantic View output
time_dimensions:
- name: created_at
expr: CREATED_AT
data_type: TIMESTAMP
分组维度 → 转换为 CASE WHEN 表达式:
# Omni
device_type_groups:
sql: ${device_type}
label: Device Type Groups
groups:
- filter:
is: [ mobile, tablet ]
name: Handheld
- filter:
is: desktop
name: Desktop
else: Other
-- expr value
CASE
WHEN "DEVICE_TYPE" IN ('mobile', 'tablet') THEN 'Handheld'
WHEN "DEVICE_TYPE" = 'desktop' THEN 'Desktop'
ELSE 'Other'
END
分箱维度 → 转换为 CASE WHEN 范围表达式:
# Omni
age_bin:
sql: ${age}
bin_boundaries: [ 18, 35, 50, 65 ]
-- expr value
CASE
WHEN "AGE" < 18 THEN 'below 18'
WHEN "AGE" >= 18 AND "AGE" < 35 THEN '>= 18 and < 35'
WHEN "AGE" >= 35 AND "AGE" < 50 THEN '>= 35 and < 50'
WHEN "AGE" >= 50 AND "AGE" < 65 THEN '>= 50 and < 65'
WHEN "AGE" >= 65 THEN '65 and above'
ELSE NULL
END
持续时间维度 → 转换为 TIMESTAMPDIFF 表达式:
# Omni
fulfillment_days:
duration:
sql_start: ${created_at[date]}
sql_end: ${delivered_at[date]}
intervals: [ days ]
-- expr value (days)
CASE
WHEN TIMESTAMPADD(DAY, 1 * TIMESTAMPDIFF(DAY, DATE_TRUNC('DAY', "CREATED_AT"), DATE_TRUNC('DAY', "DELIVERED_AT")), DATE_TRUNC('DAY', "CREATED_AT"))
<= DATE_TRUNC('DAY', "DELIVERED_AT")
THEN TIMESTAMPDIFF(DAY, DATE_TRUNC('DAY', "CREATED_AT"), DATE_TRUNC('DAY', "DELIVERED_AT"))
ELSE TIMESTAMPDIFF(DAY, DATE_TRUNC('DAY', "CREATED_AT"), DATE_TRUNC('DAY', "DELIVERED_AT")) - 1
END
布尔维度 → 转换为命名 filter(而非维度):
# Omni
is_returned:
sql: '"IS_RETURNED"'
description: Whether the item was returned
completed_orders:
sql: ${status} = 'Complete'
label: Completed Orders
# Semantic View output — goes in filters, not dimensions
filters:
- name: is_returned
expr: IS_RETURNED
description: Whether the item was returned
- name: completed_orders
expr: STATUS = 'Complete'
description: Completed Orders
度量 → metrics
sql 字段引用源列,aggregate_type 定义聚合方式。
标准度量:
# Omni
total_sale_price:
sql: ${sale_price}
aggregate_type: sum
label: Total Sale Price
description: Total revenue of orders
synonyms: [ Total Revenue, Total Receipts ]
# Semantic View output
metrics:
- name: total_sale_price
expr: COALESCE(SUM("SALE_PRICE"), 0)
description: Total revenue of orders
synonyms: [ Total Revenue, Total Receipts ]
派生度量(无 aggregate_type)— 引用其他度量:
# Omni
gross_margin:
sql: ${total_sale_price} - ${total_cost}
label: Gross Margin
# Semantic View output — top-level derived metric
metrics:
- name: gross_margin
expr: total_sale_price - total_cost
筛选度量 → 包装在 CASE WHEN 中:
# Omni
california_revenue:
sql: ${sale_price}
aggregate_type: sum
filters:
users.state:
is: California
-- expr value
COALESCE(SUM(CASE WHEN "users"."STATE" = 'California' THEN "SALE_PRICE" ELSE NULL END), 0)
数组和布尔筛选变体:
# is: [New York, New Jersey] → "STATE" IN ('New York', 'New Jersey')
# is: true → field IS TRUE
仅用于筛选的字段(模板化筛选器)
Omni 中仅用于筛选的字段是参数化输入,在视图 YAML 的 filters: 块中定义。它们不映射到真实列,而是通过 Mustache 模板在查询时将动态值注入其他字段的 SQL 中。Snowflake Semantic Views 没有对应的运行时参数系统,因此转换时必须将这些字段解析为静态 SQL。
仅用于筛选的字段定义示例:
# In a view file — under a top-level `filters:` key
filters:
status_filter:
type: string
default_filter:
is: "Complete"
order_date_filter:
type: timestamp
在维度/度量 SQL 中检测以下两种引用模式:
模式 1 — 块(条件)语法:
{{# order_items.status_filter.filter }} order_items.status {{/ order_items.status_filter.filter }}
当用户应用筛选器时,该块才会生效。若未应用筛选器,{{^ }} 块则作为回退:
{{# order_items.status_filter.filter }} order_items.status {{/ order_items.status_filter.filter }}
{{^ order_items.status_filter.filter }} 1=1 {{/ order_items.status_filter.filter }}
模式 2 — 值(直接注入)语法:
-- Direct value substitution
DATE_TRUNC('day', created_at) >= '{{filters.order_items.order_date_filter.value}}'
-- Range variants for date filters
created_at BETWEEN '{{filters.order_items.order_date_filter.range_start}}' AND '{{filters.order_items.order_date_filter.range_end}}'
使用以下特征扫描所有维度和度量 SQL,查找这两种模式:
{{# <view>.<field>.filter }}— 块开始{{^ <view>.<field>.filter }}— 回退块开始{{filters.<view>.<field>.value}}— 直接值{{filters.<view>.<field>.range_start}}/{{filters.<view>.<field>.range_end}}— 日期范围
解析逻辑 — 对找到的每个引用按以下顺序应用:
步骤 1 — 内联 {{^ }} 回退(仅适用于块模式):
如果 SQL 包含 {{^ view.field.filter }} 回退块,则使用回退表达式作为静态 SQL — 这是未应用筛选器时执行的内容。
-- Input
{{# order_items.status_filter.filter }} order_items.status {{/ order_items.status_filter.filter }}
{{^ order_items.status_filter.filter }} 1=1 {{/ order_items.status_filter.filter }}
-- Resolved static SQL
1=1
步骤 2 — 筛选字段定义中的 default_filter:
在视图 YAML 中找到引用的仅用于筛选的字段,并检查是否存在 default_filter 属性。将其转换为静态 SQL 条件:
default_filter 值 | 转换后的 SQL |
|---|---|
is: "Complete" | field = 'Complete' |
is: ["New York", "CA"] | field IN ('New York', 'CA') |
is: true | field IS TRUE |
greater_than: 100 | field > 100 |
对于值语法,将转换后的标量值直接替换到 SQL 字符串中:
-- Input
DATE_TRUNC('day', created_at) >= '{{filters.order_items.order_date_filter.value}}'
-- default_filter: is: "2024-01-01"
-- Resolved
DATE_TRUNC('day', created_at) >= '2024-01-01'
对于范围语法,如果 default_filter 中已定义,则使用 range_start / range_end;否则向用户发出警告。
步骤 3 — 没有回退且没有 default_filter — 向用户发出警告:
⚠️ 字段
<view>.<field>引用了仅用于筛选的字段<view>.<filter_field>,但既没有default_filter,也没有内联的{{^ }}回退。无法将其静态解析为 Snowflake Semantic View。可选方案:
- 完全跳过此字段,不将其纳入输出。
- 询问用户要硬编码的静态值或条件。
- 使用
1=1(恒为真的空操作),保留字段结构但不产生筛选效果。
决策流程总结:
Does the SQL reference a filter-only field?
(via {{# view.field.filter }} OR {{filters.view.field.value}} etc.)
│
├── Block pattern → Does SQL have {{^ view.field.filter }} fallback?
│ ├── Yes → Use fallback expression as static SQL
│ └── No → Go to default_filter check ↓
│
├── Value pattern → Go to default_filter check ↓
│
└── default_filter check:
├── Yes → Render default_filter as static SQL / value substitution
└── No → Warn user, ask how to proceed
⚠️ 仅用于筛选的字段本身绝不会作为维度、度量或筛选器输出到 Semantic View 中 — 它们仅用于为其他字段的 SQL 提供参数。
AI 上下文 → module_custom_instructions
如果主题有 ai_context 参数,则将其包含为:
module_custom_instructions:
question_categorization: <ai_context value>
示例查询 → verified_queries
将 sample_queries 下的每个条目转换为 verified_queries 中的 SQL 语句。将 prompt 用作 question,并将 description 作为编写 SQL 的上下文。
✋ 停止 — 在生成最终输出之前,与用户确认所有维度、度量和关系。
步骤 7 — 生成并执行 Semantic View
输出 YAML 结构
name: <name>
description: <string>
tables:
- name: <name>
description: <string>
base_table:
database: <database>
schema: <schema>
table: <table name>
dimensions:
- name: <name>
synonyms: [ <string>, ... ]
description: <string>
expr: <SQL expression>
data_type: <data type>
unique: <boolean>
time_dimensions:
- name: <name>
synonyms: [ <string>, ... ]
description: <string>
expr: <SQL expression>
data_type: <data type>
facts:
- name: <name>
synonyms: [ <string>, ... ]
description: <string>
expr: <SQL expression>
data_type: <data type>
metrics:
- name: <name>
synonyms: [ <string>, ... ]
description: <string>
expr: <SQL expression>
filters:
- name: <name>
synonyms: [ <string>, ... ]
description: <string>
expr: <SQL expression>
relationships:
- name: <string>
left_table: <table>
right_table: <table>
relationship_columns:
- left_column: <column>
right_column: <column>
metrics:
- name: <name>
synonyms: [ <string>, ... ]
description: <string>
expr: <SQL expression>
verified_queries:
- name: <string>
question: <string>
sql: <string>
use_as_onboarding_question: <boolean>
仅限有效的顶层键:
name、description、tables、relationships、metrics、verified_queries、module_custom_instructions
创建 Semantic View
无论环境如何,SQL 都相同。不同之处在于执行方式。
要运行的 SQL:
CALL SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML('<database>.<schema>', $$
<yaml content here>
$$);
各环境中的执行方式:
| 环境 | 命令 |
|---|---|
| Cortex Code | 在当前 Snowflake 会话中直接执行 SQL |
snow CLI | snow sql -q "CALL SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML(...)" 或将 YAML 写入临时文件并通过管道传入 |
| SnowSQL | snowsql -a <account> -u <user> -f <sql_file.sql> |
| Python | conn.cursor().execute("<sql>") |
提示(适用于 CLI 环境): 如果 YAML 很长,请先将其写入临时
.sql文件,然后执行该文件,而不要以内联方式传入,这样可以避免因$$美元符号引用而产生的 shell 转义问题。
# snow CLI example with a file
snow sql -f /tmp/create_semantic_view.sql
授予访问权限
GRANT SELECT ON SEMANTIC VIEW <database>.<schema>.<name> TO ROLE <role>;
使用 Runtime Environment Detection 步骤中确定的连接方式,以与上述 CREATE 调用相同的方式执行此操作。