Skip to content

Commit

Permalink
add case insensitive like operator
Browse files Browse the repository at this point in the history
  • Loading branch information
fredcarle committed Mar 4, 2024
1 parent b730d3f commit c46458d
Show file tree
Hide file tree
Showing 18 changed files with 958 additions and 24 deletions.
4 changes: 4 additions & 0 deletions connor/connor.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ func matchWith(op string, conditions, data any) (bool, error) {
return like(conditions, data)
case "_nlike":
return nlike(conditions, data)
case "_ilike":
return ilike(conditions, data)
case "_nilike":
return nilike(conditions, data)
case "_not":
return not(conditions, data)
default:
Expand Down
30 changes: 30 additions & 0 deletions connor/ilike.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package connor

import (
"strings"

"github.com/sourcenetwork/immutable"

"github.com/sourcenetwork/defradb/client"
)

// ilike is an operator which performs case insensitive string equality tests.
func ilike(condition, data any) (bool, error) {
switch d := data.(type) {
case immutable.Option[string]:
if !d.HasValue() {
return condition == nil, nil
}
data = d.Value()

Check warning on line 18 in connor/ilike.go

View check run for this annotation

Codecov / codecov/patch

connor/ilike.go#L14-L18

Added lines #L14 - L18 were not covered by tests
}

switch cn := condition.(type) {
case string:
if d, ok := data.(string); ok {
return like(strings.ToLower(cn), strings.ToLower(d))
}
return false, nil
default:
return false, client.NewErrUnhandledType("condition", cn)

Check warning on line 28 in connor/ilike.go

View check run for this annotation

Codecov / codecov/patch

connor/ilike.go#L26-L28

Added lines #L26 - L28 were not covered by tests
}
}
41 changes: 41 additions & 0 deletions connor/ilike_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package connor

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestILike(t *testing.T) {
const testString = "Source Is The Glue of Web3"

// case insensitive exact match
result, err := ilike("source is the glue of web3", testString)
require.NoError(t, err)
require.True(t, result)

// case insensitive no match
result, err = ilike("source is the glue", testString)
require.NoError(t, err)
require.False(t, result)

// case insensitive match prefix
result, err = ilike("source%", testString)
require.NoError(t, err)
require.True(t, result)

// case insensitive match suffix
result, err = ilike("%web3", testString)
require.NoError(t, err)
require.True(t, result)

// case insensitive match contains
result, err = ilike("%glue%", testString)
require.NoError(t, err)
require.True(t, result)

// case insensitive match start and end with
result, err = ilike("source%web3", testString)
require.NoError(t, err)
require.True(t, result)
}
6 changes: 3 additions & 3 deletions connor/like.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import (
// like is an operator which performs string equality
// tests.
func like(condition, data any) (bool, error) {
switch arr := data.(type) {
switch d := data.(type) {
case immutable.Option[string]:
if !arr.HasValue() {
if !d.HasValue() {

Check warning on line 16 in connor/like.go

View check run for this annotation

Codecov / codecov/patch

connor/like.go#L16

Added line #L16 was not covered by tests
return condition == nil, nil
}
data = arr.Value()
data = d.Value()

Check warning on line 19 in connor/like.go

View check run for this annotation

Codecov / codecov/patch

connor/like.go#L19

Added line #L19 was not covered by tests
}

switch cn := condition.(type) {
Expand Down
15 changes: 15 additions & 0 deletions connor/nilike.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package connor

import "fmt"

// nilike performs case insensitive string inequality comparisons by inverting
// the result of the Like operator for non-error cases.
func nilike(conditions, data any) (bool, error) {
m, err := ilike(conditions, data)
fmt.Println(m, err)

Check failure on line 9 in connor/nilike.go

View workflow job for this annotation

GitHub Actions / Lint GoLang job

use of `fmt.Println` forbidden by pattern `fmt\.Print.*` (forbidigo)
if err != nil {
return false, err
}

Check warning on line 12 in connor/nilike.go

View check run for this annotation

Codecov / codecov/patch

connor/nilike.go#L11-L12

Added lines #L11 - L12 were not covered by tests

return !m, err
}
41 changes: 41 additions & 0 deletions connor/nilike_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package connor

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestNILike(t *testing.T) {
const testString = "Source Is The Glue of Web3"

// case insensitive exact match
result, err := nilike("source is the glue of web3", testString)
require.NoError(t, err)
require.False(t, result)

// case insensitive no match
result, err = nilike("source is the glue", testString)
require.NoError(t, err)
require.True(t, result)

// case insensitive match prefix
result, err = nilike("source%", testString)
require.NoError(t, err)
require.False(t, result)

// case insensitive match suffix
result, err = nilike("%web3", testString)
require.NoError(t, err)
require.False(t, result)

// case insensitive match contains
result, err = nilike("%glue%", testString)
require.NoError(t, err)
require.False(t, result)

// case insensitive match start and end with
result, err = nilike("source%web3", testString)
require.NoError(t, err)
require.False(t, result)
}
41 changes: 41 additions & 0 deletions connor/nlike_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package connor

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestNLike(t *testing.T) {
const testString = "Source is the glue of web3"

// exact match
result, err := nlike(testString, testString)
require.NoError(t, err)
require.False(t, result)

// exact match error
result, err = nlike("Source is the glue", testString)
require.NoError(t, err)
require.True(t, result)

// match prefix
result, err = nlike("Source%", testString)
require.NoError(t, err)
require.False(t, result)

// match suffix
result, err = nlike("%web3", testString)
require.NoError(t, err)
require.False(t, result)

// match contains
result, err = nlike("%glue%", testString)
require.NoError(t, err)
require.False(t, result)

// match start and end with
result, err = nlike("Source%web3", testString)
require.NoError(t, err)
require.False(t, result)
}
56 changes: 35 additions & 21 deletions db/fetcher/indexer_iterators.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ import (
)

const (
opEq = "_eq"
opGt = "_gt"
opGe = "_ge"
opLt = "_lt"
opLe = "_le"
opNe = "_ne"
opIn = "_in"
opNin = "_nin"
opLike = "_like"
opNlike = "_nlike"
opEq = "_eq"
opGt = "_gt"
opGe = "_ge"
opLt = "_lt"
opLe = "_le"
opNe = "_ne"
opIn = "_in"
opNin = "_nin"
opLike = "_like"
opNlike = "_nlike"
opILike = "_ilike"
opNILike = "_nilike"
// it's just there for composite indexes. We construct a slice of value matchers with
// every matcher being responsible for a corresponding field in the index to match.
// For some fields there might not be any criteria to match. For examples if you have
Expand Down Expand Up @@ -382,16 +384,18 @@ func (m *indexInArrayMatcher) Match(value any) (bool, error) {

// checks if the index value satisfies the LIKE condition
type indexLikeMatcher struct {
hasPrefix bool
hasSuffix bool
startAndEnd []string
isLike bool
value string
hasPrefix bool
hasSuffix bool
startAndEnd []string
isLike bool
isCaseInsensitive bool
value string
}

func newLikeIndexCmp(filterValue string, isLike bool) (*indexLikeMatcher, error) {
func newLikeIndexCmp(filterValue string, isLike bool, isCaseInsensitive bool) (*indexLikeMatcher, error) {
matcher := &indexLikeMatcher{
isLike: isLike,
isLike: isLike,
isCaseInsensitive: isCaseInsensitive,
}
if len(filterValue) >= 2 {
if filterValue[0] == '%' {
Expand All @@ -406,7 +410,11 @@ func newLikeIndexCmp(filterValue string, isLike bool) (*indexLikeMatcher, error)
matcher.startAndEnd = strings.Split(filterValue, "%")
}
}
matcher.value = filterValue
if isCaseInsensitive {
matcher.value = strings.ToLower(filterValue)
} else {
matcher.value = filterValue
}

return matcher, nil
}
Expand All @@ -417,6 +425,10 @@ func (m *indexLikeMatcher) Match(value any) (bool, error) {
return false, NewErrUnexpectedTypeValue[string](currentVal)
}

if m.isCaseInsensitive {
currentVal = strings.ToLower(currentVal)
}

return m.doesMatch(currentVal) == m.isLike, nil
}

Expand Down Expand Up @@ -571,7 +583,7 @@ func (f *IndexFetcher) createIndexIterator() (indexIterator, error) {
}
case opIn:
return f.newInIndexIterator(fieldConditions, matchers)
case opGt, opGe, opLt, opLe, opNe, opNin, opLike, opNlike:
case opGt, opGe, opLt, opLe, opNe, opNin, opLike, opNlike, opILike, opNILike:
return &scanningIndexIterator{
queryResultIterator: f.newQueryResultIterator(),
indexKey: f.newIndexDataStoreKey(),
Expand Down Expand Up @@ -627,12 +639,14 @@ func createValueMatcher(condition *fieldFilterCond) (valueMatcher, error) {
return nil, ErrInvalidInOperatorValue
}
return newNinIndexCmp(inArr, condition.kind, condition.op == opIn)
case opLike, opNlike:
case opLike, opNlike, opILike, opNILike:
strVal, ok := condition.val.(string)
if !ok {
return nil, NewErrUnexpectedTypeValue[string](condition.val)
}
return newLikeIndexCmp(strVal, condition.op == opLike)
isLike := condition.op == opLike || condition.op == opILike
isCaseInsensitive := condition.op == opILike || condition.op == opNILike
return newLikeIndexCmp(strVal, isLike, isCaseInsensitive)
case opAny:
return &anyMatcher{}, nil
}
Expand Down
16 changes: 16 additions & 0 deletions request/graphql/schema/types/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ var StringOperatorBlock = gql.NewInputObject(gql.InputObjectConfig{
Description: nlikeStringOperatorDescription,
Type: gql.String,
},
"_ilike": &gql.InputObjectFieldConfig{
Description: ilikeStringOperatorDescription,
Type: gql.String,
},
"_nilike": &gql.InputObjectFieldConfig{
Description: nilikeStringOperatorDescription,
Type: gql.String,
},
},
})

Expand Down Expand Up @@ -323,6 +331,14 @@ var NotNullstringOperatorBlock = gql.NewInputObject(gql.InputObjectConfig{
Description: nlikeStringOperatorDescription,
Type: gql.String,
},
"_ilike": &gql.InputObjectFieldConfig{
Description: ilikeStringOperatorDescription,
Type: gql.String,
},
"_nilike": &gql.InputObjectFieldConfig{
Description: nilikeStringOperatorDescription,
Type: gql.String,
},
},
})

Expand Down
10 changes: 10 additions & 0 deletions request/graphql/schema/types/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ The like operator - if the target value contains the given sub-string the check
The not-like operator - if the target value does not contain the given sub-string the check will
pass. '%' characters may be used as wildcards, for example '_nlike: "%Ritchie"' would match on
the string 'Quentin Tarantino'.
`
ilikeStringOperatorDescription string = `
The case insensitive like operator - if the target value contains the given case insensitive sub-string the check
will pass. '%' characters may be used as wildcards, for example '_like: "%ritchie"' would match on strings
ending in 'Ritchie'.
`
nilikeStringOperatorDescription string = `
The case insensitive not-like operator - if the target value does not contain the given case insensitive sub-string
the check will pass. '%' characters may be used as wildcards, for example '_nlike: "%ritchie"' would match on
the string 'Quentin Tarantino'.
`
AndOperatorDescription string = `
The and operator - all checks within this clause must pass in order for this check to pass.
Expand Down
Loading

0 comments on commit c46458d

Please sign in to comment.