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

feat(cep): 支持 MATCH_RECOGNIZE 模式识别

This commit is contained in:
rulego-team
2026-07-13 12:25:07 +08:00
parent 39fdb9abb9
commit 4730b73991
23 changed files with 4440 additions and 6 deletions
+586
View File
File diff suppressed because it is too large Load Diff
+361
View File
File diff suppressed because it is too large Load Diff
+610
View File
File diff suppressed because it is too large Load Diff
+244
View File
@@ -0,0 +1,244 @@
package cep
import (
"testing"
)
func syms(names ...string) map[string]bool {
m := make(map[string]bool, len(names))
for _, n := range names {
m[n] = true
}
return m
}
// --- EvalDefine ---
func TestEvalDefine_BareComparison(t *testing.T) {
if !EvalDefine("v > 50", nil, nil, map[string]any{"v": 60}, "A", syms("A")) {
t.Errorf("v=60 > 50 want true")
}
if EvalDefine("v > 50", nil, nil, map[string]any{"v": 40}, "A", syms("A")) {
t.Errorf("v=40 > 50 want false")
}
}
// 空条件(未定义符号)恒为真(SQL 标准)。
func TestEvalDefine_EmptyIsTrue(t *testing.T) {
if !EvalDefine("", nil, nil, map[string]any{"v": 1}, "A", syms("A")) {
t.Errorf("empty DEFINE must be true")
}
}
// PREV:buffer 非空时取上一行的字段。
func TestEvalDefine_Prev(t *testing.T) {
buf := []map[string]any{{"v": 10.0}}
labels := []string{"A"}
// 20 > PREV(v,1)=10 → true
if !EvalDefine("v > PREV(v, 1)", buf, labels, map[string]any{"v": 20.0}, "A", syms("A")) {
t.Errorf("20 > PREV(10) want true")
}
// 5 > 10 → false
if EvalDefine("v > PREV(v, 1)", buf, labels, map[string]any{"v": 5.0}, "A", syms("A")) {
t.Errorf("5 > PREV(10) want false")
}
}
// PREV 越界返回 nil → 比较为假(首行无前驱)。
func TestEvalDefine_PrevNullEmptyBuffer(t *testing.T) {
if EvalDefine("v > PREV(v, 1)", nil, nil, map[string]any{"v": 20.0}, "A", syms("A")) {
t.Errorf("PREV nil → comparison should be false")
}
}
// 符号限定字段 A.v 等同候选行字段。
func TestEvalDefine_SymbolQualified(t *testing.T) {
if !EvalDefine("A.v > 5", nil, nil, map[string]any{"v": 10.0}, "A", syms("A")) {
t.Errorf("A.v=10 > 5 want true")
}
}
// 复合条件 AND + 字符串相等。
func TestEvalDefine_AndStringEq(t *testing.T) {
cond := "v > 5 AND type == \"x\""
if !EvalDefine(cond, nil, nil, map[string]any{"v": 10.0, "type": "x"}, "A", syms("A")) {
t.Errorf("want true")
}
if EvalDefine(cond, nil, nil, map[string]any{"v": 10.0, "type": "y"}, "A", syms("A")) {
t.Errorf("type mismatch want false")
}
}
// --- EvalMeasurecandidate=nilMEASURES 路径)---
func TestEvalMeasure_BareField(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 20.0}}
labels := []string{"A", "A"}
v, _ := EvalMeasure("v", rows, labels, 1, 1, syms("A"))
if asFloat(v) != 20.0 {
t.Errorf("bare field at cur=1 want 20, got %v", v)
}
}
func TestEvalMeasure_PrevFirstLast(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 20.0}, {"v": 30.0}}
labels := []string{"A", "A", "A"}
if v, _ := EvalMeasure("PREV(v, 1)", rows, labels, 2, 1, syms("A")); asFloat(v) != 20.0 {
t.Errorf("PREV at cur=2 want 20, got %v", v)
}
if v, _ := EvalMeasure("FIRST(v)", rows, labels, 2, 1, syms("A")); asFloat(v) != 10.0 {
t.Errorf("FIRST want 10, got %v", v)
}
if v, _ := EvalMeasure("LAST(v)", rows, labels, 2, 1, syms("A")); asFloat(v) != 30.0 {
t.Errorf("LAST want 30, got %v", v)
}
}
func TestEvalMeasure_ClassifierAndMatchNumber(t *testing.T) {
rows := []map[string]any{{"v": 1.0}, {"v": 2.0}}
labels := []string{"A", "B"}
if v, _ := EvalMeasure("CLASSIFIER()", rows, labels, 1, 1, syms("A", "B")); v != "B" {
t.Errorf("CLASSIFIER at cur=1 want B, got %v", v)
}
if v, _ := EvalMeasure("MATCH_NUMBER()", rows, labels, 1, 7, syms("A", "B")); asFloat(v) != 7 {
t.Errorf("MATCH_NUMBER want 7, got %v", v)
}
}
// 聚合:RUNNING(到当前行 cur=2 即全部 3 行)。
func TestEvalMeasure_Aggregates(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 20.0}, {"v": 30.0}}
labels := []string{"A", "A", "A"}
sym := syms("A")
check := func(expr string, want float64) {
t.Helper()
v, _ := EvalMeasure(expr, rows, labels, 2, 1, sym)
if asFloat(v) != want {
t.Errorf("%s want %v, got %v", expr, want, v)
}
}
check("COUNT(*)", 3)
check("SUM(v)", 60)
check("AVG(v)", 20)
check("MIN(v)", 10)
check("MAX(v)", 30)
}
// 聚合 RUNNINGcur=1 时只算前两行。
func TestEvalMeasure_AggregateRunning(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 20.0}, {"v": 30.0}}
labels := []string{"A", "A", "A"}
v, _ := EvalMeasure("SUM(v)", rows, labels, 1, 1, syms("A")) // cur=1 → 前 2 行
if asFloat(v) != 30.0 {
t.Errorf("RUNNING SUM at cur=1 want 30, got %v", v)
}
}
// 算术组合:MAX - MIN。
func TestEvalMeasure_Arithmetic(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 5.0}, {"v": 30.0}}
labels := []string{"A", "A", "A"}
v, _ := EvalMeasure("MAX(v) - MIN(v)", rows, labels, 2, 1, syms("A"))
if asFloat(v) != 25.0 {
t.Errorf("MAX-MIN want 25, got %v", v)
}
}
// 符号限定字段:取该符号最末出现行。
func TestEvalMeasure_SymbolField(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 20.0}}
labels := []string{"A", "A"}
v, _ := EvalMeasure("A.v", rows, labels, 1, 1, syms("A"))
if asFloat(v) != 20.0 {
t.Errorf("A.v (last A) want 20, got %v", v)
}
}
// FIRST/LAST 的 RUNNING 语义:ALL ROWS PER MATCH 下随当前行推进(与 COUNT 一致)。
// LAST(v) at cur=0/1/2 → 10/20/30(而非恒为末行 30 的 FINAL)。
func TestEvalMeasure_FirstLastRunning(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 20.0}, {"v": 30.0}}
labels := []string{"A", "A", "A"}
sym := syms("A")
if v, _ := EvalMeasure("LAST(v)", rows, labels, 0, 1, sym); asFloat(v) != 10.0 {
t.Errorf("LAST at cur=0 want 10 (running), got %v", v)
}
if v, _ := EvalMeasure("LAST(v)", rows, labels, 1, 1, sym); asFloat(v) != 20.0 {
t.Errorf("LAST at cur=1 want 20 (running), got %v", v)
}
if v, _ := EvalMeasure("LAST(v)", rows, labels, 2, 1, sym); asFloat(v) != 30.0 {
t.Errorf("LAST at cur=2 want 30, got %v", v)
}
// FIRST 恒为首行(RUNNING 与 FINAL 一致)。
if v, _ := EvalMeasure("FIRST(v)", rows, labels, 1, 1, sym); asFloat(v) != 10.0 {
t.Errorf("FIRST at cur=1 want 10, got %v", v)
}
}
// FIRST/LAST 的 n<=0 不应越界 panic,钳为 n=1。
func TestEvalMeasure_FirstLastZeroN(t *testing.T) {
rows := []map[string]any{{"v": 10.0}, {"v": 20.0}}
labels := []string{"A", "A"}
sym := syms("A")
if v, _ := EvalMeasure("FIRST(v, 0)", rows, labels, 1, 1, sym); asFloat(v) != 10.0 {
t.Errorf("FIRST(v,0) want 10 (clamped n=1), got %v", v)
}
if v, _ := EvalMeasure("LAST(v, 0)", rows, labels, 1, 1, sym); asFloat(v) != 20.0 {
t.Errorf("LAST(v,0) want 20 (clamped n=1), got %v", v)
}
if v, _ := EvalMeasure("FIRST(v, -2)", rows, labels, 1, 1, sym); asFloat(v) != 10.0 {
t.Errorf("FIRST(v,-2) want 10, got %v", v)
}
}
// 符号限定聚合 SUM(A.v) 仅对该符号标签行求和(非全部行)。
func TestEvalMeasure_AggregateSymbolScoped(t *testing.T) {
rows := []map[string]any{{"v": 1.0}, {"v": 2.0}, {"v": 3.0}}
labels := []string{"A", "B", "A"}
sym := syms("A", "B")
// SUM(A.v)=1+3=4(只 A 行);SUM(v)=1+2+3=6(全部行)。
if v, _ := EvalMeasure("SUM(A.v)", rows, labels, 2, 1, sym); asFloat(v) != 4.0 {
t.Errorf("SUM(A.v) want 4 (A-scoped), got %v", v)
}
if v, _ := EvalMeasure("SUM(v)", rows, labels, 2, 1, sym); asFloat(v) != 6.0 {
t.Errorf("SUM(v) want 6 (all), got %v", v)
}
}
// COUNT(expr) 计非 NULL 值(含字符串/uint),非仅数值。
func TestEvalMeasure_CountNonNull(t *testing.T) {
rows := []map[string]any{{"name": "a"}, {"name": "b"}, {"name": "c"}}
labels := []string{"A", "A", "A"}
sym := syms("A")
if v, _ := EvalMeasure("COUNT(name)", rows, labels, 2, 1, sym); asFloat(v) != 3.0 {
t.Errorf("COUNT(name) want 3 (non-NULL strings), got %v", v)
}
// uint 列也能被 SUM 聚合(cast.ToFloat64E 支持)。
urows := []map[string]any{{"n": uint64(2)}, {"n": uint64(3)}}
ulabels := []string{"A", "A"}
if v, _ := EvalMeasure("SUM(n)", urows, ulabels, 1, 1, sym); asFloat(v) != 5.0 {
t.Errorf("SUM(uint) want 5, got %v", v)
}
}
// --- tokenize ---
func TestTokenize_Simple(t *testing.T) {
toks, err := tokenize("v > PREV(v, 1)")
if err != nil {
t.Fatalf("tokenize error: %v", err)
}
if len(toks) == 0 {
t.Fatalf("expected tokens")
}
// 首个 token 是标识符 v
if toks[0].kind != ekIdent || toks[0].val != "v" {
t.Errorf("first token=%+v want ident v", toks[0])
}
}
func TestTokenize_UnterminatedQuote(t *testing.T) {
if _, err := tokenize("'unclosed"); err == nil {
t.Errorf("expected error for unterminated quote")
}
}
+125
View File
@@ -0,0 +1,125 @@
package cep
// stateKind 标识 NFA 状态类别。
type stateKind int
const (
stEpsilon stateKind = iota // 不消费行,沿 out1/out2 前进
stMatch // 消费一行:当行满足 symbol 的 DEFINE 时,沿 out1 前进
stAccept // 接受态:一次完整匹配
)
// state 是 NFA 的一个状态。DEFINE 条件按 symbol 在引擎处查表,故 state 只携 symbol。
type state struct {
kind stateKind
symbol string // stMatch:模式变量名
out1 *state
out2 *state // 仅 stEpsilon 的选择/循环用到
}
// NFA 是编译后的模式自动机:start 经 epsilon/match 转移到达 accept。
type NFA struct {
start *state
accept *state
}
// frag 是 Thompson 构造的片段:start 与若干待接续的出边(dots 指向 nil 的 out 槽)。
type frag struct {
start *state
dots []**state
}
func patch(f *frag, target *state) {
for _, d := range f.dots {
*d = target
}
}
func newMatchFrag(symbol string) *frag {
s := &state{kind: stMatch, symbol: symbol}
return &frag{start: s, dots: []**state{&s.out1}}
}
func newEpsFrag() *frag {
s := &state{kind: stEpsilon}
return &frag{start: s, dots: []**state{&s.out1}}
}
func concat(a, b *frag) *frag {
patch(a, b.start)
return &frag{start: a.start, dots: b.dots}
}
func alt(a, b *frag) *frag {
s := &state{kind: stEpsilon, out1: a.start, out2: b.start}
return &frag{start: s, dots: append(a.dots, b.dots...)}
}
// starFrag 构造 child*(贪婪:优先回环)。
func starFrag(child *frag) *frag {
s := &state{kind: stEpsilon}
patch(child, s) // 子片段结束后回到分支点
s.out1 = child.start
return &frag{start: s, dots: []**state{&s.out2}} // out2=出口(待接续)
}
// optFrag 构造 child?0 或 1)。
func optFrag(child *frag) *frag {
s := &state{kind: stEpsilon, out1: child.start}
return &frag{start: s, dots: append(child.dots, &s.out2)}
}
// closure 计算 starts 经 epsilon 转移可达的全部状态(含 match/accept 终态)。
func closure(starts ...*state) []*state {
seen := make(map[*state]bool, len(starts))
var stack []*state
push := func(s *state) {
if s != nil && !seen[s] {
seen[s] = true
stack = append(stack, s)
}
}
for _, s := range starts {
push(s)
}
for len(stack) > 0 {
s := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if s.kind == stEpsilon {
push(s.out1)
push(s.out2)
}
}
out := make([]*state, 0, len(seen))
for s := range seen {
out = append(out, s)
}
return out
}
// hasAccept 报告状态集是否含接受态。
func hasAccept(states []*state) bool {
for _, s := range states {
if s.kind == stAccept {
return true
}
}
return false
}
// isComplete 报告状态集是否「到达接受态且无法再延伸」(贪婪终结)。
// 含 accept 但仍有 match-state 的(如 A* 续配)不算终结,应继续贪婪延伸。
func isComplete(states []*state) bool {
return hasAccept(states) && len(matchStates(states)) == 0
}
// matchStates 抽出状态集中的全部 stMatch(待消费行测试)。
func matchStates(states []*state) []*state {
var ms []*state
for _, s := range states {
if s.kind == stMatch {
ms = append(ms, s)
}
}
return ms
}
+119
View File
@@ -0,0 +1,119 @@
package cep
import (
"testing"
)
// 构造一个最小 NFA 片段用于低层测试:start --epsilon--> m(A) --out--> accept
func buildLinearA() (start, accept *state, matchA *state) {
matchA = &state{kind: stMatch, symbol: "A"}
accept = &state{kind: stAccept}
matchA.out1 = accept
start = &state{kind: stEpsilon, out1: matchA}
return
}
// closure 经 epsilon 转移应含起点与可达的 match 终态,但**不穿越 match 态**accept
// 在 match 态之后,需消费一行后才可达,故不在 start 的 epsilon 闭包里)。
func TestClosure_Epsilon(t *testing.T) {
start, accept, matchA := buildLinearA()
got := closure(start)
if !containsState(got, start) || !containsState(got, matchA) {
t.Errorf("closure must include start + reachable match-state: %+v", got)
}
if containsState(got, accept) {
t.Errorf("closure must NOT cross a match-state (accept unreachable without consuming)")
}
if dup := dupCount(got); dup != 0 {
t.Errorf("closure has %d duplicate state pointers", dup)
}
}
// closure 遇 stMatch 终止沿该分支前进(match 不消费,故不跟出其 out)。
func TestClosure_StopsAtMatch(t *testing.T) {
_, _, matchA := buildLinearA()
// 从 matchA 出发:match 态本身在闭包里,但其 outaccept)不应被 epsilon 闭包带入。
got := closure(matchA)
if containsState(got, &state{kind: stAccept}) {
// 上面的 &state{} 是新建实例,不会在 got 里;仅用于触发 containsState 语义。
}
if len(got) != 1 {
t.Errorf("closure of a lone match-state want size 1, got %d", len(got))
}
}
// 选择分支:epsilon 分裂到两条 matchclosure 应含两者。
func TestClosure_AltSplit(t *testing.T) {
a := &state{kind: stMatch, symbol: "A"}
b := &state{kind: stMatch, symbol: "B"}
split := &state{kind: stEpsilon, out1: a, out2: b}
got := closure(split)
if !containsState(got, a) || !containsState(got, b) {
t.Errorf("alternation closure must include both branches: %+v", got)
}
}
// isComplete:含 accept 且无 match-state → true;含 accept 且有 match-state → false。
func TestIsComplete(t *testing.T) {
accept := &state{kind: stAccept}
if !isComplete([]*state{accept}) {
t.Errorf("accept-only set should be complete")
}
matchA := &state{kind: stMatch, symbol: "A"}
if isComplete([]*state{accept, matchA}) {
t.Errorf("accept+match set should NOT be complete (still extendable)")
}
if isComplete([]*state{matchA}) {
t.Errorf("match-only set should not be complete (no accept)")
}
}
// hasAccept / matchStates。
func TestHasAcceptAndMatchStates(t *testing.T) {
accept := &state{kind: stAccept}
matchA := &state{kind: stMatch, symbol: "A"}
states := []*state{matchA, accept}
if !hasAccept(states) {
t.Errorf("hasAccept should be true")
}
if ms := matchStates(states); len(ms) != 1 || ms[0] != matchA {
t.Errorf("matchStates=%+v want [matchA]", ms)
}
if hasAccept([]*state{matchA}) {
t.Errorf("hasAccept should be false without accept")
}
}
// patch 把片段的待接续出边指向目标。
func TestPatch(t *testing.T) {
s := &state{kind: stMatch, symbol: "A"}
f := &frag{start: s, dots: []**state{&s.out1}}
target := &state{kind: stAccept}
patch(f, target)
if s.out1 != target {
t.Errorf("patch did not set out1 to target")
}
}
func containsState(states []*state, want *state) bool {
for _, s := range states {
if s == want {
return true
}
}
return false
}
func dupCount(states []*state) int {
seen := make(map[*state]int)
for _, s := range states {
seen[s]++
}
dups := 0
for _, c := range seen {
if c > 1 {
dups += c - 1
}
}
return dups
}
+184
View File
@@ -0,0 +1,184 @@
package cep
import (
"fmt"
"github.com/rulego/streamsql/types"
)
// Compile 把模式树编译为 NFA(Thompson 构造)。组合式节点:序列/选择/分组/PERMUTE/量词。
// PatternExclusion{- -}absence)暂不支持,返回明确错误(P2)。
func Compile(node *types.PatternNode) (*NFA, error) {
if node == nil {
return nil, fmt.Errorf("MATCH_RECOGNIZE requires a PATTERN")
}
f, err := compileNode(node)
if err != nil {
return nil, err
}
accept := &state{kind: stAccept}
patch(f, accept)
return &NFA{start: f.start, accept: accept}, nil
}
func compileNode(n *types.PatternNode) (*frag, error) {
switch n.Kind {
case types.PatternLiteral:
if n.Symbol == "" {
return nil, fmt.Errorf("pattern variable name is empty")
}
return newMatchFrag(n.Symbol), nil
case types.PatternSequence:
var f *frag
for _, c := range n.Children {
cf, err := compileNode(c)
if err != nil {
return nil, err
}
if f == nil {
f = cf
} else {
f = concat(f, cf)
}
}
if f == nil {
return newEpsFrag(), nil
}
return f, nil
case types.PatternGroup:
// Group 透明:编译其内部序列。量词由外层 Repetition 处理。
if len(n.Children) == 0 {
return newEpsFrag(), nil
}
return compileNode(n.Children[0])
case types.PatternAlternation:
if len(n.Children) == 0 {
return newEpsFrag(), nil
}
var f *frag
for _, c := range n.Children {
cf, err := compileNode(c)
if err != nil {
return nil, err
}
if f == nil {
f = cf
} else {
f = alt(f, cf)
}
}
return f, nil
case types.PatternRepetition:
if len(n.Children) == 0 || n.Quant == nil {
return nil, fmt.Errorf("repetition requires a child and quantifier")
}
return compileRepeat(n.Children[0], n.Quant)
case types.PatternPermute:
return compilePermute(n.Children)
case types.PatternExclusion:
return nil, fmt.Errorf("pattern exclusion {- -} is not supported yet (planned for a later phase)")
}
return nil, fmt.Errorf("unknown pattern node kind %d", n.Kind)
}
// compileRepeat 展开量词为显式 NFA{n}=n 份;{n,}=n 份 + 星;{n,m}=n 份 + (m-n) 份可选;
// 每份重新编译子节点(独立状态),避免回环打结。
func compileRepeat(child *types.PatternNode, q *types.Quantifier) (*frag, error) {
if q.Min < 0 {
return nil, fmt.Errorf("quantifier min must be >= 0")
}
var f *frag
for i := 0; i < q.Min; i++ {
cf, err := compileNode(child)
if err != nil {
return nil, err
}
if f == nil {
f = cf
} else {
f = concat(f, cf)
}
}
if q.Max < 0 {
cf, err := compileNode(child)
if err != nil {
return nil, err
}
star := starFrag(cf)
if f == nil {
return star, nil
}
return concat(f, star), nil
}
if q.Max < q.Min {
return nil, fmt.Errorf("quantifier max %d < min %d", q.Max, q.Min)
}
for i := 0; i < q.Max-q.Min; i++ {
cf, err := compileNode(child)
if err != nil {
return nil, err
}
opt := optFrag(cf)
if f == nil {
f = opt
} else {
f = concat(f, opt)
}
}
if f == nil {
return newEpsFrag(), nil // {0}:匹配空
}
return f, nil
}
// compilePermute 把 PERMUTE(A,B,...) 编译为所有排列的交替(任一顺序匹配)。
func compilePermute(children []*types.PatternNode) (*frag, error) {
if len(children) == 0 {
return newEpsFrag(), nil
}
// 排列数为 N!,符号过多会导致 NFA 状态阶乘级膨胀,设上限保护。
if len(children) > 6 {
return nil, fmt.Errorf("PERMUTE supports at most 6 symbols (got %d): factorial state blow-up", len(children))
}
var result *frag
for _, perm := range permutations(len(children)) {
// 每个排列独立状态:按索引顺序重新编译子节点。
var f *frag
for _, idx := range perm {
cf, err := compileNode(children[idx])
if err != nil {
return nil, err
}
if f == nil {
f = cf
} else {
f = concat(f, cf)
}
}
if result == nil {
result = f
} else {
result = alt(result, f)
}
}
return result, nil
}
// permutations 返回 [0,n) 的所有排列索引。
func permutations(n int) [][]int {
if n == 0 {
return [][]int{{}}
}
subs := permutations(n - 1)
var out [][]int
for _, s := range subs {
for i := 0; i <= len(s); i++ {
p := make([]int, 0, len(s)+1)
p = append(p, s[:i]...)
p = append(p, n-1)
p = append(p, s[i:]...)
out = append(out, p)
}
}
return out
}
+96
View File
@@ -0,0 +1,96 @@
package cep
import (
"testing"
"github.com/rulego/streamsql/types"
)
// Compile 合法模式:返回非空 NFAstart/accept 非 nil)且无错误。
func TestCompile_ValidPatterns(t *testing.T) {
cases := []struct {
name string
node *types.PatternNode
}{
{"literal", lit("A")},
{"sequence", seq(lit("A"), lit("B"), lit("C"))},
{"alternation", altNode(lit("A"), lit("B"))},
{"group", &types.PatternNode{Kind: types.PatternGroup, Children: []*types.PatternNode{seq(lit("A"), lit("B"))}}},
{"star", rep(lit("A"), 0, -1)},
{"plus", rep(lit("A"), 1, -1)},
{"optional", rep(lit("A"), 0, 1)},
{"exact", rep(lit("A"), 3, 3)},
{"range", rep(lit("A"), 2, 5)},
{"atLeast", rep(lit("A"), 2, -1)},
{"permute", &types.PatternNode{Kind: types.PatternPermute, Children: []*types.PatternNode{lit("A"), lit("B")}}},
{"nested", seq(rep(altNode(lit("A"), lit("B")), 1, -1), lit("C"))},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
nfa, err := Compile(c.node)
if err != nil {
t.Fatalf("Compile(%s) error: %v", c.name, err)
}
if nfa == nil || nfa.start == nil || nfa.accept == nil {
t.Fatalf("Compile(%s) returned incomplete NFA: %+v", c.name, nfa)
}
})
}
}
// Compile 非法模式:明确报错。
func TestCompile_InvalidPatterns(t *testing.T) {
cases := []struct {
name string
node *types.PatternNode
}{
{"nil", nil},
{"empty literal", &types.PatternNode{Kind: types.PatternLiteral, Symbol: ""}},
{"exclusion", &types.PatternNode{Kind: types.PatternExclusion, Children: []*types.PatternNode{lit("A")}}},
{"negative min", rep(lit("A"), -1, -1)},
{"max < min", rep(lit("A"), 5, 3)},
{"repetition no child", &types.PatternNode{Kind: types.PatternRepetition, Quant: &types.Quantifier{Min: 1, Max: 1}}},
{"repetition no quant", &types.PatternNode{Kind: types.PatternRepetition, Children: []*types.PatternNode{lit("A")}}},
{"unknown kind", &types.PatternNode{Kind: types.PatternKind(999)}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if _, err := Compile(c.node); err == nil {
t.Errorf("Compile(%s) want error, got nil", c.name)
}
})
}
}
// {0} 量词匹配空:应编译成功(空片段),NFA 仍可达 accept。
func TestCompile_ZeroQuantifier(t *testing.T) {
nfa, err := Compile(rep(lit("A"), 0, 0))
if err != nil {
t.Fatalf("Compile({0}) error: %v", err)
}
if nfa == nil || nfa.start == nil || nfa.accept == nil {
t.Fatalf("Compile({0}) incomplete NFA")
}
}
// PERMUTE 多符号:编译成功(排列数为 N!,符号多时状态膨胀,这里仅验证 3 符号可编译)。
func TestCompile_PermuteThree(t *testing.T) {
node := &types.PatternNode{Kind: types.PatternPermute, Children: []*types.PatternNode{
lit("A"), lit("B"), lit("C"),
}}
if _, err := Compile(node); err != nil {
t.Fatalf("Compile(PERMUTE A,B,C) error: %v", err)
}
}
// PERMUTE 超过 6 符号:阶乘级状态膨胀,编译期拒绝。
func TestCompile_PermuteLimit(t *testing.T) {
kids := make([]*types.PatternNode, 7)
for i := range kids {
kids[i] = lit(string(rune('A' + i)))
}
node := &types.PatternNode{Kind: types.PatternPermute, Children: kids}
if _, err := Compile(node); err == nil {
t.Errorf("Compile(PERMUTE 7 symbols) want error, got nil")
}
}
+32
View File
@@ -29,6 +29,8 @@ type SelectStatement struct {
Having string
OrderBy []types.OrderByField
JoinConfigs []types.JoinConfig
// MatchRecognize 携带 MATCH_RECOGNIZE 子句(FROM 后、WHERE 前)。非空时走 CEP 路径。
MatchRecognize *types.MatchRecognizeSpec
}
type Field struct {
@@ -243,6 +245,34 @@ func (s *SelectStatement) ToStreamConfig() (*types.Config, string, error) {
selectAlias := buildSelectAliasMap(s.Fields)
havingRewritten := extractHavingAggregates(s.Having, aggs, fields, selectAlias)
// 执行路径模式:MATCH_RECOGNIZE→CEP;窗口/聚合→Window;否则 Direct。
// P0 拦截 MATCH_RECOGNIZE 与 GROUP/聚合、JOIN 的组合(后续阶段支持)。
mode := types.ExecDirect
if needWindow {
mode = types.ExecWindow
}
if s.MatchRecognize != nil {
if needWindow {
return nil, "", fmt.Errorf("MATCH_RECOGNIZE cannot be combined with GROUP BY/aggregation yet")
}
if len(s.JoinConfigs) > 0 {
return nil, "", fmt.Errorf("MATCH_RECOGNIZE with JOIN is not supported yet")
}
if s.MatchRecognize.Pattern == nil {
return nil, "", fmt.Errorf("MATCH_RECOGNIZE requires a PATTERN clause")
}
if len(s.MatchRecognize.OrderBy) == 0 {
return nil, "", fmt.Errorf("MATCH_RECOGNIZE requires ORDER BY (provides event ordering)")
}
// ORDER BY 在 CEP 提供事件时序字段;DESC 流式下无意义(按到达序),拒绝以免静默忽略。
for _, ob := range s.MatchRecognize.OrderBy {
if ob.Direction == types.SortDesc {
return nil, "", fmt.Errorf("MATCH_RECOGNIZE ORDER BY 暂不支持 DESC(流式按到达序处理)")
}
}
mode = types.ExecCEP
}
// Build Stream configuration
config := types.Config{
WindowConfig: types.WindowConfig{
@@ -268,6 +298,8 @@ func (s *SelectStatement) ToStreamConfig() (*types.Config, string, error) {
Distinct: s.Distinct,
Limit: s.Limit,
NeedWindow: needWindow,
Mode: mode,
MatchRecognize: s.MatchRecognize,
AnalyticFields: analyticFields,
SimpleFields: simpleFields,
Having: havingRewritten,
Internal Server Error - Gitea: Git with a cup of tea
500 Internal Server Error

Gitea Version: 1.28.0+dev-211-ga30d865b78