diff --git a/pkg/common/sql_helpers.go b/pkg/common/sql_helpers.go index 3093d49..82b1ae8 100644 --- a/pkg/common/sql_helpers.go +++ b/pkg/common/sql_helpers.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "reflect" "regexp" "strings" @@ -925,3 +926,36 @@ func extractLeftSideOfComparison(cond string) string { return "" } + +// FilterValueToSlice converts a filter value to []interface{} for use with IN operators. +// JSON-decoded arrays arrive as []interface{}, but typed slices (e.g. []string) also work. +// Returns a single-element slice if the value is not a slice type. +func FilterValueToSlice(v interface{}) []interface{} { + if v == nil { + return nil + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Slice { + result := make([]interface{}, rv.Len()) + for i := 0; i < rv.Len(); i++ { + result[i] = rv.Index(i).Interface() + } + return result + } + return []interface{}{v} +} + +// BuildInCondition builds a parameterized IN condition from a filter value. +// Returns the condition string (e.g. "col IN (?,?)") and the individual values as args. +// Returns ("", nil) if the value is empty or not a slice. +func BuildInCondition(column string, v interface{}) (query string, args []interface{}) { + values := FilterValueToSlice(v) + if len(values) == 0 { + return "", nil + } + placeholders := make([]string, len(values)) + for i := range values { + placeholders[i] = "?" + } + return fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ",")), values +} diff --git a/pkg/resolvespec/filter_test.go b/pkg/resolvespec/filter_test.go index 177997f..87e116f 100644 --- a/pkg/resolvespec/filter_test.go +++ b/pkg/resolvespec/filter_test.go @@ -44,8 +44,8 @@ func TestBuildFilterCondition(t *testing.T) { Operator: "in", Value: []string{"active", "pending"}, }, - expectedCondition: "status IN (?)", - expectedArgsCount: 1, + expectedCondition: "status IN (?,?)", + expectedArgsCount: 2, }, { name: "LIKE operator", diff --git a/pkg/resolvespec/handler.go b/pkg/resolvespec/handler.go index fcf2227..dc87539 100644 --- a/pkg/resolvespec/handler.go +++ b/pkg/resolvespec/handler.go @@ -1546,8 +1546,10 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionStr condition = fmt.Sprintf("%s ILIKE ?", filter.Column) args = []interface{}{filter.Value} case "in": - condition = fmt.Sprintf("%s IN (?)", filter.Column) - args = []interface{}{filter.Value} + condition, args = common.BuildInCondition(filter.Column, filter.Value) + if condition == "" { + return "", nil + } default: return "", nil } @@ -1588,8 +1590,10 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti condition = fmt.Sprintf("%s ILIKE ?", filter.Column) args = []interface{}{filter.Value} case "in": - condition = fmt.Sprintf("%s IN (?)", filter.Column) - args = []interface{}{filter.Value} + condition, args = common.BuildInCondition(filter.Column, filter.Value) + if condition == "" { + return query + } default: return query } diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index cc84ce3..bfbbe67 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -2146,7 +2146,11 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti // Column is already cast to TEXT if needed return applyWhere(fmt.Sprintf("%s ILIKE ?", qualifiedColumn), filter.Value) case "in": - return applyWhere(fmt.Sprintf("%s IN (?)", qualifiedColumn), filter.Value) + cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value) + if cond == "" { + return query + } + return applyWhere(cond, inArgs...) case "between": // Handle between operator - exclusive (> val1 AND < val2) if values, ok := filter.Value.([]interface{}); ok && len(values) == 2 { @@ -2239,7 +2243,8 @@ func (h *Handler) buildFilterCondition(qualifiedColumn string, filter *common.Fi case "ilike": return fmt.Sprintf("%s ILIKE ?", qualifiedColumn), []interface{}{filter.Value} case "in": - return fmt.Sprintf("%s IN (?)", qualifiedColumn), []interface{}{filter.Value} + cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value) + return cond, inArgs case "between": // Handle between operator - exclusive (> val1 AND < val2) if values, ok := filter.Value.([]interface{}); ok && len(values) == 2 { diff --git a/pkg/websocketspec/handler.go b/pkg/websocketspec/handler.go index cbdedf3..9d0b3c7 100644 --- a/pkg/websocketspec/handler.go +++ b/pkg/websocketspec/handler.go @@ -628,7 +628,10 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata countQuery := h.db.NewSelect().Model(hookCtx.ModelPtr).Table(hookCtx.TableName) if hookCtx.Options != nil { for _, filter := range hookCtx.Options.Filters { - countQuery = countQuery.Where(fmt.Sprintf("%s %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value) + cond, args := h.buildFilterCondition(filter) + if cond != "" { + countQuery = countQuery.Where(cond, args...) + } } } count, _ := countQuery.Count(hookCtx.Context) @@ -800,14 +803,12 @@ func (h *Handler) applyFilterGroup(query common.SelectQuery, filters []common.Fi // buildFilterCondition builds a filter condition and returns it with args func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionString string, conditionArgs []interface{}) { - var condition string - var args []interface{} - + if strings.EqualFold(filter.Operator, "in") { + cond, args := common.BuildInCondition(filter.Column, filter.Value) + return cond, args + } operatorSQL := h.getOperatorSQL(filter.Operator) - condition = fmt.Sprintf("%s %s ?", filter.Column, operatorSQL) - args = []interface{}{filter.Value} - - return condition, args + return fmt.Sprintf("%s %s ?", filter.Column, operatorSQL), []interface{}{filter.Value} } // setRowNumbersOnRecords sets the RowNumber field on each record if it exists