Skip to content

Commit

Permalink
Add ability to order associations (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zach McElrath authored Jun 26, 2019
1 parent e258631 commit 64c4701
Show file tree
Hide file tree
Showing 13 changed files with 581 additions and 376 deletions.
19 changes: 8 additions & 11 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

sq "github.com/Masterminds/squirrel"
"github.com/skuid/picard/query"
qp "github.com/skuid/picard/queryparts"
"github.com/skuid/picard/reflectutil"
"github.com/skuid/picard/stringutil"
"github.com/skuid/picard/tags"
Expand All @@ -16,18 +17,12 @@ import (
type FilterRequest struct {
FilterModel interface{}
Associations []tags.Association
OrderBy []OrderByRequest
OrderBy []qp.OrderByRequest
Runner sq.BaseRunner
// Fields []string // For use later when we implement selecting specific columns
}

// OrderByRequest holds information about a request to order by a field
type OrderByRequest struct {
Field string
Descending bool
}

func addOrderBy(builder sq.SelectBuilder, orderBy []OrderByRequest, filterMetadata *tags.TableMetadata) sq.SelectBuilder {
func addOrderBy(builder sq.SelectBuilder, orderBy []qp.OrderByRequest, filterMetadata *tags.TableMetadata) sq.SelectBuilder {
orderStatements := []string{}
for _, order := range orderBy {
columnName := filterMetadata.GetField(order.Field).GetColumnName()
Expand Down Expand Up @@ -66,7 +61,7 @@ func (p PersistenceORM) getMultiFilterResults(request FilterRequest, filterMetad
}

ors := sq.Or{}
var tbl *query.Table
var tbl *qp.Table
var filterModel interface{}

for i := 0; i < modelVal.Len(); i++ {
Expand Down Expand Up @@ -102,10 +97,10 @@ func (p PersistenceORM) getMultiFilterResults(request FilterRequest, filterMetad

}

tbl.Wheres = make([]query.Where, 0)
tbl.Wheres = make([]qp.Where, 0)

for _, join := range tbl.Joins {
join.Table.Wheres = make([]query.Where, 0)
join.Table.Wheres = make([]qp.Where, 0)
}

sql := tbl.BuildSQL()
Expand Down Expand Up @@ -210,6 +205,8 @@ func (p PersistenceORM) FilterModel(request FilterRequest) ([]interface{}, error
childResults, err := p.FilterModel(FilterRequest{
FilterModel: newFilterList.Interface(),
Associations: association.Associations,
OrderBy: association.OrderBy,
Runner: request.Runner,
})
if err != nil {
return nil, err
Expand Down
179 changes: 170 additions & 9 deletions filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

sqlmock "github.com/DATA-DOG/go-sqlmock"
qp "github.com/skuid/picard/queryparts"
"github.com/skuid/picard/tags"
"github.com/skuid/picard/testdata"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -1097,6 +1098,7 @@ func TestDoFilterSelectWithJSONBField(t *testing.T) {

func TestFilterModel(t *testing.T) {
orgID := "00000000-0000-0000-0000-000000000001"
parentId := "00000000-0000-0000-0000-000000000002"
testCases := []struct {
description string
filterRequest FilterRequest
Expand All @@ -1107,15 +1109,15 @@ func TestFilterModel(t *testing.T) {
"basic filter",
FilterRequest{
FilterModel: testdata.ToyModel{
ParentID: "00000000-0000-0000-0000-000000000002",
ParentID: parentId,
},
},
[]interface{}{
testdata.ToyModel{
ID: "00000000-0000-0000-0000-000000000011",
OrganizationID: orgID,
Name: "lego",
ParentID: "00000000-0000-0000-0000-000000000002",
ParentID: parentId,
},
},
func(mock sqlmock.Sqlmock) {
Expand All @@ -1129,7 +1131,7 @@ func TestFilterModel(t *testing.T) {
FROM toymodel AS t0
WHERE t0.organization_id = $1 AND t0.parent_id = $2
`)).
WithArgs(orgID, "00000000-0000-0000-0000-000000000002").
WithArgs(orgID, parentId).
WillReturnRows(
sqlmock.NewRows([]string{
"t0.id",
Expand All @@ -1141,7 +1143,7 @@ func TestFilterModel(t *testing.T) {
"00000000-0000-0000-0000-000000000011",
orgID,
"lego",
"00000000-0000-0000-0000-000000000002",
parentId,
),
)
mock.ExpectCommit()
Expand All @@ -1151,7 +1153,7 @@ func TestFilterModel(t *testing.T) {
"basic filter with no returns",
FilterRequest{
FilterModel: testdata.ToyModel{
ParentID: "00000000-0000-0000-0000-000000000002",
ParentID: parentId,
},
},
[]interface{}{},
Expand All @@ -1166,7 +1168,7 @@ func TestFilterModel(t *testing.T) {
FROM toymodel AS t0
WHERE t0.organization_id = $1 AND t0.parent_id = $2
`)).
WithArgs(orgID, "00000000-0000-0000-0000-000000000002").
WithArgs(orgID, parentId).
WillReturnRows(
sqlmock.NewRows([]string{
"t0.id",
Expand All @@ -1182,7 +1184,7 @@ func TestFilterModel(t *testing.T) {
"basic filter with no returns with single order by",
FilterRequest{
FilterModel: testdata.ToyModel{},
OrderBy: []OrderByRequest{
OrderBy: []qp.OrderByRequest{
{
Field: "Name",
},
Expand Down Expand Up @@ -1217,7 +1219,7 @@ func TestFilterModel(t *testing.T) {
"basic filter with no returns with multiple order by",
FilterRequest{
FilterModel: testdata.ToyModel{},
OrderBy: []OrderByRequest{
OrderBy: []qp.OrderByRequest{
{
Field: "Name",
},
Expand Down Expand Up @@ -1255,7 +1257,7 @@ func TestFilterModel(t *testing.T) {
"basic filter with no returns with multiple order by and descending",
FilterRequest{
FilterModel: testdata.ToyModel{},
OrderBy: []OrderByRequest{
OrderBy: []qp.OrderByRequest{
{
Field: "Name",
Descending: true,
Expand Down Expand Up @@ -1290,6 +1292,165 @@ func TestFilterModel(t *testing.T) {
mock.ExpectCommit()
},
},
{
"ordered filter with with ordered associations",
FilterRequest{
FilterModel: testdata.ParentModel{},
Associations: []tags.Association{
{
Name: "Children",
OrderBy: []qp.OrderByRequest{
{
Field: "Name",
Descending: true,
},
},
},
{
Name: "Animals",
OrderBy: []qp.OrderByRequest{
{
Field: "Name",
},
},
},
},
OrderBy: []qp.OrderByRequest{
{
Field: "Name",
},
},
},
[]interface{}{
testdata.ParentModel{
ID: parentId,
OrganizationID: orgID,
Name: "pops",
ParentID: "00000000-0000-0000-0000-000000000004",
Children: []testdata.ChildModel{
{
ID: "00000000-0000-0000-0000-000000000012",
OrganizationID: orgID,
Name: "Betty",
ParentID: parentId,
},
{
ID: "00000000-0000-0000-0000-000000000011",
OrganizationID: orgID,
Name: "Alex",
ParentID: parentId,
},
},
Animals: []testdata.PetModel{
{
ID: "00000000-0000-0000-0000-000000000031",
OrganizationID: orgID,
Name: "Cheerios",
ParentID: parentId,
},
{
ID: "00000000-0000-0000-0000-000000000032",
OrganizationID: orgID,
Name: "Pinkerton",
ParentID: parentId,
},
},
},
},
func(mock sqlmock.Sqlmock) {
mock.ExpectBegin()
// parent query
mock.ExpectQuery(testdata.FmtSQLRegex(`
SELECT
t0.id AS "t0.id",
t0.organization_id AS "t0.organization_id",
t0.name AS "t0.name",
t0.parent_id AS "t0.parent_id"
FROM parentmodel AS t0
WHERE t0.organization_id = $1
ORDER BY name
`)).
WithArgs(orgID).
WillReturnRows(
sqlmock.NewRows([]string{
"t0.id",
"t0.organization_id",
"t0.name",
"t0.parent_id",
}).AddRow(
parentId,
orgID,
"pops",
"00000000-0000-0000-0000-000000000004",
),
)
// children
mock.ExpectQuery(testdata.FmtSQLRegex(`
SELECT
t0.id AS "t0.id",
t0.organization_id AS "t0.organization_id",
t0.name AS "t0.name",
t0.parent_id AS "t0.parent_id"
FROM childmodel AS t0
WHERE t0.organization_id = $1 AND ((t0.parent_id = $2))
ORDER BY name DESC
`)).
WithArgs(orgID, parentId).
WillReturnRows(
sqlmock.NewRows([]string{
"t0.id",
"t0.organization_id",
"t0.name",
"t0.parent_id",
}).
AddRow(
"00000000-0000-0000-0000-000000000012",
orgID,
"Betty",
parentId,
).
AddRow(
"00000000-0000-0000-0000-000000000011",
orgID,
"Alex",
parentId,
),
)
// Pets/Animals
mock.ExpectQuery(testdata.FmtSQLRegex(`
SELECT
t0.id AS "t0.id",
t0.organization_id AS "t0.organization_id",
t0.name AS "t0.name",
t0.parent_id AS "t0.parent_id"
FROM petmodel AS t0
WHERE t0.organization_id = $1 AND ((t0.parent_id = $2))
ORDER BY name
`)).
WithArgs(orgID, parentId).
WillReturnRows(
sqlmock.NewRows([]string{
"t0.id",
"t0.organization_id",
"t0.name",
"t0.parent_id",
}).
AddRow(
"00000000-0000-0000-0000-000000000031",
orgID,
"Cheerios",
parentId,
).
AddRow(
"00000000-0000-0000-0000-000000000032",
orgID,
"Pinkerton",
parentId,
),
)
mock.ExpectCommit()
},
},
}

for _, tc := range testCases {
Expand Down
5 changes: 3 additions & 2 deletions query/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"reflect"

qp "github.com/skuid/picard/queryparts"
"github.com/skuid/picard/reflectutil"
"github.com/skuid/picard/stringutil"
"github.com/skuid/picard/tags"
Expand All @@ -13,7 +14,7 @@ import (
Build takes the filter model and returns a query object. It takes the
multitenancy value, current reflected value, and any tags
*/
func Build(multitenancyVal, model interface{}, associations []tags.Association) (*Table, error) {
func Build(multitenancyVal, model interface{}, associations []tags.Association) (*qp.Table, error) {

val, err := stringutil.GetStructValue(model)
if err != nil {
Expand Down Expand Up @@ -63,7 +64,7 @@ func buildQuery(
associations []tags.Association,
onlyJoin bool,
counter int,
) (*Table, error) {
) (*qp.Table, error) {
// Inspect current reflected value, and add select/where clauses

tableName, pkName := reflectutil.ReflectTableInfo(modelType)
Expand Down
9 changes: 5 additions & 4 deletions query/hydrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import (

"github.com/skuid/picard/crypto"
"github.com/skuid/picard/stringutil"
qp "github.com/skuid/picard/queryparts"
"github.com/skuid/picard/tags"
)

/*
Hydrate takes the rows and pops them into the correct struct, in the correct
order. This is usually called after you've built and executed the query model.
*/
func Hydrate(filterModel interface{}, aliasMap map[string]FieldDescriptor, rows *sql.Rows) ([]*reflect.Value, error) {
func Hydrate(filterModel interface{}, aliasMap map[string]qp.FieldDescriptor, rows *sql.Rows) ([]*reflect.Value, error) {
modelVal, err := stringutil.GetStructValue(filterModel)
if err != nil {
return nil, err
Expand Down Expand Up @@ -52,7 +53,7 @@ func hydrate(typ reflect.Type, mapped map[string]map[string]interface{}, counter

alias := fmt.Sprintf("t%d", counter)

mappedFields := mapped[fmt.Sprintf(aliasedField, alias, meta.GetTableName())]
mappedFields := mapped[fmt.Sprintf(qp.AliasedField, alias, meta.GetTableName())]

for _, field := range meta.GetFields() {
fieldVal := mappedFields[field.GetColumnName()]
Expand Down Expand Up @@ -149,7 +150,7 @@ This function would return something like:
*/
func mapRows2Cols(meta *tags.TableMetadata, aliasMap map[string]FieldDescriptor, rows *sql.Rows) ([]map[string]map[string]interface{}, error) {
func mapRows2Cols(meta *tags.TableMetadata, aliasMap map[string]qp.FieldDescriptor, rows *sql.Rows) ([]map[string]map[string]interface{}, error) {
results := make([]map[string]map[string]interface{}, 0)

cols, err := rows.Columns()
Expand All @@ -175,7 +176,7 @@ func mapRows2Cols(meta *tags.TableMetadata, aliasMap map[string]FieldDescriptor,
// storing it in the map with the name of the column as the key.
for i, colName := range cols {
tmap := aliasMap[colName]
aliasedTbl := fmt.Sprintf(aliasedField, tmap.Alias, tmap.Table)
aliasedTbl := fmt.Sprintf(qp.AliasedField, tmap.Alias, tmap.Table)

if result[aliasedTbl] == nil {
result[aliasedTbl] = make(map[string]interface{})
Expand Down
Loading

0 comments on commit 64c4701

Please sign in to comment.