@@ -698,6 +698,84 @@ func transactionSearchTopNShape(req *SearchObjectV4) bool {
698698 strings .EqualFold (orderBy , "time desc" )
699699}
700700
701+ // sipEventSubTables are the physical hep_proto_1_<x> tables merged by
702+ // queryTransactionSearchAllEventTypes for a SIP (proto_type 1) search whose
703+ // event_type filter is "all" (see isAllEventType). siprec is intentionally
704+ // excluded: it is not exposed as a Form-search event_type option today.
705+ var sipEventSubTables = []string {"call" , "registration" , "default" }
706+
707+ // transactionSearchWantsAllEventTypes reports whether a v4 transaction search
708+ // should merge results across every physical SIP event-type table
709+ // (hep_proto_1_call / _registration / _default) instead of a single one.
710+ //
711+ // Only proto_type 1 (SIP) has this call/registration/default split — OTLP
712+ // and Line Protocol tables do not. Aggregations (custom select/group_by) and
713+ // non-default ordering are excluded too: their rows cannot be correctly
714+ // merged across tables at the Go layer (see
715+ // queryTransactionSearchAllEventTypes), so those fall back to the single
716+ // "default" table via normalizeSIPTransactionType's default branch.
717+ func transactionSearchWantsAllEventTypes (req * SearchObjectV4 ) bool {
718+ protoType := req .Filter .ProtoType
719+ if protoType == 0 {
720+ protoType = 1
721+ }
722+ if protoType != 1 {
723+ return false
724+ }
725+ if ! isAllEventType (req .Filter .EventType ) {
726+ return false
727+ }
728+ return transactionSearchTopNShape (req )
729+ }
730+
731+ // queryTransactionSearchAllEventTypes runs the normal single-table v4 search
732+ // pipeline (SQL build + queryTransactionSearch, including lazy payload
733+ // hydration and the configured lake_topn_strategy) once per physical
734+ // event-type table, then merges the rows newest-first and re-applies the
735+ // requested LIMIT.
736+ //
737+ // The merge happens in Go, not via a SQL UNION, so each sub-query keeps
738+ // going through the existing, well-tested single-table hot+cold/tiered-volume
739+ // query path unchanged (see node.rewriteQueryForVolumes / node.
740+ // buildMemoryUnionQueries) — those rewrites key off a single table reference
741+ // per query today, so folding three different tables into one UNION-based
742+ // query text would silently drop cold/memory-buffer rows for two of the
743+ // three tables instead of fixing the "Form search misses cold data" class of
744+ // bug this is meant to address.
745+ func (h * SearchHandler ) queryTransactionSearchAllEventTypes (ctx context.Context , req * SearchObjectV4 , virtualRules map [string ]services.VirtualFieldRule ) ([]map [string ]interface {}, error ) {
746+ limit := effectiveSearchLimit (req .Param .Limit )
747+
748+ var (
749+ all []map [string ]interface {}
750+ firstErr error
751+ okCount int
752+ )
753+ for _ , evt := range sipEventSubTables {
754+ subReq := * req
755+ subReq .Filter .EventType = evt
756+
757+ sql , err := buildSearchSQLV4 (h .flightService .LakeName (), & subReq , virtualRules )
758+ if err != nil {
759+ return nil , fmt .Errorf ("event_type=%s: %w" , evt , err )
760+ }
761+ rows , err := h .queryTransactionSearch (ctx , sql , & subReq , virtualRules )
762+ if err != nil {
763+ logger .Warn ("V4TransactionsSearch: event_type=all sub-query failed" ,
764+ "event_type" , evt , "error" , err )
765+ if firstErr == nil {
766+ firstErr = err
767+ }
768+ continue
769+ }
770+ okCount ++
771+ all = append (all , rows ... )
772+ }
773+ if okCount == 0 && firstErr != nil {
774+ return nil , firstErr
775+ }
776+ return sortRowsByTimestampDescLimit (all , limit ), nil
777+ }
778+
701779// queryTransactionSearch executes a v4 transaction search. fullSQL is the
702780// already-validated query for the whole time range. The lake_topn_strategy
703781// config selects how a long-range timestamp-DESC top-N is run:
@@ -932,16 +1010,28 @@ func (h *SearchHandler) V4TransactionsSearch(c echo.Context) error {
9321010 }
9331011
9341012 virtualRules := h .loadVirtualRulesForReq (c .Request ().Context (), & req )
935- sql , err := buildSearchSQLV4 (h .flightService .LakeName (), & req , virtualRules )
936- if err != nil {
937- logger .Error (fmt .Sprintf ("V4TransactionsSearch: SQL validation failed: %v" , err ))
938- return writeError (c , http .StatusBadRequest , "Bad Request" , fmt .Sprintf ("SQL validation failed: %v" , err ))
939- }
940- logger .Info ("V4TransactionsSearch" , "proto" , req .Filter .ProtoType , "event" , req .Filter .EventType , "sql" , sql )
941- results , err := h .queryTransactionSearch (c .Request ().Context (), sql , & req , virtualRules )
942- if err != nil {
943- logger .Error (fmt .Sprintf ("V4TransactionsSearch: query error: %v" , err ))
944- return writeError (c , http .StatusInternalServerError , "Server Error" , "Query failed" )
1013+
1014+ var results []map [string ]interface {}
1015+ if transactionSearchWantsAllEventTypes (& req ) {
1016+ rows , err := h .queryTransactionSearchAllEventTypes (c .Request ().Context (), & req , virtualRules )
1017+ if err != nil {
1018+ logger .Error (fmt .Sprintf ("V4TransactionsSearch: query error: %v" , err ))
1019+ return writeError (c , http .StatusInternalServerError , "Server Error" , "Query failed" )
1020+ }
1021+ results = rows
1022+ } else {
1023+ sql , err := buildSearchSQLV4 (h .flightService .LakeName (), & req , virtualRules )
1024+ if err != nil {
1025+ logger .Error (fmt .Sprintf ("V4TransactionsSearch: SQL validation failed: %v" , err ))
1026+ return writeError (c , http .StatusBadRequest , "Bad Request" , fmt .Sprintf ("SQL validation failed: %v" , err ))
1027+ }
1028+ logger .Info ("V4TransactionsSearch" , "proto" , req .Filter .ProtoType , "event" , req .Filter .EventType , "sql" , sql )
1029+ rows , err := h .queryTransactionSearch (c .Request ().Context (), sql , & req , virtualRules )
1030+ if err != nil {
1031+ logger .Error (fmt .Sprintf ("V4TransactionsSearch: query error: %v" , err ))
1032+ return writeError (c , http .StatusInternalServerError , "Server Error" , "Query failed" )
1033+ }
1034+ results = rows
9451035 }
9461036 logger .Info ("V4TransactionsSearch: got results" , "count" , len (results ))
9471037
@@ -958,6 +1048,26 @@ func (h *SearchHandler) V4TransactionsSearch(c echo.Context) error {
9581048 return c .JSON (http .StatusOK , resp )
9591049}
9601050
1051+ // resolveSearchTables returns the physical DuckLake tables a v4 lookup
1052+ // (message/transaction detail endpoints) should query for a given
1053+ // proto_type/event_type. For SIP (proto_type 1) with event_type "all" (see
1054+ // isAllEventType) this is every hep_proto_1_<x> table the Form-search merge
1055+ // can return rows from (sipEventSubTables); otherwise it is the single table
1056+ // getTableName would have resolved on its own.
1057+ func resolveSearchTables (lakeName string , protoType int , eventType string ) []string {
1058+ if protoType == 0 {
1059+ protoType = 1
1060+ }
1061+ if protoType == 1 && isAllEventType (eventType ) {
1062+ tables := make ([]string , len (sipEventSubTables ))
1063+ for i , evt := range sipEventSubTables {
1064+ tables [i ] = getTableName (lakeName , protoType , evt )
1065+ }
1066+ return tables
1067+ }
1068+ return []string {getTableName (lakeName , protoType , eventType )}
1069+ }
1070+
9611071// queryTransactionMessages loads SIP (or other proto) rows for a transaction session request (same rules as POST /transactions/messages).
9621072//
9631073// When a Lua correlation script is registered for (proto_type, event_type) the
@@ -967,6 +1077,11 @@ func (h *SearchHandler) V4TransactionsSearch(c echo.Context) error {
9671077// session_ids, reissue the query on the expanded set and return the
9681078// merged result. Any script/SQL failure is non-fatal — the handler falls
9691079// back to the base rows and logs a warning.
1080+ //
1081+ // event_type "all" (see isAllEventType) queries every physical SIP
1082+ // event-type table and merges the rows; correlation is skipped in that case
1083+ // since a Lua script is registered against a single (proto_type, event_type)
1084+ // pair and the merged view has no single one.
9701085func (h * SearchHandler ) queryTransactionMessages (ctx context.Context , req * TransactionSessionRequestV4 ) ([]map [string ]interface {}, error ) {
9711086 multi := normalizeTransactionSessionIDs (req .SessionIDs )
9721087 if len (multi ) == 0 {
@@ -984,7 +1099,29 @@ func (h *SearchHandler) queryTransactionMessages(ctx context.Context, req *Trans
9841099 if eventType == "" {
9851100 eventType = "call"
9861101 }
987- table := getTableName (h .flightService .LakeName (), protoType , eventType )
1102+
1103+ tables := resolveSearchTables (h .flightService .LakeName (), protoType , eventType )
1104+ if len (tables ) > 1 {
1105+ var merged []map [string ]interface {}
1106+ var firstErr error
1107+ for _ , table := range tables {
1108+ rows , err := h .executeTransactionMessagesSQL (ctx , table , multi , req .Timestamp .From , req .Timestamp .To )
1109+ if err != nil {
1110+ logger .Warn ("V4TransactionMessages: event_type=all sub-query failed" , "table" , table , "error" , err )
1111+ if firstErr == nil {
1112+ firstErr = err
1113+ }
1114+ continue
1115+ }
1116+ merged = append (merged , rows ... )
1117+ }
1118+ if merged == nil && firstErr != nil {
1119+ return nil , firstErr
1120+ }
1121+ sortTransactionMessageRowsByTimestampAsc (merged )
1122+ return merged , nil
1123+ }
1124+ table := tables [0 ]
9881125
9891126 baseRows , err := h .executeTransactionMessagesSQL (ctx , table , multi , req .Timestamp .From , req .Timestamp .To )
9901127 if err != nil {
@@ -1175,14 +1312,12 @@ func (h *SearchHandler) V4MessageGet(c echo.Context) error {
11751312 if eventType == "" {
11761313 eventType = "call"
11771314 }
1178- table := getTableName (h .flightService .LakeName (), protoType , eventType )
11791315 where := fmt .Sprintf ("uuid = '%s'" , sqlvalidator .SafeString (req .UUID ))
11801316 if req .Timestamp .From > 0 && req .Timestamp .To > 0 {
11811317 where += fmt .Sprintf (" AND timestamp >= (to_timestamp(%d / 1000.0) AT TIME ZONE 'UTC') AND timestamp <= (to_timestamp(%d / 1000.0) AT TIME ZONE 'UTC')" ,
11821318 req .Timestamp .From , req .Timestamp .To )
11831319 }
1184- sql := fmt .Sprintf ("SELECT * FROM %s WHERE %s LIMIT 1" , table , where )
1185- results , err := h .flightService .Query (c .Request ().Context (), sql )
1320+ results , err := h .queryMessageByUUIDAcrossTables (c .Request ().Context (), protoType , eventType , where )
11861321 if err != nil {
11871322 return writeError (c , http .StatusInternalServerError , "Server Error" , "Query failed" )
11881323 }
@@ -1197,6 +1332,33 @@ func (h *SearchHandler) V4MessageGet(c echo.Context) error {
11971332 return c .JSON (http .StatusOK , resp )
11981333}
11991334
1335+ // queryMessageByUUIDAcrossTables runs a "SELECT * FROM <table> WHERE <where> LIMIT 1"
1336+ // lookup, trying every table resolveSearchTables returns (more than one only
1337+ // when event_type is "all") until one produces a row. uuid is assigned once
1338+ // per message at ingest, so at most one table is expected to match; trying
1339+ // the rest costs one cheap indexed-column lookup each.
1340+ func (h * SearchHandler ) queryMessageByUUIDAcrossTables (ctx context.Context , protoType int , eventType , where string ) ([]map [string ]interface {}, error ) {
1341+ tables := resolveSearchTables (h .flightService .LakeName (), protoType , eventType )
1342+ var firstErr error
1343+ for _ , table := range tables {
1344+ sql := fmt .Sprintf ("SELECT * FROM %s WHERE %s LIMIT 1" , table , where )
1345+ rows , err := h .flightService .Query (ctx , sql )
1346+ if err != nil {
1347+ if firstErr == nil {
1348+ firstErr = err
1349+ }
1350+ continue
1351+ }
1352+ if len (rows ) > 0 {
1353+ return rows , nil
1354+ }
1355+ }
1356+ if firstErr != nil {
1357+ return nil , firstErr
1358+ }
1359+ return nil , nil
1360+ }
1361+
12001362func (h * SearchHandler ) V4MessageDecoded (c echo.Context ) error {
12011363 var req MessageGetRequestV4
12021364 if err := c .Bind (& req ); err != nil {
@@ -1214,14 +1376,12 @@ func (h *SearchHandler) V4MessageDecoded(c echo.Context) error {
12141376 if eventType == "" {
12151377 eventType = "call"
12161378 }
1217- table := getTableName (h .flightService .LakeName (), protoType , eventType )
12181379 where := fmt .Sprintf ("uuid = '%s'" , sqlvalidator .SafeString (req .UUID ))
12191380 if req .Timestamp .From > 0 && req .Timestamp .To > 0 {
12201381 where += fmt .Sprintf (" AND timestamp >= (to_timestamp(%d / 1000.0) AT TIME ZONE 'UTC') AND timestamp <= (to_timestamp(%d / 1000.0) AT TIME ZONE 'UTC')" ,
12211382 req .Timestamp .From , req .Timestamp .To )
12221383 }
1223- sql := fmt .Sprintf ("SELECT * FROM %s WHERE %s LIMIT 1" , table , where )
1224- results , err := h .flightService .Query (c .Request ().Context (), sql )
1384+ results , err := h .queryMessageByUUIDAcrossTables (c .Request ().Context (), protoType , eventType , where )
12251385 if err != nil {
12261386 return writeError (c , http .StatusInternalServerError , "Server Error" , "Query failed" )
12271387 }
0 commit comments