feat(restheadspec): match NULL when x-fieldfilter compares a numeric column to 0

Adds a SetNullAsZero handler option: on by default in restheadspec (for
x-fieldfilter only), off by default in resolvespec. When enabled, an eq
filter of 0 on a numeric column renders (col = 0 OR col IS NULL).
This commit is contained in:
Hein
2026-10-12 15:49:57 +02:00
parent fbfc1aadb5
commit d3252f3d02
7 changed files with 239 additions and 2 deletions
+27
View File
@@ -34,6 +34,7 @@ type Handler struct {
openAPIGenerator func() (string, error)
defaultSort map[string][]common.SortOption
disallowNulls bool
nullAsZero bool
}
// NewHandler creates a new API handler with database and registry abstractions
@@ -42,6 +43,8 @@ func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
db: db,
registry: registry,
hooks: NewHookRegistry(),
// x-fieldfilter on a numeric column with 0 also matches NULLs by default
nullAsZero: true,
}
// Initialize nested processor
handler.nestedProcessor = common.NewNestedCUDProcessor(db, registry, handler)
@@ -68,6 +71,12 @@ func (h *Handler) SetDisallowNulls(disallow bool) {
h.disallowNulls = disallow
}
// SetNullAsZero controls whether an x-fieldfilter equality against 0 on a numeric
// column also matches NULLs: (col = 0 OR col IS NULL). Enabled by default.
func (h *Handler) SetNullAsZero(enabled bool) {
h.nullAsZero = enabled
}
// SetFallbackHandler sets a fallback handler to be called when no model is found
// If not set, the handler will simply return (pass through to next route)
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
@@ -2518,6 +2527,9 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
switch strings.ToLower(filter.Operator) {
case "eq", "equals":
if h.nullAsZeroApplies(&filter, model, needsCast) {
return applyWhere(fmt.Sprintf("(%s = ? OR %s IS NULL)", rawQualifiedColumn, rawQualifiedColumn), filter.Value)
}
return applyWhere(fmt.Sprintf("%s = ?", qualifiedColumn), filter.Value)
case "neq", "not_equals", "ne":
return applyWhere(fmt.Sprintf("%s != ?", qualifiedColumn), filter.Value)
@@ -2588,6 +2600,16 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
}
}
// nullAsZeroApplies reports whether an eq filter should also match NULLs, i.e.
// (col = 0 OR col IS NULL). This is the case for x-fieldfilter on a numeric
// column compared against zero, where a NULL is treated the same as 0.
func (h *Handler) nullAsZeroApplies(filter *common.FilterOption, model interface{}, needsCast bool) bool {
if !h.nullAsZero || !filter.NullAsZero || needsCast {
return false
}
return common.IsNumericZeroFilter(model, filter.Column, filter.Value)
}
// applyOrFilterGroup applies a group of OR filters as a single grouped condition
// This ensures OR conditions are properly grouped with parentheses to prevent OR logic from escaping
func (h *Handler) applyOrFilterGroup(query common.SelectQuery, filters []*common.FilterOption, castInfo []ColumnCastInfo, tableName string, model interface{}) common.SelectQuery {
@@ -2627,6 +2649,11 @@ func (h *Handler) applyOrFilterGroup(query common.SelectQuery, filters []*common
}
// Build the condition based on operator
if strings.EqualFold(filter.Operator, "eq") && h.nullAsZeroApplies(filter, model, castInfo[i].NeedsCast) {
conditions = append(conditions, fmt.Sprintf("(%s = ? OR %s IS NULL)", rawQualifiedColumn, rawQualifiedColumn))
args = append(args, filter.Value)
continue
}
condition, filterArgs := h.buildFilterCondition(qualifiedColumn, filter, tableName)
if condition != "" {
conditions = append(conditions, condition)