@@ -18,39 +18,61 @@ const runLimit = 1 << 21 // 2mb
1818 * @param {ParquetReadOptions & { bloomFiltersByGroup?: Record<string, BloomFilter>[], schemaElements?: Record<string, SchemaElement>, pageRangesByGroup?: (PageRanges | undefined)[], pageLocationsByGroup?: Record<string, PageLocation[]>[] } } options
1919 * @returns {QueryPlan }
2020 */
21- export function parquetPlan ( { metadata, rowStart = 0 , rowEnd = Infinity , columns, filter, filterStrict = true , useOffsetIndex = false , bloomFiltersByGroup, schemaElements, pageRangesByGroup, pageLocationsByGroup } ) {
21+ export function parquetPlan ( options ) {
22+ const { metadata, rowStart = 0 , columns, useOffsetIndex = false } = options
2223 if ( ! metadata ) throw new Error ( 'parquetPlan requires metadata' )
2324 /** @type {GroupPlan[] } */
2425 const groups = [ ]
2526 /** @type {ByteRange[] } */
2627 const fetches = [ ]
2728 /** @type {ByteRange[] } */
2829 const indexes = [ ]
30+ const scanPlan = parquetPlanGroups ( options )
31+ for ( const group of scanPlan . groups ) {
32+ const groupPlan = parquetPlanGroup ( { ...group , columns, useOffsetIndex } )
33+ groups . push ( ...groupPlan . groups )
34+ fetches . push ( ...groupPlan . fetches )
35+ indexes . push ( ...groupPlan . indexes )
36+ }
37+ fetches . push ( ...indexes )
38+
39+ return { metadata, rowStart, rowEnd : scanPlan . rowEnd , columns, fetches, groups }
40+ }
41+
42+ /**
43+ * Select physical row-group ranges without planning column reads.
44+ *
45+ * @param {ParquetReadOptions & { bloomFiltersByGroup?: Record<string, BloomFilter>[], schemaElements?: Record<string, SchemaElement>, pageRangesByGroup?: (PageRanges | undefined)[], pageLocationsByGroup?: Record<string, PageLocation[]>[] } } options
46+ * @returns {{groups: {rowGroup: RowGroup, groupIndex: number, groupStart: number, groupRows: number, ranges: PageRanges, pageRanges?: PageRanges, pageLocations?: Record<string, PageLocation[]>}[], rowEnd: number} }
47+ */
48+ export function parquetPlanGroups ( { metadata, rowStart = 0 , rowEnd = Infinity , columns, filter, filterStrict = true , bloomFiltersByGroup, schemaElements, pageRangesByGroup, pageLocationsByGroup } ) {
49+ if ( ! metadata ) throw new Error ( 'parquetPlan requires metadata' )
2950 const schemaTree = parquetSchema ( metadata )
3051 const physicalColumns = getPhysicalColumns ( schemaTree )
3152 const elementsByPath = filter ? {
3253 ...physicalSchemaElements ( schemaTree ) ,
3354 ...schemaElements ,
3455 } : schemaElements
35-
36- // find which row groups to read
37- let groupStart = 0 // first row index of the current group
38- let rgIdx = 0
39- for ( const rowGroup of metadata . row_groups ) {
56+ const groups = [ ]
57+ let groupStart = 0
58+ for ( let groupIndex = 0 ; groupIndex < metadata . row_groups . length ; groupIndex ++ ) {
59+ const rowGroup = metadata . row_groups [ groupIndex ]
4060 const groupRows = Number ( rowGroup . num_rows )
4161 const groupEnd = groupStart + groupRows
42- const bloomFilters = bloomFiltersByGroup ?. [ rgIdx ]
43- // if row group overlaps with row range, add it to the plan
44- if ( groupRows > 0 && groupEnd > rowStart && groupStart < rowEnd && ! canSkipRowGroup ( { rowGroup, physicalColumns, filter, strict : filterStrict , bloomFilters, schemaElements : elementsByPath } ) ) {
62+ if ( groupRows > 0 && groupEnd > rowStart && groupStart < rowEnd && ! canSkipRowGroup ( {
63+ rowGroup,
64+ physicalColumns,
65+ filter,
66+ strict : filterStrict ,
67+ bloomFilters : bloomFiltersByGroup ?. [ groupIndex ] ,
68+ schemaElements : elementsByPath ,
69+ } ) ) {
4570 const selectStart = Math . max ( rowStart - groupStart , 0 )
4671 const selectEnd = Math . min ( rowEnd - groupStart , groupRows )
47-
48- // page-level pruning: split the group selection into candidate sub-ranges.
49- // an empty list of sub-ranges skips the group entirely.
50- const pageRanges = pageRangesByGroup ?. [ rgIdx ]
51- const pageLocations = pageLocationsByGroup ?. [ rgIdx ]
72+ const pageRanges = pageRangesByGroup ?. [ groupIndex ]
73+ const pageLocations = pageLocationsByGroup ?. [ groupIndex ]
5274 /** @type {PageRanges } */
53- let subranges = pageRanges
75+ let ranges = pageRanges
5476 ? pageRanges
5577 . map ( ( [ start , end ] ) => {
5678 /** @type {[number, number] } */
@@ -60,105 +82,97 @@ export function parquetPlan({ metadata, rowStart = 0, rowEnd = Infinity, columns
6082 . filter ( ( [ start , end ] ) => start < end )
6183 : [ [ selectStart , selectEnd ] ]
6284
63- // splitting requires page reads for every included chunk, or full chunks
64- // would be fetched once per sub-range; collapse to one covering range otherwise
65- if ( subranges . length > 1 ) {
85+ if ( ranges . length > 1 ) {
6686 const canSplit = rowGroup . columns . every ( chunk => {
6787 const columnName = chunk . meta_data ?. path_in_schema [ 0 ]
6888 const columnPath = chunk . meta_data ?. path_in_schema . join ( '.' )
6989 if ( columns && columnName && ! columns . includes ( columnName ) ) return true
7090 return ! ! ( chunk . offset_index_offset && chunk . offset_index_length ) || ! ! ( columnPath && pageLocations ?. [ columnPath ] )
7191 } )
72- if ( ! canSplit ) {
73- subranges = [ [ subranges [ 0 ] [ 0 ] , subranges [ subranges . length - 1 ] [ 1 ] ] ]
74- } else {
75- subranges = coalesceOverlappingPageRanges ( subranges , rowGroup , columns , pageLocations )
76- }
92+ ranges = canSplit
93+ ? coalesceOverlappingPageRanges ( ranges , rowGroup , columns , pageLocations )
94+ : [ [ ranges [ 0 ] [ 0 ] , ranges [ ranges . length - 1 ] [ 1 ] ] ]
7795 }
78-
79- if ( subranges . length ) {
80- /** @type {ChunkPlan[] } */
81- const chunks = [ ]
82- // Multiple sub-ranges are necessarily narrower than the whole group.
83- const narrowed = subranges . length > 1 ||
84- subranges [ 0 ] [ 0 ] > 0 || subranges [ 0 ] [ 1 ] < groupRows
85- // loop through each column chunk
86- for ( const chunk of rowGroup . columns ) {
87- const meta = chunk . meta_data
88- if ( chunk . file_path ) throw new Error ( 'parquet file_path not supported' )
89- if ( ! meta ) throw new Error ( 'parquet column metadata is undefined' )
90- // add included column chunks to the plan
91- if ( ! columns || columns . includes ( meta . path_in_schema [ 0 ] ) ) {
92- // full column chunk
93- const columnOffset = meta . dictionary_page_offset || meta . data_page_offset
94- const startByte = Number ( columnOffset )
95- const endByte = Number ( columnOffset + meta . total_compressed_size )
96- const chunkPageLocations = pageLocations ?. [ meta . path_in_schema . join ( '.' ) ]
97-
98- if ( chunkPageLocations && narrowed ) {
99- // page locations already parsed during page index prefetch
100- chunks . push ( {
101- columnMetadata : meta ,
102- pageLocations : chunkPageLocations ,
103- range : { startByte, endByte } ,
104- } )
105- } else if ( ( useOffsetIndex || pageRanges ) && chunk . offset_index_offset && chunk . offset_index_length && narrowed ) {
106- const offsetIndexStart = Number ( chunk . offset_index_offset )
107- chunks . push ( {
108- columnMetadata : meta ,
109- offsetIndex : {
110- startByte : offsetIndexStart ,
111- endByte : offsetIndexStart + chunk . offset_index_length ,
112- } ,
113- range : { startByte, endByte } ,
114- } )
115- } else {
116- chunks . push ( {
117- columnMetadata : meta ,
118- range : { startByte, endByte } ,
119- } )
120- }
121-
122- }
123- }
124-
125- for ( const [ subStart , subEnd ] of subranges ) {
126- groups . push ( { chunks, rowGroup, groupStart, groupRows, selectStart : subStart , selectEnd : subEnd } )
127- }
128-
129- // combine runs of column chunks
130- /** @type {ByteRange | undefined } */
131- let run
132- for ( const chunk of chunks ) {
133- if ( 'pageLocations' in chunk ) {
134- // pages are fetched on demand in readRowGroup
135- } else if ( 'offsetIndex' in chunk ) {
136- indexes . push ( chunk . offsetIndex )
137- } else {
138- const { range } = chunk
139- if ( columns ) {
140- fetches . push ( range )
141- } else if ( run && range . endByte - run . startByte <= runLimit ) {
142- // extend range
143- run . endByte = range . endByte
144- } else {
145- // new range
146- if ( run ) fetches . push ( run )
147- run = { ...range }
148- }
149- }
150- }
151- if ( run ) fetches . push ( run )
96+ if ( ranges . length ) {
97+ groups . push ( { rowGroup, groupIndex, groupStart, groupRows, ranges, pageRanges, pageLocations } )
15298 }
15399 }
154-
155100 groupStart = groupEnd
156- rgIdx ++
157101 }
158- if ( ! isFinite ( rowEnd ) ) rowEnd = groupStart
159- fetches . push ( ...indexes )
102+ return { groups, rowEnd : isFinite ( rowEnd ) ? rowEnd : groupStart }
103+ }
104+
105+ /**
106+ * Build byte plans for retained ranges in one row group.
107+ *
108+ * @param {object } options
109+ * @param {RowGroup } options.rowGroup
110+ * @param {number } options.groupStart
111+ * @param {number } options.groupRows
112+ * @param {PageRanges } options.ranges
113+ * @param {string[] } [options.columns]
114+ * @param {boolean } [options.useOffsetIndex]
115+ * @param {PageRanges } [options.pageRanges]
116+ * @param {Record<string, PageLocation[]> } [options.pageLocations]
117+ * @returns {{groups: GroupPlan[], fetches: ByteRange[], indexes: ByteRange[]} }
118+ */
119+ export function parquetPlanGroup ( { rowGroup, groupStart, groupRows, ranges, columns, useOffsetIndex = false , pageRanges, pageLocations } ) {
120+ /** @type {ChunkPlan[] } */
121+ const chunks = [ ]
122+ /** @type {ByteRange[] } */
123+ const fetches = [ ]
124+ /** @type {ByteRange[] } */
125+ const indexes = [ ]
126+ const narrowed = ranges . length > 1 || ranges [ 0 ] [ 0 ] > 0 || ranges [ 0 ] [ 1 ] < groupRows
127+ for ( const chunk of rowGroup . columns ) {
128+ const meta = chunk . meta_data
129+ if ( chunk . file_path ) throw new Error ( 'parquet file_path not supported' )
130+ if ( ! meta ) throw new Error ( 'parquet column metadata is undefined' )
131+ if ( columns && ! columns . includes ( meta . path_in_schema [ 0 ] ) ) continue
132+ const columnOffset = meta . dictionary_page_offset || meta . data_page_offset
133+ const startByte = Number ( columnOffset )
134+ const endByte = Number ( columnOffset + meta . total_compressed_size )
135+ const chunkPageLocations = pageLocations ?. [ meta . path_in_schema . join ( '.' ) ]
160136
161- return { metadata, rowStart, rowEnd, columns, fetches, groups }
137+ if ( chunkPageLocations && narrowed ) {
138+ chunks . push ( { columnMetadata : meta , pageLocations : chunkPageLocations , range : { startByte, endByte } } )
139+ } else if ( ( useOffsetIndex || pageRanges ) && chunk . offset_index_offset && chunk . offset_index_length && narrowed ) {
140+ const startByte = Number ( chunk . offset_index_offset )
141+ chunks . push ( {
142+ columnMetadata : meta ,
143+ offsetIndex : { startByte, endByte : startByte + chunk . offset_index_length } ,
144+ range : { startByte : Number ( columnOffset ) , endByte } ,
145+ } )
146+ } else {
147+ chunks . push ( { columnMetadata : meta , range : { startByte, endByte } } )
148+ }
149+ }
150+
151+ /** @type {ByteRange | undefined } */
152+ let run
153+ for ( const chunk of chunks ) {
154+ if ( 'pageLocations' in chunk ) continue
155+ if ( 'offsetIndex' in chunk ) {
156+ indexes . push ( chunk . offsetIndex )
157+ } else if ( columns ) {
158+ fetches . push ( chunk . range )
159+ } else if ( run && chunk . range . endByte - run . startByte <= runLimit ) {
160+ run . endByte = chunk . range . endByte
161+ } else {
162+ if ( run ) fetches . push ( run )
163+ run = { ...chunk . range }
164+ }
165+ }
166+ if ( run ) fetches . push ( run )
167+ const groups = ranges . map ( ( [ selectStart , selectEnd ] ) => ( {
168+ chunks,
169+ rowGroup,
170+ groupStart,
171+ groupRows,
172+ selectStart,
173+ selectEnd,
174+ } ) )
175+ return { groups, fetches, indexes }
162176}
163177
164178/**
0 commit comments