| |
| |
| |
|
|
| import { columnsNeededForFilter, matchFilter } from './filter.js' |
| import { parquetMetadataAsync, parquetSchema } from './metadata.js' |
| import { parquetPlan, prefetchAsyncBuffer, prefetchBloomFilters } from './plan.js' |
| import { assembleAsync, asyncGroupToRows, readRowGroup } from './rowgroup.js' |
| import { concat } from './utils.js' |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function parquetRead(options) { |
| |
| options.metadata ??= await parquetMetadataAsync(options.file, options) |
|
|
| const { rowStart = 0, rowEnd, columns, onChunk, onComplete, rowFormat, filter, filterStrict = true } = options |
|
|
| |
| if (filter && rowFormat !== 'object') { |
| throw new Error('parquet filter requires rowFormat: "object"') |
| } |
|
|
| |
| const filterColumns = columnsNeededForFilter(filter) |
| if (filterColumns.length) { |
| const schemaColumns = parquetSchema(options.metadata).children.map(c => c.element.name) |
| const missingColumns = filterColumns.filter(c => !schemaColumns.includes(c)) |
| if (missingColumns.length) { |
| throw new Error(`parquet filter columns not found: ${missingColumns.join(', ')}`) |
| } |
| } |
| let readColumns = columns |
| let requiresProjection = false |
| if (columns && filter) { |
| const missingFilterColumns = filterColumns.filter(c => !columns.includes(c)) |
| if (missingFilterColumns.length) { |
| readColumns = [...columns, ...missingFilterColumns] |
| requiresProjection = true |
| } |
| } |
|
|
| |
| let readOptions = readColumns !== columns ? { ...options, columns: readColumns } : options |
| readOptions = await withBloomFilters(readOptions) |
| const asyncGroups = parquetReadAsync(readOptions) |
|
|
| |
| if (!onComplete && !onChunk) { |
| await awaitAllColumns(asyncGroups) |
| return |
| } |
|
|
| |
| const schemaTree = parquetSchema(options.metadata) |
| const assembled = asyncGroups.map(arg => assembleAsync(arg, schemaTree, options.parsers)) |
|
|
| |
| if (onChunk) { |
| for (const asyncGroup of assembled) { |
| for (const asyncColumn of asyncGroup.asyncColumns) { |
| asyncColumn.data.then(({ data, skipped }) => { |
| let rowStart = asyncGroup.groupStart + skipped |
| for (const columnData of data) { |
| onChunk({ |
| columnName: asyncColumn.pathInSchema[0], |
| columnData, |
| rowStart, |
| rowEnd: rowStart + columnData.length, |
| }) |
| rowStart += columnData.length |
| } |
| }, () => {}) |
| } |
| } |
| } |
|
|
| |
| if (onComplete) { |
| |
| await awaitAllColumns(assembled) |
| |
| |
| const rows = [] |
| for (const asyncGroup of assembled) { |
| |
| const selectStart = Math.max(rowStart - asyncGroup.groupStart, 0) |
| const selectEnd = Math.min((rowEnd ?? Infinity) - asyncGroup.groupStart, asyncGroup.groupRows) |
| |
| const groupData = rowFormat === 'object' ? |
| await asyncGroupToRows(asyncGroup, selectStart, selectEnd, readColumns, 'object') : |
| await asyncGroupToRows(asyncGroup, selectStart, selectEnd, columns, 'array') |
|
|
| |
| if (filter) { |
| |
| for (const row of (groupData)) { |
| if (matchFilter(row, filter, filterStrict)) { |
| if (requiresProjection && columns) { |
| for (const col of filterColumns) { |
| if (!columns.includes(col)) delete row[col] |
| } |
| } |
| rows.push(row) |
| } |
| } |
| } else { |
| concat(rows, groupData) |
| } |
| } |
| onComplete(rows) |
| } else { |
| |
| await awaitAllColumns(assembled) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function awaitAllColumns(asyncGroups) { |
| const all = asyncGroups.flatMap(g => g.asyncColumns.map(c => c.data)) |
| const results = await Promise.allSettled(all) |
| const failed = results.find(r => r.status === 'rejected') |
| if (failed) throw failed.reason |
| } |
|
|
| |
| |
| |
| |
| export function parquetReadAsync(options) { |
| if (!options.metadata) throw new Error('parquet requires metadata') |
| |
|
|
| |
| const plan = parquetPlan(options) |
| options.file = prefetchAsyncBuffer(options.file, plan) |
|
|
| |
| return plan.groups.map(groupPlan => readRowGroup(options, plan, groupPlan)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function parquetReadColumn(options) { |
| if (options.columns?.length !== 1) { |
| throw new Error('parquetReadColumn expected columns: [columnName]') |
| } |
| options.metadata ??= await parquetMetadataAsync(options.file, options) |
| const asyncGroups = parquetReadAsync(await withBloomFilters(options)) |
|
|
| |
| const schemaTree = parquetSchema(options.metadata) |
| const assembled = asyncGroups.map(arg => assembleAsync(arg, schemaTree, options.parsers)) |
|
|
| |
| await awaitAllColumns(assembled) |
|
|
| |
| const columnData = [] |
| for (const rg of assembled) { |
| const { data } = await rg.asyncColumns[0].data |
| for (const chunk of data) { |
| concat(columnData, chunk) |
| } |
| } |
| return columnData |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function withBloomFilters(options) { |
| if (!options.useBloomFilters) return options |
| if (!options.filter || !options.metadata) return options |
| const schemaTree = parquetSchema(options.metadata) |
| |
| const schemaElements = {} |
| for (const child of schemaTree.children) schemaElements[child.element.name] = child.element |
| const bloomFiltersByGroup = await prefetchBloomFilters({ |
| file: options.file, |
| metadata: options.metadata, |
| filter: options.filter, |
| filterStrict: options.filterStrict, |
| }) |
| |
| return ({ ...options, bloomFiltersByGroup, schemaElements }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function parquetReadObjects(options) { |
| return new Promise((onComplete, reject) => { |
| parquetRead({ |
| ...options, |
| rowFormat: 'object', |
| onComplete, |
| }).catch(reject) |
| }) |
| } |
|
|