| import { bloomEligibleColumns, readBloomFilter } from './bloom.js' |
| import { canSkipRowGroup } from './filter.js' |
| import { parquetSchema } from './metadata.js' |
| import { getPhysicalColumns } from './schema.js' |
|
|
| |
| |
| |
|
|
| |
| const runLimit = 1 << 21 |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function parquetPlan({ metadata, rowStart = 0, rowEnd = Infinity, columns, filter, filterStrict = true, useOffsetIndex = false, bloomFiltersByGroup, schemaElements }) { |
| if (!metadata) throw new Error('parquetPlan requires metadata') |
| |
| const groups = [] |
| |
| const fetches = [] |
| |
| const indexes = [] |
| const physicalColumns = getPhysicalColumns(parquetSchema(metadata)) |
|
|
| |
| let groupStart = 0 |
| let rgIdx = 0 |
| for (const rowGroup of metadata.row_groups) { |
| const groupRows = Number(rowGroup.num_rows) |
| const groupEnd = groupStart + groupRows |
| const bloomFilters = bloomFiltersByGroup?.[rgIdx] |
| |
| if (groupRows > 0 && groupEnd > rowStart && groupStart < rowEnd && !canSkipRowGroup({ rowGroup, physicalColumns, filter, strict: filterStrict, bloomFilters, schemaElements })) { |
| |
| const chunks = [] |
| let groupStartByte = Infinity |
| let groupEndByte = -Infinity |
| |
| for (const chunk of rowGroup.columns) { |
| const meta = chunk.meta_data |
| if (chunk.file_path) throw new Error('parquet file_path not supported') |
| if (!meta) throw new Error('parquet column metadata is undefined') |
| |
| if (!columns || columns.includes(meta.path_in_schema[0])) { |
| |
| const columnOffset = meta.dictionary_page_offset || meta.data_page_offset |
| const startByte = Number(columnOffset) |
| const endByte = Number(columnOffset + meta.total_compressed_size) |
| |
| if (startByte < groupStartByte) groupStartByte = startByte |
| if (endByte > groupEndByte) groupEndByte = endByte |
|
|
| if (useOffsetIndex && chunk.offset_index_offset && chunk.offset_index_length && (rowStart > groupStart || rowEnd < groupEnd)) { |
| const offsetIndexStart = Number(chunk.offset_index_offset) |
| chunks.push({ |
| columnMetadata: meta, |
| offsetIndex: { |
| startByte: offsetIndexStart, |
| endByte: offsetIndexStart + chunk.offset_index_length, |
| }, |
| range: { startByte, endByte }, |
| }) |
| } else { |
| chunks.push({ |
| columnMetadata: meta, |
| range: { startByte, endByte }, |
| }) |
| } |
|
|
| } |
| } |
| const selectStart = Math.max(rowStart - groupStart, 0) |
| const selectEnd = Math.min(rowEnd - groupStart, groupRows) |
| groups.push({ chunks, rowGroup, groupStart, groupRows, selectStart, selectEnd }) |
|
|
| |
| |
| let run |
| for (const chunk of chunks) { |
| if ('offsetIndex' in chunk) { |
| indexes.push(chunk.offsetIndex) |
| } else { |
| const { range } = chunk |
| if (columns) { |
| fetches.push(range) |
| } else if (run && range.endByte - run.startByte <= runLimit) { |
| |
| run.endByte = range.endByte |
| } else { |
| |
| if (run) fetches.push(run) |
| run = { ...range } |
| } |
| } |
| } |
| if (run) fetches.push(run) |
| } |
|
|
| groupStart = groupEnd |
| rgIdx++ |
| } |
| if (!isFinite(rowEnd)) rowEnd = groupStart |
| fetches.push(...indexes) |
|
|
| return { metadata, rowStart, rowEnd, columns, fetches, groups } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function prefetchBloomFilters({ file, metadata, filter, filterStrict = true }) { |
| const result = metadata.row_groups.map(() => ({})) |
| const eligibleCols = bloomEligibleColumns(filter) |
| if (eligibleCols.size === 0) return result |
| const physicalColumns = getPhysicalColumns(parquetSchema(metadata)) |
|
|
| |
| const tasks = [] |
| metadata.row_groups.forEach((rowGroup, rgIdx) => { |
| if (canSkipRowGroup({ rowGroup, physicalColumns, filter, strict: filterStrict })) return |
| for (const colName of eligibleCols) { |
| const columnIdx = physicalColumns.indexOf(colName) |
| if (columnIdx === -1) continue |
| const meta = rowGroup.columns[columnIdx]?.meta_data |
| if (!meta?.bloom_filter_offset || !meta.bloom_filter_length) continue |
| const start = Number(meta.bloom_filter_offset) |
| const end = start + meta.bloom_filter_length |
| tasks.push((async () => { |
| const buffer = await file.slice(start, end) |
| const bloom = readBloomFilter({ view: new DataView(buffer), offset: 0 }) |
| if (bloom) result[rgIdx][colName] = bloom |
| })()) |
| } |
| }) |
|
|
| if (tasks.length) await Promise.all(tasks) |
| return result |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function prefetchAsyncBuffer(file, { fetches }) { |
| |
| const promises = fetches.map(({ startByte, endByte }) => file.slice(startByte, endByte)) |
| return { |
| byteLength: file.byteLength, |
| slice(start, end = file.byteLength) { |
| |
| const index = fetches.findIndex(({ startByte, endByte }) => startByte <= start && end <= endByte) |
| if (index < 0) { |
| |
| return file.slice(start, end) |
| } |
| if (fetches[index].startByte !== start || fetches[index].endByte !== end) { |
| |
| const startOffset = start - fetches[index].startByte |
| const endOffset = end - fetches[index].startByte |
| if (promises[index] instanceof Promise) { |
| return promises[index].then(buffer => buffer.slice(startOffset, endOffset)) |
| } else { |
| return promises[index].slice(startOffset, endOffset) |
| } |
| } else { |
| return promises[index] |
| } |
| }, |
| } |
| } |
|
|