Skip to content

fix(orionbelt): carry field and metric datatype in both directions - #410

Open
ralfbecher wants to merge 2 commits into
apache:mainfrom
ralfbecher:fix/orionbelt-converter-datatype
Open

ralfbecher wants to merge 2 commits into
apache:mainfrom
ralfbecher:fix/orionbelt-converter-datatype

Conversation

@ralfbecher

Copy link
Copy Markdown
Contributor

Summary

The OrionBelt converter lost a field's logical type in both directions (#409): import read the non-spec data_type key against a lowercase map, so the spec datatype was ignored in favour of the name heuristic, and export never wrote datatype at all. This ports the fix that already shipped in the downstream osi-orionbelt package (ralforion/orionbelt-semantic-layer#246).

Ossie -> OBML

  • Field datatype maps to OBML abstractType. Precedence: datatype > legacy data_type > name heuristic.
  • Decimal narrows to float (OBML models exact decimals at the physical layer, not in abstractType); Opaque falls back to the heuristic.
  • The obml_abstract_type stash written on export is now restored on import, so OBML -> Ossie -> OBML stays exact through the narrowing (json, time_tz).
  • Metric datatype maps to the exact measure/metric dataType (Decimal -> decimal(18, 2), Integer -> integer, Float -> double), unless one was already restored from the extension.

OBML -> Ossie

  • Fields always emit datatype from abstractType (json -> Opaque, timestamp_tz -> DateTimeTz), in addition to the existing extension bookkeeping.
  • Measures/metrics emit datatype only when an explicit dataType is declared, so plain measures stay unchanged and round trips remain idempotent.
Ossie datatype OBML field abstractType OBML measure/metric dataType
String string string
Integer int integer
Float float double
Decimal float decimal(18, 2)
Boolean boolean boolean
Date / Time date / time date / time
DateTime timestamp timestamp
DateTimeTz timestamp_tz timestamp
Opaque name heuristic not set

Related Issues

Closes #409

Notes for reviewers

Checklist

Converters

  • Converter logic in converters/ is updated to reflect spec or ontology changes
  • New converters include tests under the converter's test directory

Documentation

  • Mapping analysis (converters/orionbelt/ossie_obml_mapping_analysis.md) updated

Tests

Compliance

  • ASF license headers are present on all new source files
  • No third-party dependencies are added

Closes apache#409. The OrionBelt converter ignored the spec `datatype` on import (it
read the non-spec `data_type` key against a lowercase map) and never wrote it on
export, so other Ossie tools saw wrong or missing logical types.

- Ossie -> OBML: field `datatype` maps to OBML `abstractType` (precedence:
  `datatype` > legacy `data_type` > name heuristic). `Decimal` narrows to
  `float`, `Opaque` falls back to the heuristic. The stashed
  `obml_abstract_type` is now restored, so OBML-origin round trips stay exact.
- Ossie -> OBML: metric `datatype` maps to the exact measure/metric `dataType`
  (`Decimal` -> `decimal(18, 2)`), unless one was restored from the extension.
- OBML -> Ossie: fields always emit `datatype`; measures/metrics emit it only
  when an explicit `dataType` is declared, keeping round trips idempotent.

Ported from the downstream osi-orionbelt package, where this shipped in
ralforion/orionbelt-semantic-layer#246.
@jbonofre
jbonofre self-requested a review September 17, 2026 13:04
"""
if not data_type:
return None
normalized = data_type.strip().lower()

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.

data_type.strip() assumes data_type is a string, but nothing guarantees that if the source OBML document is malformed or hand-authored (the type hint is str | None, not enforced). A non-string dataType (e.g. 123) crashes with AttributeError: 'int' object has no attribute 'strip' and aborts the whole conversion.

I think it's worth guarding with isinstance(data_type, str) up front (returning None otherwise) rather than relying on upstream schema validation that may not have run?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed in 7375c7e. obml_datatype_to_ossie now returns None for anything that is not a non-empty string, so a hand-authored dataType: 123 is treated like an unknown type and the conversion carries on. I applied the same guard on the import side: a non-string field datatype or legacy data_type counts as absent, and a non-string metric datatype maps to nothing. Before, a list there raised TypeError on the dict lookup. Covered by TestMalformedDatatype.

# `dataType` (physical vocabulary: `integer`/`double`/`decimal(p, s)`/...), which
# is where `Decimal` genuinely belongs. So Ossie metric `datatype` maps to that
# field, not the coarse `abstractType`.
OBML_DECIMAL_DEFAULT = "decimal(18, 2)" # mirrors OrionBelt's built-in default

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.

OBML_DECIMAL_DEFAULT = "decimal(18, 2)" is applied unconditionally whenever an Ossie metric has datatype: "Decimal". But OBML models can override the default numeric type via settings.defaultNumericDataType, this hardcodes past that override, so a model configured for e.g. decimal(20, 6) gets metrics silently emitted as decimal(18, 2) instead.

Should this read obml_settings.defaultNumericDataType (when present) before falling back to the "decimal(18, 2)" constant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, fixed in 7375c7e. An Ossie Decimal metric now takes settings.defaultNumericDataType when the model has one, and falls back to decimal(18, 2) otherwise. The settings only exist on an OBML-origin model, in the stashed obml_settings, and the importer restored them after converting metrics. So they are now read before the metrics, and the restore itself stays where it was. OrionBelt rejects a defaultNumericDataType that is not a decimal(p, s), so any other value falls back to the built-in default here too. Covered by TestDecimalDefaultFromSettings.

# already restored from an OBML-origin extension, and skip
# Opaque/unknown (absent from the map).
ossie_dt = m.get("datatype")
if ossie_dt and not target.get("dataType"):

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 guard skips setting dataType whenever target already has one from _apply_obml_measure_extras (the obml_data_type custom_extensions stash). But that stash can be stale, a user edits a metric's datatype in Ossie after a prior OBML round-trip, and the fresh datatype is silently dropped in favor of the old cached value, with no warning.

Should an explicit datatype on the Ossie metric take precedence over the stashed extension, since it reflects the user's latest edit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, the edit should win, fixed in 7375c7e for metrics and for fields, which had the same problem with obml_abstract_type. I did not make datatype win unconditionally, though. On a plain round trip the stash is more exact than the map: decimal(20, 6) and bigint would come back as decimal(18, 2) and integer. So the stash is kept while it agrees with datatype, meaning it maps back to the same Ossie type. When it maps to a different type, the datatype was edited after the export and replaces it. Covered by TestEditedDatatypeBeatsStaleStash, including a check that an agreeing stash stays exact.

- A non-string `dataType` in OBML, or a non-string field or metric
  `datatype` in Ossie, no longer raises: it has no mapping, the same as an
  unknown type, instead of aborting the conversion.
- An Ossie `Decimal` metric takes the model's
  `settings.defaultNumericDataType` when the OBML-origin model carries one,
  and falls back to `decimal(18, 2)` otherwise. OrionBelt only accepts a
  `decimal(p, s)` there, so anything else falls back too. The settings are
  read ahead of the metrics; they are still restored after them.
- A stashed `obml_data_type` or `obml_abstract_type` is kept only while it
  agrees with `datatype`. When it names a different type, `datatype` was
  edited in Ossie after the export and wins. A stash that agrees is kept,
  so `decimal(20, 6)` and `bigint` still round-trip exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OrionBelt converter drops a field's logical datatype in both directions

2 participants