本体(实验性)¶
实验性功能
本页介绍的功能仍在成形中。参数、输出和错误负载都可能不经通知就改变。
Dosi 可以在核心 Ossie 模型之外——或者取而代之——加载一份 Ossie 本体。本体并不带来 vendor 扩展装不下的信息; 它带来的是同一份模型可以承载多份本体,而且它们可以互相冲突:运营部说"机场延误" 指起飞机场,地服部说同一句指到达机场。两者都对,各自是同一批表之上的一份本体。
下文都是 dosi 命令行;Python 一节给出 Python 绑定里的同一组调用。示例用的是安装包自带的三个示例模型($DOSI_EXAMPLES
见验证是否可用):ossie-mini-flights(两个概念)、
aviation(手写模型加一份本体)和 ossie-flights(上游参考本体)。
加载本体¶
本体通过映射文档(mapping document)进入 Dosi。一份映射文档指明一个本体和一个语义模型, 并把模型里的列映射到本体的概念上。三种文档里只有它会引用另外两份,所以要加载的就是它。
以下参数对每条命令都生效:
| 参数 | 环境变量 | 含义 |
|---|---|---|
--mapping <path> |
DOSI_MAPPING |
映射文档 |
--ontology <path> |
DOSI_ONTOLOGY |
本体文档,用于映射文档自己找不到它的情况。与 --model 目录同时给出时,用来选出这个本体的映射 |
--model <path> |
DOSI_MODEL |
语义模型。传入映射文档或本体文档时按结构识别;传入目录时把整个目录当作 catalog 读取 |
三种写法¶
单文件。 映射文档把本体和模型都内联在 document: 下。这是上手最快的写法,
ossie-mini-flights 就是这样写的:
version: 0.2.0.dev0
name: mini_flights_mapping
ontology_ref:
name: MiniFlights
document: # 完整的本体文档
version: 0.2.0.dev0
name: MiniFlights
ontology: [...]
semantic_model_ref:
name: mini_flights_model
document: # 完整的语义模型
version: 0.2.0.dev0
name: mini_flights_model
datasets: [...]
concept_mappings: [...]
三个文件。 每个引用改写 iri,相对映射文档所在的目录解析(iri: ./ontology.yaml)。
ossie-flights 和 aviation 是这样写的。
一个目录。 --model <dir> 读取目录顶层所有 .yaml、.yml 和 .json 文件;
既没有 iri 也没有 document 的引用按 name 查找。不是 Ossie 文档的文件(比如
queries.yaml)会被跳过。目录里的映射指向不止一个本体时,加上 --ontology <file>
加载其中一个:只读取 ontology_ref 指向它的映射,以及这些映射各自引用的模型。内联了本体的
映射绑定的是它自己的那个本体,会被跳过。没有任何映射指向该本体时,加载会被拒绝
(ontology_ref_unresolved)。--mapping 不能和目录一起用。
$ dosi describe --mapping $DOSI_EXAMPLES/ossie-mini-flights/mapping.yaml
$ dosi describe --mapping $DOSI_EXAMPLES/aviation/mapping.yaml
$ dosi describe --model $DOSI_EXAMPLES/aviation
$ dosi describe --model ./catalog --ontology ./catalog/flights.ontology.yaml
Note
内联的 document: 已在 Ossie 社区提出,但还没有进入已发布的映射 schema,
在它合入之前上游校验器会拒绝这种写法。Dosi 在两种引擎模式下都会读取它。
以下规则对每种写法都成立:
- 引用的
name必须等于它解析到的那份文档自己的名字(ontology_ref_mismatch)。 --ontology和--model优先于映射文档里写的iri,名字照样校验。同一个引用既内联了document又用参数指定了文件,会被拒绝(ontology_ref_conflict)。iri只能是本地路径或file://IRI。Dosi 不通过网络获取文档:请在你的 catalog 里解析好,再把文件传进来。- 目录里多份映射指向同一个模型时会合并;同一个概念被映射两次会被拒绝
(
ontology_mapping_conflict)。指向不同模型的映射目前会被拒绝,请一次加载一份。 - 模型就是映射文档指明的那一份。像
aviation这样完整的手写模型,声明过的东西都保留; 如果它已经声明了一条与本体 link 列完全相同的 join,概念就沿用模型里这条关系,不会再加第二条。 像ossie-flights这样只有 dataset 和字段的模型,主键、关系和指标都由本体生成。
内嵌写法(已弃用)¶
本体仍然可以在 ontology_mappings 下带着映射,并把语义模型内嵌在里面,这是这个格式最初的写法。
--ontology o.yaml 可以加载它,同时给出 legacy_embedded_mapping 提示;
--model m.yaml --ontology o.yaml 会用 m.yaml 替换内嵌的模型。一次只加载一份内嵌映射,
有多份时请拆成映射文档。Ossie 规范停止支持这种写法时,Dosi 也会停止读取。
加载后得到什么¶
本体会被降级(lower)成一份普通的核心 Ossie 模型,之后与手写模型走完全相同的 校验 → 编译 → 规划流程。旁边另存一份 ConceptMap,它只负责命名:把概念路径映射到 它所解析成的 dataset、字段与连接路径。
| 本体构造 | 变成什么 |
|---|---|
带对象映射的 identify_by |
dataset 的 primary_key,模型已声明时以模型为准 |
| Entity → Value 关系 | 已有字段的一个名字;时间类值类型会标记为 is_time |
Entity → Entity,ManyToOne / OneToOne |
名为 <owner>_<link> 的关系,如 flight_departs_from |
| 存在另一个实体表上的实体 | 不 join、不设主键、不能作查询根——见限制 |
| 带过滤映射的一元关系 | 布尔字段 <owner>_<name>,可用 Concept.name 访问 |
derived_by: AGG[<value> WHERE <correlation> GROUP BY <concept>] |
指标 <owner>_<relationship>——见派生指标 |
没有被降级的构造,每次调用都会在 stderr 上打一行 note::
$ dosi describe --mapping $DOSI_EXAMPLES/ossie-flights/mapping.yaml
note: City.state: State has no table of its own; reached without a join
note: Airport.city: City has no table of its own; reached without a join
note: Flight.registers_latitude_series: n-ary relationship recorded as unmapped
...
浏览:describe 与 paths¶
dosi describe [concept]¶
不带参数时返回概念目录:
$ dosi describe --mapping $DOSI_EXAMPLES/ossie-mini-flights/mapping.yaml
ontology MiniFlights
Two airports' worth of ontology, kept small enough to read whole.
concept identity out in props root
Airport code 0 2 3 yes
Flight id 2 0 2 yes
out 与 in 是 link 数,props 是属性数,root 表示查询能否以该概念为根。
带概念名时,返回针对它写查询所需的一切:
$ dosi describe FlightAlert --mapping $DOSI_EXAMPLES/aviation/mapping.yaml
concept FlightAlert
identity: id (root: flight_alerts)
properties
path type domain time
FlightAlert.id AlertId (String)
FlightAlert.type AlertType (String)
FlightAlert.priority AlertPriority (String) one of [P0, P1, P2, P3]
FlightAlert.status AlertStatus (String) one of [OPEN, RESOLVED]
FlightAlert.created_at DateTime yes
FlightAlert.resolved_at DateTime yes
FlightAlert.resolution_comment String
links out
name role to multiplicity join
flight flight Flight ManyToOne yes
metrics by group-by
group by metrics
FlightAlert.id alert_count, open_alert_count, flights_with_open_alert, maintenance_alerts
time dimensions: FlightAlert.created_at, FlightAlert.flight.actual_arrival, FlightAlert.flight.actual_departure, FlightAlert.flight.scheduled_arrival, FlightAlert.flight.scheduled_departure, FlightAlert.resolved_at
| 区块 | 告诉你什么 |
|---|---|
properties |
每个已映射属性的概念路径,带值类型、封闭值域与时间标记 |
links out |
名称、角色、目标与 multiplicity。join: none 表示目标就存在本表上 |
links in (query from) |
链接到这里的概念——查询应该从那一侧起根 |
metrics reachable |
本体派生出的指标,带 group-by,以及赋予它含义的边(via) |
metrics by group-by |
本概念能分组的模型自有指标,按 group-by 写法分行列出,写法从指标所在的概念出发。每条写法打印前都先经 planner 验证 |
time dimensions |
本概念及一跳之外的时间属性,可用于时间范围 |
unmapped |
已声明但未降级的关系:n 元关系,或没有映射的 link |
not queryable |
没有绑定到列的属性(多数是行级 derived_by) |
表格里的每条路径都能原样粘进 select 或 query。 这条契约是这个视图值得调用的原因。
--format json 返回相同内容:concept、identity、root、properties(path、
type_name、domain、domain_summary、is_time、mapped)、links、incoming
(query_from)、metrics_reachable(name、metric、grain、group_by、via)、
groupable_metrics(group_by、from、metrics)、time_dimensions_reachable、
default_projection 与 unmapped。目录则是 ontology、description、requires 与
concepts(name、identity、links_out、links_in、properties、queryable、
summary)。
没有本体时,不带参数的 describe 改为列出 dataset 与指标,带概念名则报错:
$ dosi describe Airport --model $DOSI_EXAMPLES/ossie-mini-flights/model.yaml
error: this model has no ontology, so "Airport" is not a concept; pass --mapping <path>, or use `list` for datasets and metrics
dosi paths --from <concept> --to <concept>¶
两个概念之间三跳以内的全部命名路径,带走过的角色,以及本体声明为等价的路径对:
$ dosi paths --from Flight --to Airport --mapping $DOSI_EXAMPLES/aviation/mapping.yaml
paths Flight → Airport
path roles hops multiplicity join
Flight.arrives_at destination 1 ManyToOne yes
Flight.departs_from origin 1 ManyToOne yes
Flight.follows_route.destination follows_route → destination 2 ManyToOne yes
Flight.follows_route.origin follows_route → origin 2 ManyToOne yes
equivalent: Flight.departs_from ≡ Flight.follows_route.origin (Flight.follows_route.origin == origin)
equivalent: Flight.arrives_at ≡ Flight.follows_route.destination (Flight.follows_route.destination == destination)
到同一目标的两条路径,正是裸 dataset.field 引用解析不了的情况
(ambiguous_join_path);区分它们的是角色名。等价关系读自形如
Concept.link.role == role 的关系级 requires。只有一条路径时,JSON 会带
unique: true。
查询里的概念路径¶
加载本体后,select 与 query 在所有接受字段的地方都接受概念拼写:
| 位置 | 拼写 |
|---|---|
select --from |
概念名:FlightAlert |
字段、--group-by |
Concept.property、Concept.link.property、Concept.link.link.property |
--where |
同样的路径;字符串字面量永远不会被改写 |
--time-dimension |
时间属性路径:Delay.flight.scheduled_departure |
--metrics |
派生指标的概念路径:Airport.average_departure_delay |
select --from、--group-by |
一个派生子类型:PremierCustomer —— 父概念中满足其规则的行,或作为布尔维度的成员资格 |
--where、D-DERIVE filter.where |
<Concept> is [not] <Subtype>:Customer is PremierCustomer、Membership.held_by is not PremierCustomer |
每一个都解析成 planner 早就认识的 dataset、字段与关系路径,所以 SQL 与 dataset 拼写完全一致:
$ dosi select --mapping $DOSI_EXAMPLES/aviation/mapping.yaml \
--from FlightAlert \
--fields "FlightAlert.id,FlightAlert.priority,FlightAlert.flight.arrives_at.code" \
--where "FlightAlert.status = 'OPEN' AND FlightAlert.priority = 'P1'"
SELECT flight_alerts.alert_id AS alert_id, flight_alerts.priority AS priority, airports.airport_code AS flight_alerts_flight__flights_arrives_at__airport_code FROM flight_alerts AS flight_alerts LEFT JOIN flights AS flights ON flight_alerts.flight_id = flights.flight_id LEFT JOIN airports AS airports ON flights.destination_airport_code = airports.airport_code WHERE flight_alerts.status = 'OPEN' AND flight_alerts.priority = 'P1' LIMIT 100
输出列沿用 planner 起的名字,--order 也接受这些列名,而不是概念路径。
派生指标¶
derived_by 规则的相关路径才是赋予指标含义的东西。两个这样的指标可以编译出逐字节
相同的 SQL,含义却不同:
$ duckdb mini.duckdb < $DOSI_EXAMPLES/ossie-mini-flights/seed.sql
$ dosi query --mapping $DOSI_EXAMPLES/ossie-mini-flights/mapping.yaml \
--metrics Airport.average_departure_delay --group-by Airport.code --execute --db mini.duckdb
code airport_average_departure_delay
ATL 16
ORD 30
DCA 20
SFO 32
4 rows
$ dosi query --mapping $DOSI_EXAMPLES/ossie-mini-flights/mapping.yaml \
--metrics Airport.average_inbound_departure_delay --group-by Airport.code --execute --db mini.duckdb
code airport_average_inbound_departure_delay
ATL 30
DCA 16
SFO 35
ORD 10
4 rows
两者都是 AVG(FLIGHT.dep_delay)。用概念路径指名时,每个指标沿自己的边分组——
第一个经起飞机场,第二个经到达机场。由此带来两个有意为之的结果:
- 用扁平名要模型指标(
airport_average_departure_delay)时没有指定路径, 按Airport.code分组会得到ambiguous_join_path:单靠模型分不出两者。 - 同时要两个指标时,两条路径互相矛盾,歧义会被报告,而不是悄悄选定一边。
规则要么整条降级,要么完全不降级。WHERE 不是相关条件(如
WHERE Flight.departure_delay > 10),或相关路径解析不到任何 link 时,规则保留为文本
并打一行 note:,不会生成指标。
group-by 起根在错误一侧时¶
指标沿一条从它到达不了的概念出发的路径分组时,planner 会拒绝。加载了本体时, 拒绝信息会给出修正写法——同一条路径,改从指标所在的概念出发:
$ dosi query --mapping $DOSI_EXAMPLES/aviation/mapping.yaml \
--metrics delayed_flights --group-by Flight.arrives_at.code
error: InvalidRelationshipPath: a relationship path pins this query to dataset "flights", which has no many→one path to "delays" — where measure "delays_flight_id_count_distinct" of metric "delayed_flights" lives
candidates: Delay.flight.arrives_at.code
hint: root the group-by on the concept the metric lives on: Delay.flight.arrives_at.code
每个候选写法给出前都先经过规划验证;最多三个,短的在前。--format json 下它们是
candidates 与 suggested_retry。
派生子类型¶
实体上由规则定义的子集 —— Premier 客户、延误航班、高风险订单 —— 是一个派生子类型:
一个 extends 一个父概念、带一条概念级 derived_by 规则的 EntityType,用的是规范
自己的写法(规范的例子是 Employee extends Person, derived_by EXISTS(Person.earns))。
规则只写一次,写在父概念的属性上;子类型随即可以作查询根、作维度、作过滤,还可以
挂自己的关系。
- concept: PremierCustomer
type: EntityType
extends: [Customer]
description: Premier qualification. Eligibility, not enrollment.
derived_by:
- 'Customer.margin >= 3000 AND Customer.order_count >= 6 AND Customer.return_rate <= 0.20'
relationships:
- name: qualifies_for
roles: [{concept: Membership}]
multiplicity: ManyToOne
子类型不需要 object_mappings:它继承父概念的身份、表和属性。它自己加的关系用它
自己概念名下的 link_mappings 映射,且只存在于它身上(Customer.qualifies_for 仍是
未知路径)。
lowering 产出什么。 父概念 dataset 上的一个布尔字段,以概念名命名
(customers.premier_customer),表达式是规则的一跳翻译 —— Customer.margin 换成该属性
映射到的列、一元事实换成它的字段、Customer is Gold 换成 Gold 的标志 —— 外面套
COALESCE(…, FALSE)。链式继承(PremierPlus extends [PremierCustomer])把父类型的标志
AND 进去。该字段是维度(is_dimension: true),也可以直接用 dataset 拼写点名;planner
从未听说"子类型"这个词。
规则的文法是父概念已映射属性之上的布尔代数,别无其他 —— 聚合先经 D-LOD
fixed(metric, key) 字段变成属性,所以上面的 margin 与 order_count 都是属性:
expr := expr AND expr | expr OR expr | NOT expr | '(' expr ')' | atom
atom := attr cmp value | attr IS [NOT] NULL | attr [NOT] IN '(' value, … ')'
| attr -- 布尔(一元)属性
| Concept 'is' [NOT] Concept -- 父概念的另一个子类型
value := number | 'string' | TRUE | FALSE | ':' name
attr := Concept '.' name -- 一跳:父概念自己的属性
cmp := = | == | <> | != | < | <= | > | >=
其余一切 —— 函数、聚合、两跳路径(Customer.orders.amount)、EXISTS —— 都是
ontology_subtype_rule_unsupported 并附提示,绝不猜测。
在查询面上:
$ dosi describe PremierCustomer --mapping northstar.yaml
concept PremierCustomer (extends Customer)
rule: Customer.margin >= 3000 AND Customer.order_count >= 6 AND Customer.return_rate <= 0.20
identity: id (root: customers, inherited from Customer)
…
$ dosi select --from PremierCustomer --fields PremierCustomer.name,Customer.margin # 父概念的行,已过滤
$ dosi query --metrics booked_revenue --group-by PremierCustomer # true / false
$ dosi query --metrics booked_revenue --where "Customer is PremierCustomer"
$ dosi select --from Membership --fields Membership.held_by.name --where "Membership.held_by is not PremierCustomer"
is 是本体在规划之前解掉的糖(customers.premier_customer = TRUE);它从不进入 SQL
文法,IS NULL 留给 SQL。is 后面不是该概念子类型的名字报 unknown_subtype,
candidates 列出该概念的子类型。describe Customer 列出 derived_subtypes。D-DERIVE
filter 可以用同样的拼写:"where": "Customer is PremierCustomer" —— 事实表指标也行,
因为事实表多对一可达的维度上的守卫是被允许的(S-ATTR-3′)。
阈值参数化。 规则可以带 :name 占位符,在 mapping 文档的 concept_params 下声明,
查询时用 --param margin_min=4000 绑定 —— 一次不改文件的 what-if。见
D-PARAM。字面量规则是标准 Ossie,--osi-basic
下可用;占位符规则是 DATUS 功能,基础模式把子类型记为 unmapped(可描述、不可查询)。
标志列没有自己的 verbalizes;子类型的行和每个细节层次一样在查询的时间上下文里求值:
select --from PremierCustomer --time-range … 得到的是窗口内的 Premier 集合。
值域¶
ValueType 的 requires 若枚举了一个封闭集合,过滤字面量就能在查询编译之前被判定:
$ dosi select --mapping $DOSI_EXAMPLES/aviation/mapping.yaml \
--from FlightAlert --fields FlightAlert.id --where "FlightAlert.status = 'unresolved'"
error: value_not_in_domain: "unresolved" is not a value of FlightAlert.status; its domain is a closed set
allowed: OPEN, RESOLVED
{
"code": "value_not_in_domain",
"violations": [
{
"reference": "FlightAlert.status",
"literal": "Open",
"allowed": ["OPEN", "RESOLVED"]
}
]
}
这是探测数仓做不到的事:probe 能查出一列当前有哪些取值,但只有声明才断言这个集合是封闭的。
具体检查哪些:
- 写成
X == 'A' OR X == 'B'、且每个值都是字面量的枚举。与其他东西比较的规则 (Status == Other.status)不产生值域。 - 形如
<path> = '<literal>'与<path> IN ('a', 'b')的谓词,字面量里的''读作一个引号。字符串字面量内部的文本永远不会被当成引用。 - 区间(
1 <= X, X <= 10)会在describe里显示,但不检查;数值字面量也不检查。 跨字段、跨关系与模型级约束原样保留,不由任何东西执行。
血缘¶
dosi lineage 画出指标血缘图——物理表 → dataset → 原子指标 → 派生指标——加载了本体时,
还在上面画出概念。图本身的说明见指标血缘;本节只讲本体加了什么。
$ dosi lineage view --mapping $DOSI_EXAMPLES/ossie-mini-flights/mapping.yaml
$ dosi lineage view --mapping $DOSI_EXAMPLES/ossie-flights/mapping.yaml --no-open --out flights.html
flights.html
$ dosi lineage dump --mapping $DOSI_EXAMPLES/ossie-mini-flights/mapping.yaml > graph.json
view 直接从模型生成一个 HTML 页面——不读 dump 的输出——页面是自包含的:图数据内嵌在
页面里,不请求任何外部资源,通过 file:// 就能打开。它会用默认浏览器打开页面;带
--no-open 或没有浏览器时,改为打印文件路径。加载时的
note: 仍然输出到 stderr。
页面有两个视图。Lineage 把泳道从左到右排开,在 dataset 与指标之间插入一条概念泳道;
没有自己的表的实体画成虚线框,每条 grain 边上标出它的路径。Concepts 把实体画成
ER 图,属性折叠在卡片里,平行的关系分开画。在任一视图里选中节点,都会显示它的详情和边;
点击一条边会跳到另一端,必要时自动切换视图。
本体加了什么¶
平面图里的东西一样都不动:四条泳道、七种边、每个节点与边的 id 全部不变;没有本体的模型
根本没有 ontology 键。本体加上的是:
| 新增 | 位置 | 内容 |
|---|---|---|
| 实体节点 | ontology.nodes |
每个实体概念一个,id 为 concept:<Name> |
mapping 边 |
ontology.edges |
dataset → 概念 |
relationship 边 |
ontology.edges |
概念 → 概念 |
subtype 边 |
ontology.edges |
派生子类型 → 父概念,带 rule 与 flag(它降级成的 dataset.field) |
concept 对象 |
nodes 里的指标节点 |
一条 derived_by 规则对该指标的描述:path、measure、group_by、via、realized_by |
三种边都是血缘——概念的数据在哪里。派生子类型没有自己的 mapping 边,也不重复它继承的
relationship 边:其节点带 subtype_of,subtype 边说明它保留父概念的哪些行。concept
对象则是语义,所以它和 dimensions 一样挂在节点上,而不是单独成边。
有一个字段在每张图上都新增,无论有没有本体:dataset 节点 detail.fields 里的每一项都带
label——模型里该字段的 label,没有就等于字段名。
值类型不是节点;它们作为 properties[].concept 出现在所属实体上。叠加层的边既指向平面图
的节点(dataset:、metric:),也指向实体节点(concept:),所以渲染时要把 nodes 和
ontology.nodes 放在一起查端点。
实体节点¶
{
"id": "concept:Airport", "kind": "entity", "name": "Airport", "label": "Airport",
"mapping": "table", "dataset": "AIRPORT", "key_columns": ["code"], "identity": ["code"],
"properties": [
{ "name": "code", "concept": "AirportCode", "base": "String",
"field": "AIRPORT.code", "is_time": false, "derived": false }
]
}
| 字段 | 含义 |
|---|---|
mapping |
本体映射落在了什么上。table:有自己的 dataset,一行一个对象。denormalized:没有自己的表——列在另一个实体的表上,那些行属于那个实体,它只是投影(AIRPORT 上的 City;加州两个机场会让 State 的 CA 出现两次)。unmapped:声明的东西一个都解析不到列 |
dataset、key_columns |
标识它的 dataset 与键——仅当 mapping 为 table 时出现 |
identity |
identify_by 里列出的关系 |
properties[] |
name;concept,值类型;base,它追溯到的内置类型;field,形如 dataset.field,未映射时缺省;is_time;derived,是否由 derived_by 计算;domain,有封闭值集时给出;description |
unmapped |
已声明但未降级的关系——即 describe 打印的那张列表 |
本体给了 description 时才出现。没有内容的键直接缺省,而不是 null。
边¶
| 类型 | 起点 → 终点 | 负载 | Id |
|---|---|---|---|
mapping |
dataset:D → concept:C |
via:identity(实体自己的 dataset)或 property(其属性被反规范化到的 dataset——每个这样的 dataset 一条边) |
mapping:dataset:D->concept:C |
relationship |
concept:A → concept:B |
name、role、multiplicity、description;realized_by,它走过的 join: 边 id;no_join,目标就在同一张表上时为 true(此时 realized_by 为空);equivalences,该关系的 requires 原文 |
relationship:A.<name> |
指标节点上的 concept 对象¶
只有本体派生出的指标才有。模型里手写的指标(如 aviation)保留它从 dataset 来的
aggregates 边,不增加任何东西。
| 字段 | 含义 |
|---|---|
path |
概念拼写,如 Airport.average_departure_delay——可直接粘进 query --metrics |
measure |
规则聚合的值,写成概念路径:Flight.departure_delay |
group_by |
规则按哪个概念分组 |
via |
从被度量的概念走到粒度概念所经过的 relationship: id |
realized_by |
同一条路径的平面图 join: id |
派生指标里那两个指标:
{ "id": "metric:airport_average_departure_delay", "concept": {
"path": "Airport.average_departure_delay", "measure": "Flight.departure_delay",
"group_by": "Airport", "via": ["relationship:Flight.departs_from"],
"realized_by": ["join:flight_departs_from"] } }
{ "id": "metric:airport_average_inbound_departure_delay", "concept": {
"path": "Airport.average_inbound_departure_delay", "measure": "Flight.departure_delay",
"group_by": "Airport", "via": ["relationship:Flight.arrives_at"],
"realized_by": ["join:flight_arrives_at"] } }
在平面图里,两个指标各只有一条入边 dataset:FLIGHT → metric,别无其他;区分它们的正是
via。查看器把它画成一条从粒度概念指向指标的箭头,边上标出路径。
--redact-sql 不会改动 ontology 块——它不含 SQL。在 Python 里,Engine(mapping_path=…)
加载本体,lineage() 返回同一张图。
命名:dosi 的,不是规范的¶
叠加层里的类型名与字段名是 dosi 血缘图自己的词汇。Apache Ossie 本体规范定义的是概念、关系、
角色、derived_by、requires 和本体映射;里面没有 grounds、measures、grain、
grounding。它们各自对应规范里的什么:
| 图里的名字 | 规范里的来源 |
|---|---|
实体节点;concept.path |
type: EntityType 的概念。Concept.relationship 是规范标识一个关系的方式 |
relationship 边 |
实体 → 实体的二元关系。不叫 link:规范里的 link 指关系的一个实例——一个元组——而不是关系本身 |
equivalences |
该关系的 requires 原文 |
mapping 边;节点上的 mapping 字段 |
把实体落到 dataset 上的概念映射——对象映射与 identify_by。三个取值是 dosi 对"这些映射落在了什么上"的判断,规范本身不区分 |
concept.measure、concept.group_by、concept.via |
一条聚合 derived_by 规则 AGG[<value> WHERE <correlation> GROUP BY <concept>] 拆成三部分:被聚合的值、GROUP BY 的概念、WHERE 里的关联路径。measure 与 grain 是语义层的通用词,不是规范的词 |
realized_by、no_join |
本体里没有对应——是降级产生的 join |
还要注意:规范里根本没有"指标"这个概念。它声明的是一条派生关系
(Airport.average_departure_delay 把 Airport 关联到一个 Delay)。dosi 把它降级成
core OSI 的指标,图上才有一个指标节点来承载 concept 对象。
聚合写法本身也不是规范正文定义的语法:规范的派生表达式示例都是逻辑规则
(Person.parent_of(descendant)、EXISTS ( Person.earns ))。dosi 降级的 AGG[…]
规则来自 apache/ossie 里的示例 examples/flights.yaml,也就是安装包自带的 ossie-flights 这份本体。
Python¶
Python 绑定(dosi_engine.Engine)加载本体的方式与命令行相同,通过 mapping_path
(或 mapping_text)传入,上文的每个调用都有对应方法。model_path 和 ontology_path
分别对应 --model 和 --ontology。
from dosi_engine import Engine, QueryError
e = Engine(mapping_path="aviation/mapping.yaml")
e.has_ontology # True —— 单独 Engine(ontology_path=…) 也是
e.ontology_info() # {"name": "Aviation", "description": …,
# "mappings": ["aviation_duckdb"],
# "source_path": "aviation/ontology.yaml"}
没有本体时 ontology_info() 返回 None;本体以文本或内联方式传入时 source_path 为 None。
文本没有所在位置,所以以 mapping_text 传入的映射文档必须把引用的文档内联进来。
浏览¶
| 方法 | 对应命令 | 返回 |
|---|---|---|
describe() |
dosi describe |
目录:ontology、description、concepts[](name、identity、links_out、links_in、properties、queryable、summary)、requires |
describe("FlightAlert") |
dosi describe FlightAlert |
单个概念:properties[](含 path、domain、is_time);links[](含 role、to、multiplicity);incoming、metrics_reachable、groupable_metrics、time_dimensions_reachable、default_projection |
paths("Flight", "Airport") |
dosi paths --from Flight --to Airport |
paths[](path、roles、hops、multiplicity、no_join)、equivalences[](a、b、declared_by)、unique 与 max_hops |
JSON 与 --format json 以及 MCP 的 describe、paths 工具相同。没有本体时,describe() 改为列出 dataset 与指标。
p = e.paths("Flight", "Airport")
[x["path"] for x in p["paths"]]
# ['Flight.arrives_at', 'Flight.departs_from',
# 'Flight.follows_route.destination', 'Flight.follows_route.origin']
p["equivalences"][0]
# {'a': 'Flight.departs_from', 'b': 'Flight.follows_route.origin',
# 'declared_by': 'Flight.follows_route.origin == origin'}
查询¶
compile、execute、explain、compile_select、select、explain_select
在命令行接受概念路径的地方都接受概念路径:group-by 与明细字段、from(概念名)、
where_sql、context_filter 以及时间范围的 dimension。同一个查询里可以混用概念路径与
dataset.field,两种写法编译出的 SQL 相同。order_by 仍然写输出列名。
每个 compile 与 execute 结果都带 requested_columns,把每个 group-by 或字段的原始写法与它落到的结果列配对:
r = e.select({
"from": "FlightAlert",
"fields": ["FlightAlert.id", "FlightAlert.flight.arrives_at.name"],
"where_sql": "FlightAlert.status = 'OPEN'",
}, db_path="aviation.duckdb")
r["requested_columns"]
# [{'requested': 'FlightAlert.id', 'column': 'alert_id'},
# {'requested': 'FlightAlert.flight.arrives_at.name',
# 'column': 'flight_alerts_flight__flights_arrives_at__name'}]
原有的 outputs 键不变:它仍然列出指标列及其参数绑定。
Python 里的错误¶
每个拒绝都是 QueryError,code 可供机器读取,to_dict() 带结构化字段,不只拼在消息里:
code |
由谁抛出 | 结构化字段 |
|---|---|---|
ontology_required |
没有本体时的 describe(name)、paths |
— |
unknown_concept |
describe(name) |
消息里列出已声明的概念 |
invalid_concept_path |
paths 指向未声明的概念 |
— |
value_not_in_domain |
所有查询方法,编译之前 | violations[](reference、literal、allowed[]);candidates 重复第一个 allowed |
invalid_relationship_path |
compile、execute、explain |
candidates[](最多三条,最短在前)与 hint:在指标所在概念上起根的 group-by |
ambiguous_join_path |
所有查询方法 | 普通的规划器错误载荷 |
try:
e.compile_select({"from": "FlightAlert", "fields": ["FlightAlert.id"],
"where_sql": "FlightAlert.status = 'unresolved'"})
except QueryError as err:
err.code # 'value_not_in_domain'
err.to_dict()["violations"] # [{'reference': 'FlightAlert.status',
# 'literal': 'unresolved',
# 'allowed': ['OPEN', 'RESOLVED']}]
本体声明了但 dosi 不降级的构造,会在加载时以 RuntimeWarning 提示一次,同时以
ontology_note 代码列在 Engine.warnings 里,收不到 Python 警告的服务进程也能读到。
错误¶
加载本体时的拒绝以非零状态退出,带一个或多个错误码,每个都指明出问题的构造, 有修正办法时一并给出:
| 错误码 | 含义 |
|---|---|
ontology_parse |
文档不能按它被传入时的类型解析 |
ontology_unknown_reference |
引用的概念、关系或角色不存在 |
ontology_ref_unresolved |
引用既没有 iri、没有内联 document,也没有参数指定,或者 iri 读不到;网络 IRI 一律拒绝。另外,--ontology 与目录同时给出、但目录里没有指向它的映射时也报这个错 |
ontology_ref_mismatch |
引用的 name 与它解析到的文档的名字不一致 |
ontology_ref_kind |
引用解析到了错误类型的文档,或者内联的本体带了 ontology_mappings |
ontology_ref_conflict |
同一个引用给了两种方式:document 和 iri,或者内联 document 和参数 |
ontology_federation_unsupported |
映射绑定了不止一个语义模型、指向了不止一个本体,或者本体内嵌了多份映射 |
ontology_mapping_conflict |
同一个概念被映射了不止一次 |
ontology_mapping_empty |
概念映射既没有 object_mappings 也没有 link_mappings,或者对象映射既没有 expression 也没有 referent_mappings |
ontology_duplicate_name |
目录里两份同类文档的 name 相同 |
ontology_mapping_unresolved |
映射表达式引用了模型里没有的 dataset 或字段 |
ontology_expression_unsupported |
映射表达式不是单纯的限定列名(可带 WHERE ( … ) 过滤) |
ontology_mapping_depth_mismatch |
link 映射节点所在深度与其关系的元数不符 |
ontology_multiplicity_required |
必须变成 join 的关系没有声明 multiplicity |
ontology_concept_spans_datasets |
一个概念的映射落在不止一个 dataset 上 |
ontology_identity_unresolved |
identify_by 指向了无法作为标识的东西 |
ontology_name_collision |
概念名与 dataset 或关系重名 |
ontology_type_unresolved |
extends 成环,值类型追溯不到内置类型,或 EntityType extends 了一个 ValueType |
ontology_recursion_unsupported |
derived_by 规则引用了它所定义的关系,或子类型的规则测试自己(直接,或经互相测试的子类型) |
ontology_subtype_multiple_parents |
EntityType extends 了不止一个概念;派生子类型只有一个父 |
ontology_subtype_needs_rule |
EntityType extends 了另一个概念却没有 derived_by 规则;带自有表的子类型不支持 |
ontology_multi_rule_unsupported |
派生子类型列了不止一条 derived_by 规则;用 OR 合成一条 |
ontology_subtype_identity |
派生子类型声明了 identify_by;身份是继承的 |
ontology_subtype_mapped |
派生子类型的 concept mapping 带 object_mappings;只允许它自有关系的 link_mappings |
ontology_subtype_rule_unsupported |
derived_by 规则在文法之外(函数、聚合、两跳路径、EXISTS),或点了父概念没映射的属性 |
ontology_subtype_params |
规则里的 :name 占位符与 mapping 文档的 concept_params 不一致,或声明本身无效 |
$ dosi describe --mapping $DOSI_EXAMPLES/aviation/mapping.yaml \
--model $DOSI_EXAMPLES/orders/model.yaml
error: ontology could not be lowered:
ontology_ref_mismatch: semantic_model_ref names "aviation", but the document it resolves to is named "orders_model" (aviation_duckdb.semantic_model_ref)
查询时,本体额外带来 value_not_in_domain 与 unknown_subtype(<Concept> is <Name>
没有指向任何派生子类型),并为 invalid_relationship_path 填上 candidates。其余都是
普通的查询错误。Python 绑定另有三个自己的错误码,见
Python 里的错误。
限制¶
- 寄居在别人表上的概念不能作查询根。 存在另一个概念表上的概念——如
AIRPORT上的State、City、Market——可以经它的宿主访问(Airport.city.name), 但select --from State会被拒绝:在能去重之前,它的每一行会随机场重复出现。 - n 元关系与行级
derived_by(MINUTES(a - b))不降级,分别出现在unmapped与not queryable下。 - 区间值域不强制检查(见值域)。
--order接受输出列名,不接受概念路径。paths最多三跳。- 检索不是 dosi 的命令。 检索需要索引与存储;dosi 只编译不存储,
describe的输出正是上层编排建索引的原料。 - 派生子类型 V1:一个父、一条规则、没有自有表、没有
EXISTS、只有一跳、子类型之间 不能成环互测。规则的阈值每次查询只绑一个值(不做列表展开),由参数化标志算出的字段 按默认值读它。