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
|
type SearchParameterType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SearchParameterTypeNumber SearchParameterType = "number"
|
//simple types
|
||||||
SearchParameterTypeDate SearchParameterType = "date"
|
SearchParameterTypeNumber SearchParameterType = "number"
|
||||||
|
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)
|
||||||
|
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:
|
default:
|
||||||
return fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name)
|
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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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": "*" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
Loading…
Reference in New Issue