Compare commits

..

1 Commits

Author SHA1 Message Date
Hein
aef1f96c10 fix(db): cast columns to text for LIKE/ILIKE queries
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -29m15s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -29m7s
Build , Vet Test, and Lint / Build (push) Successful in -32m31s
Build , Vet Test, and Lint / Lint Code (push) Successful in -31m40s
Tests / Integration Tests (push) Failing after -33m31s
Tests / Unit Tests (push) Successful in -31m4s
2026-04-13 14:05:17 +02:00
9 changed files with 49 additions and 28 deletions

View File

@@ -739,7 +739,7 @@ func (h *Handler) mergeQueryParams(r *http.Request, sqlquery string, variables m
colval = strings.ReplaceAll(colval, "\\", "\\\\")
colval = strings.ReplaceAll(colval, "'", "''")
if colval != "*" {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s ILIKE '%%%s%%'", ValidSQL(parmk, "colname"), colval))
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%%%s%%'", ValidSQL(parmk, "colname"), colval))
}
} else if val == "" || val == "0" {
// For empty/zero values, treat as literal 0 or empty string with quotes
@@ -806,7 +806,7 @@ func (h *Handler) mergeHeaderParams(r *http.Request, sqlquery string, variables
colname := strings.ReplaceAll(k, "x-searchfilter-", "")
sval := strings.ReplaceAll(val, "'", "")
if sval != "" {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s ILIKE '%%%s%%'", ValidSQL(colname, "colname"), ValidSQL(sval, "colvalue")))
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%%%s%%'", ValidSQL(colname, "colname"), ValidSQL(sval, "colvalue")))
}
}

View File

@@ -259,7 +259,7 @@ func (h *Handler) ApplyFilters(sqlQuery string, params *RequestParameters) strin
for colName, value := range params.SearchFilters {
sval := strings.ReplaceAll(value, "'", "")
if sval != "" {
condition := fmt.Sprintf("%s ILIKE '%%%s%%'", ValidSQL(colName, "colname"), ValidSQL(sval, "colvalue"))
condition := fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%%%s%%'", ValidSQL(colName, "colname"), ValidSQL(sval, "colvalue"))
sqlQuery = sqlQryWhere(sqlQuery, condition)
logger.Debug("Applied search filter: %s", condition)
}
@@ -307,11 +307,11 @@ func (h *Handler) buildFilterCondition(colName string, op FilterOperator) string
switch operator {
case "contains", "contain", "like":
return fmt.Sprintf("%s ILIKE '%%%s%%'", safCol, ValidSQL(value, "colvalue"))
return fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%%%s%%'", safCol, ValidSQL(value, "colvalue"))
case "beginswith", "startswith":
return fmt.Sprintf("%s ILIKE '%s%%'", safCol, ValidSQL(value, "colvalue"))
return fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%s%%'", safCol, ValidSQL(value, "colvalue"))
case "endswith":
return fmt.Sprintf("%s ILIKE '%%%s'", safCol, ValidSQL(value, "colvalue"))
return fmt.Sprintf("CAST(%s AS TEXT) ILIKE '%%%s'", safCol, ValidSQL(value, "colvalue"))
case "equals", "eq", "=":
if IsNumeric(value) {
return fmt.Sprintf("%s = %s", safCol, ValidSQL(value, "colvalue"))

View File

@@ -274,7 +274,7 @@ func TestBuildFilterCondition(t *testing.T) {
Value: "test",
Logic: "AND",
},
expected: "description ILIKE '%test%'",
expected: "CAST(description AS TEXT) ILIKE '%test%'",
},
{
name: "Starts with operator",
@@ -284,7 +284,7 @@ func TestBuildFilterCondition(t *testing.T) {
Value: "john",
Logic: "AND",
},
expected: "name ILIKE 'john%'",
expected: "CAST(name AS TEXT) ILIKE 'john%'",
},
{
name: "Ends with operator",
@@ -294,7 +294,7 @@ func TestBuildFilterCondition(t *testing.T) {
Value: "@example.com",
Logic: "AND",
},
expected: "email ILIKE '%@example.com'",
expected: "CAST(email AS TEXT) ILIKE '%@example.com'",
},
{
name: "Between operator",

View File

@@ -702,7 +702,12 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
if hookCtx.Options != nil {
// Apply filters
for _, filter := range hookCtx.Options.Filters {
query = query.Where(fmt.Sprintf("%s %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
op := strings.ToLower(filter.Operator)
if op == "like" || op == "ilike" {
query = query.Where(fmt.Sprintf("CAST(%s AS TEXT) %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
} else {
query = query.Where(fmt.Sprintf("%s %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
}
}
// Apply sorting
@@ -743,7 +748,12 @@ 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)
op := strings.ToLower(filter.Operator)
if op == "like" || op == "ilike" {
countQuery = countQuery.Where(fmt.Sprintf("CAST(%s AS TEXT) %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
} else {
countQuery = countQuery.Where(fmt.Sprintf("%s %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
}
}
}
count, _ := countQuery.Count(hookCtx.Context)

View File

@@ -735,9 +735,9 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (condition st
case "lte", "<=":
return fmt.Sprintf("%s <= ?", filter.Column), []interface{}{filter.Value}
case "like":
return fmt.Sprintf("%s LIKE ?", filter.Column), []interface{}{filter.Value}
return fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", filter.Column), []interface{}{filter.Value}
case "ilike":
return fmt.Sprintf("%s ILIKE ?", filter.Column), []interface{}{filter.Value}
return fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", filter.Column), []interface{}{filter.Value}
case "in":
condition, args := common.BuildInCondition(filter.Column, filter.Value)
return condition, args

View File

@@ -54,7 +54,7 @@ func TestBuildFilterCondition(t *testing.T) {
Operator: "like",
Value: "%@example.com",
},
expectedCondition: "email LIKE ?",
expectedCondition: "CAST(email AS TEXT) LIKE ?",
expectedArgsCount: 1,
},
}

View File

@@ -1545,10 +1545,10 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionStr
condition = fmt.Sprintf("%s <= ?", filter.Column)
args = []interface{}{filter.Value}
case "like":
condition = fmt.Sprintf("%s LIKE ?", filter.Column)
condition = fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", filter.Column)
args = []interface{}{filter.Value}
case "ilike":
condition = fmt.Sprintf("%s ILIKE ?", filter.Column)
condition = fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", filter.Column)
args = []interface{}{filter.Value}
case "in":
condition, args = common.BuildInCondition(filter.Column, filter.Value)
@@ -1589,10 +1589,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("%s LIKE ?", filter.Column)
condition = fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", filter.Column)
args = []interface{}{filter.Value}
case "ilike":
condition = fmt.Sprintf("%s ILIKE ?", filter.Column)
condition = fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", filter.Column)
args = []interface{}{filter.Value}
case "in":
condition, args = common.BuildInCondition(filter.Column, filter.Value)

View File

@@ -2118,11 +2118,12 @@ func (h *Handler) qualifyColumnName(columnName, fullTableName string) string {
func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOption, tableName string, needsCast bool, logicOp string) common.SelectQuery {
// Qualify the column name with table name if not already qualified
qualifiedColumn := h.qualifyColumnName(filter.Column, tableName)
rawQualifiedColumn := h.qualifyColumnName(filter.Column, tableName)
qualifiedColumn := rawQualifiedColumn
// Apply casting to text if needed for non-numeric columns or non-numeric values
if needsCast {
qualifiedColumn = fmt.Sprintf("CAST(%s AS TEXT)", qualifiedColumn)
qualifiedColumn = fmt.Sprintf("CAST(%s AS TEXT)", rawQualifiedColumn)
}
// Helper function to apply the correct Where method based on logic operator
@@ -2147,11 +2148,11 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
case "lte", "less_than_equals", "le":
return applyWhere(fmt.Sprintf("%s <= ?", qualifiedColumn), filter.Value)
case "like":
return applyWhere(fmt.Sprintf("%s LIKE ?", qualifiedColumn), filter.Value)
// Always cast to TEXT for LIKE/ILIKE to support date/time/timestamp columns
return applyWhere(fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", rawQualifiedColumn), filter.Value)
case "ilike":
// Use ILIKE for case-insensitive search (PostgreSQL)
// Column is already cast to TEXT if needed
return applyWhere(fmt.Sprintf("%s ILIKE ?", qualifiedColumn), filter.Value)
// Always cast to TEXT for LIKE/ILIKE to support date/time/timestamp columns
return applyWhere(fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", rawQualifiedColumn), filter.Value)
case "in":
cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value)
if cond == "" {
@@ -2203,11 +2204,16 @@ func (h *Handler) applyOrFilterGroup(query common.SelectQuery, filters []*common
for i, filter := range filters {
// Qualify the column name with table name if not already qualified
qualifiedColumn := h.qualifyColumnName(filter.Column, tableName)
rawQualifiedColumn := h.qualifyColumnName(filter.Column, tableName)
qualifiedColumn := rawQualifiedColumn
// Apply casting to text if needed for non-numeric columns or non-numeric values
if castInfo[i].NeedsCast {
qualifiedColumn = fmt.Sprintf("CAST(%s AS TEXT)", qualifiedColumn)
op := strings.ToLower(filter.Operator)
if op == "like" || op == "ilike" {
// Always cast to TEXT for LIKE/ILIKE to support date/time/timestamp columns
qualifiedColumn = fmt.Sprintf("CAST(%s AS TEXT)", rawQualifiedColumn)
} else if castInfo[i].NeedsCast {
// Apply casting to text if needed for non-numeric columns or non-numeric values
qualifiedColumn = fmt.Sprintf("CAST(%s AS TEXT)", rawQualifiedColumn)
}
// Build the condition based on operator

View File

@@ -807,6 +807,11 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionStr
cond, args := common.BuildInCondition(filter.Column, filter.Value)
return cond, args
}
op := strings.ToLower(filter.Operator)
if op == "like" || op == "ilike" {
operatorSQL := h.getOperatorSQL(filter.Operator)
return fmt.Sprintf("CAST(%s AS TEXT) %s ?", filter.Column, operatorSQL), []interface{}{filter.Value}
}
operatorSQL := h.getOperatorSQL(filter.Operator)
return fmt.Sprintf("%s %s ?", filter.Column, operatorSQL), []interface{}{filter.Value}
}