| 919 | // --------------------------------------------------------------------------- |
| 920 | |
| 921 | static PJ::Status runMimoIncremental(DerivedEngineImpl& /*impl*/, DataEngine& engine, DerivedNode& node) { |
| 922 | const std::size_t num_inputs = node.mimo_input_topic_ids.size(); |
| 923 | if (num_inputs == 0) { |
| 924 | return PJ::okStatus(); |
| 925 | } |
| 926 | |
| 927 | // 1. Collect (timestamp, chunk*, row_index) for each input topic, |
| 928 | // only for rows strictly newer than the watermark. |
| 929 | struct SampleLoc { |
| 930 | PJ::Timestamp ts; |
| 931 | const TopicChunk* chunk; |
| 932 | uint32_t row; |
| 933 | }; |
| 934 | std::vector<std::vector<SampleLoc>> per_topic(num_inputs); |
| 935 | |
| 936 | PJ::ChunkId max_chunk_seen = node.mimo_last_chunk_id; |
| 937 | for (std::size_t i = 0; i < num_inputs; ++i) { |
| 938 | const TopicStorage* storage = engine.getTopicStorage(node.mimo_input_topic_ids[i]); |
| 939 | if (!storage) { |
| 940 | return PJ::unexpected( |
| 941 | fmt::format("run_mimo_incremental: input topic {} not found", node.mimo_input_topic_ids[i])); |
| 942 | } |
| 943 | for (const TopicChunk& chunk : storage->sealedChunks()) { |
| 944 | max_chunk_seen = std::max(max_chunk_seen, chunk.id); |
| 945 | if (chunk.stats.t_max <= node.mimo_last_ts) { |
| 946 | continue; // entire chunk already processed |
| 947 | } |
| 948 | for (uint32_t r = 0; r < chunk.stats.row_count; ++r) { |
| 949 | PJ::Timestamp ts = chunk.timestamps[r]; |
| 950 | if (ts <= node.mimo_last_ts) { |
| 951 | continue; |
| 952 | } |
| 953 | per_topic[i].push_back({ts, &chunk, r}); |
| 954 | } |
| 955 | } |
| 956 | } |
| 957 | // Every committed chunk has now been considered — regression detection in |
| 958 | // the scheduler compares against this watermark. Updated even when the run |
| 959 | // produces no joins, so a fruitless chunk is not re-flagged forever. |
| 960 | node.mimo_last_chunk_id = max_chunk_seen; |
| 961 | for (std::size_t i = 0; i < num_inputs; ++i) { |
| 962 | // If any topic has no new data, no new join is possible. |
| 963 | if (per_topic[i].empty()) { |
| 964 | return PJ::okStatus(); |
| 965 | } |
| 966 | } |
| 967 | // Chunks are gathered in commit order, which under out-of-order ingest is |
| 968 | // not time order; joined_ts is derived from topic 0, so sort it (stable: |
| 969 | // duplicate timestamps keep commit order for last-write-wins lookups). |
| 970 | std::stable_sort( |
| 971 | per_topic[0].begin(), per_topic[0].end(), [](const SampleLoc& a, const SampleLoc& b) { return a.ts < b.ts; }); |
| 972 | |
| 973 | // 2. N-way timestamp intersection: find timestamps present in ALL input topics. |
| 974 | // Start from topic 0's sorted timestamps, remove any not in subsequent topics. |
| 975 | std::vector<PJ::Timestamp> joined_ts; |
| 976 | joined_ts.reserve(per_topic[0].size()); |
| 977 | for (const auto& s : per_topic[0]) { |
| 978 | joined_ts.push_back(s.ts); |
no test coverage detected