File size: 8,680 Bytes
9ae1216 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | /**
* @import {AsyncColumn, AsyncRowGroup, DecodedArray, GroupPlan, ParquetParsers, ParquetReadOptions, QueryPlan, SchemaTree} from '../src/types.js'
*/
import { assembleNested } from './assemble.js'
import { readColumn } from './column.js'
import { DEFAULT_PARSERS } from './convert.js'
import { readOffsetIndex } from './indexes.js'
import { getSchemaPath } from './schema.js'
import { flatten } from './utils.js'
/**
* Read a row group from a file-like object.
*
* @param {ParquetReadOptions} options
* @param {QueryPlan} plan
* @param {GroupPlan} groupPlan
* @returns {AsyncRowGroup} resolves to column data
*/
export function readRowGroup(options, { metadata }, groupPlan) {
/** @type {AsyncColumn[]} */
const asyncColumns = []
// read column data
for (const chunk of groupPlan.chunks) {
const { data_page_offset, dictionary_page_offset, path_in_schema: pathInSchema } = chunk.columnMetadata
const schemaPath = getSchemaPath(metadata.schema, pathInSchema)
const columnDecoder = {
pathInSchema,
element: schemaPath[schemaPath.length - 1].element,
schemaPath,
parsers: { ...DEFAULT_PARSERS, ...options.parsers },
...options,
...chunk.columnMetadata,
}
let { startByte, endByte } = chunk.range
// non-offset-index case
if (!('offsetIndex' in chunk)) {
asyncColumns.push({
pathInSchema,
data: Promise.resolve(options.file.slice(startByte, endByte))
.then(buffer => {
const reader = { view: new DataView(buffer), offset: 0 }
return readColumn(reader, groupPlan, columnDecoder, options.onPage)
}),
})
continue
}
// offset-index case
asyncColumns.push({
pathInSchema,
// fetch offset index
data: Promise.resolve(options.file.slice(chunk.offsetIndex.startByte, chunk.offsetIndex.endByte))
.then(async arrayBuffer => {
// use offset index to read only necessary pages
const { selectStart, selectEnd } = groupPlan
const pages = readOffsetIndex({ view: new DataView(arrayBuffer), offset: 0 }).page_locations
let skipped = -1
// include dictionary if present, handle polars missing dictionary_page_offset
const hasDict = dictionary_page_offset || data_page_offset < pages[0].offset
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
const pageStart = Number(page.first_row_index)
const pageEnd = i + 1 < pages.length
? Number(pages[i + 1].first_row_index)
: groupPlan.groupRows // last page extends to end of row group
// check if page overlaps with [selectStart, selectEnd)
if (skipped < 0 && !hasDict && pageEnd > selectStart) {
startByte = Number(page.offset)
skipped = pageStart
}
if (pageStart < selectEnd) {
endByte = Number(page.offset) + page.compressed_page_size
}
}
if (skipped < 0) skipped = 0
const buffer = await options.file.slice(startByte, endByte)
const reader = { view: new DataView(buffer), offset: 0 }
// adjust row selection for skipped pages
const adjustedGroupPlan = skipped ? {
...groupPlan,
groupStart: groupPlan.groupStart + skipped,
selectStart: groupPlan.selectStart - skipped,
selectEnd: groupPlan.selectEnd - skipped,
} : groupPlan
const { data, skipped: columnSkipped } = readColumn(reader, adjustedGroupPlan, columnDecoder, options.onPage)
return {
data,
skipped: skipped + columnSkipped,
}
}),
})
}
return { groupStart: groupPlan.groupStart, groupRows: groupPlan.groupRows, asyncColumns }
}
/**
* @overload
* @param {AsyncRowGroup} asyncGroup
* @param {number} selectStart
* @param {number} selectEnd
* @param {string[] | undefined} columns
* @param {'object'} rowFormat
* @returns {Promise<Record<string, any>[]>} resolves to row data
*/
/**
* @overload
* @param {AsyncRowGroup} asyncGroup
* @param {number} selectStart
* @param {number} selectEnd
* @param {string[] | undefined} columns
* @param {'array'} [rowFormat]
* @returns {Promise<any[][]>} resolves to row data
*/
/**
* @param {AsyncRowGroup} asyncGroup
* @param {number} selectStart
* @param {number} selectEnd
* @param {string[] | undefined} columns
* @param {'object' | 'array'} [rowFormat]
* @returns {Promise<Record<string, any>[] | any[][]>} resolves to row data
*/
export async function asyncGroupToRows({ asyncColumns }, selectStart, selectEnd, columns, rowFormat) {
// TODO: do it without flatten
const asyncPages = await Promise.all(asyncColumns.map(column =>
column.data.then(({ skipped, data }) => ({ skipped, data: flatten(data) }))
))
// transpose columns into rows
const selectCount = selectEnd - selectStart
if (rowFormat === 'object') {
/** @type {Record<string, any>[]} */
const groupData = Array(selectCount)
for (let selectRow = 0; selectRow < selectCount; selectRow++) {
// return each row as an object
/** @type {Record<string, any>} */
const rowData = {}
for (let i = 0; i < asyncColumns.length; i++) {
const { data, skipped } = asyncPages[i]
rowData[asyncColumns[i].pathInSchema[0]] = data[selectStart + selectRow - skipped]
}
groupData[selectRow] = rowData
}
return groupData
}
// careful mapping of column order for rowFormat: array
const includedColumnNames = asyncColumns
.map(child => child.pathInSchema[0])
.filter(name => !columns || columns.includes(name))
const columnOrder = columns ?? includedColumnNames
const columnIndexes = columnOrder.map(name => asyncColumns.findIndex(column => column.pathInSchema[0] === name))
/** @type {any[][]} */
const groupData = Array(selectCount)
for (let selectRow = 0; selectRow < selectCount; selectRow++) {
// return each row as an array
const rowData = Array(asyncColumns.length)
for (let i = 0; i < columnOrder.length; i++) {
const colIdx = columnIndexes[i]
if (colIdx < 0) throw new Error(`parquet column not found: ${columnOrder[i]}`)
const { data, skipped } = asyncPages[colIdx]
rowData[i] = data[selectStart + selectRow - skipped]
}
groupData[selectRow] = rowData
}
return groupData
}
/**
* Assemble physical columns into top-level columns asynchronously.
*
* @param {AsyncRowGroup} asyncRowGroup
* @param {SchemaTree} schemaTree
* @param {ParquetParsers} [parsers]
* @returns {AsyncRowGroup}
*/
export function assembleAsync(asyncRowGroup, schemaTree, parsers) {
const { asyncColumns } = asyncRowGroup
parsers = { ...DEFAULT_PARSERS, ...parsers }
/** @type {AsyncColumn[]} */
const assembled = []
for (const child of schemaTree.children) {
if (child.children.length) {
const childColumns = asyncColumns.filter(column => column.pathInSchema[0] === child.element.name)
if (!childColumns.length) continue
assembled.push({
pathInSchema: child.path,
data: (async () => {
// collect subcolumn data — Promise.all observes every rejection so
// a sibling failure cannot leak as an unhandledRejection
const resolved = await Promise.all(childColumns.map(c => c.data))
/** @type {Map<string, DecodedArray>} */
const subcolumnData = new Map()
let minLength = Infinity
for (let i = 0; i < childColumns.length; i++) {
const flat = flatten(resolved[i].data)
subcolumnData.set(childColumns[i].pathInSchema.join('.'), flat)
minLength = Math.min(minLength, flat.length)
}
// trim sub-columns to same length (offset index may read different pages per column)
for (const [key, value] of subcolumnData) {
if (value.length > minLength) {
subcolumnData.set(key, value.slice(0, minLength))
}
}
// assemble the column
assembleNested(subcolumnData, child, parsers)
const assembled = subcolumnData.get(child.element.name)
if (!assembled) throw new Error('parquet column data not assembled')
return { data: [assembled], skipped: 0 }
})(),
})
} else {
// leaf node, return the column
const asyncColumn = asyncColumns.find(column => column.pathInSchema[0] === child.element.name)
if (asyncColumn) assembled.push(asyncColumn)
}
}
return { ...asyncRowGroup, asyncColumns: assembled }
}
|