diff --git a/changes/unreleased/wheretype-exact.added.md b/changes/unreleased/wheretype-exact.added.md new file mode 100644 index 0000000000..a43903a0cd --- /dev/null +++ b/changes/unreleased/wheretype-exact.added.md @@ -0,0 +1 @@ +- `WhereType` takes `exact = true` to keep only the elements whose own metaclass is named — neither its subclasses nor the model's own types — as a reflective walk over the metamodel needs (`DocumentQueries` from the Extensions Library pin that declares it). diff --git a/docs/manual/query-cookbook.md b/docs/manual/query-cookbook.md index 42db2bcaeb..31a2ea94a9 100644 --- a/docs/manual/query-cookbook.md +++ b/docs/manual/query-cookbook.md @@ -291,7 +291,9 @@ v1 migration roots every table scope this way. `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 metamodel type name means the +`type = "Usage"` keeps every kind of usage; `exact = true` keeps only the elements whose own +metaclass is named (`type = "PartUsage", exact = true` keeps the part usages and nothing that +merely conforms), which is what a walk per metaclass over the metamodel needs. A metamodel type name means the metaclass even when an element of the model bears the same name; qualify the element's name to mean the element. Several names keep the elements of any of them: `type = ("PartUsage", "PortUsage")`. A name that is neither a diff --git a/internal/doc/queryexec/operations.go b/internal/doc/queryexec/operations.go index 0a6e3d189a..83aec7cd32 100644 --- a/internal/doc/queryexec/operations.go +++ b/internal/doc/queryexec/operations.go @@ -236,6 +236,12 @@ func (e *executor) evaluateWhereType(expression queryplan.Expression) (sequence, if len(typeNames) == 0 { return sequence{}, e.invalidArgument(expression, "type", "0") } + exact := false + if hasArgument(expression, "exact") { + if exact, err = e.booleanArgument(expression, "exact"); err != nil { + return sequence{}, err + } + } tests := make([]typeTest, len(typeNames)) matched := make([]bool, len(typeNames)) for i, typeName := range typeNames { @@ -247,7 +253,7 @@ func (e *executor) evaluateWhereType(expression queryplan.Expression) (sequence, result := filtered(source) for i, value := range source.values { for j, test := range tests { - if e.valueIsA(value, test) { + if (exact && e.valueIsExactly(value, test)) || (!exact && e.valueIsA(value, test)) { matched[j] = true appendSelected(&result, source, i) break @@ -269,6 +275,37 @@ func (e *executor) evaluateWhereType(expression queryplan.Expression) (sequence, } // valueIsA reports whether a row is an object or declaration of the type. +// valueIsExactly is WhereType's exact = true: the row's own metaclass (Model.MetaclassOf, so +// a transition is a TransitionUsage) is the named one, by identity; a type of the model never is. +func (e *executor) valueIsExactly(value Value, test typeTest) bool { + meta := e.exactMetaclass(test) + if meta == nil { + return false + } + if inst, _, isObject := value.Object(); isObject { + for _, typ := range objectTypes(inst) { + if symbols.SameElement(e.context.Model.MetaclassOf(typ), meta) { + return true + } + } + return false + } + sym := value.Declaration() + return sym != nil && symbols.SameElement(e.context.Model.MetaclassOf(sym), meta) +} + +// exactMetaclass is the metaclass a WhereType name denotes — simple or qualified — and nil +// for a type of the model, which an exact test never keeps. +func (e *executor) exactMetaclass(test typeTest) *symbols.Symbol { + if test.target == nil { + return nil + } + if meta := e.context.Model.Metaclass(test.target.Name); meta != nil && symbols.SameElement(meta, test.target) { + return meta + } + return nil +} + func (e *executor) valueIsA(value Value, test typeTest) bool { if _, _, isObject := value.Object(); isObject { return e.objectIsA(value, test.name, test.target) diff --git a/internal/doc/queryexec/wheretype_exact_test.go b/internal/doc/queryexec/wheretype_exact_test.go new file mode 100644 index 0000000000..e1983073a4 --- /dev/null +++ b/internal/doc/queryexec/wheretype_exact_test.go @@ -0,0 +1,101 @@ +package queryexec + +import ( + "strings" + "testing" +) + +// TestExecuteWhereTypeExactKeepsOnlyTheNamedMetaclass: by default WhereType keeps +// every element conforming to the type; exact = true keeps those whose own +// metaclass is named, so a reflective walk mirrors each element once. +func TestExecuteWhereTypeExactKeepsOnlyTheNamedMetaclass(t *testing.T) { + fixture := loadExecutionFixture(t, ` +part def Heavy; +part root { + part a : Heavy; + item b; + attribute c = 1.0; + port d; +} +calc def Usages :> Query { + in source : Element; + WhereType(source = OwnedElements(source = source), type = "Usage") +} +calc def ExactUsages :> Query { + in source : Element; + WhereType(source = OwnedElements(source = source), type = "Usage", exact = true) +} +calc def ExactParts :> Query { + in source : Element; + WhereType(source = OwnedElements(source = source), type = ("PartUsage", "ItemUsage"), exact = true) +} +calc def ExactHeavy :> Query { + in source : Element; + WhereType(source = OwnedElements(source = source), type = "Heavy", exact = true) +} +calc def ExactQualified :> Query { + in source : Element; + WhereType(source = OwnedElements(source = source), type = "SysML::Systems::PartUsage", exact = true) +} +state def Machine { + state first; + state second; + transition go first first then second; +} +calc def ExactTransitions :> Query { + in source : Element; + WhereType(source = Descendants(source = source), type = "TransitionUsage", exact = true) +} +calc def ExactStates :> Query { + in source : Element; + WhereType(source = Descendants(source = source), type = "StateUsage", exact = true) +} +`) + bindings := Bindings{"source": {ElementValue(fixture.symbol(t, "root"))}} + names := func(query string) string { + result, err := fixture.execute(t, query, bindings, Options{}) + if err != nil { + t.Fatalf("%s: %v", query, err) + } + var out []string + for _, row := range result.Rows() { + sym, _ := row.Element().Element() + out = append(out, sym.Name) + } + return strings.Join(out, " ") + } + if got := names("Usages"); got != "a b c d" { + t.Fatalf("Usages = %q, want every usage", got) + } + if got := names("ExactUsages"); got != "" { + t.Fatalf("ExactUsages = %q, want none: no element's own metaclass is Usage", got) + } + if got := names("ExactParts"); got != "a b" { + t.Fatalf("ExactParts = %q, want the part and the item", got) + } + if got := names("ExactHeavy"); got != "" { + t.Fatalf("ExactHeavy = %q, want none: a model type is not a metaclass", got) + } + if got := names("ExactQualified"); got != "a" { + t.Fatalf("ExactQualified = %q, want the part: a qualified metaclass name is the metaclass", got) + } + machine := Bindings{"source": {ElementValue(fixture.symbol(t, "Machine"))}} + exact := func(query string) string { + result, err := fixture.execute(t, query, machine, Options{}) + if err != nil { + t.Fatalf("%s: %v", query, err) + } + var out []string + for _, row := range result.Rows() { + sym, _ := row.Element().Element() + out = append(out, sym.Name) + } + return strings.Join(out, " ") + } + if got := exact("ExactTransitions"); got != "go" { + t.Fatalf("ExactTransitions = %q, want the transition: its metaclass is TransitionUsage", got) + } + if got := exact("ExactStates"); got != "first second" { + t.Fatalf("ExactStates = %q, want the two states and not the transition", got) + } +} diff --git a/internal/workspace/libs/extensions-project/.meta.json b/internal/workspace/libs/extensions-project/.meta.json index 0ca5e582e6..026058eb36 100644 --- a/internal/workspace/libs/extensions-project/.meta.json +++ b/internal/workspace/libs/extensions-project/.meta.json @@ -14,7 +14,7 @@ }, "libraries/DocumentQueries.sysml": { "algorithm": "SHA256", - "value": "370b6f880bb2dc375b534f388e3c802c5820cc45e13977c38b3d36a9d1710233" + "value": "7cad8b83772182db5aa917ed36c19feb2e0796ecd6cfbc3b6b1a4c33729b11a7" }, "libraries/IdentityMetadata.sysml": { "algorithm": "SHA256", @@ -69,7 +69,7 @@ "value": "9e00f049209584092dc8bcb398b736c08e6a39b1db667a8a744de3df72a543bf" } }, - "created": "2026-10-27T13:19:03Z", + "created": "2026-10-29T15:08:48Z", "index": { "AnalysisRecords": "libraries/AnalysisRecords.sysml", "DiagramLayout": "libraries/DiagramLayout.sysml", diff --git a/internal/workspace/libs/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index ecaa9bb75d..0eb658997d 100644 Binary files a/internal/workspace/libs/stdlib.snapshot and b/internal/workspace/libs/stdlib.snapshot differ diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml b/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml index 6f7022adb2..bcdd3f3272 100644 --- a/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/DocumentQueries.sysml @@ -144,10 +144,14 @@ library package DocumentQueries { return result : Element[0..*] ordered; } - /* WhereType keeps the elements that are of any of the named types. */ + /* WhereType keeps the elements that are of any of the named types: a metaclass + * (PartUsage) or a definition/usage of the model, subtypes included. */ calc def WhereType { in source : Element[0..*] ordered; in type : String[1..*] ordered; + /* exact = true keeps only the elements whose own metaclass is one of the named + * ones, neither its subclasses nor the model's types: the reflective walk. */ + in exact : Boolean[1] default false; return result : Element[0..*] ordered; } diff --git a/scripts/extension-libraries-pin.sh b/scripts/extension-libraries-pin.sh index 196576ba0d..2dc047faf8 100755 --- a/scripts/extension-libraries-pin.sh +++ b/scripts/extension-libraries-pin.sh @@ -8,4 +8,4 @@ # verifies, because a ref is mutable. Change them together. EXTENSIONS_REPO="${EXTENSIONS_REPO:-https://github.com/Open-MBEE/OpenSysML-Extensions-Library.git}" EXTENSIONS_REF="${EXTENSIONS_REF:-main}" -EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-745963b151f925e4543b03d5bb09b978aa492e12}" +EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-1ef079deaa98ff8be988ab7dea2f7a8c0640fb22}"