Skip to content

Commit ab210da

Browse files
committed
parquetMetadata move file into options object
1 parent 3ae587e commit ab210da

13 files changed

Lines changed: 64 additions & 66 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Rename `onPage.columnName: string` to `onPage.pathInSchema: string[]`
77
- Remove `rowFormat` and always return rows as objects
88
- Change `byteLengthFromUrl` to accept options object
9+
- Move `file` parameter to options object in `parquetMetadata`
910

1011
## [1.23.3]
1112
- Fix `readColumn` truncation in struct columns (#148)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ You can read just the metadata, including schema and data statistics using the `
7474
import { parquetMetadata, parquetSchema } from 'hyparquet'
7575

7676
const file = await asyncBufferFromUrl({ url })
77-
const metadata = await parquetMetadata(file)
77+
const metadata = await parquetMetadata({ file })
7878
// Get total number of rows (convert bigint to number)
7979
const numRows = Number(metadata.num_rows)
8080
// Get nested table schema

src/metadata.js

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,15 @@ function decode(/** @type {Uint8Array} */ value) {
3131
* This ensures that we either make one 512kb initial request for the metadata,
3232
* or a second request for up to the metadata size.
3333
*
34-
* @param {AsyncBuffer} asyncBuffer parquet file contents
35-
* @param {MetadataOptions & { initialFetchSize?: number }} options initial fetch size in bytes (default 512kb)
34+
* @param {MetadataOptions & { file: AsyncBuffer, initialFetchSize?: number }} options parquet file and metadata options
3635
* @returns {Promise<FileMetaData>} parquet metadata object
3736
*/
38-
export async function parquetMetadata(asyncBuffer, { parsers, initialFetchSize = defaultInitialFetchSize, geoparquet = true } = {}) {
39-
if (!asyncBuffer || !(asyncBuffer.byteLength >= 0)) throw new Error('parquet expected AsyncBuffer')
37+
export async function parquetMetadata({ file, parsers, initialFetchSize = defaultInitialFetchSize, geoparquet = true }) {
38+
if (!file || !(file.byteLength >= 0)) throw new Error('parquet expected AsyncBuffer')
4039

4140
// fetch last bytes (footer) of the file
42-
const footerOffset = Math.max(0, asyncBuffer.byteLength - initialFetchSize)
43-
const footerBuffer = await asyncBuffer.slice(footerOffset, asyncBuffer.byteLength)
41+
const footerOffset = Math.max(0, file.byteLength - initialFetchSize)
42+
const footerBuffer = await file.slice(footerOffset, file.byteLength)
4443

4544
// Check for parquet magic number "PAR1"
4645
const footerView = new DataView(footerBuffer)
@@ -51,38 +50,36 @@ export async function parquetMetadata(asyncBuffer, { parsers, initialFetchSize =
5150
// Parquet files store metadata at the end of the file
5251
// Metadata length is 4 bytes before the last PAR1
5352
const metadataLength = footerView.getUint32(footerBuffer.byteLength - 8, true)
54-
if (metadataLength > asyncBuffer.byteLength - 8) {
55-
throw new Error(`parquet metadata length ${metadataLength} exceeds available buffer ${asyncBuffer.byteLength - 8}`)
53+
if (metadataLength > file.byteLength - 8) {
54+
throw new Error(`parquet metadata length ${metadataLength} exceeds available buffer ${file.byteLength - 8}`)
5655
}
5756

5857
// check if metadata size fits inside the initial fetch
5958
if (metadataLength + 8 > initialFetchSize) {
6059
// fetch the rest of the metadata
61-
const metadataOffset = asyncBuffer.byteLength - metadataLength - 8
62-
const metadataBuffer = await asyncBuffer.slice(metadataOffset, footerOffset)
60+
const metadataOffset = file.byteLength - metadataLength - 8
61+
const metadataBuffer = await file.slice(metadataOffset, footerOffset)
6362
// combine initial fetch with the new slice
6463
const combinedBuffer = new ArrayBuffer(metadataLength + 8)
6564
const combinedView = new Uint8Array(combinedBuffer)
6665
combinedView.set(new Uint8Array(metadataBuffer))
6766
combinedView.set(new Uint8Array(footerBuffer), footerOffset - metadataOffset)
68-
return parquetMetadataSync(combinedBuffer, { parsers, geoparquet })
67+
return parquetMetadataSync({ file: combinedBuffer, parsers, geoparquet })
6968
} else {
7069
// parse metadata from the footer
71-
return parquetMetadataSync(footerBuffer, { parsers, geoparquet })
70+
return parquetMetadataSync({ file: footerBuffer, parsers, geoparquet })
7271
}
7372
}
7473

7574
/**
7675
* Read parquet metadata from a buffer synchronously.
7776
*
78-
* @import {KeyValue} from '../src/types.d.ts'
79-
* @param {ArrayBuffer} arrayBuffer parquet file footer
80-
* @param {MetadataOptions} options metadata parsing options
77+
* @param {MetadataOptions & { file: ArrayBuffer }} options parquet file and metadata options
8178
* @returns {FileMetaData} parquet metadata object
8279
*/
83-
export function parquetMetadataSync(arrayBuffer, { parsers, geoparquet = true } = {}) {
84-
if (!(arrayBuffer instanceof ArrayBuffer)) throw new Error('parquet expected ArrayBuffer')
85-
const view = new DataView(arrayBuffer)
80+
export function parquetMetadataSync({ file, parsers, geoparquet = true }) {
81+
if (!(file instanceof ArrayBuffer)) throw new Error('parquet expected ArrayBuffer')
82+
const view = new DataView(file)
8683

8784
// Use default parsers if not given
8885
parsers = { ...DEFAULT_PARSERS, ...parsers }
@@ -287,7 +284,7 @@ function timeUnit(unit) {
287284
/**
288285
* Convert column statistics based on column type.
289286
*
290-
* @import {AsyncBuffer, FileMetaData, LogicalType, MetadataOptions, MinMaxType, ParquetParsers, SchemaElement, SchemaTree, Statistics, TimeUnit} from '../src/types.d.ts'
287+
* @import {AsyncBuffer, FileMetaData, KeyValue, LogicalType, MetadataOptions, MinMaxType, ParquetParsers, SchemaElement, SchemaTree, Statistics, TimeUnit} from '../src/types.d.ts'
291288
* @param {any} stats
292289
* @param {SchemaElement} schema
293290
* @param {ParquetParsers} parsers

src/query.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export async function parquetQuery(options) {
1717
if (!options.file || !(options.file.byteLength >= 0)) {
1818
throw new Error('parquet expected AsyncBuffer')
1919
}
20-
options.metadata ??= await parquetMetadata(options.file, options)
20+
options.metadata ??= await parquetMetadata(options)
2121

2222
const { metadata, rowStart = 0, columns, orderBy, filter } = options
2323
if (rowStart < 0) throw new Error('parquet rowStart must be positive')
@@ -102,14 +102,13 @@ export async function parquetQuery(options) {
102102
* @returns {Promise<(Record<string, any> & {__index__: number})[]>}
103103
*/
104104
async function parquetReadRows(options) {
105-
const { file, rows } = options
106-
options.metadata ??= await parquetMetadata(file, options)
105+
options.metadata ??= await parquetMetadata(options)
107106
const { row_groups: rowGroups } = options.metadata
108107
// Compute row groups to fetch
109108
const groupIncluded = Array(rowGroups.length).fill(false)
110109
let groupStart = 0
111110
const groupEnds = rowGroups.map(group => groupStart += Number(group.num_rows))
112-
for (const index of rows) {
111+
for (const index of options.rows) {
113112
const groupIndex = groupEnds.findIndex(end => index < end)
114113
groupIncluded[groupIndex] = true
115114
}

src/read.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { concat, flatten } from './utils.js'
2121
*/
2222
export async function parquetRead(options) {
2323
// load metadata if not provided
24-
options.metadata ??= await parquetMetadata(options.file, options)
24+
options.metadata ??= await parquetMetadata(options)
2525

2626
const { rowStart = 0, rowEnd, columns, onChunk, onComplete, filter, filterStrict = true } = options
2727

@@ -143,7 +143,7 @@ export async function parquetReadColumn(options) {
143143
if (options.columns?.length !== 1) {
144144
throw new Error('parquetReadColumn expected columns: [columnName]')
145145
}
146-
options.metadata ??= await parquetMetadata(options.file, options)
146+
options.metadata ??= await parquetMetadata(options)
147147
const asyncGroups = parquetReadAsync(options)
148148

149149
// assemble struct columns

test/column.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('readColumn', () => {
1616
const testFile = 'test/files/float16_nonzeros_and_nans.parquet'
1717
const file = await asyncBufferFromFile(testFile)
1818
const arrayBuffer = await file.slice(0)
19-
const metadata = await parquetMetadata(arrayBuffer)
19+
const metadata = await parquetMetadata({ file })
2020

2121
const column = metadata.row_groups[0].columns[0]
2222
if (!column.meta_data) throw new Error(`No column metadata for ${testFile}`)
@@ -47,7 +47,7 @@ describe('readColumn', () => {
4747
const testFile = 'test/files/datapage_v2.snappy.parquet'
4848
const file = await asyncBufferFromFile(testFile)
4949
const arrayBuffer = await file.slice(0)
50-
const metadata = await parquetMetadata(arrayBuffer)
50+
const metadata = await parquetMetadata({ file })
5151

5252
const column = metadata.row_groups[0].columns[1] // second column
5353
if (!column.meta_data) throw new Error(`No column metadata for ${testFile}`)

test/indexes.test.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ describe('readColumnIndex', () => {
1010
const columnIndexesFiles = fs.readdirSync('test/files').filter(f => f.endsWith('.column_indexes.json'))
1111
const parquetFiles = columnIndexesFiles.map(f => f.replace(/.column_indexes.json$/i, '.parquet'))
1212

13-
parquetFiles.forEach((file, i) => {
14-
it(`parse column indexes from ${file}`, async () => {
15-
const arrayBuffer = await readFileToArrayBuffer(`test/files/${file}`)
16-
const metadata = await parquetMetadata(arrayBuffer)
13+
parquetFiles.forEach((parquetFile, i) => {
14+
it(`parse column indexes from ${parquetFile}`, async () => {
15+
const file = await readFileToArrayBuffer(`test/files/${parquetFile}`)
16+
const metadata = await parquetMetadata({ file })
1717

1818
const result = metadata.row_groups.map((rowGroup) => rowGroup.columns.map((column) => {
1919
if (column.column_index_offset === undefined || column.column_index_length === undefined) return null
2020
const columnIndexOffset = Number(column.column_index_offset)
2121
const columnIndexLength = Number(column.column_index_length)
22-
const columnIndexArrayBuffer = arrayBuffer.slice(columnIndexOffset, columnIndexOffset + columnIndexLength)
22+
const columnIndexArrayBuffer = file.slice(columnIndexOffset, columnIndexOffset + columnIndexLength)
2323
const columnIndexReader = { view: new DataView(columnIndexArrayBuffer), offset: 0 }
2424
const schemaPath = getSchemaPath(metadata.schema, column.meta_data?.path_in_schema ?? [])
2525
return readColumnIndex(columnIndexReader, schemaPath.at(-1)?.element || { name: '' })
@@ -34,16 +34,16 @@ describe('readOffsetIndex', () => {
3434
const offsetIndexesFiles = fs.readdirSync('test/files').filter(f => f.endsWith('.offset_indexes.json'))
3535
const parquetFiles = offsetIndexesFiles.map(f => f.replace(/.offset_indexes.json$/i, '.parquet'))
3636

37-
parquetFiles.forEach((file, i) => {
38-
it(`parse offset indexes from ${file}`, async () => {
39-
const arrayBuffer = await readFileToArrayBuffer(`test/files/${file}`)
40-
const metadata = await parquetMetadata(arrayBuffer)
37+
parquetFiles.forEach((parquetFile, i) => {
38+
it(`parse offset indexes from ${parquetFile}`, async () => {
39+
const file = await readFileToArrayBuffer(`test/files/${parquetFile}`)
40+
const metadata = await parquetMetadata({ file })
4141

4242
const result = metadata.row_groups.map((rowGroup) => rowGroup.columns.map((column) => {
4343
if (column.offset_index_offset === undefined || column.offset_index_length === undefined) return null
4444
const offsetIndexOffset = Number(column.offset_index_offset)
4545
const offsetIndexLength = Number(column.offset_index_length)
46-
const offsetIndexArrayBuffer = arrayBuffer.slice(offsetIndexOffset, offsetIndexOffset + offsetIndexLength)
46+
const offsetIndexArrayBuffer = file.slice(offsetIndexOffset, offsetIndexOffset + offsetIndexLength)
4747
const offsetIndexReader = { view: new DataView(offsetIndexArrayBuffer), offset: 0 }
4848
return readOffsetIndex(offsetIndexReader)
4949
}))

test/metadata.test.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,43 @@ describe('parquetMetadata', () => {
1212
it(`parse metadata from ${file}`, async () => {
1313
const asyncBuffer = await asyncBufferFromFile(`test/files/${file}`)
1414
const arrayBuffer = await asyncBuffer.slice(0)
15-
const result = toJson(parquetMetadataSync(arrayBuffer))
15+
const result = toJson(parquetMetadataSync({ file: arrayBuffer }))
1616
const base = file.replace('.parquet', '')
1717
const expected = fileToJson(`test/files/${base}.metadata.json`)
1818
expect(result, JSON.stringify(result, null, 2)).toEqual(expected)
1919
})
2020
})
2121

2222
it('throws for arrayBuffer undefined', () => {
23+
const file = undefined
2324
// @ts-expect-error testing invalid input
24-
expect(() => parquetMetadataSync(undefined)).toThrow('parquet expected ArrayBuffer')
25+
expect(() => parquetMetadataSync({ file })).toThrow('parquet expected ArrayBuffer')
2526
})
2627

2728
it('throws for a too short file', () => {
28-
const arrayBuffer = new ArrayBuffer(0)
29-
expect(() => parquetMetadataSync(arrayBuffer)).toThrow('parquet file is too short')
29+
const file = new ArrayBuffer(0)
30+
expect(() => parquetMetadataSync({ file })).toThrow('parquet file is too short')
3031
})
3132

3233
it('throws for invalid metadata length', () => {
33-
const arrayBuffer = new ArrayBuffer(12)
34-
const view = new DataView(arrayBuffer)
34+
const file = new ArrayBuffer(12)
35+
const view = new DataView(file)
3536
view.setUint32(0, 0x31524150, true) // magic number PAR1
3637
view.setUint32(4, 1000, true) // 1000 bytes exceeds buffer
3738
view.setUint32(8, 0x31524150, true) // magic number PAR1
38-
expect(() => parquetMetadataSync(arrayBuffer))
39+
expect(() => parquetMetadataSync({ file }))
3940
.toThrow('parquet metadata length 1000 exceeds available buffer 4')
4041
})
4142

4243
it('throws for invalid magic number', () => {
43-
const arrayBuffer = new ArrayBuffer(8)
44-
expect(() => parquetMetadataSync(arrayBuffer))
44+
const file = new ArrayBuffer(8)
45+
expect(() => parquetMetadataSync({ file }))
4546
.toThrow('parquet file invalid (footer != PAR1)')
4647
})
4748

48-
it('throws for invalid metadata length', () => {
49+
it('throws for invalid metadata length 2', () => {
4950
const { buffer } = new Uint8Array([255, 255, 255, 255, 80, 65, 82, 49])
50-
expect(() => parquetMetadataSync(buffer))
51+
expect(() => parquetMetadataSync({ file: buffer }))
5152
.toThrow('parquet metadata length 4294967295 exceeds available buffer 0')
5253
})
5354
})
@@ -56,29 +57,29 @@ describe('parquetMetadataAsync', () => {
5657
files.forEach(file => {
5758
it(`parse metadata async from ${file}`, async () => {
5859
const asyncBuffer = await asyncBufferFromFile(`test/files/${file}`)
59-
const result = await parquetMetadata(asyncBuffer)
60+
const result = await parquetMetadata({ file: asyncBuffer })
6061
const base = file.replace('.parquet', '')
6162
const expected = fileToJson(`test/files/${base}.metadata.json`)
6263
expect(toJson(result)).toEqual(expected)
6364
})
6465
})
6566

6667
it('throws for asyncBuffer undefined', async () => {
67-
const arrayBuffer = undefined
68+
const file = undefined
6869
// @ts-expect-error testing invalid input
69-
await expect(parquetMetadata(arrayBuffer)).rejects
70+
await expect(parquetMetadata({ file })).rejects
7071
.toThrow('parquet expected AsyncBuffer')
7172
})
7273

7374
it('throws for invalid magic number', async () => {
7475
const { buffer } = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255])
75-
await expect(parquetMetadata(buffer)).rejects
76+
await expect(parquetMetadata({ file: buffer })).rejects
7677
.toThrow('parquet file invalid (footer != PAR1)')
7778
})
7879

7980
it('throws for invalid metadata length', async () => {
8081
const { buffer } = new Uint8Array([255, 255, 255, 255, 80, 65, 82, 49])
81-
await expect(parquetMetadata(buffer)).rejects
82+
await expect(parquetMetadata({ file: buffer })).rejects
8283
.toThrow('parquet metadata length 4294967295 exceeds available buffer 0')
8384
})
8485
})

test/plan.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { parquetPlan } from '../src/plan.js'
66
describe('parquetPlan', () => {
77
it('generates a query plan', async () => {
88
const file = await asyncBufferFromFile('test/files/offset_indexed.parquet')
9-
const metadata = await parquetMetadata(file)
9+
const metadata = await parquetMetadata({ file })
1010
const plan = parquetPlan({ file, metadata })
1111
expect(plan).toMatchObject({
1212
metadata,

test/query.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ describe('parquetQuery', () => {
203203
it('reads data efficiently with filter', async () => {
204204
const originalFile = await asyncBufferFromFile('test/files/alpha.parquet')
205205
// don't count metadata reads
206-
const metadata = await parquetMetadata(originalFile)
206+
const metadata = await parquetMetadata({ file: originalFile })
207207
const file = countingBuffer(await asyncBufferFromFile('test/files/alpha.parquet'))
208208
// Query for rows where id = 'kk'
209209
const rows = await parquetQuery({ file, metadata, filter: { id: { $eq: 'kk' } } })
@@ -216,7 +216,7 @@ describe('parquetQuery', () => {
216216
it('reads data efficiently with filter and sort', async () => {
217217
const originalFile = await asyncBufferFromFile('test/files/alpha.parquet')
218218
// don't count metadata reads
219-
const metadata = await parquetMetadata(originalFile)
219+
const metadata = await parquetMetadata({ file: originalFile })
220220
const file = countingBuffer(await asyncBufferFromFile('test/files/alpha.parquet'))
221221
const rows = await parquetQuery({ file, metadata, filter: { id: { $gt: 'xx' } }, orderBy: 'id' } )
222222
expect(rows[0]).toEqual({ id: 'xy' })

0 commit comments

Comments
 (0)