async function parquetQuery(options) {
  if (!options.file || !(options.file.byteLength >= 0)) {
    throw new Error('parquet expected AsyncBuffer')
  }
  options.metadata ??= await parquetMetadataAsync(options.file)
  const { metadata, rowStart = 0, orderBy, filter } = options
  if (rowStart < 0) throw new Error('parquet rowStart must be positive')
  const rowEnd = options.rowEnd ?? Number(metadata.num_rows)

  if (filter && !orderBy && rowEnd < metadata.num_rows) {
    // iterate through row groups and filter until we have enough rows
    const filteredRows = new Array()
    let groupStart = 0
    for (const group of metadata.row_groups) {
      const groupEnd = groupStart + Number(group.num_rows)
      // TODO: if expected > group size, start fetching next groups
      const groupData = await parquetReadObjects({ ...options, rowStart: groupStart, rowEnd: groupEnd })
      for (const row of groupData) {
        if (matchQuery(row, filter)) {
          filteredRows.push(row)
        }
      }
      if (filteredRows.length >= rowEnd) break
      groupStart = groupEnd
    }
    return filteredRows.slice(rowStart, rowEnd)
  } else if (filter) {
    // read all rows, sort, and filter
    const results = (await parquetReadObjects({ ...options, rowStart: undefined, rowEnd: undefined }))
      .filter(row => matchQuery(row, filter))
    if (orderBy) results.sort((a, b) => compare(a[orderBy], b[orderBy]))
    return results.slice(rowStart, rowEnd)
  } else if (typeof orderBy === 'string') {
    // sorted but unfiltered: fetch orderBy column first
    const orderColumn = await parquetReadColumn({ ...options, rowStart: undefined, rowEnd: undefined, columns: [orderBy] })

    // compute row groups to fetch
    const sortedIndices = Array.from(orderColumn, (_, index) => index)
      .sort((a, b) => compare(orderColumn[a], orderColumn[b]))
      .slice(rowStart, rowEnd)

    const sparseData = await parquetReadRows({ ...options, rows: sortedIndices })
    const data = sortedIndices.map(index => sparseData[index])
    return data
  } else {
    return await parquetReadObjects(options)
  }
}
Wraps parquetRead with filter and orderBy support.
This is a parquet-aware query engine that can read a subset of rows and columns.
Accepts optional filter object to filter the results and orderBy column name to sort the results.
Note that using orderBy may SIGNIFICANTLY increase the query time.

@param {ParquetReadOptions & { filter?: ParquetQueryFilter, orderBy?: string }} options
@returns {Promise<Record<string, any>[]>} resolves when all requested rows and columns are parsed