blob: 9b7f7e0752be211c94047a6eb17cba572da34015 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Arrow.DictEncoding
Represents the "pool" of possible values for a [`DictEncoded`](@ref)
array type. Whether the order of values is significant can be checked
by looking at the `isOrdered` boolean field.
The `S` type parameter, while not tied directly to any field, is the
signed integer "index type" of the parent DictEncoded. We keep track
of this in the DictEncoding in order to validate the length of the pool
doesn't exceed the index type limit. The general workflow of writing arrow
data means the initial schema will typically be based off the data in the
first record batch, and subsequent record batches need to match the same
schema exactly. For example, if a non-first record batch dict encoded column
were to cause a DictEncoding pool to overflow on unique values, a fatal error
should be thrown.
"""
mutable struct DictEncoding{T,S,A} <: ArrowVector{T}
id::Int64
data::A
isOrdered::Bool
metadata::Union{Nothing,Base.ImmutableDict{String,String}}
end
indextype(::Type{DictEncoding{T,S,A}}) where {T,S,A} = S
indextype(::T) where {T<:DictEncoding} = indextype(T)
Base.size(d::DictEncoding) = size(d.data)
@propagate_inbounds function Base.getindex(d::DictEncoding{T}, i::Integer) where {T}
@boundscheck checkbounds(d, i)
return @inbounds ArrowTypes.fromarrow(T, d.data[i])
end
# convenience wrapper to signal that an input column should be
# dict encoded when written to the arrow format
struct DictEncodeType{T} end
getT(::Type{DictEncodeType{T}}) where {T} = T
"""
Arrow.DictEncode(::AbstractVector, id::Integer=nothing)
Signals that a column/array should be dictionary encoded when serialized
to the arrow streaming/file format. An optional `id` number may be provided
to signal that multiple columns should use the same pool when being
dictionary encoded.
"""
struct DictEncode{T,A} <: AbstractVector{DictEncodeType{T}}
id::Int64
data::A
end
DictEncode(x::A, id=-1) where {A} = DictEncode{eltype(A),A}(id, x)
Base.IndexStyle(::Type{<:DictEncode}) = Base.IndexLinear()
Base.size(x::DictEncode) = (length(x.data),)
Base.iterate(x::DictEncode, st...) = iterate(x.data, st...)
Base.getindex(x::DictEncode, i::Int) = getindex(x.data, i)
ArrowTypes.ArrowKind(::Type{<:DictEncodeType}) = DictEncodedKind()
Base.copy(x::DictEncode) = DictEncode(x.data, x.id)
"""
Arrow.DictEncoded
A dictionary encoded array type (similar to a `PooledArray`). Behaves just
like a normal array in most respects; internally, possible values are stored
in the `encoding::DictEncoding` field, while the `indices::Vector{<:Integer}`
field holds the "codes" of each element for indexing into the encoding pool.
Any column/array can be dict encoding when serializing to the arrow format
either by passing the `dictencode=true` keyword argument to [`Arrow.write`](@ref)
(which causes _all_ columns to be dict encoded), or wrapping individual columns/
arrays in [`Arrow.DictEncode(x)`](@ref).
"""
struct DictEncoded{T,S,A} <: ArrowVector{T}
arrow::Vector{UInt8} # need to hold a reference to arrow memory blob
validity::ValidityBitmap
indices::Vector{S}
encoding::DictEncoding{T,S,A}
metadata::Union{Nothing,Base.ImmutableDict{String,String}}
end
DictEncoded(
b::Vector{UInt8},
v::ValidityBitmap,
inds::Vector{S},
encoding::DictEncoding{T,S,A},
meta,
) where {S,T,A} = DictEncoded{T,S,A}(b, v, inds, encoding, meta)
Base.size(d::DictEncoded) = size(d.indices)
isdictencoded(d::DictEncoded) = true
isdictencoded(x) = false
isdictencoded(c::Compressed{Z,A}) where {Z,A<:DictEncoded} = true
function signedtype(n::Integer)
typs = (Int8, Int16, Int32, Int64)
typs[something(findfirst(n .≤ typemax.(typs)), 4)]
end
signedtype(::Type{UInt8}) = Int8
signedtype(::Type{UInt16}) = Int16
signedtype(::Type{UInt32}) = Int32
signedtype(::Type{UInt64}) = Int64
signedtype(::Type{T}) where {T<:Signed} = T
indtype(d::DictEncoded{T,S,A}) where {T,S,A} = S
indtype(c::Compressed{Z,A}) where {Z,A<:DictEncoded} = indtype(c.data)
dictencodeid(colidx, nestedlevel, fieldid) =
(Int64(nestedlevel) << 48) | (Int64(fieldid) << 32) | Int64(colidx)
getid(d::DictEncoded) = d.encoding.id
getid(c::Compressed{Z,A}) where {Z,A<:DictEncoded} = c.data.encoding.id
function arrowvector(
::DictEncodedKind,
x::DictEncoded,
i,
nl,
fi,
de,
ded,
meta;
dictencode::Bool=false,
dictencodenested::Bool=false,
kw...,
)
id = x.encoding.id
if !haskey(de, id)
de[id] = Lockable(x.encoding)
else
encodinglockable = de[id]
Base.@lock encodinglockable begin
encoding = encodinglockable.value
# in this case, we just need to check if any values in our local pool need to be delta dicationary serialized
deltas = setdiff(x.encoding, encoding)
if !isempty(deltas)
ET = indextype(encoding)
if length(deltas) + length(encoding) > typemax(ET)
error(
"fatal error serializing dict encoded column with ref index type of $ET; subsequent record batch unique values resulted in $(length(deltas) + length(encoding)) unique values, which exceeds possible index values in $ET",
)
end
data = arrowvector(
deltas,
i,
nl,
fi,
de,
ded,
nothing;
dictencode=dictencodenested,
dictencodenested=dictencodenested,
dictencoding=true,
kw...,
)
push!(
ded,
DictEncoding{eltype(data),ET,typeof(data)}(
id,
data,
false,
getmetadata(data),
),
)
if typeof(encoding.data) <: ChainedVector
append!(encoding.data, data)
else
data2 = ChainedVector([encoding.data, data])
encoding = DictEncoding{eltype(data2),ET,typeof(data2)}(
id,
data2,
false,
getmetadata(encoding),
)
de[id] = Lockable(encoding)
end
end
end
end
return x
end
function arrowvector(
::DictEncodedKind,
x,
i,
nl,
fi,
de,
ded,
meta;
dictencode::Bool=false,
dictencodenested::Bool=false,
kw...,
)
@assert x isa DictEncode
id = x.id == -1 ? dictencodeid(i, nl, fi) : x.id
x = x.data
len = length(x)
validity = ValidityBitmap(x)
if !haskey(de, id)
# dict encoding doesn't exist yet, so create for 1st time
if DataAPI.refarray(x) === x || DataAPI.refpool(x) === nothing
# need to encode ourselves
x = PooledArray(x; signed=true, compress=true)
inds = DataAPI.refarray(x)
pool = DataAPI.refpool(x)
else
pool = DataAPI.refpool(x)
refa = DataAPI.refarray(x)
inds = copyto!(similar(Vector{signedtype(length(pool))}, length(refa)), refa)
end
# horrible hack? yes. better than taking CategoricalArrays dependency? also yes.
if typeof(pool).name.name == :CategoricalRefPool
if eltype(x) >: Missing
pool = vcat(missing, DataAPI.levels(x))
else
pool = DataAPI.levels(x)
for i = 1:length(inds)
@inbounds inds[i] -= 1
end
end
else
# adjust to "offset" instead of index
for i = 1:length(inds)
@inbounds inds[i] -= 1
end
end
data = arrowvector(
pool,
i,
nl,
fi,
de,
ded,
nothing;
dictencode=dictencodenested,
dictencodenested=dictencodenested,
dictencoding=true,
kw...,
)
encoding = DictEncoding{eltype(data),eltype(inds),typeof(data)}(
id,
data,
false,
getmetadata(data),
)
de[id] = Lockable(encoding)
else
# encoding already exists
# compute inds based on it
# if value doesn't exist in encoding, push! it
# also add to deltas updates
encodinglockable = de[id]
Base.@lock encodinglockable begin
encoding = encodinglockable.value
len = length(x)
ET = indextype(encoding)
pool = Dict{Union{eltype(encoding),eltype(x)},ET}(
a => (b - 1) for (b, a) in enumerate(encoding)
)
deltas = eltype(x)[]
inds = Vector{ET}(undef, len)
categorical = typeof(x).name.name == :CategoricalArray
for (j, val) in enumerate(x)
if categorical
val = get(val)
end
@inbounds inds[j] = get!(pool, val) do
push!(deltas, val)
return length(pool)
end
end
if !isempty(deltas)
if length(deltas) + length(encoding) > typemax(ET)
error(
"fatal error serializing dict encoded column with ref index type of $ET; subsequent record batch unique values resulted in $(length(deltas) + length(encoding)) unique values, which exceeds possible index values in $ET",
)
end
data = arrowvector(
deltas,
i,
nl,
fi,
de,
ded,
nothing;
dictencode=dictencodenested,
dictencodenested=dictencodenested,
dictencoding=true,
kw...,
)
push!(
ded,
DictEncoding{eltype(data),ET,typeof(data)}(
id,
data,
false,
getmetadata(data),
),
)
if typeof(encoding.data) <: ChainedVector
append!(encoding.data, data)
else
data2 = ChainedVector([encoding.data, data])
encoding = DictEncoding{eltype(data2),ET,typeof(data2)}(
id,
data2,
false,
getmetadata(encoding),
)
de[id] = Lockable(encoding)
end
end
end
end
if meta !== nothing && getmetadata(encoding) !== nothing
meta = toidict(merge!(Dict(meta), Dict(getmetadata(encoding))))
elseif getmetadata(encoding) !== nothing
meta = getmetadata(encoding)
end
return DictEncoded(UInt8[], validity, inds, encoding, meta)
end
@propagate_inbounds function Base.getindex(d::DictEncoded, i::Integer)
@boundscheck checkbounds(d, i)
@inbounds valid = d.validity[i]
!valid && return missing
@inbounds idx = d.indices[i]
return @inbounds d.encoding[idx + 1]
end
@propagate_inbounds function Base.setindex!(d::DictEncoded{T}, v, i::Integer) where {T}
@boundscheck checkbounds(d, i)
if v === missing
@inbounds d.validity[i] = false
else
ix = findfirst(d.encoding.data, v)
if ix === nothing
push!(d.encoding.data, v)
@inbounds d.indices[i] = length(d.encoding.data) - 1
else
@inbounds d.indices[i] = ix - 1
end
end
return v
end
function Base.copy(x::DictEncoded{T,S}) where {T,S}
pool = copy(x.encoding.data)
valid = x.validity
inds = x.indices
refs = copy(inds)
@inbounds for i = 1:length(inds)
refs[i] = refs[i] + one(S)
end
return PooledArray(
PooledArrays.RefArray(refs),
Dict{T,S}(val => i for (i, val) in enumerate(pool)),
pool,
)
end
function compress(Z::Meta.CompressionType.T, comp, x::A) where {A<:DictEncoded}
len = length(x)
nc = nullcount(x)
validity = compress(Z, comp, x.validity)
inds = compress(Z, comp, x.indices)
return Compressed{Z,A}(x, [validity, inds], len, nc, Compressed[])
end
function DataAPI.levels(x::DictEncoded)
rp = DataAPI.refpool(x) # may contain missing values
Missing <: eltype(rp) || return rp
convert(AbstractArray{nonmissingtype(eltype(rp))}, deleteat!(rp, ismissing.(rp)))
end
function makenodesbuffers!(
col::DictEncoded{T,S},
fieldnodes,
fieldbuffers,
bufferoffset,
alignment,
) where {T,S}
len = length(col)
nc = nullcount(col)
push!(fieldnodes, FieldNode(len, nc))
@debugv 1 "made field node: nodeidx = $(length(fieldnodes)), col = $(typeof(col)), len = $(fieldnodes[end].length), nc = $(fieldnodes[end].null_count)"
# validity bitmap
blen = nc == 0 ? 0 : bitpackedbytes(len, alignment)
push!(fieldbuffers, Buffer(bufferoffset, blen))
@debugv 1 "made field buffer: bufferidx = $(length(fieldbuffers)), offset = $(fieldbuffers[end].offset), len = $(fieldbuffers[end].length), padded = $(padding(fieldbuffers[end].length, alignment))"
bufferoffset += blen
# indices
blen = sizeof(S) * len
push!(fieldbuffers, Buffer(bufferoffset, blen))
@debugv 1 "made field buffer: bufferidx = $(length(fieldbuffers)), offset = $(fieldbuffers[end].offset), len = $(fieldbuffers[end].length), padded = $(padding(fieldbuffers[end].length, alignment))"
bufferoffset += padding(blen, alignment)
return bufferoffset
end
DataAPI.refarray(x::DictEncoded{T,S}) where {T,S} = x.indices .+ one(S)
DataAPI.refpool(x::DictEncoded) = copy(x.encoding.data)
function writebuffer(io, col::DictEncoded, alignment)
@debugv 1 "writebuffer: col = $(typeof(col))"
@debugv 2 col
writebitmap(io, col, alignment)
# write indices
n = writearray(io, col.indices)
@debugv 1 "writing array: col = $(typeof(col.indices)), n = $n, padded = $(padding(n, alignment))"
writezeros(io, paddinglength(n, alignment))
return
end