Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions backend/src/apiserver/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func (f *Filter) matchesFilter(getField func(string) interface{}) (bool, error)
for k := range f.eq {
fieldVal := fmt.Sprint(getField(k))
for _, v := range f.eq[k] {
if fieldVal != fmt.Sprint(v) {
if !strings.EqualFold(fieldVal, fmt.Sprint(v)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @kaikaila , I'm not seeing this case insensitive behavior in the code. Is it possibly a MySQL behavioral quirk you're experiencing?

func (f *Filter) AddToSelect(sb squirrel.SelectBuilder) squirrel.SelectBuilder {
	for k := range f.eq {
		for _, v := range f.eq[k] {
			m := map[string]interface{}{k: v}
			sb = sb.Where(squirrel.Eq(m)) // plain "="; no LOWER(...)
		}
	}

	for k := range f.neq {
		for _, v := range f.neq[k] {
			m := map[string]interface{}{k: v}
			sb = sb.Where(squirrel.NotEq(m)) // plain "<>"; no LOWER(...)
		}
	}

	// ...

	for k := range f.substring {
		for _, v := range f.substring[k] {
			like := make(squirrel.Like)
			like[k] = fmt.Sprintf("%%%s%%", v)
			sb = sb.Where(like) // plain LIKE; no ILIKE / LOWER(...)
		}
	}

	return sb
}

@kaikaila kaikaila Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mprahl, thanks for the feedback!

To clarify my intent here: I believe KFP's filter API should behave consistently regardless of which pipeline store backend is used. Right now, the K8s pipeline store case-sensitive collation causes filter behavior to silently differ from MySQL's— and I don't think that's intentional.

So I'd love to get your or the community's input on what the right policy should be:

A. Case-insensitive everywhere (my current fix. align to MySQL behavior, fix K8s pipeline store to match, and postgres in #12379)
B. Case-sensitive everywhere (align to K8s pipeline store / postgres behavior, breaking change for MySQL users)
C. Leave behavior backend-dependent (K8s pipeline store differs from MySQL — users get different filter semantics depending on their deployment)

Happy to implement whichever direction is decided.

For reference, MLflow faced the same problem and they force case-sensitive behavior across all backends by using BINARY in MySQL queries. See here.

return false, nil
}
}
Expand All @@ -251,7 +251,7 @@ func (f *Filter) matchesFilter(getField func(string) interface{}) (bool, error)
for k := range f.neq {
fieldVal := fmt.Sprint(getField(k))
for _, v := range f.neq[k] {
if fieldVal == fmt.Sprint(v) {
if strings.EqualFold(fieldVal, fmt.Sprint(v)) {
return false, nil
}
}
Expand Down Expand Up @@ -283,7 +283,7 @@ func (f *Filter) matchesFilter(getField func(string) interface{}) (bool, error)
return false, nil
}
for i := 0; i < rv.Len(); i++ {
if fieldVal == fmt.Sprint(rv.Index(i).Interface()) {
if strings.EqualFold(fieldVal, fmt.Sprint(rv.Index(i).Interface())) {
inOne = true
break
}
Expand All @@ -296,9 +296,9 @@ func (f *Filter) matchesFilter(getField func(string) interface{}) (bool, error)

// SUBSTRING: all specified substrings must be present.
for k := range f.substring {
fieldVal := fmt.Sprint(getField(k))
lowerFieldVal := strings.ToLower(fmt.Sprint(getField(k)))
for _, v := range f.substring[k] {
if !strings.Contains(fieldVal, fmt.Sprint(v)) {
if !strings.Contains(lowerFieldVal, strings.ToLower(fmt.Sprint(v))) {
return false, nil
}
}
Comment on lines 298 to 304
Expand Down
40 changes: 40 additions & 0 deletions backend/src/apiserver/filter/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,26 @@ func TestFilterK8sPipelines_EQ_NEQ(t *testing.T) {
if found {
t.Fatalf("expected AND filter not to match when one predicate fails")
}

// EQ case-insensitive: "MY-PIPELINE" should match "my-pipeline"
eqCaseInsensitive := &Filter{eq: map[string][]interface{}{"pipelines.Name": {"MY-PIPELINE"}}}
found, err = eqCaseInsensitive.FilterK8sPipelines(k8sPipeline)
if err != nil {
t.Fatalf("unexpected error for EQ case-insensitive: %v", err)
}
if !found {
t.Fatalf("expected EQ filter to match case-insensitively")
}

// NEQ case-insensitive: "MY-PIPELINE" should be considered equal, so NEQ should not match
neqCaseInsensitive := &Filter{neq: map[string][]interface{}{"pipelines.Name": {"MY-PIPELINE"}}}
found, err = neqCaseInsensitive.FilterK8sPipelines(k8sPipeline)
if err != nil {
t.Fatalf("unexpected error for NEQ case-insensitive: %v", err)
}
if found {
t.Fatalf("expected NEQ filter not to match when value matches case-insensitively")
}
}

func TestFilterK8sPipelines_IN(t *testing.T) {
Expand Down Expand Up @@ -768,6 +788,16 @@ func TestFilterK8sPipelines_IN(t *testing.T) {
if !found {
t.Fatalf("expected IN multi to match when present in all lists")
}

// IN case-insensitive: "MY-PIPELINE" should match "my-pipeline"
inCaseInsensitive := &Filter{in: map[string][]interface{}{"pipelines.Name": {[]string{"a", "MY-PIPELINE"}}}}
found, err = inCaseInsensitive.FilterK8sPipelines(k8sPipeline)
if err != nil {
t.Fatalf("unexpected error for IN case-insensitive: %v", err)
}
if !found {
t.Fatalf("expected IN filter to match case-insensitively")
}
}

func TestFilterK8sPipelines_SUBSTRING(t *testing.T) {
Expand All @@ -794,6 +824,16 @@ func TestFilterK8sPipelines_SUBSTRING(t *testing.T) {
if found {
t.Fatalf("expected substring filter not to match when a substring missing")
}

// IS_SUBSTRING case-insensitive: "Pipeline" should match "my-pipeline"
subCaseInsensitive := &Filter{substring: map[string][]interface{}{"pipelines.Name": {"Pipeline"}}}
found, err = subCaseInsensitive.FilterK8sPipelines(k8sPipeline)
if err != nil {
t.Fatalf("unexpected error for substring case-insensitive: %v", err)
}
if !found {
t.Fatalf("expected IS_SUBSTRING filter to match case-insensitively")
}
}

func TestFilterK8sPipelines_UnsupportedOps(t *testing.T) {
Expand Down
Loading