diff --git a/pkg/funcspec/function_api.go b/pkg/funcspec/function_api.go index ffa7ec1..b38c45c 100644 --- a/pkg/funcspec/function_api.go +++ b/pkg/funcspec/function_api.go @@ -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"))) } } diff --git a/pkg/funcspec/parameters.go b/pkg/funcspec/parameters.go index af1a4e0..6c4113e 100644 --- a/pkg/funcspec/parameters.go +++ b/pkg/funcspec/parameters.go @@ -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")) diff --git a/pkg/funcspec/parameters_test.go b/pkg/funcspec/parameters_test.go index f2fec6c..922fcac 100644 --- a/pkg/funcspec/parameters_test.go +++ b/pkg/funcspec/parameters_test.go @@ -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", diff --git a/pkg/mqttspec/handler.go b/pkg/mqttspec/handler.go index de4a062..48c3ed4 100644 --- a/pkg/mqttspec/handler.go +++ b/pkg/mqttspec/handler.go @@ -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) diff --git a/pkg/resolvemcp/handler.go b/pkg/resolvemcp/handler.go index 2120723..53e5936 100644 --- a/pkg/resolvemcp/handler.go +++ b/pkg/resolvemcp/handler.go @@ -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 diff --git a/pkg/resolvespec/filter_test.go b/pkg/resolvespec/filter_test.go index 87e116f..7ed8565 100644 --- a/pkg/resolvespec/filter_test.go +++ b/pkg/resolvespec/filter_test.go @@ -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, }, } diff --git a/pkg/resolvespec/handler.go b/pkg/resolvespec/handler.go index 8fd163a..73be572 100644 --- a/pkg/resolvespec/handler.go +++ b/pkg/resolvespec/handler.go @@ -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) diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index b971480..ab4f086 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -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 diff --git a/pkg/websocketspec/handler.go b/pkg/websocketspec/handler.go index 9d0b3c7..0c978c4 100644 --- a/pkg/websocketspec/handler.go +++ b/pkg/websocketspec/handler.go @@ -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} }