fix(owlgen): warn on single-child covering axiom equivalence#5
Conversation
ece07d8 to
c2b7088
Compare
8841b3d to
2b9e4e9
Compare
🔍 Adversarial Review — PR #5SummarySolid, well-motivated PR. The OWL 2 semantics analysis is correct and the warning-not-skip approach aligns with upstream reviewer feedback. No actual bugs found — but there are test coverage gaps and a couple of design concerns worth discussing before merge. 🐛 Bugs & IssuesNo code bugs found. The control flow is correct: The One correctness note on the test assertion:
|
7ac6437 to
c7afa6e
Compare
e43010f to
6c55697
Compare
2a77d3c to
7306cd7
Compare
63dd8e7 to
982c7e0
Compare
Emit warnings for abstract class covering axiom edge cases: - Zero children: warn that no covering axiom will be generated - One child: warn that the covering axiom degenerates to an equivalence (Parent = Child), recommending --skip-abstract-class-as-unionof-subclasses Both axioms are still emitted when applicable (semantically correct per OWL 2), but warnings alert users who extend the ontology downstream. Tests verify warnings are logged, flag suppression works, the single-child covering axiom triple is correctly asserted, plus negative tests for multi-child and concrete class cases, and the mixin-only children edge case. Refs: linkml#3309, linkml#3219 Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
67fd16b to
7abaae7
Compare
* fix(rustgen): preserve explicit key in inlined-as-dict deserialization The `as_key_value.rs.jinja` template unconditionally inserted the inferred key into the deserialization map before handing it to serde. When the inner mapping already carried the key under the slot's alias (emitted on the struct field via `#[serde(alias = "...")]`), the unconditional insert produced a duplicate field that serde rejected at alias resolution. This bit any LinkML schema with a key/identifier slot that declares an `alias:`. The most visible example is the linkml metamodel itself: `extension_tag` (key of `extension`/`annotation`) declares `alias: tag`, so any inlined-dict carrying an explicit `tag:` inside the inner mapping failed to deserialize. The fix: * `AsKeyValue` template model now carries `key_property_aliases`, populated from the key slot's singular `alias` (matching what `property.rs.jinja` emits as a serde alias). * The template wraps the inferred insert in a guard that checks the canonical name and every accepted alias. Test covers all three cases against a regenerated crate: explicit alias in inner map, explicit canonical name in inner map, and missing inner key synthesized from the outer key. * fix(rustgen): honour SimpleDict normalization rules for inlined-dict simple form After PR linkml#2930 tightened the `simple_dict_possible` gate to `len(non_key_attrs) == 1`, any class with a key slot plus a primitive value slot *and* a multivalued recursive sibling slot stopped qualifying for the `key: <primitive>` shorthand. The linkml metamodel's own `Extension`/`Annotation` classes fit that exact shape, so every consumer schema using annotations in their simple-dict form failed to deserialize with "Cannot create a Annotation from a primitive value!". The fix: * Reuse the authoritative `get_range_associated_slots` helper, which encodes the three SimpleDict rules linkml documents (linkml#1250) and shares them with the rest of the toolchain: one non-key slot, the slot annotated `simple_dict_value: true`, or the unique required non-key slot. * Add a Rust-specific guard so we only emit primitive-form deserialization when the chosen value slot's range is a primitive/type/enum or a `linkml:Any` wildcard. Non-Any class ranges either store an inlined struct (which can't be built from a primitive) or a reference string (which would mismatch `Self::Value`). Regression test exercises the `Annotation` shape end-to-end via cargo: key slot + required primitive value slot + recursive multivalued sibling, with both flat and nested simple-dict input. --------- Co-authored-by: Frank Dekervel <frank@kapernikov.com>
`anything.rs.jinja` rendered both directions of the `Anything` <->
Python bridge with debug-stringifying fallbacks:
* `IntoPyObject::into_pyobject` only handled `Unit`/`Bool`/`String`/
`Seq`/`Map`. Every other `serde_value::Value` variant fell into a
catch-all that did `format!("{:?}", other)` and returned the result
as a `PyString`. So `Value::U64(42)` surfaced in Python as the
string `"U64(42)"`, `Value::Bytes(b"hi")` as
`"Bytes([104, 105])"`, etc.
* `FromPyObject::extract_bound` only extracted `&str` and `bool`, in
that order. Python ints and floats fell through to the
stringifying fallback at the bottom. The `&str`-before-`bool` order
was also wrong: `True` extracts as the string `"True"` rather than
`Value::Bool(true)`.
Replace both fallbacks with explicit arms:
* `value_to_py` now handles `U8`/`U16`/`U32`/`U64`/`I8`/`I16`/`I32`/
`I64`/`F32`/`F64`/`Char`/`Bytes`/`Option`/`Newtype` with the right
PyO3 conversions. Strings/bool/sequences/maps unchanged. The match
is now exhaustive over `serde_value::Value`, so a future variant
addition will fail the build rather than silently stringify.
* `py_to_value` now tries `bool`, then `i64`, then `f64`, then `&str`.
`bool` first because Python `bool` is a subclass of `int`; `i64`
before `f64` so integers don't widen to floats.
Co-authored-by: Frank Dekervel <frank@kapernikov.com>
…support (linkml#3585) * add BigQueryGenerator — BQ DDL with native ARRAY, STRUCT, type mapping * add BigQueryGenerator — native ARRAY, STRUCT, PARTITION BY, CLUSTER BY DDL * uv.lock updated * pre-commit checks cleared * bump idna 3.11 → 3.17 in uv.lock to fix pre-existing CI failures * package not found error caught * attempt one to calm codecov down * attempt two to calm codecov down * Full feature test that includes most aspects of a typical BQ query * attempt three to calm codecov down * attempt four to calm codecov down * add BigQuery DDL generator docs and compliance test registration * register BigQuery in compliance test CORE_FRAMEWORKS and pytest markers * RST title underline too short in bigquery.rst * add sqlalchemy-bigquery to tests dependency group * uv lock --------- Co-authored-by: Kevin Schaper <kevinschaper@gmail.com>
Summary
Emit a warning when an abstract class has only one direct
is_achild, since the covering axiom degenerates to an equivalence (Parent ≡ Child). The warning recommends--skip-abstract-class-as-unionof-subclassesto suppress covering axioms if the equivalence is undesired.Motivation
The covering axiom feature (linkml#3219) correctly expresses that every instance of an abstract class must belong to one of its subclasses. With a single child, this is semantically equivalent to
Parent ≡ Childper OWL 2 Primer §4.2. While OWL-correct, this may surprise users building extensible ontologies where more children are added downstream — the equivalence causes new siblings to inherit constraints from the first child via RDFS transitivity.Rather than silently skipping the axiom (which would change the OWL semantics), a warning alerts users to the implication and points them to the existing escape hatch.
Changes
owlgen.py: Addlogger.warning()when an abstract class has exactly 1 child before the covering axiom is emitted. Update field docstring and CLI help text to document the warning.test_owlgen.py: Addtest_abstract_class_with_single_child_emits_warning(validates warning is logged with flag recommendation) andtest_single_child_warning_suppressed_by_skip_flag(validates no warning when skip flag is set).Design Decision
Per review feedback from @mgskjaeveland (linkml#3309): the single-child equivalence is not a generator error — it is the expected OWL semantics. The proper response is a warning, not a silent skip. Users who need to avoid the equivalence should use
--skip-abstract-class-as-unionof-subclasses.References
rdfs:subClassOf=owl:equivalentClassDisjointUnionas proper covering construct