Skip to content

Commit

Permalink
Add support for the distinct operator.
Browse files Browse the repository at this point in the history
ex:
```
distinct {1, 2, 2}
```
should return `{1, 2}`
PiperOrigin-RevId: 705586267
  • Loading branch information
evan-gordon authored and copybara-github committed Dec 12, 2024
1 parent 7015a55 commit 9cd34c6
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 1 deletion.
7 changes: 7 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo
Result: evalExists,
},
}, nil
case *model.Distinct:
return []convert.Overload[evalUnarySignature]{
{
Operands: []types.IType{&types.List{ElementType: types.Any}},
Result: evalDistinct,
},
}, nil
case *model.First:
return []convert.Overload[evalUnarySignature]{
{
Expand Down
25 changes: 25 additions & 0 deletions interpreter/operator_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,31 @@ func evalInList(m model.IBinaryExpression, lObj, listObj result.Value) (result.V
return result.New(valueInList(lObj, r))
}

// Distinct(argument List<T>) List<T>
// https://cql.hl7.org/09-b-cqlreference.html#distinct
// In the future we should make result.Value hashable so we can use a map instead of a list to
// check for duplicates.
func evalDistinct(m model.IUnaryExpression, listObj result.Value) (result.Value, error) {
if result.IsNull(listObj) {
return result.New(nil)
}
list, err := result.ToSlice(listObj)
if err != nil {
return result.Value{}, err
}

var distinctList []result.Value
for _, elemObj := range list {
if !valueInList(elemObj, distinctList) {
distinctList = append(distinctList, elemObj)
}
}
return result.New(result.List{
Value: distinctList,
StaticType: listObj.GolangValue().(result.List).StaticType,
})
}

// First(argument List<T>) T
// https://cql.hl7.org/09-b-cqlreference.html#first
func evalFirst(m model.IUnaryExpression, listObj result.Value) (result.Value, error) {
Expand Down
6 changes: 6 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,9 @@ type Last struct {

var _ IUnaryExpression = &Last{}

// Distinct is https://cql.hl7.org/04-logicalspecification.html#distinct.
type Distinct struct{ *UnaryExpression }

// Abs is https://cql.hl7.org/04-logicalspecification.html#abs.
type Abs struct{ *UnaryExpression }

Expand Down Expand Up @@ -1218,6 +1221,9 @@ func (n *Not) GetName() string { return "Not" }
// GetName returns the name of the system operator.
func (a *Truncate) GetName() string { return "Truncate" }

// GetName returns the name of the system operator.
func (a *Distinct) GetName() string { return "Distinct" }

// GetName returns the name of the system operator.
func (f *First) GetName() string { return "First" }

Expand Down
2 changes: 2 additions & 0 deletions parser/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func (v *visitor) VisitExpression(tree antlr.Tree) model.IExpression {
m = v.VisitElementExtractorExpressionTerm(t)
case *cql.IndexedExpressionTermContext:
m = v.VisitIndexedExpressionTermContext(t)
case *cql.AggregateExpressionTermContext:
m = v.VisitAggregateExpressionTerm(t)

// All cases that have a single child and recurse to the child are handled below. For example in
// the CQL grammar the only child of QueryExpression is Query, so QueryExpression can be handled
Expand Down
12 changes: 12 additions & 0 deletions parser/operator_expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,15 @@ func (v *visitor) VisitIndexedExpressionTermContext(ctx *cql.IndexedExpressionTe
}
return m
}

func (v *visitor) VisitAggregateExpressionTerm(ctx *cql.AggregateExpressionTermContext) model.IExpression {
name := ctx.GetChild(0).(antlr.TerminalNode).GetText()
if name != "distinct" {
return v.badExpression(fmt.Sprintf("unsupported aggregate expression: %s", name), ctx)
}
m, err := v.parseFunction("", "Distinct", []antlr.Tree{ctx.Expression()}, false)
if err != nil {
return v.badExpression(err.Error(), ctx)
}
return m
}
14 changes: 14 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ func (v *visitor) resolveFunction(libraryName, funcName string, operands []model
// Last(List<T>) T is a special case because the ResultType is not known until invocation.
listType := resolved.WrappedOperands[0].GetResultType().(*types.List)
t.Expression = model.ResultType(listType.ElementType)
case *model.Distinct:
// Distinct(List<T>) List<T> is a special case because the ResultType is not known until invocation.
listType := resolved.WrappedOperands[0].GetResultType().(*types.List)
t.Expression = model.ResultType(&types.List{ElementType: listType.ElementType})
case *model.Indexer:
switch opType := resolved.WrappedOperands[0].GetResultType().(type) {
case types.System:
Expand Down Expand Up @@ -1564,6 +1568,16 @@ func (p *Parser) loadSystemOperators() error {
}
},
},
{
name: "Distinct",
operands: [][]types.IType{
{&types.List{ElementType: types.Any}}},
model: func() model.IExpression {
return &model.Distinct{
UnaryExpression: &model.UnaryExpression{},
}
},
},
{
name: "Intersect",
operands: [][]types.IType{{&types.List{ElementType: types.Any}, &types.List{ElementType: types.Any}}},
Expand Down
17 changes: 17 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,23 @@ func TestBuiltInFunctions(t *testing.T) {
},
},
},
{
name: "Distinct",
cql: "Distinct({1, 2, 1})",
want: &model.Distinct{
UnaryExpression: &model.UnaryExpression{
Operand: &model.List{
Expression: model.ResultType(&types.List{ElementType: types.Integer}),
List: []model.IExpression{
model.NewLiteral("1", types.Integer),
model.NewLiteral("2", types.Integer),
model.NewLiteral("1", types.Integer),
},
},
Expression: model.ResultType(&types.List{ElementType: types.Integer}),
},
},
},
{
name: "Intersect",
cql: "Intersect({1}, {1})",
Expand Down
89 changes: 89 additions & 0 deletions tests/enginetests/operator_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,95 @@ func TestInList(t *testing.T) {
}
}

func TestDistinctList(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
{
name: "Distinct list",
cql: "distinct {1, 2, 1}",
wantModel: &model.Distinct{
UnaryExpression: &model.UnaryExpression{
Operand: &model.List{
Expression: model.ResultType(&types.List{ElementType: types.Integer}),
List: []model.IExpression{
model.NewLiteral("1", types.Integer),
model.NewLiteral("2", types.Integer),
model.NewLiteral("1", types.Integer),
},
},
Expression: model.ResultType(&types.List{ElementType: types.Integer}),
},
},
wantResult: newOrFatal(t, result.List{
Value: []result.Value{
newOrFatal(t, int32(1)),
newOrFatal(t, int32(2)),
},
StaticType: &types.List{ElementType: types.Integer},
}),
},
{
name: "Distinct list with null",
cql: "distinct {1, null}",
wantResult: newOrFatal(t, result.List{
Value: []result.Value{
newOrFatal(t, int32(1)),
newOrFatal(t, nil),
},
StaticType: &types.List{ElementType: types.Integer},
}),
},
{
name: "distinct list with no duplicates",
cql: "distinct {1, 2, 3}",
wantResult: newOrFatal(t, result.List{
Value: []result.Value{
newOrFatal(t, int32(1)),
newOrFatal(t, int32(2)),
newOrFatal(t, int32(3)),
},
StaticType: &types.List{ElementType: types.Integer},
}),
},
{
name: "Functional syntax: In list",
cql: "Distinct({1, 2})",
wantResult: newOrFatal(t, result.List{
Value: []result.Value{
newOrFatal(t, int32(1)),
newOrFatal(t, int32(2)),
},
StaticType: &types.List{ElementType: types.Integer},
}),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := newFHIRParser(t)
parsedLibs, err := p.Libraries(context.Background(), wrapInLib(t, tc.cql), parser.Config{})
if err != nil {
t.Fatalf("Parse returned unexpected error: %v", err)
}
if diff := cmp.Diff(tc.wantModel, getTESTRESULTModel(t, parsedLibs)); tc.wantModel != nil && diff != "" {
t.Errorf("Parse diff (-want +got):\n%s", diff)
}

results, err := interpreter.Eval(context.Background(), parsedLibs, defaultInterpreterConfig(t, p))
if err != nil {
t.Fatalf("Eval returned unexpected error: %v", err)
}
if diff := cmp.Diff(tc.wantResult, getTESTRESULT(t, results), protocmp.Transform()); diff != "" {
t.Errorf("Eval diff (-want +got)\n%v", diff)
}

})
}
}

func TestFirst(t *testing.T) {
tests := []struct {
name string
Expand Down
1 change: 0 additions & 1 deletion tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
GroupExcludes: []string{
// TODO: b/342061715 - unsupported operators.
"Descendents",
"Distinct",
"Except",
"Flatten",
"Includes",
Expand Down

0 comments on commit 9cd34c6

Please sign in to comment.