Skip to content

Query Cookbook

Every recipe in this chapter runs against one model, examples/cookbook.sysml, and every output shown is what the sysml binary printed. The model is a small observatory:

package Cookbook {
    private import DocumentQueries::*;
    private import KerML::Root::Element;
    private import ScalarValues::*;

    part def Subsystem {
        attribute mass : Real;
    }
    part def OpticalSubsystem :> Subsystem;
    part def MirrorAssembly :> OpticalSubsystem {
        attribute :>> mass = 10.0;
    }

    metadata def Critical;

    port def OpticalPort;
    port def DataPort;

    part telescope {
        part primaryMirror : MirrorAssembly {
            @Critical;
            port opticalOut : OpticalPort;
        }
        part instrumentCluster : Subsystem {
            attribute redefines mass = 4.5;
            port opticalIn : OpticalPort;
            port dataOut : DataPort;
        }
        part mountControl : Subsystem {
            attribute redefines mass = 15.0;
            port dataIn : DataPort;
        }
        connection opticalPath connect primaryMirror.opticalOut to instrumentCluster.opticalIn;
        connection dataPath connect instrumentCluster.dataOut to mountControl.dataIn;
    }

    part def Computer;
    part scienceComputer : Computer;
    allocation processing allocate telescope.instrumentCluster to scienceComputer;

    requirement massRequirement;
    part observatory {
        satisfy massRequirement by telescope;
    }

    verification def MassTest;
    verification massVerification : MassTest {
        objective {
            verify massRequirement;
        }
    }

    // ... the recipe queries below ...
}

Each recipe is a calc def specializing DocumentQueries::Query declared in the same package. Run one with:

$ sysml docs/manual/examples/cookbook.sysml -run-query "<name> [<parameter>=<expression> ...]"

A binding whose expression is a name binds the element it denotes; anything else is evaluated as an expression (strings in quotes, numbers as literals).

Anatomy of a query

calc def MassTable :> Query {
    in root : Element;                 // entry parameters, bound by the caller
    Project(                            // operations compose inside-out
        source = PartsByMass(root = root),  // ... and queries invoke queries
        properties = ("name", "mass", "qualifiedName")
    )
}
  • A query is a calc def specializing DocumentQueries::Query.
  • Its in parameters are the entry bindings a caller supplies — an element, a string, a number, a boolean, or a sequence of them.
  • Its body is one expression composing the library operations; source arguments chain them, innermost first.
  • A query can invoke another query by name, with its own bindings. Invocation is dependency-ordered and cycle-checked, with depth and count budgets.

Results are ordered element sequences. Order is the model's declaration order until an OrderBy says otherwise, and elements are deduplicated by identity, so a query is deterministic by construction.

Collection

Direct children: OwnedElements

calc def Children :> Query {
    in root : Element;
    OwnedElements(source = root)
}
$ sysml cookbook.sysml -run-query "Cookbook::Children root=Cookbook::telescope"
✓ Query Cookbook::Children returned 5 rows
  Row 1: Cookbook::telescope::primaryMirror
  Row 2: Cookbook::telescope::instrumentCluster
  Row 3: Cookbook::telescope::mountControl
  Row 4: Cookbook::telescope::opticalPath
  Row 5: Cookbook::telescope::dataPath

Everything the element owns is returned — here the three parts and the two connections, in declaration order. Filter afterwards to narrow.

Descendants to a depth: Descendants

