Skip to content

Commit

Permalink
feat: Inline array filters (#3028)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #2857

## Description

This PR adds three new filter types for inline array types: `_all`,
`_any`, and `_none`.

## Tasks

- [x] I made sure the code is well commented, particularly
hard-to-understand areas.
- [x] I made sure the repository-held documentation is changed
accordingly.
- [x] I made sure the pull request title adheres to the conventional
commit style (the subset used in the project can be found in
[tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)).
- [x] I made sure to discuss its limitations such as threats to
validity, vulnerability to mistake and misuse, robustness to
invalidation of assumptions, resource requirements, ...

## How has this been tested?

Added integration tests

Specify the platform(s) on which this was tested:
- MacOS
  • Loading branch information
nasdf authored Sep 20, 2024
1 parent cddc6d6 commit 55e56a5
Show file tree
Hide file tree
Showing 17 changed files with 1,467 additions and 118 deletions.
53 changes: 53 additions & 0 deletions internal/connor/all.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package connor

import (
"github.com/sourcenetwork/defradb/client"

"github.com/sourcenetwork/immutable"
)

// all is an operator which allows the evaluation of
// a number of conditions over a list of values
// matching if all of them match.
func all(condition, data any) (bool, error) {
switch t := data.(type) {
case []string:
return allSlice(condition, t)

case []immutable.Option[string]:
return allSlice(condition, t)

case []int64:
return allSlice(condition, t)

case []immutable.Option[int64]:
return allSlice(condition, t)

case []bool:
return allSlice(condition, t)

case []immutable.Option[bool]:
return allSlice(condition, t)

case []float64:
return allSlice(condition, t)

case []immutable.Option[float64]:
return allSlice(condition, t)

default:
return false, client.NewErrUnhandledType("data", data)
}
}

func allSlice[T any](condition any, data []T) (bool, error) {
for _, c := range data {
m, err := eq(condition, c)
if err != nil {
return false, err
} else if !m {
return false, nil
}
}
return true, nil
}
2 changes: 1 addition & 1 deletion internal/connor/and.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ func and(condition, data any) (bool, error) {
return false, nil
}
}

return true, nil

default:
return false, client.NewErrUnhandledType("condition", cn)
}
Expand Down
53 changes: 53 additions & 0 deletions internal/connor/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package connor

import (
"github.com/sourcenetwork/defradb/client"

"github.com/sourcenetwork/immutable"
)

// anyOp is an operator which allows the evaluation of
// a number of conditions over a list of values
// matching if any of them match.
func anyOp(condition, data any) (bool, error) {
switch t := data.(type) {
case []string:
return anySlice(condition, t)

case []immutable.Option[string]:
return anySlice(condition, t)

case []int64:
return anySlice(condition, t)

case []immutable.Option[int64]:
return anySlice(condition, t)

case []bool:
return anySlice(condition, t)

case []immutable.Option[bool]:
return anySlice(condition, t)

case []float64:
return anySlice(condition, t)

case []immutable.Option[float64]:
return anySlice(condition, t)

default:
return false, client.NewErrUnhandledType("data", data)
}
}

