fix(restheadspec,resolvespec): stop casting SqlNull-wrapped and citext columns to TEXT in filters

reflect.Type.Kind() on spectypes.SqlNull[T] wrappers (SqlInt16/32/64,
SqlFloat64, SqlBool, SqlString, and embedders like SqlTimeStamp) always
reports reflect.Struct, never the wrapped T. ValidateAndAdjustFilterForColumnType
treated those as "complex" columns and forced CAST(col AS TEXT) on eq/gt/lt
filters, e.g. CAST(atdetail.rid_parent AS TEXT) = '90446096', which can't
use the index on rid_parent.

Add spectypes.UnwrapKind to see through SqlNull wrappers to the underlying
Kind, and use it in GetColumnTypeFromModel so numeric/string SqlNull columns
are recognized correctly and compared natively.

Also stop unconditionally casting to TEXT for LIKE/ILIKE and add
reflection.IsCitextColumn: citext columns are already case-insensitive, so
casting them to TEXT flips to case-sensitive matching and defeats a citext
index.
This commit is contained in:
Hein
2026-09-15 11:18:13 +02:00
parent 1885ce016b
commit 6bd6a6f164
8 changed files with 253 additions and 21 deletions
+16 -4
View File
@@ -1937,10 +1937,10 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption, model interfa
condition = fmt.Sprintf("%s <= ?", filter.Column)
args = []interface{}{filter.Value}
case "like":
condition = fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", filter.Column)
condition = fmt.Sprintf("%s LIKE ?", likeColumn(filter.Column, model))
args = []interface{}{filter.Value}
case "ilike":
condition = fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", filter.Column)
condition = fmt.Sprintf("%s ILIKE ?", likeColumn(filter.Column, model))
args = []interface{}{filter.Value}
case "in":
condition, args = common.BuildInCondition(filter.Column, filter.Value)
@@ -1973,6 +1973,18 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption, model interfa
return condition, args
}
// likeColumn returns the column expression to use for LIKE/ILIKE. citext
// columns are compared natively — they're already case-insensitive, and
// CAST(... AS TEXT) would switch to case-sensitive matching and defeat a
// citext index. Every other column is cast to TEXT so LIKE/ILIKE also works
// against date/time/timestamp and numeric columns.
func likeColumn(column string, model interface{}) string {
if reflection.IsCitextColumn(model, column) {
return column
}
return fmt.Sprintf("CAST(%s AS TEXT)", column)
}
func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOption, model interface{}) common.SelectQuery {
// Determine which method to use based on LogicOperator
useOrLogic := strings.EqualFold(filter.LogicOperator, "OR")
@@ -2007,10 +2019,10 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
condition = fmt.Sprintf("%s <= ?", filter.Column)
args = []interface{}{filter.Value}
case "like":
condition = fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", filter.Column)
condition = fmt.Sprintf("%s LIKE ?", likeColumn(filter.Column, model))
args = []interface{}{filter.Value}
case "ilike":
condition = fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", filter.Column)
condition = fmt.Sprintf("%s ILIKE ?", likeColumn(filter.Column, model))
args = []interface{}{filter.Value}
case "in":
condition, args = common.BuildInCondition(filter.Column, filter.Value)