calc def AllParts :> Query {
    in root : Element;
    WhereType(
        source = Descendants(source = root, maxDepth = 10),
        type = "PartUsage"
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::AllParts root=Cookbook::telescope"
✓ Query Cookbook::AllParts returned 5 rows
  Row 1: Cookbook::telescope::primaryMirror
  Row 2: Cookbook::telescope::instrumentCluster
  Row 3: Cookbook::telescope::mountControl
  Row 4: Cookbook::telescope::opticalPath
  Row 5: Cookbook::telescope::dataPath

maxDepth bounds the walk; each level is visited in declaration order. Note that the connections are still here: a connection usage is a PartUsage in the SysML metamodel (its metaclass conforms to it). Use a feature or name filter, or type = "ConnectionUsage", to separate them — see Type filters.

Ancestors: Ancestors

calc def Enclosing :> Query {
    in leaf : Element;
    Ancestors(source = leaf, maxDepth = 2)
}
$ sysml cookbook.sysml -run-query "Cookbook::Enclosing leaf=Cookbook::telescope::primaryMirror::opticalOut"
✓ Query Cookbook::Enclosing returned 2 rows
  Row 1: Cookbook::telescope::primaryMirror
  Row 2: Cookbook::telescope

Owners are returned nearest-first, up to maxDepth levels.

Type filters

WhereType keeps elements whose metamodel type matches — "PartUsage", "ConnectionUsage", "RequirementUsage", "AttributeUsage", "PortUsage", "PartDefinition" and so on — including metaclass conformance, so type = "Usage" keeps every kind of usage. A name that is neither a known metamodel type nor resolvable in the model is a typed unknown-classification error rather than a silently-empty result.

calc def Connections :> Query {
    in root : Element;
    WhereType(
        source = Descendants(source = root, maxDepth = 10),
        type = "ConnectionUsage"
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::Connections root=Cookbook::telescope"
✓ Query Cookbook::Connections returned 2 rows
  Row 1: Cookbook::telescope::opticalPath
  Row 2: Cookbook::telescope::dataPath

To select by a model-defined classification — "every part typed by Subsystem" — filter on what distinguishes those elements instead: a metadata annotation (below) or a characteristic attribute (property filters).

Metadata filters

WhereMetadata keeps elements annotated with a metadata definition, matching specializations of it too. The model marks primaryMirror with @Critical:

calc def CriticalParts :> Query {
    in root : Element;
    WhereMetadata(
        source = AllParts(root = root),
        'metadata' = "Cookbook::Critical"
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::CriticalParts root=Cookbook::telescope"
✓ Query Cookbook::CriticalParts returned 1 row
  Row 1: Cookbook::telescope::primaryMirror

('metadata' is quoted because metadata is a SysML keyword.)

Name filters

WhereName compares each element's effective name against a value:

calc def MirrorParts :> Query {
    in root : Element;
    WhereName(
        source = AllParts(root = root),
        operator = "contains",
        value = "Mirror"
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::MirrorParts root=Cookbook::telescope"
✓ Query Cookbook::MirrorParts returned 1 row
  Row 1: Cookbook::telescope::primaryMirror

Text operators: =/==, !=/<>, contains, startsWith, endsWith (also spelled starts-with/ends-with), and matches with a regular expression.

Property filters

WhereFeature compares an attribute's constant value. The comparison is typed: numbers compare numerically (<, <=, >, >= and equality, with * accepted as infinity), booleans by equality, strings with the text operators above. An element without the attribute simply does not match; a property no element in the source has is a typed unknown-property error.

calc def HeavyParts :> Query {
    in root : Element;
    in threshold : String;
    WhereFeature(
        source = AllParts(root = root),
        'feature' = "mass",
        operator = ">=",
        value = threshold
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::HeavyParts root=Cookbook::telescope threshold=\"10\""
✓ Query Cookbook::HeavyParts returned 2 rows
  Row 1: Cookbook::telescope::primaryMirror
  Row 2: Cookbook::telescope::mountControl

Two details worth noting: value is always written as a string and parsed by the operator's type, and primaryMirror matches through its definitionMirrorAssembly fixes mass = 10.0, and the usage inherits it.

Sorting

OrderBy sorts by a property with every policy explicit — there are no defaults to guess:

  • direction: "ascending" or "descending".
  • missing: where elements without the property go — "first", "last", or "error" to refuse them.
  • multiple: which value to sort by when the property has several — "first", "last", or "error".

The sort is stable, so equal keys keep their declaration order. Mixing incomparable value types across elements is a typed invalid-order error.

calc def PartsByMass :> Query {
    in root : Element;
    OrderBy(
        source = AllParts(root = root),
        property = "mass",
        direction = "descending",
        missing = "last",
        multiple = "error"
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::PartsByMass root=Cookbook::telescope"
✓ Query Cookbook::PartsByMass returned 5 rows
  Row 1: Cookbook::telescope::mountControl
  Row 2: Cookbook::telescope::primaryMirror
  Row 3: Cookbook::telescope::instrumentCluster
  Row 4: Cookbook::telescope::opticalPath
  Row 5: Cookbook::telescope::dataPath

The connections have no mass, so missing = "last" places them after the sorted parts.

Projection

Project turns elements into rows of named, typed cells — what a document table renders. Beyond the model's own attributes, these built-in properties are always projectable:

Property Value
name The effective name
declaredName The declared name, absent when the name is derived
qualifiedName, @id The fully-qualified name
owner The owner's qualified name
@type The metamodel type (PartUsage, ...)
type The declared type's qualified name
isAbstract Boolean
multiplicityLower, multiplicityUpper Integers, * as unbounded
calc def MassTable :> Query {
    in root : Element;
    Project(
        source = PartsByMass(root = root),
        properties = ("name", "mass", "qualifiedName")
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::MassTable root=Cookbook::telescope"
✓ Query Cookbook::MassTable returned 5 rows
  Columns: name, mass, qualifiedName
  Row 1: Cookbook::telescope::mountControl
    name = "mountControl"
    mass = 15.0
    qualifiedName = "Cookbook::telescope::mountControl"
  Row 2: Cookbook::telescope::primaryMirror
    name = "primaryMirror"
    mass = 10.0
    qualifiedName = "Cookbook::telescope::primaryMirror"
  Row 3: Cookbook::telescope::instrumentCluster
    name = "instrumentCluster"
    mass = 4.5
    qualifiedName = "Cookbook::telescope::instrumentCluster"
  Row 4: Cookbook::telescope::opticalPath
    name = "opticalPath"
    mass = (none)
    qualifiedName = "Cookbook::telescope::opticalPath"
  Row 5: Cookbook::telescope::dataPath
    name = "dataPath"
    mass = (none)
    qualifiedName = "Cookbook::telescope::dataPath"

A cell for a property the element lacks is empty ((none) in the CLI's row listing, an empty table cell in a document).

Computed columns

A projection may also derive columns: each Column(name, expression) entry appends a named column whose expression is evaluated once per row over the row element's declared features. Arithmetic (+, -, *, /), string concatenation with + and ?? defaults for absent values are supported:

calc def MassBudget :> Query {
    in root : Element;
    Project(
        source = PartsByMass(root = root),
        properties = ("name", "mass"),
        columns = (
            Column(name = "massLbs", expression = (Subsystem::mass ?? 0.0) * 2.2),
            Column(name = "label", expression = "part: " + Element::name)
        )
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::MassBudget root=Cookbook::telescope"
✓ Query Cookbook::MassBudget returned 5 rows
  Columns: name, mass, massLbs, label
  Row 1: Cookbook::telescope::mountControl
    name = "mountControl"
    mass = 15.0
    massLbs = 33.0
    label = "part: mountControl"
  ...

Feature references name the declaring definition (Subsystem::mass, Element::name); a row element that lacks the feature makes the expression fail with a typed error naming the query, column and row — unless a ?? default covers it, which is why MassBudget's massLbs defaults to 0.0 for the two connections in its results. Computed names join the projection: OrderBy can sort by them and a table's groupBy can group by them.

Query invokes query

MassTable above already shows it: PartsByMass(root = root) invokes the other query with its own bindings, and AllParts invokes Children's sibling the same way. Factoring collection into one base query and deriving filtered/sorted/projected variants from it is the intended style. The engine compiles the invocation graph up front: an unknown name, a cycle, or blowing the depth/count budget is a typed error at that point.

Relationship traversal

RelatedElements walks one named relationship kind from each source element:

RelatedElements(
    source = <elements>,
    relationshipKind = "<kind>",   // specialization, subsetting, redefinition,
                                    // typing, connection, allocation,
                                    // satisfaction or verification
    direction = "<direction>",     // outgoing or incoming
    maxDepth = <n>
)

Direction is from the relationship's own point of view — outgoing follows it as declared, incoming follows it backwards. Traversal is breadth-first to maxDepth, deduplicated, in declaration order, and bounded by a visit budget so a pathological model terminates with a typed error rather than hanging.

Connections

Connection edges run port to port — traverse from the connector's endpoint, not from the part that owns it:

calc def ConnectedTo :> Query {
    in origin : Element;
    RelatedElements(
        source = origin,
        relationshipKind = "connection",
        direction = "outgoing",
        maxDepth = 1
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::ConnectedTo origin=Cookbook::telescope::primaryMirror::opticalOut"
✓ Query Cookbook::ConnectedTo returned 1 row
  Row 1: Cookbook::telescope::instrumentCluster::opticalIn

outgoing follows connect A to B from A's endpoint to B's; incoming follows it the other way. Untyped connect clauses carry connection edges too.

Allocations

allocate X to Y is outgoing from X, incoming to Y:

calc def AllocatedTargets :> Query {
    in origin : Element;
    RelatedElements(
        source = origin,
        relationshipKind = "allocation",
        direction = "outgoing",
        maxDepth = 1
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::AllocatedTargets origin=Cookbook::telescope::instrumentCluster"
✓ Query Cookbook::AllocatedTargets returned 1 row
  Row 1: Cookbook::scienceComputer

Satisfy relationships

satisfy R by P points from the satisfying element to the requirement, so "who satisfies this requirement" is an incoming traversal from the requirement:

calc def SatisfiedBy :> Query {
    in req : Element;
    RelatedElements(
        source = req,
        relationshipKind = "satisfaction",
        direction = "incoming",
        maxDepth = 1
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::SatisfiedBy req=Cookbook::massRequirement"
✓ Query Cookbook::SatisfiedBy returned 1 row
  Row 1: Cookbook::telescope

Verify relationships

Likewise, "which verifications cover this requirement" is incoming from the requirement; the result is the verification usage whose objective verifys it:

calc def VerifiedBy :> Query {
    in req : Element;
    RelatedElements(
        source = req,
        relationshipKind = "verification",
        direction = "incoming",
        maxDepth = 1
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::VerifiedBy req=Cookbook::massRequirement"
✓ Query Cookbook::VerifiedBy returned 1 row
  Row 1: Cookbook::massVerification

Specialization (and the other structural kinds)

specialization, subsetting, redefinition and typing traverse the declaration hierarchy. Incoming specialization from a general type finds what specializes it, transitively to maxDepth:

calc def Specializers :> Query {
    in general : Element;
    RelatedElements(
        source = general,
        relationshipKind = "specialization",
        direction = "incoming",
        maxDepth = 2
    )
}
$ sysml cookbook.sysml -run-query "Cookbook::Specializers general=Cookbook::Subsystem"
✓ Query Cookbook::Specializers returned 2 rows
  Row 1: Cookbook::OpticalSubsystem
  Row 2: Cookbook::MirrorAssembly

Traversal results are elements like any others — feed them into Project for a traceability table, as the worked example does for its requirement section.