feat(restheadspec): add x-searchop-notcontain operator

Maps notcontain/notcontains/notlike to a not_ilike filter rendered as
(col IS NULL OR CAST(col AS TEXT) NOT ILIKE ?), so NULL rows are kept.
This commit is contained in:
Hein
2026-10-12 15:56:42 +02:00
parent d3252f3d02
commit 5dbe187915
6 changed files with 55 additions and 2 deletions
+9 -1
View File
@@ -2550,6 +2550,9 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
// columns are compared natively (see likeColumn above) since citext is
// already case-insensitive.
return applyWhere(fmt.Sprintf("%s ILIKE ?", likeColumn), filter.Value)
case "not_ilike":
// NULLs don't contain the value, so they are kept
return applyWhere(fmt.Sprintf("(%s IS NULL OR %s NOT ILIKE ?)", rawQualifiedColumn, likeColumn), filter.Value)
case "in":
cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value)
if cond == "" {
@@ -2636,7 +2639,7 @@ func (h *Handler) applyOrFilterGroup(query common.SelectQuery, filters []*common
qualifiedColumn := rawQualifiedColumn
op := strings.ToLower(filter.Operator)
if op == "like" || op == "ilike" {
if op == "like" || op == "ilike" || op == "not_ilike" {
// Cast to TEXT for LIKE/ILIKE to support date/time/timestamp columns.
// citext columns are left native: they're already case-insensitive and
// casting would defeat a citext index.
@@ -2692,6 +2695,9 @@ func (h *Handler) buildFilterCondition(qualifiedColumn string, filter *common.Fi
return fmt.Sprintf("%s LIKE ?", qualifiedColumn), []interface{}{filter.Value}
case "ilike":
return fmt.Sprintf("%s ILIKE ?", qualifiedColumn), []interface{}{filter.Value}
case "not_ilike":
rawCol := h.qualifyColumnName(filter.Column, tableName)
return fmt.Sprintf("(%s IS NULL OR %s NOT ILIKE ?)", rawCol, qualifiedColumn), []interface{}{filter.Value}
case "in":
cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value)
return cond, inArgs
@@ -3440,6 +3446,8 @@ func (h *Handler) buildFilterSQL(filter *common.FilterOption, tableName string)
return fmt.Sprintf("%s LIKE '%v'", qualifiedColumn, filter.Value)
case "ilike":
return fmt.Sprintf("%s ILIKE '%v'", qualifiedColumn, filter.Value)
case "not_ilike":
return fmt.Sprintf("(%s IS NULL OR CAST(%s AS TEXT) NOT ILIKE '%v')", qualifiedColumn, qualifiedColumn, filter.Value)
case "in":
if values, ok := filter.Value.([]any); ok {
valueStrs := make([]string, len(values))