Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Apache Ossie provides a single JSON- and YAML-based specification that any tool

- [`core-spec/`](core-spec/) — The Ossie core specification (`spec.md`), the machine-readable schema (`spec.yaml`, `osi-schema.json`), and accompanying documentation.
- [`converters/`](converters/) — Reference converters that translate between Ossie and other semantic formats (e.g., dbt, GoodData, Polaris, Salesforce).
- [`examples/`](examples/) — Example semantic models, including a complete TPC-DS model.
- [`examples/`](examples/) — Example semantic models, including a complete TPC-DS model and ontology-layer models of flights and SAP Business Partner.
- [`validation/`](validation/) — Tooling for validating semantic models against the Ossie schema.
- [`docs/`](docs/) — Project documentation and overview.

Expand Down
349 changes: 349 additions & 0 deletions examples/sap_business_partner_ontology.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
# yaml-language-server: $schema=../ontology/ontology.json
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

# SAP Business Partner — Ontology Layer Example
#
# An ontology-layer model of a Business Partner domain: business partners, the
# customer role derived from them, and customer master data held per sales area.
#
# What this example demonstrates:
# - value types that carry their constraints as `requires` expressions
# - simple identity (one identifying relationship) and compound identity
# (four identifying relationships on a single entity type)
# - associations with functional multiplicity, and an association without it
# - a subtype whose population is defined by `derived_by` rather than asserted
# - verbalizations on relationships, in both reading directions
# - `ontology_mappings` binding the ontology to a logical-layer semantic
# model, including a nested referent mapping that resolves one entity
# through the identity of another
#
# Field names follow SAP's One Domain Model business naming. Technical origins
# in the SAP Data Dictionary — DDIC domains and check tables — are noted in the
# descriptions, to show how source-system provenance can be recorded alongside
# the business-level model.

version: "0.2.0.dev0"
name: sap_business_partner
description: >
An ontology-layer model of the SAP Business Partner domain, covering business
partners, the customer role derived from them, and the customer master data
maintained per sales area.

ontology:

# ---- Value types carrying DDIC domain semantics -------------------------

- concept: BusinessPartnerNumber

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The six sibling ValueType.requires blocks mix block-list and inline-flow YAML style for structurally identical constraints. Given this file is meant as a template for future SAP fields, a consistent style would help (and might have caught the = 2 issue 😄 ).

type: ValueType
description: >
Identifier of a business partner. Technical origin: DDIC domain BU_PARTNER
(CHAR 10, ALPHA conversion). The leading-zero conversion routine is a
representation concern and is recorded here, not modeled.
extends: [ String ]
requires:
- "LENGTH(BusinessPartnerNumber) <= 10"

- concept: BusinessPartnerCategory
type: ValueType
description: >
Category of a business partner. Technical origin: DDIC domain BU_TYPE with
fixed values — 1 = person, 2 = organization, 3 = group. The fixed values
become a requires constraint over a closed set.
extends: [ String ]
requires:
- "BusinessPartnerCategory IN ('1', '2', '3')"

- concept: CustomerNumber
type: ValueType
description: >
Identifier of a business partner in its customer role. Technical origin:
DDIC domain KUNNR (CHAR 10, ALPHA).
extends: [ String ]
requires:
- "LENGTH(CustomerNumber) <= 10"

