@@ -13,18 +13,22 @@ import (
1313 "github.com/apache/arrow-go/v18/parquet/pqarrow"
1414)
1515
16- // ParquetReader reads data from Parquet files
16+ // ParquetReader reads data from Parquet files using row-group streaming
17+ // to avoid loading the entire file into memory
1718type ParquetReader struct {
1819 file * os.File
1920 reader * file.Reader
2021 arrowReader * pqarrow.FileReader
2122 schema * arrow.Schema
2223 columnNames []string
2324 columnTypes []string
24- rowGroupIdx int
25- currentBatch arrow.RecordBatch
26- batchIdx int
25+ allColumns []int // Explicit list of all leaf column indices; nil means "no columns" in pqarrow
26+ rowGroupIdx int // Current row group being processed
27+ numRowGroups int // Total number of row groups
28+ currentBatch arrow.RecordBatch // Current record batch
29+ batchIdx int // Current row index within current batch
2730 totalRows int64
31+ exhausted bool // True when all data has been read
2832}
2933
3034// NewParquetReader creates a new Parquet reader
@@ -73,13 +77,23 @@ func NewParquetReader(filename string) (*ParquetReader, error) {
7377 }
7478
7579
80+ // Build explicit list of all leaf column indices. pqarrow.ReadRowGroups
81+ // treats nil as "no columns", not "all columns" — see ReadTable in
82+ // vendor/.../pqarrow/file_reader.go for the equivalent pattern.
83+ numCols := reader .MetaData ().Schema .NumColumns ()
84+ allColumns := make ([]int , numCols )
85+ for i := range allColumns {
86+ allColumns [i ] = i
87+ }
88+
7689 return & ParquetReader {
7790 file : f ,
7891 reader : reader ,
7992 arrowReader : arrowReader ,
8093 schema : schema ,
8194 columnNames : columnNames ,
8295 columnTypes : columnTypes ,
96+ allColumns : allColumns ,
8397 totalRows : reader .NumRows (),
8498 }, nil
8599}
@@ -104,63 +118,83 @@ func (r *ParquetReader) NumRowGroups() int {
104118 return r .reader .NumRowGroups ()
105119}
106120
107- // ReadBatch reads the next batch of rows
121+ // ReadBatch reads the next batch of rows using row-group streaming
122+ // to avoid loading the entire file into memory
108123func (r * ParquetReader ) ReadBatch (batchSize int ) ([]map [string ]any , error ) {
109124 ctx := context .Background ()
110125
111- // If we don't have a current batch or we've consumed it, get the next batch
112- if r .currentBatch == nil || r .batchIdx >= int (r .currentBatch .NumRows ()) {
113- // For the first batch, read the entire table
114- // Note: Row group reading seems to have issues with nested types
115- if r .rowGroupIdx == 0 {
116- table , err := r .arrowReader .ReadTable (ctx )
126+ // Initialize numRowGroups on first call
127+ if r .numRowGroups == 0 {
128+ r .numRowGroups = r .reader .NumRowGroups ()
129+ }
130+
131+ // Check if we've exhausted all data
132+ if r .exhausted {
133+ return nil , io .EOF
134+ }
135+
136+ rows := make ([]map [string ]any , 0 , batchSize )
137+
138+ for len (rows ) < batchSize {
139+ // If we don't have a current batch or we've consumed it, get the next one
140+ if r .currentBatch == nil || r .batchIdx >= int (r .currentBatch .NumRows ()) {
141+ // Release previous batch if any
142+ if r .currentBatch != nil {
143+ r .currentBatch .Release ()
144+ r .currentBatch = nil
145+ }
146+
147+ // Move to next row group if needed
148+ if r .rowGroupIdx >= r .numRowGroups {
149+ r .exhausted = true
150+ break
151+ }
152+
153+ // Read the next row group
154+ table , err := r .arrowReader .ReadRowGroups (ctx , r .allColumns , []int {r .rowGroupIdx })
117155 if err != nil {
118- return nil , fmt .Errorf ("failed to read table : %w" , err )
156+ return nil , fmt .Errorf ("failed to read row group %d : %w" , r . rowGroupIdx , err )
119157 }
120- r .rowGroupIdx ++ // Mark that we've read the table
158+ r .rowGroupIdx ++
121159
122160 if table .NumRows () == 0 {
123161 table .Release ()
124- return nil , io . EOF
162+ continue // Try next row group
125163 }
126164
127- // Create a record batch from the table
165+ // Create a record reader from the table
128166 chunkSize := int64 (batchSize )
129167 if chunkSize <= 0 || chunkSize > table .NumRows () {
130168 chunkSize = table .NumRows ()
131169 }
132- reader := array .NewTableReader (table , chunkSize )
170+ tableReader := array .NewTableReader (table , chunkSize )
133171
134- if reader .Next () {
135- r .currentBatch = reader .RecordBatch ()
136- r .currentBatch .Retain () // Keep the record alive
172+ if tableReader .Next () {
173+ r .currentBatch = tableReader .RecordBatch ()
174+ r .currentBatch .Retain () // Keep the record alive after releasing tableReader
137175 r .batchIdx = 0
138- } else {
139- table .Release ()
140- reader .Release ()
141- return nil , fmt .Errorf ("failed to read record batch from table with %d rows" , table .NumRows ())
142176 }
143- reader .Release ()
177+ tableReader .Release ()
144178 table .Release ()
145- } else {
146- return nil , io .EOF
179+
180+ if r .currentBatch == nil {
181+ continue // Try next row group
182+ }
147183 }
148- }
149184
150- // Extract rows from the current batch
151- rows := make ([]map [string ]any , 0 , batchSize )
185+ // Extract rows from the current batch
186+ for len (rows ) < batchSize && r .batchIdx < int (r .currentBatch .NumRows ()) {
187+ row := make (map [string ]any )
152188
153- for i := 0 ; i < batchSize && r .batchIdx < int (r .currentBatch .NumRows ()); i ++ {
154- row := make (map [string ]any )
189+ for colIdx , colName := range r .columnNames {
190+ col := r .currentBatch .Column (colIdx )
191+ value := extractValue (col , r .batchIdx )
192+ row [colName ] = value
193+ }
155194
156- for colIdx , colName := range r .columnNames {
157- col := r .currentBatch .Column (colIdx )
158- value := extractValue (col , r .batchIdx )
159- row [colName ] = value
195+ rows = append (rows , row )
196+ r .batchIdx ++
160197 }
161-
162- rows = append (rows , row )
163- r .batchIdx ++
164198 }
165199
166200 if len (rows ) == 0 {
0 commit comments