@@ -1075,15 +1075,155 @@ static void stage1b_reorderTrackTables(
10751075 }
10761076}
10771077
1078+ // ----------------------------------------------------------------------------
1079+ // Re-sorting tables that are STORED SORTED BY a reference
1080+ //
1081+ // Stage 1b exists because a track table grouped by fIndexCollisions stops being
1082+ // sliceable once the collision table is reordered. Exactly the same is true of
1083+ // every other table stored sorted by a reference into a table this tool
1084+ // reorders — O2v0_002 and O2cascade_001 by fIndexCollisions, O2fwdtrkcl by
1085+ // fIndexFwdTracks, O2ambiguoustrack and O2trackqa_003 by fIndexTracks.
1086+ // Remapping their values while leaving their rows in place turns a sorted
1087+ // column into an unsorted one, which is the same defect that produced
1088+ // "Table ... index fIndexCollisions has a group with index -1 that is split".
1089+ //
1090+ // Rather than hardcode which tables are grouped by what — the enumeration habit
1091+ // that caused O2-7098 — the decision is derived from the data: IF a column is
1092+ // non-decreasing in the input, THEN it is an ordering the file carries and the
1093+ // output must preserve it. That is self-maintaining across schema changes, and
1094+ // it correctly leaves O2mfttrackcov alone: its fIndexMFTTracks is not sorted in
1095+ // the input, so there is no ordering to preserve.
1096+
1097+ struct ResortCandidate {
1098+ size_t planIdx = 0 ;
1099+ std ::string tableName ;
1100+ std ::string keyBranch ;
1101+ std ::vector < Long64_t > keyValues ; // raw input values of keyBranch
1102+ std ::vector < std ::string > refPrefixes ; // referent of keyBranch, from kIndexRefs
1103+ };
1104+
1105+ // Read a scalar Int_t-like index column. Returns false for VLA / fixed-array
1106+ // columns, which are not row orderings.
1107+ static bool readScalarIndexColumn (TTree * t , const char * branch ,
1108+ std ::vector < Long64_t > & out ) {
1109+ TBranch * br = t -> GetBranch (branch );
1110+ if (!br ) return false;
1111+ TLeaf * leaf = static_cast < TLeaf * > (br -> GetListOfLeaves ()-> At (0 ));
1112+ if (!leaf || leaf -> GetLeafCount () || leaf -> GetLen () != 1 ) return false;
1113+ ScalarTag tag = tagOf (leaf );
1114+ size_t sz = byteSize (tag );
1115+ if (sz == 0 ) return false;
1116+ std ::vector < unsigned char > buf (sz , 0 );
1117+ br -> SetAddress (buf .data ());
1118+ out .clear ();
1119+ out .reserve (t -> GetEntries ());
1120+ for (Long64_t i = 0 ; i < t -> GetEntries (); ++ i ) {
1121+ br -> GetEntry (i );
1122+ out .push_back (readAsInt (buf .data (), tag ));
1123+ }
1124+ br -> ResetAddress ();
1125+ return true;
1126+ }
1127+
1128+ // The convention this tool writes (and Stage 1b establishes): valid values
1129+ // ascending, the -1 "ambiguous" group as one contiguous run at the end.
1130+ static bool isOrderedWithNullsLast (const std ::vector < Long64_t > & v ) {
1131+ Long64_t prev = -1 ;
1132+ bool seenNull = false;
1133+ for (auto x : v ) {
1134+ if (x < 0 ) { seenNull = true; continue ; }
1135+ if (seenNull ) return false ; // a valid value after a null: not this convention
1136+ if (x < prev ) return false;
1137+ prev = x ;
1138+ }
1139+ return true;
1140+ }
1141+
1142+ // Pick the column a table is stored sorted by, if any. fIndexCollisions wins
1143+ // when several qualify, since that is the grouping O2's slicing cache checks.
1144+ static bool findGroupingColumn (TTree * src , std ::string & keyBranch ,
1145+ std ::vector < Long64_t > & keyValues ,
1146+ std ::vector < std ::string > & refPrefixes ) {
1147+ if (!src || src -> GetEntries () < 2 ) return false ;
1148+ bool found = false;
1149+ for (auto & [branchName , prefixes ] : kIndexRefs ) {
1150+ std ::vector < Long64_t > vals ;
1151+ if (!readScalarIndexColumn (src , branchName .c_str (), vals )) continue ;
1152+ if (!isOrderedWithNullsLast (vals )) continue ;
1153+ bool preferred = (branchName == "fIndexCollisions" );
1154+ if (found && !preferred ) continue ;
1155+ keyBranch = branchName ;
1156+ keyValues = std ::move (vals );
1157+ refPrefixes = prefixes ;
1158+ found = true;
1159+ if (preferred ) break ;
1160+ }
1161+ return found ;
1162+ }
1163+
1164+ // Re-sort each candidate by its remapped grouping column. Iterated to a fixed
1165+ // point because these tables reference each other: O2cascade_001 is sorted by
1166+ // fIndexV0s, and O2v0_002 may itself have just been re-sorted.
1167+ static void resortByGroupingColumn (
1168+ std ::vector < TablePlan > & plans ,
1169+ std ::unordered_map < std ::string , PermMap > & allPerms ,
1170+ const std ::vector < ResortCandidate > & candidates ) {
1171+
1172+ const int kMaxPasses = 8 ;
1173+ int pass = 0 ;
1174+ for (; pass < kMaxPasses ; ++ pass ) {
1175+ bool changed = false;
1176+ for (auto & cand : candidates ) {
1177+ const PermMap * refPerm = findPermFor (allPerms , cand .refPrefixes );
1178+ if (!refPerm ) continue ; // referent absent from this DF
1179+
1180+ Long64_t n = (Long64_t )cand .keyValues .size ();
1181+ struct SortEntry { Long64_t key ; Long64_t srcRow ; };
1182+ std ::vector < SortEntry > entries ;
1183+ entries .reserve (n );
1184+ for (Long64_t i = 0 ; i < n ; ++ i ) {
1185+ Long64_t old = cand .keyValues [i ];
1186+ Long64_t nw = (old >= 0 && old < (Long64_t )refPerm -> size ()) ? (* refPerm )[old ] : -1 ;
1187+ entries .push_back ({nw , i });
1188+ }
1189+ // Same ordering convention as Stage 1: -1 sinks to a contiguous tail.
1190+ std ::stable_sort (entries .begin (), entries .end (),
1191+ [](const SortEntry & a , const SortEntry & b ) {
1192+ if (a .key < 0 && b .key >= 0 ) return false;
1193+ if (a .key >= 0 && b .key < 0 ) return true;
1194+ return a .key < b .key ;
1195+ });
1196+ std ::vector < Long64_t > rowOrder ;
1197+ rowOrder .reserve (n );
1198+ for (auto & e : entries ) rowOrder .push_back (e .srcRow );
1199+
1200+ if (rowOrder == plans [cand .planIdx ].rowOrder ) continue ;
1201+ plans [cand .planIdx ].rowOrder = rowOrder ;
1202+ allPerms [cand .tableName ] = permFromRowOrder (n , rowOrder );
1203+ changed = true;
1204+ std ::cout << " Re-sort: " << cand .tableName << " by remapped "
1205+ << cand .keyBranch << " (was sorted by it on input)\n" ;
1206+ }
1207+ if (!changed ) break ;
1208+ }
1209+ if (pass == kMaxPasses )
1210+ std ::cerr << " [warn] re-sort did not reach a fixed point after "
1211+ << kMaxPasses << " passes — check for a reference cycle\n" ;
1212+ }
1213+
10781214// Plan every table not yet claimed by an earlier stage: paste-join children
1079- // follow their parent's row order, everything else keeps its own. The index
1080- // columns they carry are NOT enumerated here any more — buildRemaps() derives
1081- // them from kIndexRefs when processDF writes.
1215+ // follow their parent's row order, everything else keeps its own — unless it is
1216+ // stored sorted by a reference, in which case resortByGroupingColumn() below
1217+ // re-establishes that ordering. The index columns these tables carry are NOT
1218+ // enumerated here — buildRemaps() derives them from kIndexRefs when processDF
1219+ // writes.
10821220static void planRemainingTables (
10831221 TDirectory * dirIn , std ::vector < TablePlan > & plans ,
10841222 std ::unordered_map < std ::string , PermMap > & allPerms ,
10851223 std ::unordered_set < std ::string > & planned ) {
10861224
1225+ std ::vector < ResortCandidate > resortCandidates ;
1226+
10871227 TIter it (dirIn -> GetListOfKeys ());
10881228 while (TKey * key = static_cast < TKey * > (it ())) {
10891229 if (TString (key -> GetClassName ()) != "TTree" ) continue ;
@@ -1139,10 +1279,26 @@ static void planRemainingTables(
11391279 std ::iota (rowOrder .begin (), rowOrder .end (), 0LL );
11401280 }
11411281
1282+ // A table that arrives here with its own row order may still be STORED
1283+ // SORTED BY one of its index columns; if that column's referent gets
1284+ // reordered, the sortedness has to be re-established. Record what is
1285+ // needed for that; the decision is made below, once every table in this
1286+ // stage has a permutation.
1287+ if (!parentPerm ) {
1288+ ResortCandidate cand ;
1289+ if (findGroupingColumn (src , cand .keyBranch , cand .keyValues , cand .refPrefixes )) {
1290+ cand .planIdx = plans .size ();
1291+ cand .tableName = tname ;
1292+ resortCandidates .push_back (std ::move (cand ));
1293+ }
1294+ }
1295+
11421296 allPerms [tname ] = permFromRowOrder (nSrc , rowOrder );
11431297 plans .push_back ({tname , std ::move (rowOrder )});
11441298 planned .insert (tname );
11451299 }
1300+
1301+ resortByGroupingColumn (plans , allPerms , resortCandidates );
11461302}
11471303
11481304// ============================================================================
@@ -1826,6 +1982,23 @@ static bool checkLinksDF(TDirectory *din, TDirectory *dout, const char *dfName)
18261982 linksOut .push_back ({branchName , & fpOut [refName ], nullptr });
18271983 }
18281984
1985+ // Ordering preservation: a column that is sorted on input describes a
1986+ // grouping the file carries, and O2's slicing cache relies on it. Remapping
1987+ // the values while leaving the rows in place silently destroys it — the same
1988+ // defect as the split "-1" group, just in a different table.
1989+ for (auto & [branchName , prefixes ] : kIndexRefs ) {
1990+ std ::vector < Long64_t > vIn , vOut ;
1991+ if (!readScalarIndexColumn (tIn , branchName .c_str (), vIn )) continue ;
1992+ if (!isOrderedWithNullsLast (vIn )) continue ; // no ordering to preserve
1993+ if (!readScalarIndexColumn (tOut , branchName .c_str (), vOut )) continue ;
1994+ if (!isOrderedWithNullsLast (vOut )) {
1995+ std ::cerr << " [FAIL] " << dfName << ": " << tn << "." << branchName
1996+ << " was sorted on input but is not on output"
1997+ " (grouping destroyed — slicing will misbehave)\n" ;
1998+ ok = false;
1999+ }
2000+ }
2001+
18292002 auto cIn = linkTupleCounts (tIn , fpIn [tn ], linksIn );
18302003 auto cOut = linkTupleCounts (tOut , fpOut [tn ], linksOut );
18312004
0 commit comments