func anySlice[T any](condition any, data []T) (bool, error) {
for _, c := range data {
m, err := eq(condition, c)
if err != nil {
return false, err
} else if m {
return true, nil
}
}
return false, nil
}
6 changes: 6 additions & 0 deletions internal/connor/connor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func matchWith(op string, conditions, data any) (bool, error) {
switch op {
case "_and":
return and(conditions, data)
case "_any":
return anyOp(conditions, data)
case "_all":
return all(conditions, data)
case "_eq":
return eq(conditions, data)
case "_ge":
Expand All @@ -44,6 +48,8 @@ func matchWith(op string, conditions, data any) (bool, error) {
return ilike(conditions, data)
case "_nilike":
return nilike(conditions, data)
case "_none":
return none(conditions, data)
case "_not":
return not(conditions, data)
default:
Expand Down
69 changes: 27 additions & 42 deletions internal/connor/eq.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,74 +16,59 @@ import (
func eq(condition, data any) (bool, error) {
switch arr := data.(type) {
case []core.Doc:
for _, item := range arr {
m, err := eq(condition, item)
if err != nil {
return false, err
}

if m {
return true, nil
}
}
return false, nil
return anySlice(condition, arr)

case immutable.Option[bool]:
if !arr.HasValue() {
return condition == nil, nil
}
data = arr.Value()
data = immutableValueOrNil(arr)

case immutable.Option[int64]:
if !arr.HasValue() {
return condition == nil, nil
}
data = arr.Value()
data = immutableValueOrNil(arr)

case immutable.Option[float64]:
if !arr.HasValue() {
return condition == nil, nil
}
data = arr.Value()
data = immutableValueOrNil(arr)

case immutable.Option[string]:
if !arr.HasValue() {
return condition == nil, nil
}
data = arr.Value()
data = immutableValueOrNil(arr)
}

switch cn := condition.(type) {
case map[FilterKey]any:
for prop, cond := range cn {
m, err := matchWith(prop.GetOperatorOrDefault("_eq"), cond, prop.GetProp(data))
if err != nil {
return false, err
} else if !m {
return false, nil
}
}
return true, nil

case string:
if d, ok := data.(string); ok {
return d == cn, nil
}
return false, nil

case int64:
return numbers.Equal(cn, data), nil

case int32:
return numbers.Equal(cn, data), nil

case float64:
return numbers.Equal(cn, data), nil
case map[FilterKey]any:
m := true
for prop, cond := range cn {
var err error
m, err = matchWith(prop.GetOperatorOrDefault("_eq"), cond, prop.GetProp(data))
if err != nil {
return false, err
}

if !m {
// No need to evaluate after we fail
break
}
}

return m, nil
case time.Time:
return ctime.Equal(cn, data), nil

default:
return reflect.DeepEqual(condition, data), nil
}
}

func immutableValueOrNil[T any](data immutable.Option[T]) any {
if data.HasValue() {
return data.Value()
}
return nil
}
12 changes: 12 additions & 0 deletions internal/connor/none.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package connor

// none is an operator which allows the evaluation of
// a number of conditions over a list of values
// matching if all of them do not match.
func none(condition, data any) (bool, error) {
m, err := anyOp(condition, data)
if err != nil {
return false, err
}
return !m, nil
}
8 changes: 5 additions & 3 deletions internal/planner/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1348,13 +1348,15 @@ func toFilterMap(
returnClause := map[connor.FilterKey]any{}
for innerSourceKey, innerSourceValue := range typedClause {
var innerMapping *core.DocumentMapping
switch innerSourceValue.(type) {
case map[string]any:
// innerSourceValue may refer to a child mapping or
// an inline array if we don't have a child mapping
_, ok := innerSourceValue.(map[string]any)
if ok && index < len(mapping.ChildMappings) {
// If the innerSourceValue is also a map, then we should parse the nested clause
// using the child mapping, as this key must refer to a host property in a join
// and deeper keys must refer to properties on the child items.
innerMapping = mapping.ChildMappings[index]
default:
} else {
innerMapping = mapping
}
rKey, rValue := toFilterMap(innerSourceKey, innerSourceValue, innerMapping)
Expand Down
14 changes: 10 additions & 4 deletions internal/request/graphql/schema/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,11 +1150,17 @@ func (g *Generator) genTypeFilterArgInput(obj *gql.Object) *gql.InputObject {
}
// scalars (leafs)
if gql.IsLeafType(field.Type) {
if _, isList := field.Type.(*gql.List); isList {
// Filtering by inline array value is currently not supported
continue
var operatorName string
if list, isList := field.Type.(*gql.List); isList {
if notNull, isNotNull := list.OfType.(*gql.NonNull); isNotNull {
operatorName = "NotNull" + notNull.OfType.Name() + "ListOperatorBlock"
} else {
operatorName = list.OfType.Name() + "ListOperatorBlock"
}
} else {
operatorName = field.Type.Name() + "OperatorBlock"
}
operatorType, isFilterable := g.manager.schema.TypeMap()[field.Type.Name()+"OperatorBlock"]
operatorType, isFilterable := g.manager.schema.TypeMap()[operatorName]
if !isFilterable {
continue
}
Expand Down
58 changes: 44 additions & 14 deletions internal/request/graphql/schema/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ func defaultTypes(
blobScalarType := schemaTypes.BlobScalarType()
jsonScalarType := schemaTypes.JSONScalarType()

idOpBlock := schemaTypes.IDOperatorBlock()
intOpBlock := schemaTypes.IntOperatorBlock()
floatOpBlock := schemaTypes.FloatOperatorBlock()
booleanOpBlock := schemaTypes.BooleanOperatorBlock()
stringOpBlock := schemaTypes.StringOperatorBlock()
jsonOpBlock := schemaTypes.JSONOperatorBlock(jsonScalarType)
blobOpBlock := schemaTypes.BlobOperatorBlock(blobScalarType)
dateTimeOpBlock := schemaTypes.DateTimeOperatorBlock()

notNullIntOpBlock := schemaTypes.NotNullIntOperatorBlock()
notNullFloatOpBlock := schemaTypes.NotNullFloatOperatorBlock()
notNullBooleanOpBlock := schemaTypes.NotNullBooleanOperatorBlock()
notNullStringOpBlock := schemaTypes.NotNullStringOperatorBlock()
notNullJSONOpBlock := schemaTypes.NotNullJSONOperatorBlock(jsonScalarType)
notNullBlobOpBlock := schemaTypes.NotNullBlobOperatorBlock(blobScalarType)

return []gql.Type{
// Base Scalar types
gql.Boolean,
Expand All @@ -207,20 +223,34 @@ func defaultTypes(
orderEnum,

// Filter scalar blocks
schemaTypes.BooleanOperatorBlock(),
schemaTypes.NotNullBooleanOperatorBlock(),
schemaTypes.DateTimeOperatorBlock(),
schemaTypes.FloatOperatorBlock(),
schemaTypes.NotNullFloatOperatorBlock(),
schemaTypes.IdOperatorBlock(),
schemaTypes.IntOperatorBlock(),
schemaTypes.NotNullIntOperatorBlock(),
schemaTypes.StringOperatorBlock(),
schemaTypes.NotNullstringOperatorBlock(),
schemaTypes.JSONOperatorBlock(jsonScalarType),
schemaTypes.NotNullJSONOperatorBlock(jsonScalarType),
schemaTypes.BlobOperatorBlock(blobScalarType),
schemaTypes.NotNullBlobOperatorBlock(blobScalarType),
idOpBlock,
intOpBlock,
floatOpBlock,
booleanOpBlock,
stringOpBlock,
jsonOpBlock,
blobOpBlock,
dateTimeOpBlock,

// Filter non null scalar blocks
notNullIntOpBlock,
notNullFloatOpBlock,
notNullBooleanOpBlock,
notNullStringOpBlock,
notNullJSONOpBlock,
notNullBlobOpBlock,

// Filter scalar list blocks
schemaTypes.IntListOperatorBlock(intOpBlock),
schemaTypes.FloatListOperatorBlock(floatOpBlock),
schemaTypes.BooleanListOperatorBlock(booleanOpBlock),
schemaTypes.StringListOperatorBlock(stringOpBlock),

// Filter non null scalar list blocks
schemaTypes.NotNullIntListOperatorBlock(notNullIntOpBlock),
schemaTypes.NotNullFloatListOperatorBlock(notNullFloatOpBlock),
schemaTypes.NotNullBooleanListOperatorBlock(notNullBooleanOpBlock),
schemaTypes.NotNullStringListOperatorBlock(notNullStringOpBlock),

commitsOrderArg,
commitLinkObject,
Expand Down
Loading

0 comments on commit 55e56a5

Please sign in to comment.