mirror of
https://gitee.com/rulego/streamsql.git
synced 2026-07-30 23:56:52 +00:00
226 lines
7.1 KiB
Go
226 lines
7.1 KiB
Go
package e2e
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/rulego/streamsql"
|
|
)
|
|
|
|
// deviceMetaRows is a small metadata fixture used across JOIN tests.
|
|
func deviceMetaRows() []map[string]any {
|
|
return []map[string]any{
|
|
{"deviceId": "d1", "location": "plantA", "type": "temp"},
|
|
{"deviceId": "d2", "location": "plantB", "type": "humid"},
|
|
}
|
|
}
|
|
|
|
// TestJoinMultipleTables verifies a stream can JOIN several metadata tables;
|
|
// each is registered separately and its columns are namespaced by its own alias.
|
|
func TestJoinMultipleTables(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, l.location, s.model FROM stream JOIN locations l ON deviceId = l.deviceId JOIN models s ON deviceId = s.deviceId"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
if _, err := ssql.RegisterTable("locations", []map[string]any{
|
|
{"deviceId": "d1", "location": "plantA"},
|
|
}); err != nil {
|
|
t.Fatalf("RegisterTable locations: %v", err)
|
|
}
|
|
if _, err := ssql.RegisterTable("models", []map[string]any{
|
|
{"deviceId": "d1", "model": "MX-1"},
|
|
}); err != nil {
|
|
t.Fatalf("RegisterTable models: %v", err)
|
|
}
|
|
|
|
got, err := ssql.EmitSync(map[string]any{"deviceId": "d1"})
|
|
if err != nil {
|
|
t.Fatalf("EmitSync: %v", err)
|
|
}
|
|
if got["location"] != "plantA" || got["model"] != "MX-1" || got["deviceId"] != "d1" {
|
|
t.Errorf("multi-table enrich got=%v, want location=plantA model=MX-1", got)
|
|
}
|
|
}
|
|
|
|
func TestJoinInnerEnrich(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, m.location, m.type FROM stream JOIN meta m ON deviceId = m.deviceId"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
if _, err := ssql.RegisterTable("meta", deviceMetaRows()); err != nil {
|
|
t.Fatalf("RegisterTable: %v", err)
|
|
}
|
|
|
|
got, err := ssql.EmitSync(map[string]any{"deviceId": "d1", "temp": 35})
|
|
if err != nil {
|
|
t.Fatalf("EmitSync: %v", err)
|
|
}
|
|
want := map[string]any{"deviceId": "d1", "location": "plantA", "type": "temp"}
|
|
if got["deviceId"] != want["deviceId"] || got["location"] != want["location"] || got["type"] != want["type"] {
|
|
t.Errorf("got=%v, want=%v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestJoinInnerNoMatchDropped(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, m.location FROM stream JOIN meta m ON deviceId = m.deviceId"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
if _, err := ssql.RegisterTable("meta", deviceMetaRows()); err != nil {
|
|
t.Fatalf("RegisterTable: %v", err)
|
|
}
|
|
|
|
// d3 has no metadata row -> INNER JOIN drops it (nil result, no error).
|
|
got, err := ssql.EmitSync(map[string]any{"deviceId": "d3"})
|
|
if err != nil {
|
|
t.Fatalf("EmitSync: %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Errorf("expected nil (dropped), got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestJoinLeftNullFill(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, m.location FROM stream LEFT JOIN meta m ON deviceId = m.deviceId"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
if _, err := ssql.RegisterTable("meta", deviceMetaRows()); err != nil {
|
|
t.Fatalf("RegisterTable: %v", err)
|
|
}
|
|
|
|
got, err := ssql.EmitSync(map[string]any{"deviceId": "d9"})
|
|
if err != nil {
|
|
t.Fatalf("EmitSync: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("LEFT JOIN must keep unmatched stream row")
|
|
}
|
|
if got["deviceId"] != "d9" {
|
|
t.Errorf("deviceId=%v, want d9", got["deviceId"])
|
|
}
|
|
if v, ok := got["location"]; ok && v != nil {
|
|
t.Errorf("location=%v, want nil (no match)", v)
|
|
}
|
|
}
|
|
|
|
func TestJoinCompositeKey(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, m.location FROM stream JOIN meta m ON deviceId = m.deviceId AND tenant = m.tenant"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
rows := []map[string]any{
|
|
{"deviceId": "d1", "tenant": "t1", "location": "plantA"},
|
|
{"deviceId": "d1", "tenant": "t2", "location": "plantB"},
|
|
}
|
|
// Composite key auto-derived from the two ON equalities (deviceId, tenant).
|
|
if _, err := ssql.RegisterTable("meta", rows); err != nil {
|
|
t.Fatalf("RegisterTable: %v", err)
|
|
}
|
|
|
|
got, err := ssql.EmitSync(map[string]any{"deviceId": "d1", "tenant": "t2"})
|
|
if err != nil {
|
|
t.Fatalf("EmitSync: %v", err)
|
|
}
|
|
if got["location"] != "plantB" {
|
|
t.Errorf("location=%v, want plantB (composite key)", got["location"])
|
|
}
|
|
}
|
|
|
|
// TestJoinExplicitKeyFields overrides the auto-derived key with explicit
|
|
// keyFields (useful when the index column differs from the ON field, or to
|
|
// register before the JOIN key is otherwise derivable).
|
|
func TestJoinExplicitKeyFields(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, m.location FROM stream JOIN meta m ON deviceId = m.deviceId"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
// Explicit single key override.
|
|
if _, err := ssql.RegisterTable("meta", deviceMetaRows(), "deviceId"); err != nil {
|
|
t.Fatalf("RegisterTable: %v", err)
|
|
}
|
|
got, err := ssql.EmitSync(map[string]any{"deviceId": "d1"})
|
|
if err != nil {
|
|
t.Fatalf("EmitSync: %v", err)
|
|
}
|
|
if got["location"] != "plantA" {
|
|
t.Errorf("location=%v, want plantA", got["location"])
|
|
}
|
|
}
|
|
|
|
// TestJoinRegisterTableNotInJoin errors when auto-derive can't find the table in
|
|
// any JOIN ON clause and no explicit keyFields were given.
|
|
func TestJoinRegisterTableNotInJoin(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId FROM stream"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
if _, err := ssql.RegisterTable("meta", deviceMetaRows()); err == nil {
|
|
t.Error("expected error registering a table not referenced by any JOIN, got nil")
|
|
}
|
|
}
|
|
|
|
func TestJoinUnregisteredTableErrors(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, m.location FROM stream JOIN meta m ON deviceId = m.deviceId"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
// No RegisterTable call -> EmitSync must surface the config error.
|
|
if _, err := ssql.EmitSync(map[string]any{"deviceId": "d1"}); err == nil {
|
|
t.Error("expected error for unregistered JOIN table, got nil")
|
|
}
|
|
}
|
|
|
|
// TestJoinConcurrentEmitAndUpsert verifies Lookup stays race-free under
|
|
// concurrent reads (Emit) and writes (Upsert). Run with -race in CI.
|
|
func TestJoinConcurrentEmitAndUpsert(t *testing.T) {
|
|
t.Parallel()
|
|
ssql := streamsql.New()
|
|
defer ssql.Stop()
|
|
if err := ssql.Execute("SELECT deviceId, m.location FROM stream JOIN meta m ON deviceId = m.deviceId"); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
src, err := ssql.RegisterTable("meta", deviceMetaRows())
|
|
if err != nil {
|
|
t.Fatalf("RegisterTable: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = ssql.EmitSync(map[string]any{"deviceId": "d1"})
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
src.Upsert(map[string]any{"deviceId": "d1", "location": "plantC"})
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
got, err := ssql.EmitSync(map[string]any{"deviceId": "d1"})
|
|
if err != nil {
|
|
t.Fatalf("EmitSync: %v", err)
|
|
}
|
|
if got["location"] != "plantC" {
|
|
t.Errorf("location=%v, want plantC after upsert", got["location"])
|
|
}
|
|
}
|