@@ -837,6 +837,120 @@ int main(int argc, char* argv[]) {
837837 args.context_id , args.table_name );
838838 }
839839
840+ auto resp_p = reply.serialize ();
841+ cloudsql::network::RpcHeader resp_h;
842+ resp_h.type = cloudsql::network::RpcType::QueryResults;
843+ resp_h.payload_len = static_cast <uint16_t >(resp_p.size ());
844+ char h_buf[cloudsql::network::RpcHeader::HEADER_SIZE];
845+ resp_h.encode (h_buf);
846+ static_cast <void >(
847+ send (fd, h_buf, cloudsql::network::RpcHeader::HEADER_SIZE, 0 ));
848+ static_cast <void >(send (fd, resp_p.data (), resp_p.size (), 0 ));
849+ });
850+
851+ // Handler for reporting unmatched LEFT rows after join execution
852+ // For FULL outer joins, each node identifies rows from its local left table
853+ // partition that had no matching right row during the distributed join
854+ rpc_server->set_handler (
855+ cloudsql::network::RpcType::UnmatchedLeftRowsReport,
856+ [&](const cloudsql::network::RpcHeader& h, const std::vector<uint8_t >& p,
857+ int fd) {
858+ (void )h;
859+ auto args = cloudsql::network::UnmatchedLeftRowsReportArgs::deserialize (p);
860+ cloudsql::network::UnmatchedLeftRowsReportArgs reply;
861+ reply.context_id = args.context_id ;
862+ reply.left_table = args.left_table ;
863+ reply.join_key_col = args.join_key_col ;
864+
865+ // args.unmatched_keys contains MATCHED keys from coordinator
866+ // We need to return rows that are NOT in this set
867+ std::unordered_set<std::string> matched_keys_set (
868+ args.unmatched_keys .begin (), args.unmatched_keys .end ());
869+
870+ try {
871+ // Scan local left table and collect rows that were NOT matched
872+ auto table_meta_opt = catalog->get_table_by_name (args.left_table );
873+ if (table_meta_opt.has_value ()) {
874+ const auto * table_meta = table_meta_opt.value ();
875+ cloudsql::executor::Schema schema;
876+ for (const auto & col : table_meta->columns ) {
877+ schema.add_column (col.name , col.type );
878+ }
879+ cloudsql::storage::HeapTable table (args.left_table , *bpm, schema);
880+
881+ const size_t key_idx = schema.find_column (args.join_key_col );
882+ if (key_idx != static_cast <size_t >(-1 )) {
883+ std::vector<cloudsql::executor::Tuple> unmatched_tuples;
884+ auto iter = table.scan ();
885+ cloudsql::storage::HeapTable::TupleMeta t_meta;
886+ while (iter.next_meta (t_meta)) {
887+ if (t_meta.xmax == 0 ) {
888+ const auto & key_val = t_meta.tuple .get (key_idx);
889+ std::string key_str = key_val.to_string ();
890+ // Only include if NOT in matched keys
891+ if (matched_keys_set.find (key_str) ==
892+ matched_keys_set.end ()) {
893+ reply.unmatched_keys .push_back (key_str);
894+ // Pad with NULLs for right columns and append left
895+ // row
896+ std::vector<cloudsql::common::Value> padded_values;
897+ padded_values.reserve (t_meta.tuple .size () +
898+ args.right_column_count );
899+ // Append left table column values
900+ for (size_t j = 0 ; j < t_meta.tuple .size (); ++j) {
901+ padded_values.push_back (t_meta.tuple .get (j));
902+ }
903+ // Append NULLs for right table columns
904+ for (uint32_t i = 0 ; i < args.right_column_count ;
905+ ++i) {
906+ padded_values.push_back (
907+ cloudsql::common::Value::make_null ());
908+ }
909+ unmatched_tuples.emplace_back (
910+ std::move (padded_values));
911+ }
912+ }
913+ }
914+ // Store properly padded tuples in ClusterManager for
915+ // coordinator to collect
916+ if (cluster_manager != nullptr && !unmatched_tuples.empty ()) {
917+ cluster_manager->set_unmatched_left_rows (
918+ args.context_id , args.left_table ,
919+ std::move (unmatched_tuples));
920+ }
921+ }
922+ }
923+ } catch (const std::exception& /* e*/ ) {
924+ // Return empty on error
925+ }
926+
927+ auto resp_p = reply.serialize ();
928+ cloudsql::network::RpcHeader resp_h;
929+ resp_h.type = cloudsql::network::RpcType::QueryResults;
930+ resp_h.payload_len = static_cast <uint16_t >(resp_p.size ());
931+ char h_buf[cloudsql::network::RpcHeader::HEADER_SIZE];
932+ resp_h.encode (h_buf);
933+ static_cast <void >(
934+ send (fd, h_buf, cloudsql::network::RpcHeader::HEADER_SIZE, 0 ));
935+ static_cast <void >(send (fd, resp_p.data (), resp_p.size (), 0 ));
936+ });
937+
938+ // Handler for fetching stored unmatched LEFT rows from a data node
939+ // Coordinator calls this after UnmatchedLeftRowsReport to get full unmatched tuples
940+ rpc_server->set_handler (
941+ cloudsql::network::RpcType::FetchUnmatchedLeftRows,
942+ [&](const cloudsql::network::RpcHeader& h, const std::vector<uint8_t >& p,
943+ int fd) {
944+ (void )h;
945+ auto args = cloudsql::network::FetchUnmatchedLeftRowsArgs::deserialize (p);
946+ cloudsql::network::UnmatchedRowsPushArgs reply;
947+ reply.context_id = args.context_id ;
948+
949+ if (cluster_manager != nullptr ) {
950+ reply.unmatched_rows = cluster_manager->get_unmatched_left_rows (
951+ args.context_id , args.table_name );
952+ }
953+
840954 auto resp_p = reply.serialize ();
841955 cloudsql::network::RpcHeader resp_h;
842956 resp_h.type = cloudsql::network::RpcType::QueryResults;
0 commit comments