@@ -95,37 +95,46 @@ class EdgeScanOperator implements Operator {
9595 const query = this . query ;
9696 const meta = this . meta ;
9797
98- let cols = query . projections . length > 0
99- ? meta . columns . filter ( c => query . projections . includes ( c . name ) )
100- : meta . columns ;
101-
102- if ( query . vectorSearch ) {
103- const vc = query . vectorSearch . column ;
104- if ( ! cols . some ( c => c . name === vc ) ) {
105- const ec = meta . columns . find ( c => c . name === vc ) ;
106- if ( ec ) cols = [ ...cols , ec ] ;
107- }
98+ // Determine columns to fetch: projections + all columns referenced by filters/sort/groupBy/aggregates
99+ let neededNames : Set < string > ;
100+ if ( query . projections . length > 0 ) {
101+ neededNames = new Set ( query . projections ) ;
102+ for ( const f of query . filters ) neededNames . add ( f . column ) ;
103+ if ( query . filterGroups ) for ( const g of query . filterGroups ) for ( const f of g ) neededNames . add ( f . column ) ;
104+ if ( query . sortColumn ) neededNames . add ( query . sortColumn ) ;
105+ if ( query . groupBy ) for ( const g of query . groupBy ) neededNames . add ( g ) ;
106+ if ( query . aggregates ) for ( const a of query . aggregates ) if ( a . column !== "*" ) neededNames . add ( a . column ) ;
107+ } else {
108+ neededNames = new Set ( meta . columns . map ( c => c . name ) ) ;
108109 }
110+ if ( query . vectorSearch ) neededNames . add ( query . vectorSearch . column ) ;
111+
112+ let cols = meta . columns . filter ( c => neededNames . has ( c . name ) ) ;
109113 this . cols = cols ;
110114
111- for ( const col of cols ) {
112- const keptPages : typeof col . pages = [ ] ;
113- for ( const page of col . pages ) {
114- if ( ! query . vectorSearch && canSkipPage ( page , query . filters , col . name ) ) {
115- this . pagesSkipped ++ ;
116- continue ;
115+ // Determine which pages to keep — must be uniform across all columns to avoid row misalignment.
116+ // A page is skipped only if any AND filter eliminates it (same logic as canSkipPageMultiCol).
117+ const maxPages = cols . reduce ( ( m , c ) => Math . max ( m , c . pages . length ) , 0 ) ;
118+ const keptPageIndices : number [ ] = [ ] ;
119+ for ( let pi = 0 ; pi < maxPages ; pi ++ ) {
120+ let skip = false ;
121+ if ( ! query . vectorSearch ) {
122+ for ( const f of query . filters ) {
123+ const col = cols . find ( c => c . name === f . column ) ;
124+ if ( ! col ) continue ;
125+ const page = col . pages [ pi ] ;
126+ if ( ! page ) continue ;
127+ if ( canSkipPage ( page , [ f ] , f . column ) ) { skip = true ; break ; }
117128 }
118- keptPages . push ( page ) ;
119129 }
120- this . columnPageInfos . set ( col . name , keptPages ) ;
130+ if ( skip ) { this . pagesSkipped += cols . length ; continue ; }
131+ keptPageIndices . push ( pi ) ;
121132 }
122133
123- // Count total pages from first column
124- const firstCol = cols [ 0 ] ;
125- if ( firstCol ) {
126- const keptPages = this . columnPageInfos . get ( firstCol . name ) ?? firstCol . pages ;
127- this . pageCount = keptPages . length ;
134+ for ( const col of cols ) {
135+ this . columnPageInfos . set ( col . name , keptPageIndices . map ( pi => col . pages [ pi ] ) . filter ( Boolean ) ) ;
128136 }
137+ this . pageCount = keptPageIndices . length ;
129138 }
130139
131140 /**
@@ -670,6 +679,11 @@ export class QueryDO extends DurableObject<Env> {
670679 if ( query . offset !== undefined ) feed ( String ( query . offset ) ) ;
671680 if ( query . aggregates ) for ( const a of query . aggregates ) { feed ( a . fn ) ; feed ( a . column ) ; if ( a . alias ) feed ( a . alias ) ; }
672681 if ( query . groupBy ) for ( const g of query . groupBy ) feed ( g ) ;
682+ if ( query . distinct ) feed ( "distinct" ) ;
683+ if ( query . windows ) for ( const w of query . windows ) { feed ( w . fn ) ; feed ( w . alias ) ; feed ( w . column ?? "" ) ; }
684+ if ( query . computedColumns ) for ( const cc of query . computedColumns ) feed ( cc . name ) ;
685+ if ( query . setOperation ) { feed ( query . setOperation . type ) ; feed ( query . setOperation . table ) ; }
686+ if ( query . subqueryIn ) { feed ( query . subqueryIn . column ) ; feed ( query . subqueryIn . table ) ; }
673687 return `qr:${ query . table } :${ ( h >>> 0 ) . toString ( 36 ) } ` ;
674688 }
675689
@@ -922,31 +936,52 @@ export class QueryDO extends DurableObject<Env> {
922936
923937 /** Scan only the needed pages from R2 via coalesced Range reads, with cache-before-fetch. */
924938 private async scanPages ( query : QueryDescriptor , meta : TableMeta , t0 : number ) : Promise < QueryResult > {
925- let cols = query . projections . length > 0
926- ? meta . columns . filter ( c => query . projections . includes ( c . name ) )
927- : meta . columns ;
928-
929- if ( query . vectorSearch ) {
930- const vc = query . vectorSearch . column ;
931- if ( ! cols . some ( c => c . name === vc ) ) {
932- const ec = meta . columns . find ( c => c . name === vc ) ;
933- if ( ec ) cols = [ ...cols , ec ] ;
934- }
939+ // Determine columns to fetch: projections + all referenced by filters/sort/groupBy/aggregates
940+ let neededNames : Set < string > ;
941+ if ( query . projections . length > 0 ) {
942+ neededNames = new Set ( query . projections ) ;
943+ for ( const f of query . filters ) neededNames . add ( f . column ) ;
944+ if ( query . filterGroups ) for ( const g of query . filterGroups ) for ( const f of g ) neededNames . add ( f . column ) ;
945+ if ( query . sortColumn ) neededNames . add ( query . sortColumn ) ;
946+ if ( query . groupBy ) for ( const g of query . groupBy ) neededNames . add ( g ) ;
947+ if ( query . aggregates ) for ( const a of query . aggregates ) if ( a . column !== "*" ) neededNames . add ( a . column ) ;
948+ } else {
949+ neededNames = new Set ( meta . columns . map ( c => c . name ) ) ;
935950 }
951+ if ( query . vectorSearch ) neededNames . add ( query . vectorSearch . column ) ;
952+
953+ let cols = meta . columns . filter ( c => neededNames . has ( c . name ) ) ;
936954
937- // Build per-page ranges, applying page-level skip.
938- // Track non-skipped page infos per column so buffer indices stay aligned.
955+ // Build per-page ranges, applying page-level skip uniformly across all columns.
956+ // A page is skipped only if any AND filter eliminates it — same decision for all columns
957+ // to prevent row misalignment when different columns have different page counts.
939958 const ranges : { column : string ; offset : number ; length : number } [ ] = [ ] ;
940959 const columnPageInfos = new Map < string , typeof cols [ 0 ] [ "pages" ] > ( ) ;
941960 let pagesSkipped = 0 ;
961+
962+ const maxPages = cols . reduce ( ( m , c ) => Math . max ( m , c . pages . length ) , 0 ) ;
963+ const keptPageIndices : number [ ] = [ ] ;
964+ for ( let pi = 0 ; pi < maxPages ; pi ++ ) {
965+ let skip = false ;
966+ if ( ! query . vectorSearch ) {
967+ for ( const f of query . filters ) {
968+ const col = cols . find ( c => c . name === f . column ) ;
969+ if ( ! col ) continue ;
970+ const page = col . pages [ pi ] ;
971+ if ( ! page ) continue ;
972+ if ( canSkipPage ( page , [ f ] , f . column ) ) { skip = true ; break ; }
973+ }
974+ }
975+ if ( skip ) { pagesSkipped += cols . length ; continue ; }
976+ keptPageIndices . push ( pi ) ;
977+ }
978+
942979 for ( const col of cols ) {
943- const keptPages : typeof col . pages = [ ] ;
944- for ( const page of col . pages ) {
945- if ( ! query . vectorSearch && canSkipPage ( page , query . filters , col . name ) ) { pagesSkipped ++ ; continue ; }
946- keptPages . push ( page ) ;
980+ const keptPages = keptPageIndices . map ( pi => col . pages [ pi ] ) . filter ( Boolean ) ;
981+ columnPageInfos . set ( col . name , keptPages ) ;
982+ for ( const page of keptPages ) {
947983 ranges . push ( { column : col . name , offset : Number ( page . byteOffset ) , length : page . byteLength } ) ;
948984 }
949- columnPageInfos . set ( col . name , keptPages ) ;
950985 }
951986
952987 // 3-tier cache hierarchy:
0 commit comments