mirror of
https://gitee.com/rulego/streamsql.git
synced 2026-07-30 23:56:52 +00:00
feat(cep): 支持 MATCH_RECOGNIZE 模式识别
This commit is contained in:
+586
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+610
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
// --- EvalMeasure(candidate=nil,MEASURES 路径)---
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 聚合 RUNNING:cur=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
@@ -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
@@ -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 态本身在闭包里,但其 out(accept)不应被 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 分裂到两条 match,closure 应含两者。
|
||||
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
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package cep
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rulego/streamsql/types"
|
||||
)
|
||||
|
||||
// Compile 合法模式:返回非空 NFA(start/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
@@ -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,
|
||||
|
500 Internal Server Error
Gitea Version: 1.28.0+dev-211-ga30d865b78 | ||||