mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-04-16 21:03:51 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef1f96c10 | ||
|
|
354ed2a8dc |
@@ -10,6 +10,8 @@ import (
|
|||||||
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxMetricFallbackEntityLength = 120
|
||||||
|
|
||||||
func recordQueryMetrics(enabled bool, operation, schema, entity, table string, startedAt time.Time, err error) {
|
func recordQueryMetrics(enabled bool, operation, schema, entity, table string, startedAt time.Time, err error) {
|
||||||
if !enabled {
|
if !enabled {
|
||||||
return
|
return
|
||||||
@@ -136,7 +138,7 @@ func metricTargetFromRawQuery(query, driverName string) (operation, schema, enti
|
|||||||
operation = normalizeMetricOperation(firstQueryKeyword(query))
|
operation = normalizeMetricOperation(firstQueryKeyword(query))
|
||||||
tableRef := tableFromRawQuery(query, operation)
|
tableRef := tableFromRawQuery(query, operation)
|
||||||
if tableRef == "" {
|
if tableRef == "" {
|
||||||
return operation, "", "unknown", "unknown"
|
return operation, "", fallbackMetricEntityFromQuery(query), "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
schema, table = parseTableName(tableRef, driverName)
|
schema, table = parseTableName(tableRef, driverName)
|
||||||
@@ -144,6 +146,133 @@ func metricTargetFromRawQuery(query, driverName string) (operation, schema, enti
|
|||||||
return operation, schema, entity, table
|
return operation, schema, entity, table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fallbackMetricEntityFromQuery(query string) string {
|
||||||
|
query = sanitizeMetricQueryShape(query)
|
||||||
|
if query == "" {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query) > maxMetricFallbackEntityLength {
|
||||||
|
return query[:maxMetricFallbackEntityLength-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeMetricQueryShape(query string) string {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if query == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
for i := 0; i < len(query); {
|
||||||
|
if query[i] == '\'' {
|
||||||
|
out.WriteByte('?')
|
||||||
|
i++
|
||||||
|
for i < len(query) {
|
||||||
|
if query[i] == '\'' {
|
||||||
|
if i+1 < len(query) && query[i+1] == '\'' {
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if query[i] == '?' {
|
||||||
|
out.WriteByte('?')
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if query[i] == '$' && i+1 < len(query) && isASCIIDigit(query[i+1]) {
|
||||||
|
out.WriteByte('?')
|
||||||
|
i++
|
||||||
|
for i < len(query) && isASCIIDigit(query[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if query[i] == ':' && (i == 0 || query[i-1] != ':') && i+1 < len(query) && isIdentifierStart(query[i+1]) {
|
||||||
|
out.WriteByte('?')
|
||||||
|
i++
|
||||||
|
for i < len(query) && isIdentifierPart(query[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if query[i] == '@' && (i == 0 || query[i-1] != '@') && i+1 < len(query) && isIdentifierStart(query[i+1]) {
|
||||||
|
out.WriteByte('?')
|
||||||
|
i++
|
||||||
|
for i < len(query) && isIdentifierPart(query[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if startsNumericLiteral(query, i) {
|
||||||
|
out.WriteByte('?')
|
||||||
|
i++
|
||||||
|
for i < len(query) && (isASCIIDigit(query[i]) || query[i] == '.') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteByte(query[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(strings.Fields(out.String()), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func startsNumericLiteral(query string, idx int) bool {
|
||||||
|
if idx >= len(query) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
start := idx
|
||||||
|
if query[idx] == '-' {
|
||||||
|
if idx+1 >= len(query) || !isASCIIDigit(query[idx+1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isASCIIDigit(query[start]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx > 0 && isIdentifierPart(query[idx-1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if start+1 < len(query) && query[start] == '0' && (query[start+1] == 'x' || query[start+1] == 'X') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isASCIIDigit(ch byte) bool {
|
||||||
|
return ch >= '0' && ch <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIdentifierStart(ch byte) bool {
|
||||||
|
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIdentifierPart(ch byte) bool {
|
||||||
|
return isIdentifierStart(ch) || isASCIIDigit(ch)
|
||||||
|
}
|
||||||
|
|
||||||
func firstQueryKeyword(query string) string {
|
func firstQueryKeyword(query string) string {
|
||||||
query = strings.TrimSpace(query)
|
query = strings.TrimSpace(query)
|
||||||
if query == "" {
|
if query == "" {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -268,6 +269,47 @@ func TestPgSQLAdapterRawExecRecordsMetric(t *testing.T) {
|
|||||||
assert.Equal(t, "orders", calls[0].table)
|
assert.Equal(t, "orders", calls[0].table)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPgSQLAdapterRawExecUsesSQLAsEntityWhenTargetUnknown(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
provider := &capturingMetricsProvider{}
|
||||||
|
prev := metrics.GetProvider()
|
||||||
|
metrics.SetProvider(provider)
|
||||||
|
defer metrics.SetProvider(prev)
|
||||||
|
|
||||||
|
query := `select core.c_setuserid($1)`
|
||||||
|
mock.ExpectExec(`select core\.c_setuserid\(\$1\)`).
|
||||||
|
WithArgs(42).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
_, err = adapter.Exec(context.Background(), query, 42)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
calls := provider.snapshot()
|
||||||
|
require.Len(t, calls, 1)
|
||||||
|
assert.Equal(t, "SELECT", calls[0].operation)
|
||||||
|
assert.Equal(t, "default", calls[0].schema)
|
||||||
|
assert.Equal(t, "select core.c_setuserid(?)", calls[0].entity)
|
||||||
|
assert.Equal(t, "unknown", calls[0].table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFallbackMetricEntityFromQuerySanitizesAndTruncates(t *testing.T) {
|
||||||
|
entity := fallbackMetricEntityFromQuery(" \n SELECT some_function(1, 'abc', $2, ?, :name, @p1, true, null) \t ")
|
||||||
|
assert.Equal(t, "SELECT some_function(?, ?, ?, ?, ?, ?, true, null)", entity)
|
||||||
|
|
||||||
|
entity = fallbackMetricEntityFromQuery("SELECT price::numeric, id FROM logs WHERE code = -42")
|
||||||
|
assert.Equal(t, "SELECT price::numeric, id FROM logs WHERE code = ?", entity)
|
||||||
|
|
||||||
|
longQuery := "SELECT " + strings.Repeat("x", maxMetricFallbackEntityLength)
|
||||||
|
entity = fallbackMetricEntityFromQuery(longQuery)
|
||||||
|
assert.Len(t, entity, maxMetricFallbackEntityLength)
|
||||||
|
assert.True(t, strings.HasSuffix(entity, "..."))
|
||||||
|
}
|
||||||
|
|
||||||
func TestBunAdapterRecordsEntityAndTableMetrics(t *testing.T) {
|
func TestBunAdapterRecordsEntityAndTableMetrics(t *testing.T) {
|
||||||
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
|
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -739,7 +739,7 @@ func (h *Handler) mergeQueryParams(r *http.Request, sqlquery string, variables m
|
|||||||
colval = strings.ReplaceAll(colval, "\\", "\\\\")
|
colval = strings.ReplaceAll(colval, "\\", "\\\\")
|
||||||
colval = strings.ReplaceAll(colval, "'", "''")
|
colval = strings.ReplaceAll(colval, "'", "''")
|
||||||
if 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" {
|
} else if val == "" || val == "0" {
|
||||||
// For empty/zero values, treat as literal 0 or empty string with quotes
|
// 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-", "")
|
colname := strings.ReplaceAll(k, "x-searchfilter-", "")
|
||||||
sval := strings.ReplaceAll(val, "'", "")
|
sval := strings.ReplaceAll(val, "'", "")
|
||||||
if sval != "" {
|
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")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ func (h *Handler) ApplyFilters(sqlQuery string, params *RequestParameters) strin
|
|||||||
for colName, value := range params.SearchFilters {
|
for colName, value := range params.SearchFilters {
|
||||||
sval := strings.ReplaceAll(value, "'", "")
|
sval := strings.ReplaceAll(value, "'", "")
|
||||||
if sval != "" {
|
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)
|
sqlQuery = sqlQryWhere(sqlQuery, condition)
|
||||||
logger.Debug("Applied search filter: %s", condition)
|
logger.Debug("Applied search filter: %s", condition)
|
||||||
}
|
}
|
||||||
@@ -307,11 +307,11 @@ func (h *Handler) buildFilterCondition(colName string, op FilterOperator) string
|
|||||||
|
|
||||||
switch operator {
|
switch operator {
|
||||||
case "contains", "contain", "like":
|
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":
|
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":
|
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", "=":
|
case "equals", "eq", "=":
|
||||||
if IsNumeric(value) {
|
if IsNumeric(value) {
|
||||||
return fmt.Sprintf("%s = %s", safCol, ValidSQL(value, "colvalue"))
|
return fmt.Sprintf("%s = %s", safCol, ValidSQL(value, "colvalue"))
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ func TestBuildFilterCondition(t *testing.T) {
|
|||||||
Value: "test",
|
Value: "test",
|
||||||
Logic: "AND",
|
Logic: "AND",
|
||||||
},
|
},
|
||||||
expected: "description ILIKE '%test%'",
|
expected: "CAST(description AS TEXT) ILIKE '%test%'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Starts with operator",
|
name: "Starts with operator",
|
||||||
@@ -284,7 +284,7 @@ func TestBuildFilterCondition(t *testing.T) {
|
|||||||
Value: "john",
|
Value: "john",
|
||||||
Logic: "AND",
|
Logic: "AND",
|
||||||
},
|
},
|
||||||
expected: "name ILIKE 'john%'",
|
expected: "CAST(name AS TEXT) ILIKE 'john%'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ends with operator",
|
name: "Ends with operator",
|
||||||
@@ -294,7 +294,7 @@ func TestBuildFilterCondition(t *testing.T) {
|
|||||||
Value: "@example.com",
|
Value: "@example.com",
|
||||||
Logic: "AND",
|
Logic: "AND",
|
||||||
},
|
},
|
||||||
expected: "email ILIKE '%@example.com'",
|
expected: "CAST(email AS TEXT) ILIKE '%@example.com'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Between operator",
|
name: "Between operator",
|
||||||
|
|||||||
@@ -702,8 +702,13 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
|
|||||||
if hookCtx.Options != nil {
|
if hookCtx.Options != nil {
|
||||||
// Apply filters
|
// Apply filters
|
||||||
for _, filter := range hookCtx.Options.Filters {
|
for _, filter := range hookCtx.Options.Filters {
|
||||||
|
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)
|
query = query.Where(fmt.Sprintf("%s %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
for _, sort := range hookCtx.Options.Sort {
|
for _, sort := range hookCtx.Options.Sort {
|
||||||
@@ -743,9 +748,14 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
|
|||||||
countQuery := h.db.NewSelect().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
countQuery := h.db.NewSelect().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
||||||
if hookCtx.Options != nil {
|
if hookCtx.Options != nil {
|
||||||
for _, filter := range hookCtx.Options.Filters {
|
for _, filter := range hookCtx.Options.Filters {
|
||||||
|
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)
|
countQuery = countQuery.Where(fmt.Sprintf("%s %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
count, _ := countQuery.Count(hookCtx.Context)
|
count, _ := countQuery.Count(hookCtx.Context)
|
||||||
metadata["total"] = count
|
metadata["total"] = count
|
||||||
metadata["count"] = reflection.Len(hookCtx.ModelPtr)
|
metadata["count"] = reflection.Len(hookCtx.ModelPtr)
|
||||||
|
|||||||
@@ -735,9 +735,9 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (condition st
|
|||||||
case "lte", "<=":
|
case "lte", "<=":
|
||||||
return fmt.Sprintf("%s <= ?", filter.Column), []interface{}{filter.Value}
|
return fmt.Sprintf("%s <= ?", filter.Column), []interface{}{filter.Value}
|
||||||
case "like":
|
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":
|
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":
|
case "in":
|
||||||
condition, args := common.BuildInCondition(filter.Column, filter.Value)
|
condition, args := common.BuildInCondition(filter.Column, filter.Value)
|
||||||
return condition, args
|
return condition, args
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestBuildFilterCondition(t *testing.T) {
|
|||||||
Operator: "like",
|
Operator: "like",
|
||||||
Value: "%@example.com",
|
Value: "%@example.com",
|
||||||
},
|
},
|
||||||
expectedCondition: "email LIKE ?",
|
expectedCondition: "CAST(email AS TEXT) LIKE ?",
|
||||||
expectedArgsCount: 1,
|
expectedArgsCount: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1545,10 +1545,10 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionStr
|
|||||||
condition = fmt.Sprintf("%s <= ?", filter.Column)
|
condition = fmt.Sprintf("%s <= ?", filter.Column)
|
||||||
args = []interface{}{filter.Value}
|
args = []interface{}{filter.Value}
|
||||||
case "like":
|
case "like":
|
||||||
condition = fmt.Sprintf("%s LIKE ?", filter.Column)
|
condition = fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", filter.Column)
|
||||||
args = []interface{}{filter.Value}
|
args = []interface{}{filter.Value}
|
||||||
case "ilike":
|
case "ilike":
|
||||||
condition = fmt.Sprintf("%s ILIKE ?", filter.Column)
|
condition = fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", filter.Column)
|
||||||
args = []interface{}{filter.Value}
|
args = []interface{}{filter.Value}
|
||||||
case "in":
|
case "in":
|
||||||
condition, args = common.BuildInCondition(filter.Column, filter.Value)
|
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)
|
condition = fmt.Sprintf("%s <= ?", filter.Column)
|
||||||
args = []interface{}{filter.Value}
|
args = []interface{}{filter.Value}
|
||||||
case "like":
|
case "like":
|
||||||
condition = fmt.Sprintf("%s LIKE ?", filter.Column)
|
condition = fmt.Sprintf("CAST(%s AS TEXT) LIKE ?", filter.Column)
|
||||||
args = []interface{}{filter.Value}
|
args = []interface{}{filter.Value}
|
||||||
case "ilike":
|
case "ilike":
|
||||||
condition = fmt.Sprintf("%s ILIKE ?", filter.Column)
|
condition = fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", filter.Column)
|
||||||
args = []interface{}{filter.Value}
|
args = []interface{}{filter.Value}
|
||||||
case "in":
|
case "in":
|
||||||
condition, args = common.BuildInCondition(filter.Column, filter.Value)
|
condition, args = common.BuildInCondition(filter.Column, filter.Value)
|
||||||
|
|||||||
@@ -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 {
|
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
|
// 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
|
// Apply casting to text if needed for non-numeric columns or non-numeric values
|
||||||
if needsCast {
|
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
|
// 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":
|
case "lte", "less_than_equals", "le":
|
||||||
return applyWhere(fmt.Sprintf("%s <= ?", qualifiedColumn), filter.Value)
|
return applyWhere(fmt.Sprintf("%s <= ?", qualifiedColumn), filter.Value)
|
||||||
case "like":
|
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":
|
case "ilike":
|
||||||
// Use ILIKE for case-insensitive search (PostgreSQL)
|
// Always cast to TEXT for LIKE/ILIKE to support date/time/timestamp columns
|
||||||
// Column is already cast to TEXT if needed
|
return applyWhere(fmt.Sprintf("CAST(%s AS TEXT) ILIKE ?", rawQualifiedColumn), filter.Value)
|
||||||
return applyWhere(fmt.Sprintf("%s ILIKE ?", qualifiedColumn), filter.Value)
|
|
||||||
case "in":
|
case "in":
|
||||||
cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value)
|
cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value)
|
||||||
if cond == "" {
|
if cond == "" {
|
||||||
@@ -2203,11 +2204,16 @@ func (h *Handler) applyOrFilterGroup(query common.SelectQuery, filters []*common
|
|||||||
|
|
||||||
for i, filter := range filters {
|
for i, filter := range filters {
|
||||||
// Qualify the column name with table name if not already qualified
|
// 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
|
||||||
|
|
||||||
|
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
|
// 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)", rawQualifiedColumn)
|
||||||
qualifiedColumn = fmt.Sprintf("CAST(%s AS TEXT)", qualifiedColumn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the condition based on operator
|
// Build the condition based on operator
|
||||||
|
|||||||
@@ -807,6 +807,11 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionStr
|
|||||||
cond, args := common.BuildInCondition(filter.Column, filter.Value)
|
cond, args := common.BuildInCondition(filter.Column, filter.Value)
|
||||||
return cond, args
|
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)
|
operatorSQL := h.getOperatorSQL(filter.Operator)
|
||||||
return fmt.Sprintf("%s %s ?", filter.Column, operatorSQL), []interface{}{filter.Value}
|
return fmt.Sprintf("%s %s ?", filter.Column, operatorSQL), []interface{}{filter.Value}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user