| |
| |
| |
| |
|
|
| import { deserializeTCompactProtocol } from './thrift.js' |
| import { xxhash64 } from './xxhash.js' |
|
|
| |
| |
| |
|
|
| const textEncoder = new TextEncoder() |
|
|
| const SALT = new Uint32Array([ |
| 0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d, |
| 0x705495c7, 0x2df1424b, 0x9efc4947, 0x5c6bfb31, |
| ]) |
|
|
| |
| |
| |
| |
| |
| |
| |
| function blockIndex(hash, numBlocks) { |
| return Number((hash >> 32n) * BigInt(numBlocks) >> 32n) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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] |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function readBloomFilter(reader) { |
| const header = deserializeTCompactProtocol(reader) |
| const numBytes = header.field_1 |
| if (typeof numBytes !== 'number' || numBytes <= 0 || numBytes % 32 !== 0) return undefined |
| |
| if (!header.field_2?.field_1) return undefined |
| if (!header.field_3?.field_1) return undefined |
| if (!header.field_4?.field_1) return undefined |
|
|
| const { view, offset } = reader |
| if (offset + numBytes > view.byteLength) { |
| throw new Error(`parquet bloom filter truncated: need ${numBytes} bytes, have ${view.byteLength - offset}`) |
| } |
| |
| 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 } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| return undefined |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function bloomEligibleColumns(filter) { |
| |
| const out = new Set() |
| walkBloomEligible(filter, out) |
| return 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 |
| } |
| |
| 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 { |
| |
| out.add(field) |
| } |
| } |
| } |
|
|