mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-06-28 07:47:39 +00:00
fix(sql_helpers): enhance splitByAND to handle BETWEEN and quotes
* Add support for BETWEEN-aware AND detection * Ensure AND inside single-quoted strings does not cause splits * Update tests to cover new BETWEEN and quote scenarios
This commit is contained in:
+42
-17
@@ -446,18 +446,36 @@ func containsTopLevelOR(clause string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// splitByAND splits a WHERE clause by AND operators (case-insensitive)
|
||||
// This is parenthesis-aware and won't split on AND operators inside subqueries
|
||||
// splitByAND splits a WHERE clause by AND operators (case-insensitive).
|
||||
// It is parenthesis-aware (won't split inside subqueries), quote-aware
|
||||
// (won't split on AND inside single-quoted strings), and BETWEEN-aware
|
||||
// (won't split on the AND that separates the two operands of BETWEEN x AND y).
|
||||
func splitByAND(where string) []string {
|
||||
conditions := []string{}
|
||||
currentCondition := strings.Builder{}
|
||||
depth := 0 // Track parenthesis depth
|
||||
depth := 0 // parenthesis nesting depth
|
||||
inSingleQuote := false
|
||||
afterBetween := false // true after seeing BETWEEN at depth 0; next AND belongs to it
|
||||
i := 0
|
||||
|
||||
for i < len(where) {
|
||||
ch := where[i]
|
||||
|
||||
// Track parenthesis depth
|
||||
// Track single-quote state so we never split on AND inside string literals.
|
||||
if ch == '\'' {
|
||||
inSingleQuote = !inSingleQuote
|
||||
currentCondition.WriteByte(ch)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if inSingleQuote {
|
||||
currentCondition.WriteByte(ch)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Track parenthesis depth (outside quotes only).
|
||||
if ch == '(' {
|
||||
depth++
|
||||
currentCondition.WriteByte(ch)
|
||||
@@ -470,32 +488,39 @@ func splitByAND(where string) []string {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only look for AND operators at depth 0 (not inside parentheses)
|
||||
// All keyword checks only apply at depth 0 (not inside subqueries).
|
||||
if depth == 0 {
|
||||
// Check if we're at an AND operator (case-insensitive)
|
||||
// We need at least " AND " (5 chars) or " and " (5 chars)
|
||||
if i+5 <= len(where) {
|
||||
substring := where[i : i+5]
|
||||
lowerSubstring := strings.ToLower(substring)
|
||||
// Detect " BETWEEN " (9 chars, case-insensitive) so the very next
|
||||
// top-level AND is recognised as part of the BETWEEN syntax.
|
||||
if i+9 <= len(where) && strings.ToLower(where[i:i+9]) == " between " {
|
||||
afterBetween = true
|
||||
currentCondition.WriteString(where[i : i+9])
|
||||
i += 9
|
||||
continue
|
||||
}
|
||||
|
||||
if lowerSubstring == " and " {
|
||||
// Found an AND operator at the top level
|
||||
// Add the current condition to the list
|
||||
// Detect " AND " (5 chars, case-insensitive).
|
||||
if i+5 <= len(where) && strings.ToLower(where[i:i+5]) == " and " {
|
||||
if afterBetween {
|
||||
// This AND closes a BETWEEN expression — do NOT split.
|
||||
afterBetween = false
|
||||
currentCondition.WriteString(where[i : i+5])
|
||||
i += 5
|
||||
continue
|
||||
}
|
||||
// Regular conjunction — split here.
|
||||
conditions = append(conditions, currentCondition.String())
|
||||
currentCondition.Reset()
|
||||
// Skip past the AND operator
|
||||
i += 5
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not an AND operator or we're inside parentheses, just add the character
|
||||
currentCondition.WriteByte(ch)
|
||||
i++
|
||||
}
|
||||
|
||||
// Add the last condition
|
||||
// Add the last condition.
|
||||
if currentCondition.Len() > 0 {
|
||||
conditions = append(conditions, currentCondition.String())
|
||||
}
|
||||
|
||||
@@ -520,6 +520,38 @@ func TestSplitByAND(t *testing.T) {
|
||||
input: "a = 1 AND b = 2 AND c = 3 and (select s from generate_series(1,10) s where s < 10 and s > 0 offset 2 limit 1) = 3",
|
||||
expected: []string{"a = 1", "b = 2", "c = 3", "(select s from generate_series(1,10) s where s < 10 and s > 0 offset 2 limit 1) = 3"},
|
||||
},
|
||||
// BETWEEN-aware cases: the AND inside BETWEEN x AND y must not cause a split.
|
||||
{
|
||||
name: "BETWEEN does not split on its AND",
|
||||
input: "col between '2025-08-31' and '1970-01-01'",
|
||||
expected: []string{"col between '2025-08-31' and '1970-01-01'"},
|
||||
},
|
||||
{
|
||||
name: "BETWEEN uppercase AND",
|
||||
input: "col BETWEEN '2025-08-31' AND '1970-01-01'",
|
||||
expected: []string{"col BETWEEN '2025-08-31' AND '1970-01-01'"},
|
||||
},
|
||||
{
|
||||
name: "BETWEEN followed by a regular AND conjunction",
|
||||
input: "col between 1 and 5 and other = 'x'",
|
||||
expected: []string{"col between 1 and 5", "other = 'x'"},
|
||||
},
|
||||
{
|
||||
name: "two BETWEEN conditions joined by AND",
|
||||
input: "col1 between 1 and 5 and col2 between 10 and 20",
|
||||
expected: []string{"col1 between 1 and 5", "col2 between 10 and 20"},
|
||||
},
|
||||
{
|
||||
name: "complex OR block with multiple BETWEENs (real-world case)",
|
||||
input: "tbl.applicationdate between '2025-08-31' and '1970-01-01'\n or tbl.capturedate between '2025-08-31' and '1970-01-01'\n or tbl.startdate between '2025-08-31' AND '1970-01-01'",
|
||||
expected: []string{"tbl.applicationdate between '2025-08-31' and '1970-01-01'\n or tbl.capturedate between '2025-08-31' and '1970-01-01'\n or tbl.startdate between '2025-08-31' AND '1970-01-01'"},
|
||||
},
|
||||
// Quote-aware cases: AND inside a string literal must not split.
|
||||
{
|
||||
name: "AND inside single-quoted string is not a split point",
|
||||
input: "comment = 'this and that' and status = 'active'",
|
||||
expected: []string{"comment = 'this and that'", "status = 'active'"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -917,6 +949,25 @@ where: "(true AND status = 'active')",
|
||||
tableName: "unregistered_table",
|
||||
expected: "(true AND unregistered_table.status = 'active')",
|
||||
},
|
||||
// BETWEEN regression: date literals inside BETWEEN must not be prefixed as columns.
|
||||
{
|
||||
name: "BETWEEN date range - second date must not be prefixed",
|
||||
where: "applicationdate between '2025-08-31' and '1970-01-01'",
|
||||
tableName: "unregistered_table",
|
||||
expected: "unregistered_table.applicationdate between '2025-08-31' and '1970-01-01'",
|
||||
},
|
||||
{
|
||||
name: "Already-prefixed BETWEEN column - unchanged",
|
||||
where: `"v_webui_clients".applicationdate between '2025-08-31' and '1970-01-01'`,
|
||||
tableName: "v_webui_clients",
|
||||
expected: `"v_webui_clients".applicationdate between '2025-08-31' and '1970-01-01'`,
|
||||
},
|
||||
{
|
||||
name: "Complex OR block with multiple BETWEENs - date values must not be prefixed",
|
||||
where: `("v_webui_clients".applicationdate between '2025-08-31' and '1970-01-01' or "v_webui_clients".clientcapturedate between '2025-08-31' and '1970-01-01' or "v_webui_clients".startdate between '2025-08-31' AND '1970-01-01')`,
|
||||
tableName: "v_webui_clients",
|
||||
expected: `("v_webui_clients".applicationdate between '2025-08-31' and '1970-01-01' or "v_webui_clients".clientcapturedate between '2025-08-31' and '1970-01-01' or "v_webui_clients".startdate between '2025-08-31' AND '1970-01-01')`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user