Skip to content

Commit ea6755e

Browse files
committed
Objects only, no arrays no rowFormat
1 parent 055d1c2 commit ea6755e

8 files changed

Lines changed: 55 additions & 108 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## [2.0.0]
44
- Rename `parquetMetadataAsync` to `parquetMetadata`
55
- Previous `parquetMetadata` is now `import { parquetMetadataSync } from 'hyparquet/src/metadata.js'`
6+
- Rename `onPage.columnName: string` to `onPage.pathInSchema: string[]`
7+
- Remove `rowFormat` and always return rows as objects
68

79
## [1.23.0]
810
- Replace `columnName: string` with `pathInSchema: string[]` in `onPage` callback (#144)

README.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,6 @@ await parquetRead({
171171
})
172172
```
173173

174-
### Returned row format
175-
176-
By default, the `onComplete` function returns an **array** of values for each row: `[value]`. If you would prefer each row to be an **object**: `{ columnName: value }`, set the option `rowFormat` to `'object'`.
177-
178-
```javascript
179-
import { parquetRead } from 'hyparquet'
180-
181-
await parquetRead({
182-
file,
183-
rowFormat: 'object',
184-
onComplete: data => console.log(data),
185-
})
186-
```
187-
188-
The `parquetReadObjects` function defaults to `rowFormat: 'object'`.
189-
190174
### Binary columns
191175

192176
Hyparquet defaults to decoding binary columns as utf8 text strings. A parquet `BYTE_ARRAY` column may contain arbitrary binary data or utf8 encoded text data. In theory, a column should be annotated as [LogicalType](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md) STRING if it contains utf8 text. But in practice, many parquet files omit this annotation. Hyparquet's default decoding behavior can be disabled by setting the `utf8` option to `false`. The `utf8` option only affects `BYTE_ARRAY` columns _without_ an annotation.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hyparquet",
3-
"version": "1.23.0",
3+
"version": "2.0.0",
44
"description": "Parquet file parser for JavaScript",
55
"author": "Hyperparam",
66
"homepage": "https://hyperparam.app",

src/read.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function parquetRead(options) {
2525
// read row groups
2626
const asyncGroups = parquetReadAsync(options)
2727

28-
const { rowStart = 0, rowEnd, columns, onChunk, onComplete, rowFormat } = options
28+
const { rowStart = 0, rowEnd, columns, onChunk, onComplete } = options
2929

3030
// skip assembly if no onComplete or onChunk, but wait for reading to finish
3131
if (!onComplete && !onChunk) {
@@ -61,17 +61,14 @@ export async function parquetRead(options) {
6161

6262
// onComplete transpose column chunks to rows
6363
if (onComplete) {
64-
// loosen the types to avoid duplicate code
65-
/** @type {any[]} */
64+
/** @type {Record<string, any>[]} */
6665
const rows = []
6766
for (const asyncGroup of assembled) {
6867
// filter to rows in range
6968
const selectStart = Math.max(rowStart - asyncGroup.groupStart, 0)
7069
const selectEnd = Math.min((rowEnd ?? Infinity) - asyncGroup.groupStart, asyncGroup.groupRows)
7170
// transpose column chunks to rows in output
72-
const groupData = rowFormat === 'object' ?
73-
await asyncGroupToRows(asyncGroup, selectStart, selectEnd, columns, 'object') :
74-
await asyncGroupToRows(asyncGroup, selectStart, selectEnd, columns, 'array')
71+
const groupData = await asyncGroupToRows(asyncGroup, selectStart, selectEnd, columns)
7572
concat(rows, groupData)
7673
}
7774
onComplete(rows)
@@ -135,7 +132,6 @@ export function parquetReadObjects(options) {
135132
return new Promise((onComplete, reject) => {
136133
parquetRead({
137134
...options,
138-
rowFormat: 'object', // force object output
139135
onComplete,
140136
}).catch(reject)
141137
})

src/rowgroup.js

Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -73,71 +73,33 @@ export function readRowGroup(options, { metadata, columns }, groupPlan) {
7373
}
7474

7575
/**
76-
* @overload
7776
* @param {AsyncRowGroup} asyncGroup
7877
* @param {number} selectStart
7978
* @param {number} selectEnd
8079
* @param {string[] | undefined} columns
81-
* @param {'object'} rowFormat
8280
* @returns {Promise<Record<string, any>[]>} resolves to row data
8381
*/
84-
/**
85-
* @overload
86-
* @param {AsyncRowGroup} asyncGroup
87-
* @param {number} selectStart
88-
* @param {number} selectEnd
89-
* @param {string[] | undefined} columns
90-
* @param {'array'} [rowFormat]
91-
* @returns {Promise<any[][]>} resolves to row data
92-
*/
93-
/**
94-
* @param {AsyncRowGroup} asyncGroup
95-
* @param {number} selectStart
96-
* @param {number} selectEnd
97-
* @param {string[] | undefined} columns
98-
* @param {'object' | 'array'} [rowFormat]
99-
* @returns {Promise<Record<string, any>[] | any[][]>} resolves to row data
100-
*/
101-
export async function asyncGroupToRows({ asyncColumns }, selectStart, selectEnd, columns, rowFormat) {
82+
export async function asyncGroupToRows({ asyncColumns }, selectStart, selectEnd, columns) {
10283
// columnData[i] for asyncColumns[i]
10384
// TODO: do it without flatten
10485
const columnDatas = await Promise.all(asyncColumns.map(({ data }) => data.then(flatten)))
10586

106-
// careful mapping of column order for rowFormat: array
107-
const includedColumnNames = asyncColumns
108-
.map(child => child.pathInSchema[0])
109-
.filter(name => !columns || columns.includes(name))
110-
const columnOrder = columns ?? includedColumnNames
111-
const columnIndexes = columnOrder.map(name => asyncColumns.findIndex(column => column.pathInSchema[0] === name))
87+
// filter columns
88+
const filteredColumns = columns
89+
? asyncColumns.filter(column => columns.includes(column.pathInSchema[0]))
90+
: asyncColumns
11291

11392
// transpose columns into rows
11493
const selectCount = selectEnd - selectStart
115-
if (rowFormat === 'object') {
116-
/** @type {Record<string, any>[]} */
117-
const groupData = new Array(selectCount)
118-
for (let selectRow = 0; selectRow < selectCount; selectRow++) {
119-
const row = selectStart + selectRow
120-
// return each row as an object
121-
/** @type {Record<string, any>} */
122-
const rowData = {}
123-
for (let i = 0; i < asyncColumns.length; i++) {
124-
rowData[asyncColumns[i].pathInSchema[0]] = columnDatas[i][row]
125-
}
126-
groupData[selectRow] = rowData
127-
}
128-
return groupData
129-
}
130-
131-
/** @type {any[][]} */
94+
/** @type {Record<string, any>[]} */
13295
const groupData = new Array(selectCount)
13396
for (let selectRow = 0; selectRow < selectCount; selectRow++) {
13497
const row = selectStart + selectRow
135-
// return each row as an array
136-
const rowData = new Array(asyncColumns.length)
137-
for (let i = 0; i < columnOrder.length; i++) {
138-
if (columnIndexes[i] >= 0) {
139-
rowData[i] = columnDatas[columnIndexes[i]][row]
140-
}
98+
/** @type {Record<string, any>} */
99+
const rowData = {}
100+
for (let i = 0; i < filteredColumns.length; i++) {
101+
const columnIndex = asyncColumns.indexOf(filteredColumns[i])
102+
rowData[filteredColumns[i].pathInSchema[0]] = columnDatas[columnIndex][row]
141103
}
142104
groupData[selectRow] = rowData
143105
}

src/types.d.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface MetadataOptions {
2323
/**
2424
* Parquet query options for reading data
2525
*/
26-
export interface BaseParquetReadOptions {
26+
export interface ParquetReadOptions {
2727
file: AsyncBuffer // file-like object containing parquet data
2828
metadata?: FileMetaData // parquet metadata, will be parsed if not provided
2929
columns?: string[] // columns to read, all columns if undefined
@@ -32,21 +32,14 @@ export interface BaseParquetReadOptions {
3232
rowEnd?: number // last requested row index (exclusive)
3333
onChunk?: (chunk: ColumnData) => void // called when a column chunk is parsed. chunks may contain data outside the requested range.
3434
onPage?: (chunk: SubColumnData) => void // called when a data page is parsed. pages may contain data outside the requested range.
35+
onComplete?: (rows: Record<string, any>[]) => void // called when all requested rows and columns are parsed
3536
compressors?: Compressors // custom decompressors
3637
utf8?: boolean // decode byte arrays as utf8 strings (default true)
3738
parsers?: ParquetParsers // custom parsers to decode advanced types
3839
geoparquet?: boolean // parse geoparquet metadata and set logical type to geometry/geography for geospatial columns (default true)
3940
}
4041

41-
interface ArrayRowFormat {
42-
rowFormat?: 'array' // format of each row passed to the onComplete function. Can be omitted, as it's the default.
43-
onComplete?: (rows: any[][]) => void // called when all requested rows and columns are parsed
44-
}
45-
interface ObjectRowFormat {
46-
rowFormat: 'object' // format of each row passed to the onComplete function
47-
onComplete?: (rows: Record<string, any>[]) => void // called when all requested rows and columns are parsed
48-
}
49-
export type ParquetReadOptions = BaseParquetReadOptions & (ArrayRowFormat | ObjectRowFormat)
42+
export type BaseParquetReadOptions = ParquetReadOptions
5043

5144
/**
5245
* Parquet query options for filtering data

test/read.test.js

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('parquetRead', () => {
2727
rowStart: 2,
2828
rowEnd: 4,
2929
onComplete(rows) {
30-
expect(rows).toEqual([[3n], [4n]])
30+
expect(rows).toEqual([{ numbers: 3n }, { numbers: 4n }])
3131
},
3232
})
3333
})
@@ -39,7 +39,9 @@ describe('parquetRead', () => {
3939
rowEnd: 100,
4040
onComplete(rows) {
4141
expect(rows).toEqual([
42-
[1n], [2n], [3n], [4n], [5n], [6n], [7n], [8n], [9n], [10n], [11n], [12n], [13n], [14n], [15n],
42+
{ numbers: 1n }, { numbers: 2n }, { numbers: 3n }, { numbers: 4n }, { numbers: 5n },
43+
{ numbers: 6n }, { numbers: 7n }, { numbers: 8n }, { numbers: 9n }, { numbers: 10n },
44+
{ numbers: 11n }, { numbers: 12n }, { numbers: 13n }, { numbers: 14n }, { numbers: 15n },
4345
])
4446
},
4547
})
@@ -77,11 +79,11 @@ describe('parquetRead', () => {
7779
},
7880
onComplete(rows) {
7981
expect(rows).toEqual([
80-
[[1, 2, 3]],
81-
[undefined],
82-
[undefined],
83-
[[1, 2, 3]],
84-
[[1, 2]],
82+
{ e: [1, 2, 3] },
83+
{ e: undefined },
84+
{ e: undefined },
85+
{ e: [1, 2, 3] },
86+
{ e: [1, 2] },
8587
])
8688
},
8789
})
@@ -110,24 +112,23 @@ describe('parquetRead', () => {
110112
},
111113
onComplete(rows) {
112114
expect(rows).toEqual([
113-
[{ k1: 1, k2: 100 }],
114-
[{ k1: 2, k2: null }],
115-
[{ }],
116-
[{ }],
117-
[{ }],
118-
[undefined],
119-
[{ k1: null, k3: null }],
115+
{ int_map: { k1: 1, k2: 100 } },
116+
{ int_map: { k1: 2, k2: null } },
117+
{ int_map: { } },
118+
{ int_map: { } },
119+
{ int_map: { } },
120+
{ int_map: undefined },
121+
{ int_map: { k1: null, k3: null } },
120122
])
121123
},
122124
})
123125
})
124126

125-
it('format row as object', async () => {
127+
it('read single column as objects', async () => {
126128
const file = await asyncBufferFromFile('test/files/datapage_v2.snappy.parquet')
127129
await parquetRead({
128130
file,
129131
columns: ['c'],
130-
rowFormat: 'object',
131132
onComplete(rows) {
132133
expect(rows).toEqual([
133134
{ c: 2 },
@@ -140,18 +141,18 @@ describe('parquetRead', () => {
140141
})
141142
})
142143

143-
it('read columns out of order', async () => {
144+
it('read selected columns', async () => {
144145
const file = await asyncBufferFromFile('test/files/datapage_v2.snappy.parquet')
145146
await parquetRead({
146147
file,
147-
columns: ['c', 'missing', 'b', 'c'],
148+
columns: ['c', 'b'],
148149
onComplete(rows) {
149150
expect(rows).toEqual([
150-
[2, undefined, 1, 2],
151-
[3, undefined, 2, 3],
152-
[4, undefined, 3, 4],
153-
[5, undefined, 4, 5],
154-
[2, undefined, 5, 2],
151+
{ b: 1, c: 2 },
152+
{ b: 2, c: 3 },
153+
{ b: 3, c: 4 },
154+
{ b: 4, c: 5 },
155+
{ b: 5, c: 2 },
155156
])
156157
},
157158
})

test/readFiles.test.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import { parquetMetadata, parquetRead, toJson } from '../src/index.js'
55
import { asyncBufferFromFile } from '../src/node.js'
66
import { fileToJson } from './helpers.js'
77

8+
/**
9+
* Convert object rows to array rows for comparison with legacy JSON test files.
10+
* @param {Record<string, any>[]} rows
11+
* @returns {any[][]}
12+
*/
13+
function toArrays(rows) {
14+
return rows.map(row => Object.values(row))
15+
}
16+
817
describe('parquetRead test files', () => {
918
const files = fs.readdirSync('test/files').filter(f => f.endsWith('.parquet'))
1019

@@ -18,7 +27,7 @@ describe('parquetRead test files', () => {
1827
const base = filename.replace('.parquet', '')
1928
const expected = fileToJson(`test/files/${base}.json`)
2029
// stringify and parse to make legal json (NaN, -0, etc)
21-
expect(JSON.parse(JSON.stringify(toJson(rows)))).toEqual(expected)
30+
expect(JSON.parse(JSON.stringify(toJson(toArrays(rows))))).toEqual(expected)
2231
},
2332
})
2433
})
@@ -39,7 +48,7 @@ describe('parquetRead test files', () => {
3948
const base = filename.replace('.parquet', '')
4049
if (rows.length) {
4150
const expected = [fileToJson(`test/files/${base}.json`).at(-1)]
42-
expect(toJson(rows)).toEqual(expected)
51+
expect(toJson(toArrays(rows))).toEqual(expected)
4352
}
4453
},
4554
})

0 commit comments

Comments
 (0)