From 490ae37c6deba83adadcab58f3c37756703d3ab7 Mon Sep 17 00:00:00 2001 From: Hein Date: Wed, 10 Dec 2025 11:48:03 +0200 Subject: [PATCH] Fixed bugs in extractTableAndColumn --- pkg/common/sql_helpers.go | 40 ++++++++++++++++++++++++++- pkg/common/sql_helpers_test.go | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/pkg/common/sql_helpers.go b/pkg/common/sql_helpers.go index e110750..8d62168 100644 --- a/pkg/common/sql_helpers.go +++ b/pkg/common/sql_helpers.go @@ -430,7 +430,45 @@ func extractTableAndColumn(cond string) (table string, column string) { // Remove any quotes columnRef = strings.Trim(columnRef, "`\"'") - // Check if it contains a dot (qualified reference) + // Check if there's a function call (contains opening parenthesis) + openParenIdx := strings.Index(columnRef, "(") + + if openParenIdx >= 0 { + // There's a function call - find the FIRST dot after the opening paren + // This handles cases like: ifblnk(users.status, orders.status) - extracts users.status + dotIdx := strings.Index(columnRef[openParenIdx:], ".") + if dotIdx > 0 { + dotIdx += openParenIdx // Adjust to absolute position + + // Extract table name (between paren and dot) + // Find the last opening paren before this dot + lastOpenParen := strings.LastIndex(columnRef[:dotIdx], "(") + table = columnRef[lastOpenParen+1 : dotIdx] + + // Find the column name - it ends at comma, closing paren, whitespace, or end of string + columnStart := dotIdx + 1 + columnEnd := len(columnRef) + + for i := columnStart; i < len(columnRef); i++ { + ch := columnRef[i] + if ch == ',' || ch == ')' || ch == ' ' || ch == '\t' { + columnEnd = i + break + } + } + + column = columnRef[columnStart:columnEnd] + + // Remove quotes from table and column if present + table = strings.Trim(table, "`\"'") + column = strings.Trim(column, "`\"'") + + return table, column + } + } + + // No function call - check if it contains a dot (qualified reference) + // Use LastIndex to handle schema.table.column properly if dotIdx := strings.LastIndex(columnRef, "."); dotIdx > 0 { table = columnRef[:dotIdx] column = columnRef[dotIdx+1:] diff --git a/pkg/common/sql_helpers_test.go b/pkg/common/sql_helpers_test.go index 0add7f1..2874c19 100644 --- a/pkg/common/sql_helpers_test.go +++ b/pkg/common/sql_helpers_test.go @@ -286,6 +286,48 @@ func TestExtractTableAndColumn(t *testing.T) { expectedTable: "", expectedCol: "", }, + { + name: "function call with table.column - ifblnk", + input: "ifblnk(users.status,0) in (1,2,3,4)", + expectedTable: "users", + expectedCol: "status", + }, + { + name: "function call with table.column - coalesce", + input: "coalesce(users.age, 0) = 25", + expectedTable: "users", + expectedCol: "age", + }, + { + name: "nested function calls", + input: "upper(trim(users.name)) = 'JOHN'", + expectedTable: "users", + expectedCol: "name", + }, + { + name: "function with multiple args and table.column", + input: "substring(users.email, 1, 5) = 'admin'", + expectedTable: "users", + expectedCol: "email", + }, + { + name: "cast function with table.column", + input: "cast(orders.total as decimal) > 100", + expectedTable: "orders", + expectedCol: "total", + }, + { + name: "complex nested functions", + input: "coalesce(nullif(users.status, ''), 'default') = 'active'", + expectedTable: "users", + expectedCol: "status", + }, + { + name: "function with multiple table.column refs (extracts first)", + input: "greatest(users.created_at, users.updated_at) > '2024-01-01'", + expectedTable: "users", + expectedCol: "created_at", + }, } for _, tt := range tests { @@ -352,6 +394,14 @@ func TestSanitizeWhereClauseWithPreloads(t *testing.T) { }, expected: "users.status = 'active' AND Department.name = 'Engineering'", }, + + { + name: "Function Call with correct table prefix - unchanged", + where: "ifblnk(users.status,0) in (1,2,3,4)", + tableName: "users", + options: nil, + expected: "ifblnk(users.status,0) in (1,2,3,4)", + }, { name: "no options provided - works as before", where: "wrong_table.status = 'active'",