Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Commit

Permalink
Query language support for child iteration & area
Browse files Browse the repository at this point in the history
  • Loading branch information
baijum committed Jul 25, 2018
1 parent 8b6c848 commit 1635850
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 3 deletions.
20 changes: 20 additions & 0 deletions criteria/expression_child.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package criteria

// ChildExpression represents the negation operator
type ChildExpression struct {
binaryExpression
}

// Ensure ChildExpression implements the Expression interface
var _ Expression = &ChildExpression{}
var _ Expression = (*ChildExpression)(nil)

// Accept implements ExpressionVisitor
func (t *ChildExpression) Accept(visitor ExpressionVisitor) interface{} {
return visitor.Child(t)
}

// Child constructs a ChildExpression
func Child(left Expression, right Expression) Expression {
return reparent(&ChildExpression{binaryExpression{expression{}, left, right}})
}
1 change: 1 addition & 0 deletions criteria/expression_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ type ExpressionVisitor interface {
Parameter(v *ParameterExpression) interface{}
Literal(c *LiteralExpression) interface{}
Not(e *NotExpression) interface{}
Child(e *ChildExpression) interface{}
IsNull(e *IsNullExpression) interface{}
}
4 changes: 4 additions & 0 deletions criteria/iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func (i *postOrderIterator) Not(exp *NotExpression) interface{} {
return i.binary(exp)
}

func (i *postOrderIterator) Child(exp *ChildExpression) interface{} {
return i.binary(exp)
}

