THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

fix(global-window): 编程式配置的复合/相等 TRIGGER WHEN 谓词编译崩溃——buildTrigger 加 normalizeTriggerPredicate(AND→&&/OR→||/单=→==,跳过引号字面量)

This commit is contained in:
rulego-team
2026-07-08 13:02:50 +08:00
parent 69b19f5f17
commit bad4e643b9
2 changed files with 228 additions and 0 deletions
+104
View File
@@ -206,6 +206,12 @@ type triggerAggRef struct {
// trigger-only ones), rewrites the predicate to use placeholders, and compiles
// it with the condition engine.
func (gw *GlobalWindow) buildTrigger(predicate string) error {
// Normalize SQL logical/equality operators to the expr-lang form the
// condition engine compiles. The rsql parser already lowers AND/OR/=, but a
// programmatic WindowConfig (rulego component config, tests) may pass them
// verbatim; without this, "a AND b" or "x = 3" fails to compile. Quoted
// literals are left untouched.
predicate = normalizeTriggerPredicate(predicate)
refs := gw.findAggCalls(predicate)
rewritten := predicate
gw.triggerSpecs = nil
@@ -272,6 +278,104 @@ func (gw *GlobalWindow) findAggCalls(predicate string) []triggerAggRefWithMatch
return out
}
// normalizeTriggerPredicate rewrites SQL logical/equality operators to expr-lang
// form: AND->&&, OR->||, and a bare '=' (not already part of ==, >=, <=, !=)
// becomes '=='. String literals and backtick identifiers are skipped so
// operator-like text inside them is preserved. Mirrors the lowering the rsql
// parser applies, so SQL-sourced predicates are unaffected.
func normalizeTriggerPredicate(s string) string {
var b strings.Builder
b.Grow(len(s) + 4)
inQuote := byte(0)
prev := byte(0)
n := len(s)
for i := 0; i < n; i++ {
c := s[i]
if inQuote != 0 {
b.WriteByte(c)
if c == inQuote {
inQuote = 0
}
prev = c
continue
}
switch c {
case '\'', '"', '`':
inQuote = c
b.WriteByte(c)
prev = c
case '=':
next := byte(0)
if i+1 < n {
next = s[i+1]
}
// Only a standalone '=' needs doubling; leave ==, >=, <=, != as-is.
if !isOpChar(prev) && !isOpChar(next) {
b.WriteString("==")
prev = '='
} else {
b.WriteByte(c)
prev = c
}
default:
if (c == 'A' || c == 'a') && hasWordAt(s, i, "and") && !isWordChar(prev) {
next := byte(0)
if i+3 < n {
next = s[i+3]
}
if !isWordChar(next) {
b.WriteString("&&")
i += 2
prev = '&'
continue
}
}
if (c == 'O' || c == 'o') && hasWordAt(s, i, "or") && !isWordChar(prev) {
next := byte(0)
if i+2 < n {
next = s[i+2]
}
if !isWordChar(next) {
b.WriteString("||")
i += 1
prev = '|'
continue
}
}
b.WriteByte(c)
prev = c
}
}
return b.String()
}
func hasWordAt(s string, i int, word string) bool {
if i+len(word) > len(s) {
return false
}
for j := 0; j < len(word); j++ {
if toLower(s[i+j]) != word[j] {
return false
}
}
return true
}
func toLower(c byte) byte {
if c >= 'A' && c <= 'Z' {
return c + 32
}
return c
}
func isWordChar(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
}
func isOpChar(c byte) bool {
return c == '=' || c == '>' || c == '<' || c == '!'
}
// findOutputSpec returns the index of an output aggregate matching type+field,
// or -1. count(*) is matched by inputField=="*".
func (gw *GlobalWindow) findOutputSpec(aggType aggregator.AggregateType, inputField string) int {
Internal Server Error - Gitea: Git with a cup of tea
500 Internal Server Error

Gitea Version: 1.28.0+dev-211-ga30d865b78