- concept: CountryCode
type: ValueType
description: >
Country key. Technical origin: DDIC domain LAND1, check table T005.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says population comes "from a country dataset" preserving the T005 check-table relationship, but no such dataset exists in ontology_mappings (the actual mapping just copies BUSINESS_PARTNER.COUNTRY straight through.

It might be worth correcting the description or adding the dataset it describes.

The check-table relationship is preserved by mapping this concept's
population from a country dataset rather than enumerating values here.
extends: [ String ]
requires:
- "LENGTH(CountryCode) = 2"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only bare = in the file/corpus, every sibling constraint uses <= and ontology.md/flights.yaml use == exclusivity for equality. Worth double-checking the ontology-layer expression dialect accepts bare =. Separately, SAP's LAND1 field is commonly CHAR(3), so an exact length-2 constraint would reject legitimate 3-character country codes.


- concept: SalesOrganization
type: ValueType
description: "Sales organization. DDIC domain VKORG (CHAR 4, check table TVKO)."
extends: [ String ]
requires: [ "LENGTH(SalesOrganization) <= 4" ]

- concept: DistributionChannel
type: ValueType
description: "Distribution channel. DDIC domain VTWEG (CHAR 2, check table TVTW)."
extends: [ String ]
requires: [ "LENGTH(DistributionChannel) <= 2" ]

- concept: Division
type: ValueType
description: "Division. DDIC domain SPART (CHAR 2, check table TSPA)."
extends: [ String ]
requires: [ "LENGTH(Division) <= 2" ]

- concept: PartnerName
type: ValueType
description: "Full name of the business partner (BUT000 name fields, concatenated business view)."
extends: [ String ]

# ---- Entity types -------------------------------------------------------

- concept: BusinessPartner
type: EntityType
description: >
A person, organization, or group with which the company has a business
interest. Source entity: BusinessPartner (BUT000-class). Identity is
simple: a single identifying relationship.
identify_by: [ nr ]
relationships:
- name: nr
description: "Preferred identifier (key element BusinessPartner)."
roles:
- concept: BusinessPartnerNumber
multiplicity: OneToOne
verbalizes:
- "{BusinessPartner} is identified by {BusinessPartnerNumber}"
- "{BusinessPartnerNumber} identifies {BusinessPartner}"
- name: category
description: "Element BusinessPartnerCategory (domain BU_TYPE)."
roles:
- concept: BusinessPartnerCategory
multiplicity: ManyToOne
verbalizes:
- "{BusinessPartner} is of category {BusinessPartnerCategory}"
- name: full_name
description: "Element BusinessPartnerFullName; the source label is carried into the verbalization."
roles:
- concept: PartnerName
multiplicity: ManyToOne
verbalizes:
- "{BusinessPartner} is named {PartnerName}"
- name: country
description: >
Element Country, a foreign key into the country entity (check table
T005). Modeled as a relationship to the CountryCode value type; the
check-table population arrives through the country dataset mapping.
roles:
- concept: CountryCode
multiplicity: ManyToOne
verbalizes:
- "{BusinessPartner} is based in country {CountryCode}"
- name: customer_nr
description: >
Association _Customer (I_Customer / FLCU00-class role view). A
business partner may hold the customer role; when it does, exactly one
customer number applies.
roles:
- concept: CustomerNumber
multiplicity: ManyToOne
verbalizes:
- "{BusinessPartner} acts as customer {CustomerNumber}"

- concept: Customer
type: EntityType
description: >
A business partner in its customer role. Source entity: Customer
(I_Customer). Modeled as a derived subtype: rather than asserting its
population, `derived_by` defines it as exactly those business partners
that carry a customer number. This is how SAP's role concept reads under
a closed-world assumption.
extends: [ BusinessPartner ]
derived_by:
- "EXISTS ( BusinessPartner.customer_nr )"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

derived_by requires EXISTS ( BusinessPartner.customer_nr) , but customer_nr has no link_mapping anywhere in the BusinessPartner nor CUSTOMER_SALES_AREA has a field that could feed it.

Running this file's own mapping yields zero Customer objects, so the derived-population feature this example is meant to demonstrate can't actually be observed.

relationships:
- name: has_sales_area

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no concept: Customer entry anywhere in concept_mappings, only BusinessPartner and CustomerSalesArea are mapped.

has_sales_area gets zero link_mappings and zero derived_by, so this relationship (meant to illustrate "association without functional multiplicity") can never by populated.

description: >
Association _SalesArea (to-many). No multiplicity is declared, because
the association is not functional: one customer sells through many
sales areas.
roles:
- concept: CustomerSalesArea
verbalizes:
- "{Customer} sells through {CustomerSalesArea}"
- "{CustomerSalesArea} belongs to {Customer}"

- concept: CustomerSalesArea
type: EntityType
description: >
Customer master data per sales area. Source entity: CustomerSalesArea
(KNVV-class), key = Customer + SalesOrganization + DistributionChannel +
Division. The compound identifier is declared as four identifying
relationships.
identify_by: [ customer, sales_org, channel, division ]
requires:
- "CustomerSalesArea.customer"
- "CustomerSalesArea.sales_org"
- "CustomerSalesArea.channel"
- "CustomerSalesArea.division"
relationships:
- name: customer
roles:
- concept: Customer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These look like the same real-world relationship modeled as two independent declarations instead of one dual-verbalized relationship, the pattern ontology.md's Person.parent_of uses.

That means keeping them in sync by hand, and as noted above, the Customer.has_sales_area side isn't even mapped.

multiplicity: ManyToOne
verbalizes:
- "{CustomerSalesArea} is customer master data of {Customer}"
- name: sales_org
roles:
- concept: SalesOrganization
multiplicity: ManyToOne
verbalizes:
- "{CustomerSalesArea} is maintained in sales organization {SalesOrganization}"
- name: channel
roles:
- concept: DistributionChannel
multiplicity: ManyToOne
verbalizes:
- "{CustomerSalesArea} sells via distribution channel {DistributionChannel}"
- name: division
roles:
- concept: Division
multiplicity: ManyToOne
verbalizes:
- "{CustomerSalesArea} covers division {Division}"
- name: delivery_blocked

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DELIVERY_BLOCK exists in the CUSTOMER_SALES_AREA dataset specifically for this, but CustomerSalesArea's concept_mapping stops after division, there's no delivery_blocked link_mapping. Compare flights.yaml's canceled flag, mapped via FLIGHT.id WHERE ( FLIGHT.cancelled == TRUE ), for the pattern this is missing.

description: >
Element DeliveryIsBlocked (a domain with fixed values, reduced here to
a unary flag). A relationship with no roles and no multiplicity: it
either holds of a CustomerSalesArea or it does not.
verbalizes:
- "{CustomerSalesArea} is blocked for delivery"

# ---- Mapping: logical model → ontology -----------------------------------
# The logical layer below is a minimal semantic model over two datasets;
# expressions use the ANSI_SQL dialect.

ontology_mappings:

- name: customer_sales_area_from_datasets
description: >
Populates the ontology from a pair of customer and master data datasets.
The nested referent mapping under 'customer' resolves a sales-area row to
its customer through the business partner number: one part of a compound
identifier referring to an entity that is itself identified by a
relationship.
semantic_model:
name: sap_customer_sales
description: "Customer sales master data, projected from CDS views in the source system."
datasets:
- name: CUSTOMER_SALES_AREA
source: "sales.customer_sales_area"
primary_key: [ CUSTOMER_ID, SALES_ORG, DISTR_CHANNEL, DIVISION ]
fields:
- name: CUSTOMER_ID
expression:
dialects:
- dialect: ANSI_SQL
expression: "CUSTOMER_ID"
datatype: String
- name: SALES_ORG
expression:
dialects:
- dialect: ANSI_SQL
expression: "SALES_ORG"
datatype: String
- name: DISTR_CHANNEL
expression:
dialects:
- dialect: ANSI_SQL
expression: "DISTR_CHANNEL"
datatype: String
- name: DIVISION
expression:
dialects:
- dialect: ANSI_SQL
expression: "DIVISION"
datatype: String
- name: DELIVERY_BLOCK
expression:
dialects:
- dialect: ANSI_SQL
expression: "DELIVERY_BLOCK"
datatype: String
- name: BUSINESS_PARTNER
source: "master_data.business_partner"
primary_key: [ BP_NUMBER ]
fields:
- name: BP_NUMBER
expression:
dialects:
- dialect: ANSI_SQL
expression: "BP_NUMBER"
datatype: String
- name: BP_CATEGORY
expression:
dialects:
- dialect: ANSI_SQL
expression: "BP_CATEGORY"
datatype: String
- name: BP_FULL_NAME
expression:
dialects:
- dialect: ANSI_SQL
expression: "BP_FULL_NAME"
datatype: String
- name: COUNTRY
expression:
dialects:
- dialect: ANSI_SQL
expression: "COUNTRY"
datatype: String
concept_mappings:

- concept: BusinessPartner
object_mappings:
- referent_mappings:
- relationship: nr
expression: "BUSINESS_PARTNER.BP_NUMBER"
link_mappings:
- object_mapping:
referent_mappings:
- relationship: nr
expression: "BUSINESS_PARTNER.BP_NUMBER"
children:
- relationship: BusinessPartner.category

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These LinkMapping.relationship values are concept-qualified, but ontology.md's worked example and every occurence in flights.yaml use bare relationship names for LinkMapping.relationship (concept-qualification is for ReferentMapping.relationship instead).

If the mapping engine resolves by exact local name, category/full_name/country would never resolve and stay unpopulated.

object_mapping:
expression: "BUSINESS_PARTNER.BP_CATEGORY"
- relationship: BusinessPartner.full_name
object_mapping:
expression: "BUSINESS_PARTNER.BP_FULL_NAME"
- relationship: BusinessPartner.country
object_mapping:
expression: "BUSINESS_PARTNER.COUNTRY"

- concept: CustomerSalesArea
object_mappings:
- referent_mappings:
- relationship: customer
referent_mappings:
- relationship: BusinessPartner.nr

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customer declares no identify_by of its own, and neither the spec nor the schema says whether identify_by is inherited via extends ("inherit" doesn't appear anywhere in ontology.md/spec.md/the JSON schemas).

Worth flagging that against the actual converter, identify_by is built only from a concept's own YAML list, so Customer.identify_by resolved {} here.

On top of that, lookup_concept_relationship always prefixes f"{concept.name}.{name}", so an already-qualified value like BusinessPartner.nr would get double-prefixed and fail to resolve. This isn't specific to this file (it'd hit ontology.md's own CustOrder.nr example too), but this PR looks like the first example to actually exercise this path.

expression: "CUSTOMER_SALES_AREA.CUSTOMER_ID"
- relationship: sales_org
expression: "CUSTOMER_SALES_AREA.SALES_ORG"
- relationship: channel
expression: "CUSTOMER_SALES_AREA.DISTR_CHANNEL"
- relationship: division
expression: "CUSTOMER_SALES_AREA.DIVISION"