@@ -94,6 +94,12 @@ pub struct PersistedTableIndex {
9494 /// admits a built one into `id_caches`.
9595 #[ serde( default ) ]
9696 pub id_index : IdIndex ,
97+ /// The built ORDER BY index, persisted under the SAME fingerprint so a cold
98+ /// process serves its first ordered scan from the sidecar instead of paying
99+ /// the population scan. Unbuilt when the writer never ran an ordered query
100+ /// on the table; the load path only admits a built one into `ordered_caches`.
101+ #[ serde( default ) ]
102+ pub ordered_index : OrderedColIndex ,
97103}
98104
99105/// A transferable snapshot of built index pairs (Arc-backed — cloning copies
@@ -102,7 +108,7 @@ pub struct PersistedTableIndex {
102108/// by [`Connection::adopt_built_indexes`] on a freshly-opened reader.
103109#[ derive( Debug , Clone , Default ) ]
104110pub struct IndexSnapshot {
105- tables : Vec < ( String , i64 , ColIndex , IdIndex ) > ,
111+ tables : Vec < ( String , i64 , ColIndex , IdIndex , OrderedColIndex ) > ,
106112}
107113
108114/// A single result column's description, mirroring the DB-API `description`
@@ -399,7 +405,8 @@ impl Connection {
399405 // `open_read_only` (both route through `from_store`), so the daemon-down
400406 // RO recall reader loads-not-rebuilds for free. The id-index rides the
401407 // same gate, so a warm boot serves `WHERE id = ?` with no build scan.
402- let ( mut col_caches, mut id_caches) = load_col_indexes ( & store, & meta, & catalog, & root_map) ;
408+ let ( mut col_caches, mut id_caches, ordered_caches) =
409+ load_col_indexes ( & store, & meta, & catalog, & root_map) ;
403410 // A read-write connection maintains its indexes per committed write, so
404411 // it must own private postings: materialize the copy-on-write clone HERE,
405412 // once, on the open (boot) path — never lazily on the first awake-path
@@ -459,7 +466,7 @@ impl Connection {
459466 id_caches,
460467 conflict_caches : HashMap :: new ( ) ,
461468 col_caches,
462- ordered_caches : HashMap :: new ( ) ,
469+ ordered_caches,
463470 bare_count_cache : HashMap :: new ( ) ,
464471 parse_cache : HashMap :: new ( ) ,
465472 } )
@@ -528,7 +535,15 @@ impl Connection {
528535 . get ( table)
529536 . cloned ( )
530537 . unwrap_or_default ( ) ;
531- tables. push ( ( table. clone ( ) , generation, cidx. clone ( ) , iidx) ) ;
538+ // The ordered index rides the same generation stamp: rebuilt from
539+ // scratch by every fresh reader otherwise (its population scan is
540+ // the dominant reopen cost after the col/id indexes are adopted).
541+ let oidx = self
542+ . ordered_caches
543+ . get ( table)
544+ . cloned ( )
545+ . unwrap_or_default ( ) ;
546+ tables. push ( ( table. clone ( ) , generation, cidx. clone ( ) , iidx, oidx) ) ;
532547 }
533548 IndexSnapshot { tables }
534549 }
@@ -540,7 +555,7 @@ impl Connection {
540555 /// table is skipped (the lazy build stays the always-correct fallback);
541556 /// an already-built local index is never overwritten.
542557 pub fn adopt_built_indexes ( & mut self , snapshot : & IndexSnapshot ) {
543- for ( table, generation, cidx, iidx) in & snapshot. tables {
558+ for ( table, generation, cidx, iidx, oidx ) in & snapshot. tables {
544559 let already_built = self
545560 . col_caches
546561 . get ( table)
@@ -568,9 +583,27 @@ impl Connection {
568583 {
569584 self . id_caches . insert ( table. clone ( ) , iidx. clone ( ) ) ;
570585 }
586+ if oidx. is_built ( ) && !self
587+ . ordered_caches
588+ . get ( table)
589+ . map ( |o| o. is_built ( ) )
590+ . unwrap_or ( false )
591+ {
592+ self . ordered_caches . insert ( table. clone ( ) , oidx. clone ( ) ) ;
593+ }
571594 }
572595 }
573596
597+ /// True when `table`'s ORDER BY index is resident and built — exposed so
598+ /// integration tests can assert a sidecar load or a snapshot adoption
599+ /// admitted it without paying the population scan.
600+ pub fn ordered_index_is_built ( & self , table : & str ) -> bool {
601+ self . ordered_caches
602+ . get ( table)
603+ . map ( |o| o. is_built ( ) )
604+ . unwrap_or ( false )
605+ }
606+
574607 /// Parse `sql` into its AST, reusing a cached parse when one exists.
575608 ///
576609 /// The AST depends only on the SQL text, so a hit returns a clone of the
@@ -702,6 +735,15 @@ impl Connection {
702735 } else {
703736 IdIndex :: new ( )
704737 } ;
738+ // The ordered index is persisted only when the writer already
739+ // built one (it exists per ORDER BY usage, never force-built) —
740+ // an unbuilt default round-trips as "nothing to adopt".
741+ let ordered_snapshot = self
742+ . ordered_caches
743+ . get ( & table)
744+ . filter ( |o| o. is_built ( ) )
745+ . cloned ( )
746+ . unwrap_or_default ( ) ;
705747 let generation = self . meta . col_generation ( & self . store , & table) ?;
706748 let row_count = tree. count_cells ( ) . map_err ( open_err) ?;
707749 tables. push ( PersistedTableIndex {
@@ -711,6 +753,7 @@ impl Connection {
711753 row_count,
712754 index : col_snapshot,
713755 id_index : id_snapshot,
756+ ordered_index : ordered_snapshot,
714757 } ) ;
715758 }
716759
@@ -1801,8 +1844,12 @@ fn load_col_indexes(
18011844 meta : & MetaTable ,
18021845 catalog : & Catalog ,
18031846 root_map : & BTreeMap < String , u32 > ,
1804- ) -> ( HashMap < String , ColIndex > , HashMap < String , IdIndex > ) {
1805- let empty = || ( HashMap :: new ( ) , HashMap :: new ( ) ) ;
1847+ ) -> (
1848+ HashMap < String , ColIndex > ,
1849+ HashMap < String , IdIndex > ,
1850+ HashMap < String , OrderedColIndex > ,
1851+ ) {
1852+ let empty = || ( HashMap :: new ( ) , HashMap :: new ( ) , HashMap :: new ( ) ) ;
18061853 let sidecar = col_index_sidecar_path ( store. path ( ) ) ;
18071854 let Some ( envelope) = cached_sidecar_envelope ( & sidecar) else {
18081855 return empty ( ) ;
@@ -1812,6 +1859,7 @@ fn load_col_indexes(
18121859 }
18131860 let mut loaded: HashMap < String , ColIndex > = HashMap :: new ( ) ;
18141861 let mut loaded_ids: HashMap < String , IdIndex > = HashMap :: new ( ) ;
1862+ let mut loaded_ordered: HashMap < String , OrderedColIndex > = HashMap :: new ( ) ;
18151863 for entry in & envelope. tables {
18161864 let Some ( & root) = root_map. get ( & entry. table ) else {
18171865 continue ;
@@ -1843,9 +1891,15 @@ fn load_col_indexes(
18431891 if entry. id_index . is_built ( ) {
18441892 loaded_ids. insert ( entry. table . clone ( ) , entry. id_index . clone ( ) ) ;
18451893 }
1894+ // The ordered index rides the SAME fingerprint gate; only a built one
1895+ // (the writer had run an ordered query on the table) is admitted, so a
1896+ // cold process serves its first ORDER BY from the sidecar scan-free.
1897+ if entry. ordered_index . is_built ( ) {
1898+ loaded_ordered. insert ( entry. table . clone ( ) , entry. ordered_index . clone ( ) ) ;
1899+ }
18461900 loaded. insert ( entry. table . clone ( ) , entry. index . clone ( ) ) ;
18471901 }
1848- ( loaded, loaded_ids)
1902+ ( loaded, loaded_ids, loaded_ordered )
18491903}
18501904
18511905/// How many distinct sidecar paths the process-wide decoded-envelope cache
0 commit comments