Skip to content

Commit

Permalink
api/v1/attribute_list_params: implement support for Attribute Operators
Browse files Browse the repository at this point in the history
Querying multiple Attributes would always use the 'AND' operator in the
SQL query, this is limiting and required to support the 'OR' operator
when required by the client.

This change extends the AttributeListParams to support an Attribute Operator - 'and' 'or'
to enable the client to define how multiple Attributes should be SQL queried.

This change is backwards compatible and defaults to 'and' which is the previous
behaviour.

The client can query multiple attributes and specify which of them should looked up
with an OR, by specifying a final '~' separated value - 'or'.

"attr=hollow.versioned~a.name~like~nemo&attr=hollow.versioned~a.name~like~bluefin~or"
  • Loading branch information
joelrebel committed Aug 14, 2023
1 parent 7f63c49 commit cf5890d
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 8 deletions.
39 changes: 35 additions & 4 deletions pkg/api/v1/attribute_list_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ import (
// OperatorType is used to control what kind of search is performed for an AttributeListParams value.
type OperatorType string

// AttributeOperatorType is used to define how one or more AttributeListParam values should be SQL queried.
type AttributeOperatorType string

const (
// AttributeLogicalOR can be passed into a AttributeListParam to SQL select the attribute an OR clause.
AttributeLogicalOR AttributeOperatorType = "or"
// AttributeLogicalAND is the default attribute operator, it can be passed into a AttributeListParam to SQL select the attribute a AND clause.
AttributeLogicalAND AttributeOperatorType = "and"
)

const (
// OperatorEqual means the value has to match the keys exactly
OperatorEqual OperatorType = "eq"
Expand All @@ -29,6 +39,9 @@ type AttributeListParams struct {
Keys []string
Operator OperatorType
Value string
// AttributeOperatorType is used to define how this AttributeListParam value should be SQL queried
// this value defaults to AttributeLogicalAND.
AttributeOperator AttributeOperatorType
}

func encodeAttributesListParams(alp []AttributeListParams, key string, q url.Values) {
Expand All @@ -43,15 +56,24 @@ func encodeAttributesListParams(alp []AttributeListParams, key string, q url.Val
}
}

if ap.AttributeOperator != "" {
value += "~" + string(ap.AttributeOperator)
}

q.Add(key, value)
}
}

func parseQueryAttributesListParams(c *gin.Context, key string) []AttributeListParams {
alp := []AttributeListParams{}

for _, p := range c.QueryArray(key) {
// format is "ns~keys.dot.seperated~operation~value"
attrQueryParams := c.QueryArray(key)

for _, p := range attrQueryParams {
// format accepted
// "ns~keys.dot.seperated~operation~value"
// With attr OR operator: "ns~keys.dot.seperated~operation~value~or"
// With attr AND operator: "ns~keys.dot.seperated~operation~value~and"
parts := strings.Split(p, "~")

param := AttributeListParams{
Expand All @@ -65,14 +87,23 @@ func parseQueryAttributesListParams(c *gin.Context, key string) []AttributeListP

param.Keys = strings.Split(parts[1], ".")

if len(parts) == 4 { // nolint

if len(parts) == 4 || len(parts) == 5 { // nolint
switch o := (*OperatorType)(&parts[2]); *o {
case OperatorEqual, OperatorLike, OperatorGreaterThan, OperatorLessThan:
param.Operator = *o
param.Value = parts[3]
}

// An attribute operator is only applicable when,
// - Theres 5 parts in the attr param string when split on `~`.
// - Theres multiple attribute query parameters defined.
if len(parts) == 5 && len(attrQueryParams) > 1 {
switch o := (*AttributeOperatorType)(&parts[4]); *o {
case AttributeLogicalAND, AttributeLogicalOR:
param.AttributeOperator = *o
}
}

// if the like search doesn't contain any % add one at the end
if param.Operator == OperatorLike && !strings.Contains(param.Value, "%") {
param.Value += "%"
Expand Down
76 changes: 73 additions & 3 deletions pkg/api/v1/attribute_list_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
)

func Test_encodeAttributesListParams(t *testing.T) {
func TestEncodeAttributesListParams(t *testing.T) {
testCases := []struct {
alp []AttributeListParams
key string
Expand All @@ -32,7 +32,7 @@ func Test_encodeAttributesListParams(t *testing.T) {
"hollow.versioned",
},
},
"query with",
"query with namespace",
},
{
[]AttributeListParams{
Expand Down Expand Up @@ -68,6 +68,25 @@ func Test_encodeAttributesListParams(t *testing.T) {
},
"query with namespace, keys and operator, value",
},
{
[]AttributeListParams{
{
Namespace: "hollow.versioned",
Keys: []string{"a", "b"},
Operator: "lt",
Value: "5",
AttributeOperator: AttributeLogicalOR,
},
},
"key",
make(url.Values),
url.Values{
"key": []string{
"hollow.versioned~a.b~lt~5~or",
},
},
"query with namespace, keys and operator, value, OR Attribute Operator",
},
}

for _, tc := range testCases {
Expand All @@ -78,7 +97,7 @@ func Test_encodeAttributesListParams(t *testing.T) {
}
}

func Test_parseQueryAttributesListParams(t *testing.T) {
func TestParseQueryAttributesListParams(t *testing.T) {
testCases := []struct {
key string
query string
Expand Down Expand Up @@ -118,6 +137,57 @@ func Test_parseQueryAttributesListParams(t *testing.T) {
},
"query with namespace, attribute list params and 'like' operator",
},
{
"attr",
"attr=hollow.versioned~a.name~like~nemo&attr=hollow.versioned~a.name~like~bluefin~or",
[]AttributeListParams{
{
Namespace: "hollow.versioned",
Keys: []string{
"a",
"name",
},
Operator: "like",
Value: "nemo%",
},
{
Namespace: "hollow.versioned",
Keys: []string{
"a",
"name",
},
Operator: "like",
Value: "bluefin%",
AttributeOperator: AttributeLogicalOR,
},
},
"query with Attribute Operator - OR",
},
{
"attr",
"attr=hollow.versioned~a.name~like~nemo&attr=hollow.versioned~a.name~like~bluefin~wtfbbq",
[]AttributeListParams{
{
Namespace: "hollow.versioned",
Keys: []string{
"a",
"name",
},
Operator: "like",
Value: "nemo%",
},
{
Namespace: "hollow.versioned",
Keys: []string{
"a",
"name",
},
Operator: "like",
Value: "bluefin%",
},
},
"query with invalid attribute operator defaults to Attribute operator - AND",
},
}

setupGinCtx := func(queryURL string) *gin.Context {
Expand Down
1 change: 0 additions & 1 deletion pkg/api/v1/firmware_set_list_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ func (p *ComponentFirmwareSetListParams) queryMods(tableName string) []qm.QueryM
attrJoinAsTableName := fmt.Sprintf("%s_attr_%d", tableName, i)
whereStmt := fmt.Sprintf("%s as %s on %s.firmware_set_id = %s.id", models.TableNames.AttributesFirmwareSet, attrJoinAsTableName, attrJoinAsTableName, tableName)
mods = append(mods, qm.LeftOuterJoin(whereStmt))

mods = append(mods, lp.queryMods(attrJoinAsTableName))
}
}
Expand Down

0 comments on commit cf5890d

Please sign in to comment.