provide mechanism to call functions when doing aggregations.
provide a standardized format for token aggregation ($.system|$.code)
This commit is contained in:
parent
af2344ec00
commit
01c293bf40
|
@ -18,18 +18,19 @@ import (
|
|||
type SearchParameterType string
|
||||
|
||||
const (
|
||||
//simple types
|
||||
SearchParameterTypeNumber SearchParameterType = "number"
|
||||
SearchParameterTypeDate SearchParameterType = "date"
|
||||
SearchParameterTypeUri SearchParameterType = "uri"
|
||||
SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive.
|
||||
|
||||
//complex types
|
||||
SearchParameterTypeString SearchParameterType = "string"
|
||||
SearchParameterTypeToken SearchParameterType = "token"
|
||||
SearchParameterTypeReference SearchParameterType = "reference"
|
||||
SearchParameterTypeUri SearchParameterType = "uri"
|
||||
SearchParameterTypeQuantity SearchParameterType = "quantity"
|
||||
SearchParameterTypeComposite SearchParameterType = "composite"
|
||||
SearchParameterTypeSpecial SearchParameterType = "special"
|
||||
|
||||
SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive.
|
||||
|
||||
)
|
||||
|
||||
const TABLE_ALIAS = "fhir"
|
||||
|
@ -58,7 +59,7 @@ func (sr *SqliteRepository) QueryResources(ctx context.Context, query models.Que
|
|||
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{}{}
|
||||
clientResp := sqlQuery.Find(&results)
|
||||
return results, clientResp.Error
|
||||
|
@ -148,27 +149,30 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
|
|||
|
||||
//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
|
||||
query.Aggregations.OrderBy = "count(*) DESC"
|
||||
query.Aggregations.OrderBy = &models.QueryResourceAggregation{
|
||||
Field: "*",
|
||||
Function: "count",
|
||||
}
|
||||
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
|
||||
// `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
|
||||
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.
|
||||
if !strings.HasPrefix(query.Aggregations.OrderBy, "count(*)") {
|
||||
orderAggregationParam, err := ProcessAggregationParameter(query.Aggregations.OrderBy, searchCodeToTypeLookup)
|
||||
if !(query.Aggregations.OrderBy.Field == "*") {
|
||||
orderAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.OrderBy, searchCodeToTypeLookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam)
|
||||
orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam.SearchParameter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -186,17 +190,17 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
|
|||
orderClause = fmt.Sprintf("%s DESC", orderClause)
|
||||
}
|
||||
} else {
|
||||
orderClause = query.Aggregations.OrderBy
|
||||
orderClause = fmt.Sprintf("%s(%s) DESC", query.Aggregations.OrderBy.Function, query.Aggregations.OrderBy.Field)
|
||||
}
|
||||
}
|
||||
|
||||
//process group by clause
|
||||
if len(query.Aggregations.GroupBy) > 0 {
|
||||
groupAggregationParam, err := ProcessAggregationParameter(query.Aggregations.GroupBy, searchCodeToTypeLookup)
|
||||
if query.Aggregations.GroupBy != nil {
|
||||
groupAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.GroupBy, searchCodeToTypeLookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam)
|
||||
groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam.SearchParameter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -205,8 +209,22 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
|
|||
groupClause = AggregationParameterToClause(groupAggregationParam)
|
||||
selectClauses = []string{
|
||||
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.Compact(fromClauses)
|
||||
|
||||
fluentQuery := sr.GormClient.WithContext(ctx).
|
||||
sqlQuery := sr.GormClient.WithContext(ctx).
|
||||
Select(strings.Join(selectClauses, ", ")).
|
||||
Where(strings.Join(whereClauses, " AND "), whereNamedParameters).
|
||||
Group(groupClause).
|
||||
Order(orderClause)
|
||||
Order(orderClause).
|
||||
Table(strings.Join(fromClauses, ", "))
|
||||
|
||||
//add limit and offset clauses if present
|
||||
if query.Limit != nil {
|
||||
fluentQuery = fluentQuery.Limit(*query.Limit)
|
||||
sqlQuery = sqlQuery.Limit(*query.Limit)
|
||||
}
|
||||
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
|
||||
|
@ -242,6 +261,11 @@ type SearchParameter struct {
|
|||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
func AggregationParameterToClause(aggParameter SearchParameter) string {
|
||||
func AggregationParameterToClause(aggParameter AggregationParameter) string {
|
||||
var clause string
|
||||
|
||||
switch aggParameter.Type {
|
||||
case SearchParameterTypeQuantity, SearchParameterTypeToken, SearchParameterTypeString:
|
||||
case SearchParameterTypeQuantity, SearchParameterTypeString:
|
||||
//setup the clause
|
||||
return fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier)
|
||||
default:
|
||||
return fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name)
|
||||
clause = fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier)
|
||||
case SearchParameterTypeToken:
|
||||
//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:
|
||||
|
@ -602,12 +643,15 @@ func AggregationParameterToClause(aggParameter SearchParameter) string {
|
|||
// 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
|
||||
func ProcessAggregationParameter(aggregationFieldWithProperty string, searchParamTypeLookup map[string]string) (SearchParameter, error) {
|
||||
aggregationParameter := SearchParameter{}
|
||||
func ProcessAggregationParameter(aggregationFieldWithFn models.QueryResourceAggregation, searchParamTypeLookup map[string]string) (AggregationParameter, error) {
|
||||
aggregationParameter := AggregationParameter{
|
||||
SearchParameter: SearchParameter{},
|
||||
Function: aggregationFieldWithFn.Function,
|
||||
}
|
||||
|
||||
//determine the searchCode searchCodeModifier
|
||||
//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.Modifier = aggregationFieldParts[1]
|
||||
} 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)
|
||||
searchParamTypeStr, searchParamTypeOk := searchParamTypeLookup[aggregationParameter.Name]
|
||||
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 {
|
||||
aggregationParameter.Type = SearchParameterType(searchParamTypeStr)
|
||||
}
|
||||
|
@ -628,6 +672,8 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara
|
|||
if len(aggregationParameter.Modifier) > 0 {
|
||||
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 {
|
||||
//complex types must have a modifier
|
||||
if len(aggregationParameter.Modifier) == 0 {
|
||||
|
|
|
@ -98,7 +98,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL() {
|
|||
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson",
|
||||
"WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)",
|
||||
"GROUP BY `fhir`.`id`",
|
||||
"ORDER BY fhir.sort_date ASC",
|
||||
"ORDER BY fhir.sort_date DESC",
|
||||
}, " "),
|
||||
sqlString)
|
||||
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",
|
||||
"WHERE ((codeJson.value ->> '$.code' = ?)) AND ((categoryJson.value ->> '$.code' = ?)) AND (user_id = ?)",
|
||||
"GROUP BY `fhir`.`id`",
|
||||
"ORDER BY fhir.sort_date ASC",
|
||||
"ORDER BY fhir.sort_date DESC",
|
||||
}, " "),
|
||||
sqlString)
|
||||
require.Equal(suite.T(), sqlParams, []interface{}{
|
||||
|
@ -158,7 +158,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveOrderBy
|
|||
"activityCode": "test_code",
|
||||
},
|
||||
From: "CarePlan",
|
||||
Aggregations: &models.QueryResourceAggregations{OrderBy: "instantiatesUri"},
|
||||
Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
var results []map[string]interface{}
|
||||
|
@ -193,7 +193,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordOrderByAg
|
|||
Select: []string{},
|
||||
Where: map[string]interface{}{},
|
||||
From: "CarePlan",
|
||||
Aggregations: &models.QueryResourceAggregations{OrderBy: "id"},
|
||||
Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "id"}},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
var results []map[string]interface{}
|
||||
|
@ -230,7 +230,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexOrderByAg
|
|||
"code": "test_code",
|
||||
},
|
||||
From: "Observation",
|
||||
Aggregations: &models.QueryResourceAggregations{OrderBy: "valueString:value"},
|
||||
Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "valueString:value"}},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
var results []map[string]interface{}
|
||||
|
@ -267,7 +267,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveCountBy
|
|||
"activityCode": "test_code",
|
||||
},
|
||||
From: "CarePlan",
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: "instantiatesUri"},
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
var results []map[string]interface{}
|
||||
|
@ -304,7 +304,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordCountByAg
|
|||
"activityCode": "test_code",
|
||||
},
|
||||
From: "CarePlan",
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: "source_resource_type"},
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "source_resource_type"}},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
var results []map[string]interface{}
|
||||
|
@ -339,7 +339,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithWildcardCountByA
|
|||
Select: []string{},
|
||||
Where: map[string]interface{}{},
|
||||
From: "CarePlan",
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: "*"},
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "*"}},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
var results []map[string]interface{}
|
||||
|
@ -376,7 +376,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg
|
|||
"code": "test_code",
|
||||
},
|
||||
From: "Observation",
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: "code:code"},
|
||||
Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "code:code"}},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
var results []map[string]interface{}
|
||||
|
@ -398,3 +398,120 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg
|
|||
"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",
|
||||
})
|
||||
}
|
||||
|
|
|
@ -19,10 +19,15 @@ type QueryResource 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"`
|
||||
OrderBy string `json:"order_by"`
|
||||
GroupBy *QueryResourceAggregation `json:"group_by,omitempty"`
|
||||
OrderBy *QueryResourceAggregation `json:"order_by,omitempty"`
|
||||
}
|
||||
|
||||
type QueryResourceAggregation struct {
|
||||
Field string `json:"field"`
|
||||
Function string `json:"fn"`
|
||||
}
|
||||
|
||||
func (q *QueryResource) Validate() error {
|
||||
|
@ -38,26 +43,44 @@ func (q *QueryResource) Validate() error {
|
|||
if len(q.Select) > 0 {
|
||||
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")
|
||||
}
|
||||
if len(q.Aggregations.OrderBy) > 0 {
|
||||
if q.Aggregations.OrderBy != nil {
|
||||
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'")
|
||||
}
|
||||
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 {
|
||||
|
|
|
@ -13,15 +13,18 @@ func TestQueryResource_Validate(t *testing.T) {
|
|||
}{
|
||||
{QueryResource{Use: "test"}, "'use' is not supported yet", 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{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test", GroupBy: "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"}, GroupBy: &QueryResourceAggregation{Field: "test"}}}, "cannot use 'count_by' and 'group_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{CountBy: "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{GroupBy: "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{CountBy: &QueryResourceAggregation{Field: "test:property"}}}, "", false},
|
||||
{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: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "group_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
|
||||
|
|
|
@ -132,7 +132,7 @@
|
|||
"from": "Observation",
|
||||
"where": {},
|
||||
"aggregations":{
|
||||
"count_by": "code:code"
|
||||
"count_by": {"field": "code:code" }
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
@ -158,7 +158,7 @@
|
|||
"from": "Immunization",
|
||||
"where": {},
|
||||
"aggregations":{
|
||||
"count_by": "*"
|
||||
"count_by": {"field": "*" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -171,7 +171,7 @@
|
|||
"from": "Claim",
|
||||
"where": {},
|
||||
"aggregations":{
|
||||
"count_by": "*"
|
||||
"count_by": {"field": "*" }
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
|
Loading…
Reference in New Issue