provide mechanism to call functions when doing aggregations.

provide a standardized format for token aggregation ($.system|$.code)
This commit is contained in:
Jason Kulatunga 2023-10-02 13:42:41 -07:00
parent af2344ec00
commit 01c293bf40
No known key found for this signature in database
5 changed files with 257 additions and 68 deletions

View File

@ -18,18 +18,19 @@ import (
type SearchParameterType string type SearchParameterType string
const ( const (
//simple types
SearchParameterTypeNumber SearchParameterType = "number" SearchParameterTypeNumber SearchParameterType = "number"
SearchParameterTypeDate SearchParameterType = "date" SearchParameterTypeDate SearchParameterType = "date"
SearchParameterTypeUri SearchParameterType = "uri"
SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive.
//complex types
SearchParameterTypeString SearchParameterType = "string" SearchParameterTypeString SearchParameterType = "string"
SearchParameterTypeToken SearchParameterType = "token" SearchParameterTypeToken SearchParameterType = "token"
SearchParameterTypeReference SearchParameterType = "reference" SearchParameterTypeReference SearchParameterType = "reference"
SearchParameterTypeUri SearchParameterType = "uri"
SearchParameterTypeQuantity SearchParameterType = "quantity" SearchParameterTypeQuantity SearchParameterType = "quantity"
SearchParameterTypeComposite SearchParameterType = "composite" SearchParameterTypeComposite SearchParameterType = "composite"
SearchParameterTypeSpecial SearchParameterType = "special" SearchParameterTypeSpecial SearchParameterType = "special"
SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive.
) )
const TABLE_ALIAS = "fhir" const TABLE_ALIAS = "fhir"
@ -58,7 +59,7 @@ func (sr *SqliteRepository) QueryResources(ctx context.Context, query models.Que
return nil, err return nil, err
} }
if query.Aggregations != nil && (len(query.Aggregations.GroupBy) > 0 || len(query.Aggregations.CountBy) > 0) { if query.Aggregations != nil && (query.Aggregations.GroupBy != nil || query.Aggregations.CountBy != nil) {
results := []map[string]interface{}{} results := []map[string]interface{}{}
clientResp := sqlQuery.Find(&results) clientResp := sqlQuery.Find(&results)
return results, clientResp.Error return results, clientResp.Error
@ -148,27 +149,30 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
//Handle Aggregations //Handle Aggregations
if len(query.Aggregations.CountBy) > 0 { if query.Aggregations.CountBy != nil {
//populate the group by and order by clause with the count by values //populate the group by and order by clause with the count by values
query.Aggregations.OrderBy = "count(*) DESC" query.Aggregations.OrderBy = &models.QueryResourceAggregation{
Field: "*",
Function: "count",
}
query.Aggregations.GroupBy = query.Aggregations.CountBy query.Aggregations.GroupBy = query.Aggregations.CountBy
if query.Aggregations.GroupBy == "*" { if query.Aggregations.GroupBy.Field == "*" {
//we need to get the count of all resources, so we need to remove the group by clause and replace it by //we need to get the count of all resources, so we need to remove the group by clause and replace it by
// `source_resource_type` which will be the same for all resources // `source_resource_type` which will be the same for all resources
query.Aggregations.GroupBy = "source_resource_type" query.Aggregations.GroupBy.Field = "source_resource_type"
} }
} }
//process order by clause //process order by clause
if len(query.Aggregations.OrderBy) > 0 { if query.Aggregations.OrderBy != nil {
orderAsc := true //default to ascending, switch to desc if parameter is a date type. orderAsc := true //default to ascending, switch to desc if parameter is a date type.
if !strings.HasPrefix(query.Aggregations.OrderBy, "count(*)") { if !(query.Aggregations.OrderBy.Field == "*") {
orderAggregationParam, err := ProcessAggregationParameter(query.Aggregations.OrderBy, searchCodeToTypeLookup) orderAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.OrderBy, searchCodeToTypeLookup)
if err != nil { if err != nil {
return nil, err return nil, err
} }
orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam) orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam.SearchParameter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -186,17 +190,17 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
orderClause = fmt.Sprintf("%s DESC", orderClause) orderClause = fmt.Sprintf("%s DESC", orderClause)
} }
} else { } else {
orderClause = query.Aggregations.OrderBy orderClause = fmt.Sprintf("%s(%s) DESC", query.Aggregations.OrderBy.Function, query.Aggregations.OrderBy.Field)
} }
} }
//process group by clause //process group by clause
if len(query.Aggregations.GroupBy) > 0 { if query.Aggregations.GroupBy != nil {
groupAggregationParam, err := ProcessAggregationParameter(query.Aggregations.GroupBy, searchCodeToTypeLookup) groupAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.GroupBy, searchCodeToTypeLookup)
if err != nil { if err != nil {
return nil, err return nil, err
} }
groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam) groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam.SearchParameter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -205,8 +209,22 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
groupClause = AggregationParameterToClause(groupAggregationParam) groupClause = AggregationParameterToClause(groupAggregationParam)
selectClauses = []string{ selectClauses = []string{
fmt.Sprintf("%s as %s", groupClause, "label"), fmt.Sprintf("%s as %s", groupClause, "label"),
"count(*) as value",
} }
if query.Aggregations.OrderBy == nil || query.Aggregations.OrderBy.Field == "*" {
selectClauses = append(selectClauses, fmt.Sprintf("%s as %s", "count(*)", "value"))
orderClause = fmt.Sprintf("%s DESC", "count(*)")
} else {
//use the orderBy aggregation as the value
orderAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.OrderBy, searchCodeToTypeLookup)
if err != nil {
return nil, err
}
orderSelectClause := AggregationParameterToClause(orderAggregationParam)
selectClauses = append(selectClauses, fmt.Sprintf("%s as %s", orderSelectClause, "value"))
}
} }
} }
@ -216,21 +234,22 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
fromClauses = lo.Uniq(fromClauses) fromClauses = lo.Uniq(fromClauses)
fromClauses = lo.Compact(fromClauses) fromClauses = lo.Compact(fromClauses)
fluentQuery := sr.GormClient.WithContext(ctx). sqlQuery := sr.GormClient.WithContext(ctx).
Select(strings.Join(selectClauses, ", ")). Select(strings.Join(selectClauses, ", ")).
Where(strings.Join(whereClauses, " AND "), whereNamedParameters). Where(strings.Join(whereClauses, " AND "), whereNamedParameters).
Group(groupClause). Group(groupClause).
Order(orderClause) Order(orderClause).
Table(strings.Join(fromClauses, ", "))
//add limit and offset clauses if present //add limit and offset clauses if present
if query.Limit != nil { if query.Limit != nil {
fluentQuery = fluentQuery.Limit(*query.Limit) sqlQuery = sqlQuery.Limit(*query.Limit)
} }
if query.Offset != nil { if query.Offset != nil {
fluentQuery = fluentQuery.Offset(*query.Offset) sqlQuery = sqlQuery.Offset(*query.Offset)
} }
return fluentQuery.Table(strings.Join(fromClauses, ", ")), nil return sqlQuery, nil
} }
/// INTERNAL functionality. These functions are exported for testing, but are not available in the Interface /// INTERNAL functionality. These functions are exported for testing, but are not available in the Interface
@ -242,6 +261,11 @@ type SearchParameter struct {
Modifier string Modifier string
} }
type AggregationParameter struct {
SearchParameter
Function string //count, sum, avg, min, max, etc
}
// Lists in the SearchParameterValueOperatorTree are AND'd together, and items within each SearchParameterValueOperatorTree list are OR'd together // Lists in the SearchParameterValueOperatorTree are AND'd together, and items within each SearchParameterValueOperatorTree list are OR'd together
// For example, the following would be AND'd together, and then OR'd with the next SearchParameterValueOperatorTree // For example, the following would be AND'd together, and then OR'd with the next SearchParameterValueOperatorTree
// //
@ -584,14 +608,31 @@ func SearchCodeToFromClause(searchParam SearchParameter) (string, error) {
return "", nil return "", nil
} }
func AggregationParameterToClause(aggParameter SearchParameter) string { func AggregationParameterToClause(aggParameter AggregationParameter) string {
var clause string
switch aggParameter.Type { switch aggParameter.Type {
case SearchParameterTypeQuantity, SearchParameterTypeToken, SearchParameterTypeString: case SearchParameterTypeQuantity, SearchParameterTypeString:
//setup the clause //setup the clause
return fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier) clause = fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier)
default: case SearchParameterTypeToken:
return fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name) //modifier is optional for token types.
if aggParameter.Modifier != "" {
clause = fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier)
} else {
//if no modifier is specified, use the system and code to generate the clause
//((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code'))
clause = fmt.Sprintf("((%sJson.value ->> '$.system') || '|' || (%sJson.value ->> '$.code'))", aggParameter.Name, aggParameter.Name)
} }
default:
clause = fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name)
}
if len(aggParameter.Function) > 0 {
clause = fmt.Sprintf("%s(%s)", aggParameter.Function, clause)
}
return clause
} }
// ProcessAggregationParameter processes the aggregation parameters which are fields with optional properties: // ProcessAggregationParameter processes the aggregation parameters which are fields with optional properties:
@ -602,12 +643,15 @@ func AggregationParameterToClause(aggParameter SearchParameter) string {
// eg. `identifier:code` // eg. `identifier:code`
// //
// if the a property is specified, its set as the modifier, and used when generating the SQL query groupBy, orderBy, etc clause // if the a property is specified, its set as the modifier, and used when generating the SQL query groupBy, orderBy, etc clause
func ProcessAggregationParameter(aggregationFieldWithProperty string, searchParamTypeLookup map[string]string) (SearchParameter, error) { func ProcessAggregationParameter(aggregationFieldWithFn models.QueryResourceAggregation, searchParamTypeLookup map[string]string) (AggregationParameter, error) {
aggregationParameter := SearchParameter{} aggregationParameter := AggregationParameter{
SearchParameter: SearchParameter{},
Function: aggregationFieldWithFn.Function,
}
//determine the searchCode searchCodeModifier //determine the searchCode searchCodeModifier
//TODO: this is only applicable to string, token, reference and uri type (however unknown names & modifiers are ignored) //TODO: this is only applicable to string, token, reference and uri type (however unknown names & modifiers are ignored)
if aggregationFieldParts := strings.SplitN(aggregationFieldWithProperty, ":", 2); len(aggregationFieldParts) == 2 { if aggregationFieldParts := strings.SplitN(aggregationFieldWithFn.Field, ":", 2); len(aggregationFieldParts) == 2 {
aggregationParameter.Name = aggregationFieldParts[0] aggregationParameter.Name = aggregationFieldParts[0]
aggregationParameter.Modifier = aggregationFieldParts[1] aggregationParameter.Modifier = aggregationFieldParts[1]
} else { } else {
@ -618,7 +662,7 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara
//next, determine the searchCodeType for this Resource (or throw an error if it is unknown) //next, determine the searchCodeType for this Resource (or throw an error if it is unknown)
searchParamTypeStr, searchParamTypeOk := searchParamTypeLookup[aggregationParameter.Name] searchParamTypeStr, searchParamTypeOk := searchParamTypeLookup[aggregationParameter.Name]
if !searchParamTypeOk { if !searchParamTypeOk {
return aggregationParameter, fmt.Errorf("unknown search parameter: %s", aggregationParameter.Name) return aggregationParameter, fmt.Errorf("unknown search parameter in aggregation: %s", aggregationParameter.Name)
} else { } else {
aggregationParameter.Type = SearchParameterType(searchParamTypeStr) aggregationParameter.Type = SearchParameterType(searchParamTypeStr)
} }
@ -628,6 +672,8 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara
if len(aggregationParameter.Modifier) > 0 { if len(aggregationParameter.Modifier) > 0 {
return aggregationParameter, fmt.Errorf("primitive aggregation parameter %s cannot have a property (%s)", aggregationParameter.Name, aggregationParameter.Modifier) return aggregationParameter, fmt.Errorf("primitive aggregation parameter %s cannot have a property (%s)", aggregationParameter.Name, aggregationParameter.Modifier)
} }
} else if aggregationParameter.Type == SearchParameterTypeToken {
//modifier is optional for token types
} else { } else {
//complex types must have a modifier //complex types must have a modifier
if len(aggregationParameter.Modifier) == 0 { if len(aggregationParameter.Modifier) == 0 {

View File

@ -98,7 +98,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL() {
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson",
"WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)", "WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)",
"GROUP BY `fhir`.`id`", "GROUP BY `fhir`.`id`",
"ORDER BY fhir.sort_date ASC", "ORDER BY fhir.sort_date DESC",
}, " "), }, " "),
sqlString) sqlString)
require.Equal(suite.T(), sqlParams, []interface{}{ require.Equal(suite.T(), sqlParams, []interface{}{
@ -136,7 +136,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithMultipleWhereCon
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson, json_each(fhir.category) as categoryJson", "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson, json_each(fhir.category) as categoryJson",
"WHERE ((codeJson.value ->> '$.code' = ?)) AND ((categoryJson.value ->> '$.code' = ?)) AND (user_id = ?)", "WHERE ((codeJson.value ->> '$.code' = ?)) AND ((categoryJson.value ->> '$.code' = ?)) AND (user_id = ?)",
"GROUP BY `fhir`.`id`", "GROUP BY `fhir`.`id`",
"ORDER BY fhir.sort_date ASC", "ORDER BY fhir.sort_date DESC",
}, " "), }, " "),
sqlString) sqlString)
require.Equal(suite.T(), sqlParams, []interface{}{ require.Equal(suite.T(), sqlParams, []interface{}{
@ -158,7 +158,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveOrderBy
"activityCode": "test_code", "activityCode": "test_code",
}, },
From: "CarePlan", From: "CarePlan",
Aggregations: &models.QueryResourceAggregations{OrderBy: "instantiatesUri"}, Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}},
}) })
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
var results []map[string]interface{} var results []map[string]interface{}
@ -193,7 +193,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordOrderByAg
Select: []string{}, Select: []string{},
Where: map[string]interface{}{}, Where: map[string]interface{}{},
From: "CarePlan", From: "CarePlan",
Aggregations: &models.QueryResourceAggregations{OrderBy: "id"}, Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "id"}},
}) })
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
var results []map[string]interface{} var results []map[string]interface{}
@ -230,7 +230,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexOrderByAg
"code": "test_code", "code": "test_code",
}, },
From: "Observation", From: "Observation",
Aggregations: &models.QueryResourceAggregations{OrderBy: "valueString:value"}, Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "valueString:value"}},
}) })
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
var results []map[string]interface{} var results []map[string]interface{}
@ -267,7 +267,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveCountBy
"activityCode": "test_code", "activityCode": "test_code",
}, },
From: "CarePlan", From: "CarePlan",
Aggregations: &models.QueryResourceAggregations{CountBy: "instantiatesUri"}, Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}},
}) })
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
var results []map[string]interface{} var results []map[string]interface{}
@ -304,7 +304,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordCountByAg
"activityCode": "test_code", "activityCode": "test_code",
}, },
From: "CarePlan", From: "CarePlan",
Aggregations: &models.QueryResourceAggregations{CountBy: "source_resource_type"}, Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "source_resource_type"}},
}) })
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
var results []map[string]interface{} var results []map[string]interface{}
@ -339,7 +339,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithWildcardCountByA
Select: []string{}, Select: []string{},
Where: map[string]interface{}{}, Where: map[string]interface{}{},
From: "CarePlan", From: "CarePlan",
Aggregations: &models.QueryResourceAggregations{CountBy: "*"}, Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "*"}},
}) })
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
var results []map[string]interface{} var results []map[string]interface{}
@ -376,7 +376,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg
"code": "test_code", "code": "test_code",
}, },
From: "Observation", From: "Observation",
Aggregations: &models.QueryResourceAggregations{CountBy: "code:code"}, Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "code:code"}},
}) })
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
var results []map[string]interface{} var results []map[string]interface{}
@ -398,3 +398,120 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg
"test_code", "00000000-0000-0000-0000-000000000000", "test_code", "00000000-0000-0000-0000-000000000000",
}) })
} }
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexGroupByWithOrderByMaxFnAggregation() {
//setup
sqliteRepo := suite.TestRepository.(*SqliteRepository)
sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true})
//test
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{
Select: []string{},
Where: map[string]interface{}{
"code": "test_code",
},
From: "Observation",
Aggregations: &models.QueryResourceAggregations{
GroupBy: &models.QueryResourceAggregation{Field: "code:code"},
OrderBy: &models.QueryResourceAggregation{Field: "sort_date", Function: "max"},
},
})
require.NoError(suite.T(), err)
var results []map[string]interface{}
statement := sqlQuery.Find(&results).Statement
sqlString := statement.SQL.String()
sqlParams := statement.Vars
//assert
require.NoError(suite.T(), err)
require.Equal(suite.T(),
strings.Join([]string{
"SELECT (codeJson.value ->> '$.code') as label, max(fhir.sort_date) as value",
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson",
"WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)",
"GROUP BY (codeJson.value ->> '$.code')",
"ORDER BY max(fhir.sort_date) DESC",
}, " "), sqlString)
require.Equal(suite.T(), sqlParams, []interface{}{
"test_code", "00000000-0000-0000-0000-000000000000",
})
}
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenGroupByNoModifier() {
//setup
sqliteRepo := suite.TestRepository.(*SqliteRepository)
sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true})
//test
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{
Select: []string{},
Where: map[string]interface{}{},
From: "Observation",
Aggregations: &models.QueryResourceAggregations{
GroupBy: &models.QueryResourceAggregation{Field: "code"},
},
})
require.NoError(suite.T(), err)
var results []map[string]interface{}
statement := sqlQuery.Find(&results).Statement
sqlString := statement.SQL.String()
sqlParams := statement.Vars
//assert
require.NoError(suite.T(), err)
require.Equal(suite.T(),
strings.Join([]string{
"SELECT ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code')) as label, count(*) as value",
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson",
"WHERE (user_id = ?)",
"GROUP BY ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code'))",
"ORDER BY count(*) DESC",
}, " "), sqlString)
require.Equal(suite.T(), sqlParams, []interface{}{
"00000000-0000-0000-0000-000000000000",
})
}
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenGroupByNoModifierWithLimit() {
//setup
sqliteRepo := suite.TestRepository.(*SqliteRepository)
sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true})
//test
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
limit := 10
sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{
Select: []string{},
Where: map[string]interface{}{},
From: "Observation",
Limit: &limit,
Aggregations: &models.QueryResourceAggregations{
GroupBy: &models.QueryResourceAggregation{Field: "code"},
},
})
require.NoError(suite.T(), err)
var results []map[string]interface{}
statement := sqlQuery.Find(&results).Statement
sqlString := statement.SQL.String()
sqlParams := statement.Vars
//assert
require.NoError(suite.T(), err)
require.Equal(suite.T(),
strings.Join([]string{
"SELECT ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code')) as label, count(*) as value",
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson",
"WHERE (user_id = ?)",
"GROUP BY ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code'))",
"ORDER BY count(*) DESC",
"LIMIT 10",
}, " "), sqlString)
require.Equal(suite.T(), sqlParams, []interface{}{
"00000000-0000-0000-0000-000000000000",
})
}

View File

@ -19,10 +19,15 @@ type QueryResource struct {
} }
type QueryResourceAggregations struct { type QueryResourceAggregations struct {
CountBy string `json:"count_by"` //alias for both groupby and orderby, cannot be used together CountBy *QueryResourceAggregation `json:"count_by,omitempty"` //alias for both groupby and orderby, cannot be used together
GroupBy string `json:"group_by"` GroupBy *QueryResourceAggregation `json:"group_by,omitempty"`
OrderBy string `json:"order_by"` OrderBy *QueryResourceAggregation `json:"order_by,omitempty"`
}
type QueryResourceAggregation struct {
Field string `json:"field"`
Function string `json:"fn"`
} }
func (q *QueryResource) Validate() error { func (q *QueryResource) Validate() error {
@ -38,26 +43,44 @@ func (q *QueryResource) Validate() error {
if len(q.Select) > 0 { if len(q.Select) > 0 {
return fmt.Errorf("cannot use 'select' and 'aggregations' together") return fmt.Errorf("cannot use 'select' and 'aggregations' together")
} }
if len(q.Aggregations.CountBy) > 0 {
if len(q.Aggregations.GroupBy) > 0 { if q.Aggregations.CountBy != nil {
if len(q.Aggregations.CountBy.Field) == 0 {
return fmt.Errorf("if 'count_by' is present, field must be populated")
}
if strings.Contains(q.Aggregations.CountBy.Field, " ") {
return fmt.Errorf("count_by cannot have spaces (or aliases)")
}
}
if q.Aggregations.GroupBy != nil {
if len(q.Aggregations.GroupBy.Field) == 0 {
return fmt.Errorf("if 'group_by' is present, field must be populated")
}
if strings.Contains(q.Aggregations.GroupBy.Field, " ") {
return fmt.Errorf("group_by cannot have spaces (or aliases)")
}
}
if q.Aggregations.OrderBy != nil {
if len(q.Aggregations.OrderBy.Field) == 0 {
return fmt.Errorf("if 'order_by' is present, field must be populated")
}
if strings.Contains(q.Aggregations.OrderBy.Field, " ") {
return fmt.Errorf("order_by cannot have spaces (or aliases)")
}
}
if q.Aggregations.CountBy != nil {
if q.Aggregations.GroupBy != nil {
return fmt.Errorf("cannot use 'count_by' and 'group_by' together") return fmt.Errorf("cannot use 'count_by' and 'group_by' together")
} }
if len(q.Aggregations.OrderBy) > 0 { if q.Aggregations.OrderBy != nil {
return fmt.Errorf("cannot use 'count_by' and 'order_by' together") return fmt.Errorf("cannot use 'count_by' and 'order_by' together")
} }
} }
if len(q.Aggregations.CountBy) == 0 && len(q.Aggregations.OrderBy) == 0 && len(q.Aggregations.GroupBy) == 0 { if q.Aggregations.CountBy == nil && q.Aggregations.OrderBy == nil && q.Aggregations.GroupBy == nil {
return fmt.Errorf("aggregations must have at least one of 'count_by', 'group_by', or 'order_by'") return fmt.Errorf("aggregations must have at least one of 'count_by', 'group_by', or 'order_by'")
} }
if strings.Contains(q.Aggregations.CountBy, " ") {
return fmt.Errorf("count_by cannot have spaces (or aliases)")
}
if strings.Contains(q.Aggregations.GroupBy, " ") {
return fmt.Errorf("group_by cannot have spaces (or aliases)")
}
if strings.Contains(q.Aggregations.OrderBy, " ") {
return fmt.Errorf("order_by cannot have spaces (or aliases)")
}
} }
if q.Limit != nil && *q.Limit < 0 { if q.Limit != nil && *q.Limit < 0 {

View File

@ -13,15 +13,18 @@ func TestQueryResource_Validate(t *testing.T) {
}{ }{
{QueryResource{Use: "test"}, "'use' is not supported yet", true}, {QueryResource{Use: "test"}, "'use' is not supported yet", true},
{QueryResource{}, "'from' is required", true}, {QueryResource{}, "'from' is required", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test"}}, "", false}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: ""}}}, "if 'count_by' is present, field must be populated", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: &QueryResourceAggregation{Field: ""}}}, "if 'group_by' is present, field must be populated", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: &QueryResourceAggregation{Field: ""}}}, "if 'order_by' is present, field must be populated", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}}}, "", false},
{QueryResource{Select: []string{"test"}, From: "test", Aggregations: &QueryResourceAggregations{}}, "cannot use 'select' and 'aggregations' together", true}, {QueryResource{Select: []string{"test"}, From: "test", Aggregations: &QueryResourceAggregations{}}, "cannot use 'select' and 'aggregations' together", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test", GroupBy: "test"}}, "cannot use 'count_by' and 'group_by' together", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}, GroupBy: &QueryResourceAggregation{Field: "test"}}}, "cannot use 'count_by' and 'group_by' together", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test", OrderBy: "test"}}, "cannot use 'count_by' and 'order_by' together", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}, OrderBy: &QueryResourceAggregation{Field: "test"}}}, "cannot use 'count_by' and 'order_by' together", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{}}, "aggregations must have at least one of 'count_by', 'group_by', or 'order_by'", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{}}, "aggregations must have at least one of 'count_by', 'group_by', or 'order_by'", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test:property"}}, "", false}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test:property"}}}, "", false},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test:property as HELLO"}}, "count_by cannot have spaces (or aliases)", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "count_by cannot have spaces (or aliases)", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: "test:property as HELLO"}}, "group_by cannot have spaces (or aliases)", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "group_by cannot have spaces (or aliases)", true},
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: "test:property as HELLO"}}, "order_by cannot have spaces (or aliases)", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "order_by cannot have spaces (or aliases)", true},
} }
//test && assert //test && assert

View File

@ -132,7 +132,7 @@
"from": "Observation", "from": "Observation",
"where": {}, "where": {},
"aggregations":{ "aggregations":{
"count_by": "code:code" "count_by": {"field": "code:code" }
} }
} }
}], }],
@ -158,7 +158,7 @@
"from": "Immunization", "from": "Immunization",
"where": {}, "where": {},
"aggregations":{ "aggregations":{
"count_by": "*" "count_by": {"field": "*" }
} }
} }
}, },
@ -171,7 +171,7 @@
"from": "Claim", "from": "Claim",
"where": {}, "where": {},
"aggregations":{ "aggregations":{
"count_by": "*" "count_by": {"field": "*" }
} }
} }
}], }],