Quazim0t0's picture
web: sync-guard gradient repair, FineWeb-Edu hardcoded (parquet /data streaming), dispatch-optimized backward (bit-identical), IEEE-754 oracle + corpus mutation tests, TEST_RESULTS.md
9ae1216 verified
Raw
History Blame Contribute Delete
8.5 kB
// Split Block Bloom Filter (https://github.com/apache/parquet-format/blob/master/BloomFilter.md)
// A bloom filter is a sequence of 32-byte blocks. Each block holds 8 little-endian uint32 words.
// Insertion sets one bit per word, chosen by salting the low 32 bits of an xxhash64.
// Membership requires all 8 bits to be set; misses are exact, hits are probabilistic.
import { deserializeTCompactProtocol } from './thrift.js'
import { xxhash64 } from './xxhash.js'
/**
* @import {BloomFilter, DataReader, ParquetQueryFilter, SchemaElement} from '../src/types.js'
*/
const textEncoder = new TextEncoder()
const SALT = new Uint32Array([
0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d,
0x705495c7, 0x2df1424b, 0x9efc4947, 0x5c6bfb31,
])
/**
* Map the high 32 bits of a hash to a block index in [0, numBlocks).
*
* @param {bigint} hash
* @param {number} numBlocks
* @returns {number}
*/
function blockIndex(hash, numBlocks) {
return Number((hash >> 32n) * BigInt(numBlocks) >> 32n)
}
/**
* Per-block mask: 8 uint32 words, each with a single bit set at position `(low32 * SALT[i]) >> 27`.
*
* @param {bigint} hash
* @returns {Uint32Array}
*/
function blockMask(hash) {
const m = new Uint32Array(8)
const low = Number(hash & 0xffffffffn) | 0
for (let i = 0; i < 8; i++) {
m[i] = 1 << (Math.imul(low, SALT[i]) >>> 27)
}
return m
}
/**
* Insert a hash into a Split Block Bloom Filter.
*
* @param {Uint32Array} blocks bloom filter words (8 * numBlocks long)
* @param {bigint} hash 64-bit xxhash of the parquet-plain-encoded value
*/
export function sbbfInsert(blocks, hash) {
const offset = blockIndex(hash, blocks.length >> 3) << 3
const m = blockMask(hash)
for (let i = 0; i < 8; i++) {
blocks[offset + i] |= m[i]
}
}
/**
* Test whether a hash might be present in a Split Block Bloom Filter.
* False positives are possible; false negatives are not.
*
* @param {Uint32Array} blocks bloom filter words (8 * numBlocks long)
* @param {bigint} hash 64-bit xxhash of the parquet-plain-encoded value
* @returns {boolean}
*/
export function sbbfContains(blocks, hash) {
const offset = blockIndex(hash, blocks.length >> 3) << 3
const m = blockMask(hash)
for (let i = 0; i < 8; i++) {
if ((blocks[offset + i] & m[i]) === 0) return false
}
return true
}
/**
* Parse a Split Block Bloom Filter from a reader positioned at the BloomFilterHeader.
* Returns undefined when the header advertises an unsupported algorithm, hash, or
* compression — callers should treat that as "cannot use this bloom filter."
*
* @param {DataReader} reader
* @returns {BloomFilter | undefined}
*/
export function readBloomFilter(reader) {
const header = deserializeTCompactProtocol(reader)
const numBytes = header.field_1
if (typeof numBytes !== 'number' || numBytes <= 0 || numBytes % 32 !== 0) return undefined
// BloomFilterAlgorithm / Hash / Compression are unions with a single supported variant each.
if (!header.field_2?.field_1) return undefined // algorithm must be BLOCK
if (!header.field_3?.field_1) return undefined // hash must be XXHASH
if (!header.field_4?.field_1) return undefined // compression must be UNCOMPRESSED
const { view, offset } = reader
if (offset + numBytes > view.byteLength) {
throw new Error(`parquet bloom filter truncated: need ${numBytes} bytes, have ${view.byteLength - offset}`)
}
// Reader offset is not 4-aligned in general, and we want endian-portable reads.
const blocks = new Uint32Array(numBytes >> 2)
for (let i = 0; i < blocks.length; i++) {
blocks[i] = view.getUint32(offset + i * 4, true)
}
reader.offset = offset + numBytes
return { numBytes, blocks }
}
/**
* Hash a JS filter value as its parquet PLAIN-encoded bytes, suitable for a
* bloom filter lookup. Returns undefined when the column's parser is lossy or
* ambiguous (DATE, TIMESTAMP_*, DECIMAL, JSON, BSON, INT96, FLOAT16, UUID,
* GEOMETRY, GEOGRAPHY, INTERVAL) or when the JS value type doesn't match the
* column. Callers must treat undefined as "bloom filter cannot help."
*
* @param {any} value
* @param {SchemaElement} element
* @returns {bigint | undefined}
*/
export function hashParquetValue(value, element) {
if (value === null || value === undefined) return undefined
const { type, converted_type, logical_type } = element
if (type === 'BOOLEAN') {
if (typeof value !== 'boolean') return undefined
return xxhash64(new Uint8Array([value ? 1 : 0]))
}
if (type === 'FLOAT') {
if (typeof value !== 'number') return undefined
const buf = new ArrayBuffer(4)
new DataView(buf).setFloat32(0, value, true)
return xxhash64(new Uint8Array(buf))
}
if (type === 'DOUBLE') {
if (typeof value !== 'number') return undefined
const buf = new ArrayBuffer(8)
new DataView(buf).setFloat64(0, value, true)
return xxhash64(new Uint8Array(buf))
}
if (type === 'INT32') {
if (converted_type === 'DATE' || converted_type === 'DECIMAL' || converted_type === 'TIME_MILLIS') return undefined
if (logical_type?.type === 'DATE' || logical_type?.type === 'TIME' || logical_type?.type === 'DECIMAL') return undefined
if (typeof value !== 'number' || !Number.isInteger(value)) return undefined
const buf = new ArrayBuffer(4)
new DataView(buf).setInt32(0, value | 0, true)
return xxhash64(new Uint8Array(buf))
}
if (type === 'INT64') {
if (converted_type === 'TIMESTAMP_MILLIS' || converted_type === 'TIMESTAMP_MICROS') return undefined
if (converted_type === 'TIME_MICROS' || converted_type === 'DECIMAL') return undefined
if (logical_type?.type === 'TIMESTAMP' || logical_type?.type === 'TIME' || logical_type?.type === 'DECIMAL') return undefined
let bigValue
if (typeof value === 'bigint') bigValue = value
else if (typeof value === 'number' && Number.isSafeInteger(value)) bigValue = BigInt(value)
else return undefined
const buf = new ArrayBuffer(8)
new DataView(buf).setBigUint64(0, BigInt.asUintN(64, bigValue), true)
return xxhash64(new Uint8Array(buf))
}
if (type === 'BYTE_ARRAY') {
if (converted_type === 'JSON' || converted_type === 'BSON' || converted_type === 'DECIMAL') return undefined
if (logical_type?.type === 'JSON' || logical_type?.type === 'BSON' || logical_type?.type === 'VARIANT') return undefined
if (logical_type?.type === 'GEOMETRY' || logical_type?.type === 'GEOGRAPHY') return undefined
if (typeof value === 'string') return xxhash64(textEncoder.encode(value))
if (value instanceof Uint8Array) return xxhash64(value)
return undefined
}
if (type === 'FIXED_LEN_BYTE_ARRAY') {
if (converted_type === 'DECIMAL' || converted_type === 'INTERVAL') return undefined
if (logical_type?.type === 'DECIMAL' || logical_type?.type === 'UUID' || logical_type?.type === 'FLOAT16') return undefined
if (logical_type?.type === 'GEOMETRY' || logical_type?.type === 'GEOGRAPHY') return undefined
if (value instanceof Uint8Array) return xxhash64(value)
return undefined
}
// INT96 deprecated, or type missing on group columns
return undefined
}
/**
* Top-level column names that appear in $eq or $in predicates within a filter.
* These are the only columns where a bloom filter can prove a value's absence
* and let us skip a row group; any other operator can't be helped by a bloom.
*
* @param {ParquetQueryFilter | undefined} filter
* @returns {Set<string>}
*/
export function bloomEligibleColumns(filter) {
/** @type {Set<string>} */
const out = new Set()
walkBloomEligible(filter, out)
return out
}
/**
* @param {ParquetQueryFilter | undefined} filter
* @param {Set<string>} out
*/
function walkBloomEligible(filter, out) {
if (!filter) return
if ('$and' in filter && Array.isArray(filter.$and)) {
for (const sub of filter.$and) walkBloomEligible(sub, out)
return
}
if ('$or' in filter && Array.isArray(filter.$or)) {
for (const sub of filter.$or) walkBloomEligible(sub, out)
return
}
// $nor would need to prove presence, not absence — bloom can't help.
if ('$nor' in filter) return
for (const [field, condition] of Object.entries(filter)) {
if (field.startsWith('$')) continue
if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
if ('$eq' in condition || '$in' in condition) out.add(field)
} else {
// primitive / null / array condition is an implicit $eq
out.add(field)
}
}
}