Skip to content

Semi-structured: a [*] path mapped to a to-many property returns a stringified array instead of a collection #5099

Description

@rafaelbey

What happens

A [*] path mapped to a to-many primitive property returns one row holding the array's text, rather than a collection.

Class wild::Row
{
  id: Integer[1];
  names: String[*];
}

names: extractFromSemiStructured([wild::store::DB]T.DOC, 'divisions[*].name', 'VARCHAR[]')

Against {"divisions":[{"name":"Alpha"},{"name":"Beta"}]}:

query result
project(~[id, v: r | $r.names]) one row, "[\"Alpha\",\"Beta\"]"
project(~[id, v: r | $r.names->joinStrings(',')]) one row, "[Alpha, Beta]"

Expected: two rows, Alpha and Beta.

Why it matters

No error is raised at compile or run time. A modeller declares String[*], sees an array-shaped value in a spot check, and ships a property that is actually a single string. The rendering differs depending on how the property is queried, which makes it easier still to mistake for a working collection.

Why it happens

Multiplicity in a relational mapping comes from row cardinality. explodeSemiStructured produces rows, so a to-many through a join works normally — meta::relational::tests::semistructured::nested::testNestedExplode maps teams: Team[*] that way and returns one row per (division, team) on DuckDB, Snowflake and Databricks.

A [*] path produces a value: one row with an array in one column. There are no rows for the to-many to range over, so the array is handed back as a single value.

What would be reasonable

Either of these would be an improvement over the current silence:

  1. Reject it at compile time — a to-many property mapped to an array-valued expression is not expressible today, and saying so is better than a wrong answer.
  2. Support it — fan the array out to rows, so [*] bound to a to-many property behaves the way a modeller reading the mapping would expect. This is the outcome that was expected when the gap was found.

Option 2 is the useful one; option 1 is worth having in the meantime, since the failure is currently invisible.

Where this came from

Found while adding [*] wildcard path support to the relational store language (#5097). The wildcard work itself is complete and tested — this is specifically about binding the result to a to-many property.

Reproduction is one property mapping; the models in legend-engine-xt-relationalStore-emit/src/test/resources/relational-emit-models/relational-semistructured-wildcard and core_relational/relational/tests/semistructured/model/wildcardPathInStoreLanguage.legend provide the surrounding setup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions