working aggregation code.
added tests for sql generation.
This commit is contained in:
parent
2c5133bef7
commit
6f56fa9c3b
|
@ -24,8 +24,8 @@ const (
|
||||||
SearchParameterTypeToken SearchParameterType = "token"
|
SearchParameterTypeToken SearchParameterType = "token"
|
||||||
SearchParameterTypeReference SearchParameterType = "reference"
|
SearchParameterTypeReference SearchParameterType = "reference"
|
||||||
SearchParameterTypeUri SearchParameterType = "uri"
|
SearchParameterTypeUri SearchParameterType = "uri"
|
||||||
SearchParameterTypeComposite SearchParameterType = "composite"
|
|
||||||
SearchParameterTypeQuantity SearchParameterType = "quantity"
|
SearchParameterTypeQuantity SearchParameterType = "quantity"
|
||||||
|
SearchParameterTypeComposite SearchParameterType = "composite"
|
||||||
SearchParameterTypeSpecial SearchParameterType = "special"
|
SearchParameterTypeSpecial SearchParameterType = "special"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -131,23 +131,67 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
|
||||||
//defaults
|
//defaults
|
||||||
selectClauses := []string{fmt.Sprintf("%s.*", TABLE_ALIAS)}
|
selectClauses := []string{fmt.Sprintf("%s.*", TABLE_ALIAS)}
|
||||||
groupClause := fmt.Sprintf("%s.id", TABLE_ALIAS)
|
groupClause := fmt.Sprintf("%s.id", TABLE_ALIAS)
|
||||||
orderClause := fmt.Sprintf("%s.sort_date asc", TABLE_ALIAS)
|
orderClause := fmt.Sprintf("%s.sort_date ASC", TABLE_ALIAS)
|
||||||
if query.Aggregations != nil {
|
if query.Aggregations != nil {
|
||||||
selectClauses = []string{}
|
|
||||||
groupClause = ""
|
|
||||||
orderClause = ""
|
|
||||||
//Handle Aggregations
|
//Handle Aggregations
|
||||||
|
|
||||||
if len(query.Aggregations.CountBy) > 0 {
|
if len(query.Aggregations.CountBy) > 0 {
|
||||||
//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.GroupBy = query.Aggregations.CountBy
|
||||||
|
}
|
||||||
|
|
||||||
//TODO:
|
//process order by clause
|
||||||
|
if len(query.Aggregations.OrderBy) > 0 {
|
||||||
|
orderAsc := true
|
||||||
|
if !strings.HasPrefix(query.Aggregations.OrderBy, "count(*)") {
|
||||||
|
orderAggregationParam, err := ProcessAggregationParameter(query.Aggregations.OrderBy, searchCodeToTypeLookup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fromClauses = append(fromClauses, orderAggregationFromClause)
|
||||||
|
|
||||||
|
orderClause = AggregationParameterToClause(orderAggregationParam)
|
||||||
|
if orderAsc {
|
||||||
|
orderClause = fmt.Sprintf("%s ASC", orderClause)
|
||||||
|
} else {
|
||||||
|
orderClause = fmt.Sprintf("%s DESC", orderClause)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
orderClause = query.Aggregations.OrderBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//process group by clause
|
||||||
|
if len(query.Aggregations.GroupBy) > 0 {
|
||||||
|
groupAggregationParam, err := ProcessAggregationParameter(query.Aggregations.GroupBy, searchCodeToTypeLookup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fromClauses = append(fromClauses, groupAggregationFromClause)
|
||||||
|
|
||||||
|
groupClause = AggregationParameterToClause(groupAggregationParam)
|
||||||
|
selectClauses = []string{
|
||||||
|
fmt.Sprintf("%s as %s", groupClause, "group_by"),
|
||||||
|
"count(*) as count",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//ensure Where and From clauses are unique
|
//ensure Where and From clauses are unique
|
||||||
whereClauses = lo.Uniq(whereClauses)
|
whereClauses = lo.Uniq(whereClauses)
|
||||||
|
whereClauses = lo.Compact(whereClauses)
|
||||||
fromClauses = lo.Uniq(fromClauses)
|
fromClauses = lo.Uniq(fromClauses)
|
||||||
|
fromClauses = lo.Compact(fromClauses)
|
||||||
|
|
||||||
return sr.GormClient.WithContext(ctx).
|
return sr.GormClient.WithContext(ctx).
|
||||||
Select(strings.Join(selectClauses, ", ")).
|
Select(strings.Join(selectClauses, ", ")).
|
||||||
|
@ -158,6 +202,7 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
|
//TODO: dont export these, instead use casting to convert the interface to the SqliteRepository struct, then call ehese functions directly
|
||||||
|
|
||||||
type SearchParameter struct {
|
type SearchParameter struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -499,3 +544,56 @@ func SearchCodeToFromClause(searchParam SearchParameter) (string, error) {
|
||||||
}
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AggregationParameterToClause(aggParameter SearchParameter) string {
|
||||||
|
switch aggParameter.Type {
|
||||||
|
case SearchParameterTypeQuantity, SearchParameterTypeToken, SearchParameterTypeString:
|
||||||
|
//setup the clause
|
||||||
|
return fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//ProcessAggregationParameter processes the aggregation parameters which are fields with optional properties:
|
||||||
|
// Fields that are primitive types (number, uri) must not have any property specified:
|
||||||
|
// eg. `probability`
|
||||||
|
//
|
||||||
|
// Fields that are complex types (token, quantity) must have a property specified:
|
||||||
|
// 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{}
|
||||||
|
|
||||||
|
//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 {
|
||||||
|
aggregationParameter.Name = aggregationFieldParts[0]
|
||||||
|
aggregationParameter.Modifier = aggregationFieldParts[1]
|
||||||
|
} else {
|
||||||
|
aggregationParameter.Name = aggregationFieldParts[0]
|
||||||
|
aggregationParameter.Modifier = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
//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)
|
||||||
|
} else {
|
||||||
|
aggregationParameter.Type = SearchParameterType(searchParamTypeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
//primitive types should not have a modifier, we need to throw an error
|
||||||
|
if aggregationParameter.Type == SearchParameterTypeNumber || aggregationParameter.Type == SearchParameterTypeUri {
|
||||||
|
if len(aggregationParameter.Modifier) > 0 {
|
||||||
|
return aggregationParameter, fmt.Errorf("primitive aggregation parameter %s cannot have a property (%s)", aggregationParameter.Name, aggregationParameter.Modifier)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//complex types must have a modifier
|
||||||
|
if len(aggregationParameter.Modifier) == 0 {
|
||||||
|
return aggregationParameter, fmt.Errorf("complex aggregation parameter %s must have a property", aggregationParameter.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aggregationParameter, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
|
||||||
|
mock_config "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config/mock"
|
||||||
|
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define the suite, and absorb the built-in basic suite
|
||||||
|
// functionality from testify - including a T() method which
|
||||||
|
// returns the current testing context
|
||||||
|
type RepositorySqlTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
MockCtrl *gomock.Controller
|
||||||
|
TestDatabase *os.File
|
||||||
|
|
||||||
|
TestRepository DatabaseRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeTest has a function to be executed right before the test starts and receives the suite and test names as input
|
||||||
|
func (suite *RepositorySqlTestSuite) BeforeTest(suiteName, testName string) {
|
||||||
|
suite.MockCtrl = gomock.NewController(suite.T())
|
||||||
|
|
||||||
|
dbFile, err := ioutil.TempFile("", fmt.Sprintf("%s.*.db", testName))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
suite.TestDatabase = dbFile
|
||||||
|
|
||||||
|
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
|
||||||
|
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
|
||||||
|
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()))
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
suite.TestRepository = dbRepo
|
||||||
|
userModel := &models.User{
|
||||||
|
Username: "test_username",
|
||||||
|
Password: "testpassword",
|
||||||
|
Email: "test@test.com",
|
||||||
|
}
|
||||||
|
err = suite.TestRepository.CreateUser(context.Background(), userModel)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterTest has a function to be executed right after the test finishes and receives the suite and test names as input
|
||||||
|
func (suite *RepositorySqlTestSuite) AfterTest(suiteName, testName string) {
|
||||||
|
suite.MockCtrl.Finish()
|
||||||
|
os.Remove(suite.TestDatabase.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// In order for 'go test' to run this suite, we need to create
|
||||||
|
// a normal test function and pass our suite to suite.Run
|
||||||
|
func TestRepositorySqlTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(RepositorySqlTestSuite))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL() {
|
||||||
|
//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",
|
||||||
|
})
|
||||||
|
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 fhir.*",
|
||||||
|
"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",
|
||||||
|
}, " "),
|
||||||
|
sqlString)
|
||||||
|
require.Equal(suite.T(), sqlParams, []interface{}{
|
||||||
|
"test_code", "00000000-0000-0000-0000-000000000000",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithMultipleWhereConditions() {
|
||||||
|
//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",
|
||||||
|
"category": "12345",
|
||||||
|
},
|
||||||
|
From: "Observation",
|
||||||
|
})
|
||||||
|
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 fhir.*",
|
||||||
|
"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",
|
||||||
|
}, " "),
|
||||||
|
sqlString)
|
||||||
|
require.Equal(suite.T(), sqlParams, []interface{}{
|
||||||
|
"test_code", "00000000-0000-0000-0000-000000000000",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveOrderByAggregation() {
|
||||||
|
//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{OrderBy: "sourceUri"},
|
||||||
|
})
|
||||||
|
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 fhir.*",
|
||||||
|
"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.sourceUri ASC",
|
||||||
|
}, " "), sqlString)
|
||||||
|
require.Equal(suite.T(), sqlParams, []interface{}{
|
||||||
|
"test_code", "00000000-0000-0000-0000-000000000000",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexOrderByAggregation() {
|
||||||
|
//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{OrderBy: "valueString:value"},
|
||||||
|
})
|
||||||
|
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 fhir.*",
|
||||||
|
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson, json_each(fhir.valueString) as valueStringJson",
|
||||||
|
"WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)",
|
||||||
|
"GROUP BY `fhir`.`id`",
|
||||||
|
"ORDER BY (valueStringJson.value ->> '$.value') ASC",
|
||||||
|
}, " "), sqlString)
|
||||||
|
require.Equal(suite.T(), sqlParams, []interface{}{
|
||||||
|
"test_code", "00000000-0000-0000-0000-000000000000",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveCountByAggregation() {
|
||||||
|
//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{CountBy: "sourceUri"},
|
||||||
|
})
|
||||||
|
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 fhir.sourceUri as group_by, count(*) as count",
|
||||||
|
"FROM fhir_observation as fhir, json_each(fhir.code) as codeJson",
|
||||||
|
"WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)",
|
||||||
|
"GROUP BY `fhir`.`sourceUri`",
|
||||||
|
"ORDER BY count(*) DESC",
|
||||||
|
}, " "), sqlString)
|
||||||
|
require.Equal(suite.T(), sqlParams, []interface{}{
|
||||||
|
"test_code", "00000000-0000-0000-0000-000000000000",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAggregation() {
|
||||||
|
//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{CountBy: "code: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 ->> '$.code') as group_by, count(*) as count",
|
||||||
|
"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 count(*) DESC",
|
||||||
|
}, " "), sqlString)
|
||||||
|
require.Equal(suite.T(), sqlParams, []interface{}{
|
||||||
|
"test_code", "00000000-0000-0000-0000-000000000000",
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// maps to frontend/src/app/models/widget/dashboard-widget-query.ts
|
// maps to frontend/src/app/models/widget/dashboard-widget-query.ts
|
||||||
type QueryResource struct {
|
type QueryResource struct {
|
||||||
|
@ -44,6 +47,15 @@ func (q *QueryResource) Validate() error {
|
||||||
if len(q.Aggregations.CountBy) == 0 && len(q.Aggregations.OrderBy) == 0 && len(q.Aggregations.GroupBy) == 0 {
|
if len(q.Aggregations.CountBy) == 0 && len(q.Aggregations.OrderBy) == 0 && len(q.Aggregations.GroupBy) == 0 {
|
||||||
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)")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,10 @@ func TestQueryResource_Validate(t *testing.T) {
|
||||||
{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", 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: "test", OrderBy: "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: "test:property as HELLO"}}, "count_by cannot have spaces", true},
|
||||||
|
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: "test:property as HELLO"}}, "group_by cannot have spaces", true},
|
||||||
|
{QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: "test:property as HELLO"}}, "order_by cannot have spaces", true},
|
||||||
}
|
}
|
||||||
|
|
||||||
//test && assert
|
//test && assert
|
||||||
|
|
Loading…
Reference in New Issue