func (i *postOrderIterator) IsNull(exp *IsNullExpression) interface{} {
return i.visit(exp)
}
Expand Down
20 changes: 17 additions & 3 deletions search/search_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,11 @@ func parseMap(queryMap map[string]interface{}, q *Query) {
q.Value = &s
case bool:
s := concreteVal
q.Negate = s
if key == "negate" {
q.Negate = s
} else if key == "child" {
q.Child = s
}
case nil:
q.Name = key
q.Value = nil
Expand Down Expand Up @@ -378,6 +382,8 @@ type Query struct {
Children []Query
// The Options represent the query options provided by the user.
Options *QueryOptions
// Consider child iteration/area
Child bool
}

func isOperator(str string) bool {
Expand Down Expand Up @@ -436,7 +442,11 @@ func (q Query) generateExpression() (criteria.Expression, error) {
if q.Substring {
myexpr = append(myexpr, criteria.Substring(left, right))
} else {
myexpr = append(myexpr, criteria.Equals(left, right))
if q.Child {
myexpr = append(myexpr, criteria.Child(left, right))
} else {
myexpr = append(myexpr, criteria.Equals(left, right))
}
}
}
} else {
Expand Down Expand Up @@ -477,7 +487,11 @@ func (q Query) generateExpression() (criteria.Expression, error) {
if child.Substring {
myexpr = append(myexpr, criteria.Substring(left, right))
} else {
myexpr = append(myexpr, criteria.Equals(left, right))
if child.Child {
myexpr = append(myexpr, criteria.Child(left, right))
} else {
myexpr = append(myexpr, criteria.Equals(left, right))
}
}
}
} else {
Expand Down
76 changes: 76 additions & 0 deletions search/search_repository_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,82 @@ func (s *searchRepositoryBlackboxTest) getTestFixture() *tf.TestFixture {
)
}

func (s *searchRepositoryBlackboxTest) TestSearchWithChildIterationWorkItems() {
s.T().Run("iterations", func(t *testing.T) {
fxt := tf.NewTestFixture(t, s.DB,
tf.Iterations(3, func(fxt *tf.TestFixture, idx int) error {
i := fxt.Iterations[idx]
switch idx {
case 0:
i.Name = "Top level iteration"
case 1:
i.Name = "Level 1 iteration"
i.MakeChildOf(*fxt.Iterations[idx-1])
case 2:
i.Name = "Level 2 iteration"
i.MakeChildOf(*fxt.Iterations[idx-1])
}
return nil
}),

tf.WorkItems(10, func(fxt *tf.TestFixture, idx int) error {
switch idx {
case 0, 1, 2:
fxt.WorkItems[idx].Fields[workitem.SystemIteration] = fxt.Iterations[0].ID.String()
case 3, 4:
fxt.WorkItems[idx].Fields[workitem.SystemIteration] = fxt.Iterations[1].ID.String()
case 5, 6, 7, 8:
fxt.WorkItems[idx].Fields[workitem.SystemIteration] = fxt.Iterations[2].ID.String()
}
return nil
}),
)
t.Run("without child iteration", func(t *testing.T) {
filter := fmt.Sprintf(`{"$AND": [{"iteration": "%s", "child": true},{"space": "%s"}]}`, fxt.Iterations[2].ID, fxt.Spaces[0].ID)
_, count, _, _, err := s.searchRepo.Filter(context.Background(), filter, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, 4, count)
})

t.Run("with one child iteration", func(t *testing.T) {
filter := fmt.Sprintf(`{"iteration": "%s", "child": true}`, fxt.Iterations[1].ID)
_, count, _, _, err := s.searchRepo.Filter(context.Background(), filter, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, 6, count)
})
t.Run("with two child iteration", func(t *testing.T) {
filter := fmt.Sprintf(`{"iteration": "%s", "child": true}`, fxt.Iterations[0].ID)
_, count, _, _, err := s.searchRepo.Filter(context.Background(), filter, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, 9, count)
})
t.Run("without child iteration - false option", func(t *testing.T) {
filter := fmt.Sprintf(`{"iteration": "%s", "child": false}`, fxt.Iterations[2].ID)
_, count, _, _, err := s.searchRepo.Filter(context.Background(), filter, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, 4, count)
})
t.Run("with one child iteration - false option", func(t *testing.T) {
filter := fmt.Sprintf(`{"iteration": "%s", "child": false}`, fxt.Iterations[1].ID)
_, count, _, _, err := s.searchRepo.Filter(context.Background(), filter, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, 2, count)
})
t.Run("with two child iteration - false option", func(t *testing.T) {
filter := fmt.Sprintf(`{"iteration": "%s", "child": false}`, fxt.Iterations[0].ID)
_, count, _, _, err := s.searchRepo.Filter(context.Background(), filter, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, 3, count)
})
t.Run("with two child iteration and space", func(t *testing.T) {
filter := fmt.Sprintf(`{"$AND": [{"iteration": "%s", "child": true},{"space": "%s"}]}`, fxt.Iterations[0].ID, fxt.Spaces[0].ID)
_, count, _, _, err := s.searchRepo.Filter(context.Background(), filter, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, 9, count)
})
})
}

func (s *searchRepositoryBlackboxTest) TestSearchWithJoin() {
s.T().Run("join iterations", func(t *testing.T) {
fxt := tf.NewTestFixture(t, s.DB,
Expand Down
57 changes: 57 additions & 0 deletions workitem/expression_compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,18 @@ func (c *expressionCompiler) expressionRefersToJoinedData(e criteria.Expression)
return nil, false
}

// ensureJoinTable returns true if the given field expression is a
// field expression and refers to joined data; otherwise false is returned.
func (c *expressionCompiler) ensureJoinTable(tn string) (*TableJoin, bool) {
for _, j := range c.joins {
if j.TableName == tn {
j.Active = true
return j, true
}
}
return nil, false
}

// Ensure expressionCompiler implements the ExpressionVisitor interface
var _ criteria.ExpressionVisitor = &expressionCompiler{}
var _ criteria.ExpressionVisitor = (*expressionCompiler)(nil)
Expand Down Expand Up @@ -394,6 +406,51 @@ func (c *expressionCompiler) Not(e *criteria.NotExpression) interface{} {
return c.binary(e, "!=")
}

func (c *expressionCompiler) Child(e *criteria.ChildExpression) interface{} {
left, ok := e.Left().(*criteria.FieldExpression)
if !ok {
c.err = append(c.err, errs.Errorf("invalid left expression (not a field expression): %+v", e.Left()))
return nil
}

if strings.Contains(left.FieldName, "'") {
// beware of injection, it's a reasonable restriction for field names,
// make sure it's not allowed when creating wi types
c.err = append(c.err, errs.Errorf("single quote not allowed in field name: %s", left.FieldName))
return nil
}
litExp, ok := e.Right().(*criteria.LiteralExpression)
if !ok {
c.err = append(c.err, errs.Errorf("failed to convert right expression to literal expression: %+v", e.Right()))
return nil
}
r, ok := litExp.Value.(string)
if !ok {
c.err = append(c.err, errs.Errorf("failed to convert value of right literal expression to string: %+v", litExp.Value))
return nil
}

var tblName string
if left.FieldName == SystemIteration {
tblName = "iterations"
} else if left.FieldName == SystemArea {
tblName = "areas"
} else {
c.err = append(c.err, errs.Errorf("invalid field name: %+v", left.FieldName))
return nil
}

c.ensureJoinTable(tblName)
c.parameters = append(c.parameters, r)
return fmt.Sprintf(`"work_items".fields->>'%[1]s'::text IN (
SELECT %[2]s.id::text
WHERE %[2]s.path <@ (SELECT i.path
FROM %[3]s i
WHERE i.id = ? AND i.space_id = "work_items".space_id
)
)`, left.FieldName, "iter", tblName)
}

func (c *expressionCompiler) Parameter(v *criteria.ParameterExpression) interface{} {
c.err = append(c.err, errs.Errorf("parameter expression not supported"))
return nil
Expand Down
4 changes: 4 additions & 0 deletions workitem/table_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ type TableJoin struct {
// will be handled by the join specified here (if any).
DelegateTo map[string]*TableJoin

// Colum to check for ltree path (e.g., areas.path, iterations.path)
PathColmun string

// TODO(kwk): Maybe introduce a column mapping table here: ColumnMapping map[string]string
}

Expand Down Expand Up @@ -107,6 +110,7 @@ func (j TableJoin) GetJoinExpression() string {
// this table join.
func (j *TableJoin) HandlesFieldName(fieldName string) bool {
for _, t := range j.PrefixActivators {
// system.iteration iteration.
if strings.HasPrefix(fieldName, t) {
return true
}
Expand Down

0 comments on commit 1635850

Please sign in to comment.