2510 lines
104 KiB
Clojure
Vendored
2510 lines
104 KiB
Clojure
Vendored
(ns tech.v3.libs.arrow
|
|
"Support for reading/writing apache arrow datasets. Datasets may be memory mapped
|
|
but default to being read via an input stream.
|
|
|
|
Supported datatypes:
|
|
|
|
* All numeric types - `:uint8`, `:int8`, `:uint16`, `:int16`, `:uint32`, `:int32`,
|
|
`:uint64`, `:int64`, `:float32`, `:float64`, `:boolean`.
|
|
* String types - `:string`, `:text`. During write you have the option to always write
|
|
data as text which can be more efficient in the memory-mapped read case as it doesnt'
|
|
require the creation of string tables at load time.
|
|
* Datetime Types - `:local-date`, `:local-time`, `:instant`. During read you have the
|
|
option to keep these types in their source numeric format e.g. 32 bit `:epoch-days`
|
|
for `:local-date` datatypes. This format can make some types of processing, such as
|
|
set creation, more efficient.
|
|
|
|
|
|
When writing a dataset an arrow file with a single record set is created. When
|
|
writing a sequence of datasets downstream schemas must be compatible with the schema
|
|
of the initial dataset so for instance a conversion of int32 to double is fine but
|
|
double to int32 is not.
|
|
|
|
mmap support on systems running JDK-17 requires the foreign or memory module to be
|
|
loaded. Appropriate JVM arguments can be found
|
|
[here](https://github.com/techascent/tech.ml.dataset/blob/0524ddd5bbcb9421a0f11290ec8a01b7795dcff9/project.clj#L69).
|
|
|
|
Example (with zstd compression):
|
|
|
|
```clojure
|
|
;; Writing
|
|
(arrow/dataset->stream! ds fname {:compression :zstd})
|
|
;; Reading
|
|
(arrow/stream->dataset path)
|
|
```
|
|
|
|
## Required Dependencies
|
|
|
|
In order to support both memory mapping and JDK-17, we only rely on the Arrow SDK's
|
|
flatbuffer and schema definitions:
|
|
|
|
```clojure
|
|
;; netty isn't required and will inevitably conflict with some more recent version
|
|
[org.apache.arrow/arrow-vector \"6.0.0\":exclusions [netty/netty io.netty/netty-common]]
|
|
[com.cnuernber/jarrow \"1.000\"]
|
|
[org.apache.commons/commons-compress \"1.21\"]
|
|
|
|
;;Compression codecs
|
|
[org.lz4/lz4-java \"1.8.0\"]
|
|
;;Required for decompressing lz4 streams with dependent blocks.
|
|
[net.java.dev.jna/jna \"5.10.0\"]
|
|
[com.github.luben/zstd-jni \"1.5.4-1\"]
|
|
```
|
|
The lz4 decompression system will fallback to lz4-java if liblz4 isn't installed or if
|
|
jna isn't loaded. The lz4-java java library will fail for arrow files that have dependent
|
|
block compression which are sometimes saved by python or R arrow implementations.
|
|
On current ubuntu, in order to install the lz4 library you need to do:
|
|
|
|
```console
|
|
sudo apt install liblz4-1
|
|
```
|
|
|
|
## Performance
|
|
|
|
Arrow has hands down highest performance of any of the formats although nippy comes very close when using
|
|
any compression. The highest performance pathway is to save out data with :strings-as-text? true and zero
|
|
compression then read them in using mmap - optionally with :text-as-strings? if you never want to see
|
|
tech.v3.datatype.Text objects in your dataset. This avoids the creation of string dictionaries during
|
|
deserialization as these have to be done greedily. It can dramatically increase many dataset sizes but
|
|
when mmap is used the overall size is irrelevant aside from iteration which can be heavily parallelized.
|
|
|
|
Example:
|
|
|
|
```clojure
|
|
;; Writing
|
|
(arrow/dataset->stream! ds fname {:strings-as-text? true})
|
|
;; Reading
|
|
(arrow/stream->dataset path {:text-as-strings? true :open-type :mmap})
|
|
```"
|
|
(:require [tech.v3.datatype.mmap :as mmap]
|
|
[tech.v3.datatype.datetime :as dtype-dt]
|
|
[tech.v3.datatype :as dtype]
|
|
[tech.v3.datatype.protocols :as dtype-proto]
|
|
[tech.v3.datatype.native-buffer :as native-buffer]
|
|
[tech.v3.datatype.nio-buffer :as nio-buffer]
|
|
[tech.v3.datatype.bitmap :as bitmap]
|
|
[tech.v3.datatype.casting :as casting]
|
|
[tech.v3.datatype.packing :as packing]
|
|
[tech.v3.datatype.array-buffer :as array-buffer]
|
|
[tech.v3.datatype.ffi :as dt-ffi]
|
|
[tech.v3.dataset.impl.column :as col-impl]
|
|
[tech.v3.dataset.impl.sparse-column :as sparse-col]
|
|
[tech.v3.dataset.impl.column-base :as col-base]
|
|
[tech.v3.dataset.column :as ds-col]
|
|
[tech.v3.dataset.protocols :as ds-proto]
|
|
[tech.v3.dataset.impl.dataset :as ds-impl]
|
|
[tech.v3.dataset.dynamic-int-list :as dyn-int-list]
|
|
[tech.v3.dataset.base :as ds-base]
|
|
[tech.v3.dataset.string-table :as str-table]
|
|
[tech.v3.dataset.utils :as ml-utils]
|
|
[tech.v3.dataset.io :as ds-io]
|
|
[tech.v3.parallel.for :as pfor]
|
|
[tech.v3.resource :as resource]
|
|
[tech.v3.io :as io]
|
|
[clojure.tools.logging :as log]
|
|
[clj-commons.primitive-math :as pmath]
|
|
[clojure.core.protocols :as clj-proto]
|
|
[clojure.datafy :refer [datafy]]
|
|
[charred.api :as json]
|
|
[ham-fisted.api :as hamf]
|
|
[ham-fisted.reduce :as hamf-rf]
|
|
[ham-fisted.lazy-noncaching :as lznc]
|
|
[ham-fisted.function :as hamf-fn]
|
|
[ham-fisted.set :as set]
|
|
[ham-fisted.protocols :as hamf-proto])
|
|
(:import [ham_fisted ArrayLists ArrayLists$ArrayOwner IMutList]
|
|
[org.apache.arrow.vector.ipc.message MessageSerializer]
|
|
[org.apache.arrow.flatbuf Message DictionaryBatch RecordBatch
|
|
FieldNode Buffer BodyCompression BodyCompressionMethod Footer Block]
|
|
[org.roaringbitmap RoaringBitmap]
|
|
[com.google.flatbuffers FlatBufferBuilder]
|
|
[org.apache.arrow.vector.types TimeUnit FloatingPointPrecision DateUnit]
|
|
[org.apache.arrow.vector.types.pojo Field Schema ArrowType$Int
|
|
ArrowType$Utf8 ArrowType$Timestamp ArrowType$Time DictionaryEncoding FieldType
|
|
ArrowType$FloatingPoint ArrowType$Bool ArrowType$Date ArrowType$Duration
|
|
ArrowType$LargeUtf8 ArrowType$Null ArrowType$List ArrowType$Binary ArrowType$FixedSizeBinary
|
|
ArrowType$Decimal]
|
|
[org.apache.arrow.flatbuf CompressionType]
|
|
[org.apache.arrow.vector.types MetadataVersion]
|
|
[org.apache.arrow.vector.ipc WriteChannel]
|
|
[tech.v3.dataset.string_table StringTable]
|
|
[tech.v3.dataset.impl.column Column]
|
|
[tech.v3.dataset Text]
|
|
[tech.v3.datatype.native_buffer NativeBuffer]
|
|
[tech.v3.datatype ObjectReader ArrayHelpers ByteConversions BooleanBuffer]
|
|
[tech.v3.datatype.array_buffer ArrayBuffer]
|
|
[java.io OutputStream InputStream ByteArrayOutputStream ByteArrayInputStream]
|
|
[java.nio ByteBuffer ByteOrder ShortBuffer IntBuffer LongBuffer DoubleBuffer
|
|
FloatBuffer]
|
|
[java.util List ArrayList Map HashMap Map$Entry Iterator Set UUID Arrays]
|
|
[java.util.concurrent ForkJoinTask]
|
|
[java.time ZoneId]
|
|
[java.nio.channels WritableByteChannel]
|
|
;;Compression codecs
|
|
[com.github.luben.zstd Zstd]
|
|
[org.apache.commons.compress.compressors.lz4 FramedLZ4CompressorInputStream
|
|
FramedLZ4CompressorOutputStream]
|
|
[net.jpountz.lz4 LZ4Factory]
|
|
;;feather support
|
|
[java.io RandomAccessFile BufferedInputStream ByteArrayInputStream
|
|
ByteArrayOutputStream]
|
|
[uk.ac.bristol.star.feather FeatherTable BufUtils
|
|
FeatherType]
|
|
[uk.ac.bristol.star.fbs.feather Type]
|
|
[uk.ac.bristol.star.fbs.feather CTable]))
|
|
|
|
|
|
(set! *warn-on-reflection* true)
|
|
(set! *unchecked-math* :warn-on-boxed)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Arrow stream adapters
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- create-in-memory-byte-channel
|
|
^WritableByteChannel []
|
|
(let [storage (dtype/make-list :int8)]
|
|
(reify
|
|
WritableByteChannel
|
|
(isOpen [this] true)
|
|
(close [this])
|
|
(write [this bb]
|
|
(let [retval (.remaining bb)]
|
|
(.addAll storage (dtype/->buffer bb))
|
|
(.position bb (+ (.position bb) (.remaining bb)))
|
|
retval))
|
|
dtype-proto/PToBuffer
|
|
(convertible-to-buffer? [this] true)
|
|
(->buffer [this] (dtype-proto/->buffer storage)))))
|
|
|
|
|
|
(defn- arrow-in-memory-writer
|
|
^WriteChannel []
|
|
(let [storage (create-in-memory-byte-channel)]
|
|
{:channel (WriteChannel. storage)
|
|
:storage storage}))
|
|
|
|
|
|
(defn- arrow-output-stream-writer
|
|
^WriteChannel [^OutputStream ostream]
|
|
(let [closed?* (atom false)]
|
|
(-> (reify WritableByteChannel
|
|
(isOpen [this] @closed?*)
|
|
(close [this]
|
|
(when (compare-and-set! closed?* false true)
|
|
(.close ostream)))
|
|
(write [this bb]
|
|
(let [written (.remaining bb)]
|
|
(.write ostream (dtype/->byte-array bb))
|
|
(.position bb (+ (.position bb) written))
|
|
written)))
|
|
(WriteChannel.))))
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Compression -
|
|
;; Compression happens in jvm-heap land.
|
|
;; Decompression happens in native-heap land.
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- ensure-bytes-array-buffer
|
|
^ArrayBuffer [data]
|
|
(if-let [ary-buf (dtype/as-array-buffer data)]
|
|
(if (= :int8 (dtype/elemwise-datatype ary-buf))
|
|
ary-buf
|
|
(dtype/make-container :int8 data))))
|
|
|
|
|
|
(defn- create-zstd-compressor
|
|
[comp-map]
|
|
(assert (= :zstd (get comp-map :compression-type)))
|
|
(let [comp-level (int (get comp-map :level 3))]
|
|
(fn [compbuf dstbuf]
|
|
(let [ninput (dtype/ecount compbuf)
|
|
maxlen (Zstd/compressBound ninput)
|
|
^bytes dstbuf (-> (if (< (dtype/ecount dstbuf) maxlen)
|
|
(byte-array maxlen)
|
|
dstbuf))
|
|
srcbuf (ensure-bytes-array-buffer compbuf)
|
|
^bytes src-data (.ary-data srcbuf)
|
|
n-written (Zstd/compressByteArray
|
|
dstbuf 0 maxlen
|
|
src-data
|
|
(unchecked-int (.offset srcbuf))
|
|
(unchecked-int (.n-elems srcbuf))
|
|
comp-level)]
|
|
{:writer-cache dstbuf
|
|
:dst-buffer (dtype/sub-buffer dstbuf 0 n-written)}))))
|
|
|
|
|
|
(defn- create-zstd-decompressor
|
|
[]
|
|
(fn [compbuf resbuf]
|
|
(if (instance? NativeBuffer compbuf)
|
|
(Zstd/decompress ^ByteBuffer (nio-buffer/as-nio-buffer resbuf)
|
|
^ByteBuffer (nio-buffer/as-nio-buffer compbuf))
|
|
(let [dst (byte-array (dtype/ecount resbuf))]
|
|
(Zstd/decompress dst (dtype/->byte-array compbuf))
|
|
(dtype/copy! dst resbuf)))))
|
|
|
|
|
|
#_(defn- create-apache-lz4-frame-compressor
|
|
[comp-map]
|
|
(assert (= :lz4 (get comp-map :compression-type)))
|
|
(fn [compbuf dstbuf]
|
|
(let [^ByteArrayOutputStream dstbuf (or dstbuf (ByteArrayOutputStream.))
|
|
os (FramedLZ4CompressorOutputStream. dstbuf)
|
|
srcbuf (ensure-bytes-array-buffer compbuf)
|
|
^bytes src-data (.ary-data srcbuf)]
|
|
(.write os src-data (unchecked-int (.offset srcbuf)) (unchecked-int (.n-elems srcbuf)))
|
|
(.finish os)
|
|
(let [final-bytes (.toByteArray dstbuf)]
|
|
(.reset dstbuf)
|
|
{:writer-cache dstbuf
|
|
:dst-buffer final-bytes}))))
|
|
|
|
|
|
(defn- create-jpnz-lz4-frame-compressor
|
|
[comp-map]
|
|
(assert (= :lz4 (get comp-map :compression-type)))
|
|
(fn [compbuf dstbuf]
|
|
(let [^ByteArrayOutputStream dstbuf (or dstbuf (ByteArrayOutputStream.))
|
|
os (net.jpountz.lz4.LZ4FrameOutputStream. dstbuf)
|
|
srcbuf (ensure-bytes-array-buffer compbuf)
|
|
^bytes src-data (.ary-data srcbuf)]
|
|
(.write os src-data (unchecked-int (.offset srcbuf)) (unchecked-int (.n-elems srcbuf)))
|
|
(.close os)
|
|
(let [final-bytes (.toByteArray dstbuf)]
|
|
(.reset dstbuf)
|
|
{:writer-cache dstbuf
|
|
:dst-buffer final-bytes}))))
|
|
|
|
|
|
(defonce ^:private init-liblz4* (delay ((requiring-resolve 'tech.v3.libs.arrow.liblz4/initialize!))))
|
|
|
|
|
|
(defn- ensure-native-buffer
|
|
^NativeBuffer [nbuf]
|
|
(if-let [retval (dtype/as-native-buffer nbuf)]
|
|
nbuf
|
|
(dtype/make-container :native-heap :int8 {:resource-type :stack} nbuf)))
|
|
|
|
(defn- create-jpnz-lz4-frame-decompressor
|
|
[]
|
|
(try
|
|
@init-liblz4*
|
|
(let [ctx-fn (requiring-resolve 'tech.v3.libs.arrow.liblz4/create-decomp-ctx)
|
|
decomp-fn (requiring-resolve 'tech.v3.libs.arrow.liblz4/LZ4F_decompress)
|
|
is-err-int (requiring-resolve 'tech.v3.libs.arrow.liblz4/LZ4F_isError)
|
|
err-str (requiring-resolve 'tech.v3.libs.arrow.liblz4/LZ4F_getErrorName)
|
|
decomp-ctx (ctx-fn)]
|
|
(fn [srcbuf dstbuf]
|
|
(resource/stack-resource-context
|
|
(let [srcbuf (ensure-native-buffer srcbuf)
|
|
n-dstbuf (ensure-native-buffer dstbuf)
|
|
srcsize (dt-ffi/make-ptr :int64 (dtype/ecount srcbuf))
|
|
dstsize (dt-ffi/make-ptr :int64 (dtype/ecount n-dstbuf))
|
|
errcode (decomp-fn decomp-ctx
|
|
n-dstbuf dstsize
|
|
srcbuf srcsize
|
|
nil)]
|
|
(when-not (== 0 (long (is-err-int errcode)))
|
|
(throw (Exception. (str (err-str errcode)))))
|
|
(when-not (identical? n-dstbuf dstbuf)
|
|
(dtype/copy! n-dstbuf dstbuf))
|
|
dstbuf))))
|
|
(catch Exception e
|
|
(log/warn "Unable to load native lz4 library, falling back to jpountz.
|
|
Dependent block frames are not supported!!")
|
|
(fn [srcbuf dstbuf]
|
|
(let [src-byte-data (dtype/->byte-array srcbuf)
|
|
bis (ByteArrayInputStream. src-byte-data)
|
|
is (net.jpountz.lz4.LZ4FrameInputStream. bis)
|
|
temp-dstbuf (byte-array (dtype/ecount dstbuf))]
|
|
(.read is temp-dstbuf)
|
|
(dtype/copy! temp-dstbuf dstbuf))))))
|
|
|
|
|
|
(def ^:private compression-info
|
|
{:lz4 {:file-type CompressionType/LZ4_FRAME
|
|
:compressor-fn create-jpnz-lz4-frame-compressor
|
|
:decompressor-fn create-jpnz-lz4-frame-decompressor}
|
|
:zstd {:file-type CompressionType/ZSTD
|
|
:compressor-fn create-zstd-compressor
|
|
:decompressor-fn create-zstd-decompressor}})
|
|
|
|
|
|
(defn- create-compressor
|
|
"Returns a function that takes two arguments, first is the buffer to be compressed
|
|
and second is a cache argument that is returned upon use of the compressor to allow
|
|
using the same buffer to compress data to. Compression happens in jvm-heap land
|
|
so the buffer to be compressed will be copied to a byte array."
|
|
[comp-map]
|
|
(if-let [comp-data (get compression-info (comp-map :compression-type))]
|
|
((comp-data :compressor-fn) comp-map)
|
|
(throw (Exception. (format "Unrecognized compressor map %s" comp-map)))))
|
|
|
|
|
|
(def ^:private file-type->compression-kwd
|
|
(->> compression-info
|
|
(map (fn [[k data]]
|
|
[(data :file-type) k]))
|
|
(into {})))
|
|
|
|
(def ^:private compression-kwd->file-type
|
|
(->> compression-info
|
|
(map (fn [[k data]]
|
|
[k (data :file-type)]))
|
|
(into {})))
|
|
|
|
|
|
(defn- create-decompressor
|
|
"Returns a function that takes a native-heap compressed buffer and a native-heap
|
|
buffer to hold the decompressed data and performs the decompression."
|
|
[^long comp-type]
|
|
(if-let [kwd (file-type->compression-kwd comp-type)]
|
|
((get-in compression-info [kwd :decompressor-fn]))
|
|
(throw (Exception. (format "Unrecognized file compression enum: %s" comp-type)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Protocol extensions for arrow schema types
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
|
(extend-protocol clj-proto/Datafiable
|
|
ArrowType$Int
|
|
(datafy [this]
|
|
(let [signed? (.getIsSigned this)
|
|
bit-width (.getBitWidth this)]
|
|
{:datatype (if signed?
|
|
(case bit-width
|
|
8 :int8
|
|
16 :int16
|
|
32 :int32
|
|
64 :int64)
|
|
(case bit-width
|
|
8 :uint8
|
|
16 :uint16
|
|
32 :uint32
|
|
64 :uint64))}))
|
|
ArrowType$Null
|
|
(datafy [this]
|
|
{:datatype :boolean
|
|
:subtype :null})
|
|
ArrowType$Utf8
|
|
(datafy [this]
|
|
{:datatype :string
|
|
:encoding :utf-8
|
|
:offset-buffer-datatype :uint32})
|
|
ArrowType$Binary
|
|
(datafy [this]
|
|
{:datatype :binary})
|
|
ArrowType$LargeUtf8
|
|
(datafy [this]
|
|
{:datatype :string
|
|
:encoding :utf-8
|
|
:offset-buffer-datatype :int64})
|
|
ArrowType$FloatingPoint
|
|
(datafy [this]
|
|
{:datatype (condp = (.getPrecision this)
|
|
FloatingPointPrecision/HALF :float16
|
|
FloatingPointPrecision/SINGLE :float32
|
|
FloatingPointPrecision/DOUBLE :float64)})
|
|
ArrowType$Timestamp
|
|
(datafy [this]
|
|
(merge
|
|
(condp = (.getUnit this)
|
|
TimeUnit/MILLISECOND {:datatype :epoch-milliseconds}
|
|
TimeUnit/MICROSECOND {:datatype :epoch-microseconds}
|
|
TimeUnit/SECOND {:datatype :epoch-second}
|
|
TimeUnit/NANOSECOND {:datatype :epoch-nanoseconds})
|
|
(when (and (.getTimezone this)
|
|
(not= 0 (count (.getTimezone this))))
|
|
{:timezone (.getTimezone this)})))
|
|
ArrowType$Time
|
|
(datafy [this]
|
|
(condp = (.getUnit this)
|
|
TimeUnit/MILLISECOND {:datatype :time-milliseconds}
|
|
TimeUnit/MICROSECOND {:datatype :time-microseconds}
|
|
TimeUnit/SECOND {:datatype :time-second}
|
|
TimeUnit/NANOSECOND {:datatype :time-nanosecond}))
|
|
ArrowType$Date
|
|
(datafy [this]
|
|
(condp = (.getUnit this)
|
|
DateUnit/MILLISECOND {:datatype :epoch-milliseconds}
|
|
DateUnit/DAY {:datatype :epoch-days}))
|
|
ArrowType$Bool
|
|
(datafy [this] {:datatype :boolean})
|
|
DictionaryEncoding
|
|
(datafy [this] {:id (.getId this)
|
|
:ordered? (.isOrdered this)
|
|
:index-type (datafy (.getIndexType this))})
|
|
ArrowType$FixedSizeBinary
|
|
(datafy [this] {:datatype :fixed-size-binary
|
|
:byte-width (.getByteWidth this)})
|
|
ArrowType$Decimal
|
|
(datafy [this] {:datatype :decimal
|
|
:scale (.getScale this)
|
|
:precision (.getPrecision this)
|
|
:bit-width (.getBitWidth this)})
|
|
ArrowType$List
|
|
(datafy [this]
|
|
{:datatype :list}))
|
|
|
|
|
|
(defn- pad
|
|
^long [^long data]
|
|
(let [padding (rem data 8)]
|
|
(if-not (== 0 padding)
|
|
(+ data (- 8 padding))
|
|
data)))
|
|
|
|
|
|
(defn- message-id->message-type
|
|
[^long message-id]
|
|
(case message-id
|
|
1 :schema
|
|
2 :dictionary-batch
|
|
3 :record-batch
|
|
{:unexpected-message-type message-id}))
|
|
|
|
|
|
(defn- message-type->message-id
|
|
^long [message-type]
|
|
(case message-type
|
|
:schema 1
|
|
:dictionary-batch 2
|
|
:record-batch 3))
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Readings messages from native buffers
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- read-msg-size
|
|
[data]
|
|
(let [msg-size (native-buffer/read-int data)]
|
|
(if (== -1 msg-size)
|
|
[(native-buffer/read-int data 4) 8]
|
|
[msg-size 4])))
|
|
|
|
(deftype MessageIter [^:unsynchronized-mutable data]
|
|
java.util.Iterator
|
|
(hasNext [this]
|
|
(and (> (dtype/ecount data) 4)
|
|
(not (== 0 (long (nth (read-msg-size data) 0))))))
|
|
(next [this]
|
|
(let [[msg-size offset] (read-msg-size data)
|
|
offset (long offset)
|
|
msg-size (long msg-size)]
|
|
(let [new-msg (Message/getRootAsMessage
|
|
(-> (dtype/sub-buffer data offset msg-size)
|
|
(nio-buffer/native-buf->nio-buf)))
|
|
next-buf (dtype/sub-buffer data (+ offset msg-size))
|
|
body-length (.bodyLength new-msg)
|
|
aligned-offset (pad (+ offset msg-size body-length))
|
|
rv {:message new-msg
|
|
:message-type (message-id->message-type (.headerType new-msg))}]
|
|
(set! data (dtype/sub-buffer data aligned-offset))
|
|
(if-not (== 0 body-length)
|
|
(assoc rv :body (dtype/sub-buffer next-buf 0 (.bodyLength new-msg)))
|
|
rv)))))
|
|
|
|
|
|
(defn- message-iterable
|
|
"Given a native buffer of arrow stream data, produce a sequence of flatbuf messages"
|
|
^Iterable [^NativeBuffer data]
|
|
(reify Iterable
|
|
(iterator [this] (MessageIter. data))))
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Reading messages from streams
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- read-int-LE
|
|
(^long [^InputStream is byte-buf]
|
|
(let [^bytes byte-buf (or byte-buf (byte-array 4))]
|
|
(if (== 4 (.read is byte-buf))
|
|
(ByteConversions/intFromBytesLE (aget byte-buf 0) (aget byte-buf 1)
|
|
(aget byte-buf 2) (aget byte-buf 3))
|
|
0)))
|
|
(^long [^InputStream is]
|
|
(read-int-LE is nil)))
|
|
|
|
(defn- read-stream-msg-size
|
|
^long [^InputStream is]
|
|
(let [msg-size (read-int-LE is)]
|
|
(if (== -1 msg-size)
|
|
(read-int-LE is)
|
|
msg-size)))
|
|
|
|
(deftype StreamMessageIter [^InputStream is ^{:unsynchronized-mutable true
|
|
:tag long} msg-size
|
|
close-input-stream?]
|
|
Iterator
|
|
(hasNext [this]
|
|
(if (== 0 msg-size)
|
|
(do
|
|
(when close-input-stream? (.close is))
|
|
false)
|
|
true))
|
|
(next [this]
|
|
(let [pad-msg-size (pad msg-size)
|
|
bytes (byte-array pad-msg-size)
|
|
_ (.read is bytes)
|
|
new-msg (Message/getRootAsMessage (nio-buffer/as-nio-buffer
|
|
(dtype/sub-buffer bytes 0 msg-size)))
|
|
body-len (.bodyLength new-msg)
|
|
pad-body-len (pad body-len)
|
|
nbuf (when-not (== 0 body-len)
|
|
(dtype/make-container :int8 pad-body-len))
|
|
read-chunk-size (* 1024 1024 100)
|
|
read-buffer (byte-array (min body-len read-chunk-size))
|
|
bytes-read (when nbuf
|
|
(loop [offset 0]
|
|
(let [amount-to-read (min (- pad-body-len offset) read-chunk-size)
|
|
n-read (long (if-not (== 0 amount-to-read)
|
|
(.read is read-buffer 0 amount-to-read)
|
|
0))]
|
|
(if-not (== 0 n-read)
|
|
(do (dtype/copy! (dtype/sub-buffer read-buffer 0 n-read)
|
|
(dtype/sub-buffer nbuf offset n-read))
|
|
(recur (+ offset n-read)))
|
|
offset))))]
|
|
(when (and nbuf (not= bytes-read pad-body-len))
|
|
(when close-input-stream? (.close is))
|
|
(throw (Exception. (format "Unable to read entire buffer - Expected %d got %d"
|
|
pad-body-len bytes-read))))
|
|
(set! msg-size (read-stream-msg-size is))
|
|
(merge {:message new-msg
|
|
:message-type (message-id->message-type (.headerType new-msg))}
|
|
(when nbuf
|
|
{:body (dtype/sub-buffer nbuf 0 body-len)})))))
|
|
|
|
(defn- stream-message-iterable
|
|
^Iterable [is options]
|
|
(reify Iterable
|
|
(iterator [this] (StreamMessageIter. is (read-stream-msg-size is)
|
|
(get options :close-input-stream? true)))))
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Datatype to schema mapping
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Create a dictionary and dictionary encoding from a string column
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- string-column->dict
|
|
[col]
|
|
(let [metadata (meta col)
|
|
colname (:name metadata)
|
|
dict-id (.hashCode ^Object colname)
|
|
str-t (ds-base/ensure-column-string-table col)
|
|
^StringTable prev-str-t (::previous-string-table metadata)
|
|
int->str (str-table/int->string str-t)
|
|
indices (dtype-proto/->array-buffer (str-table/indices str-t))
|
|
n-elems (.size int->str)
|
|
bit-width (casting/int-width (dtype/elemwise-datatype indices))
|
|
arrow-indices-type (ArrowType$Int. bit-width true)
|
|
encoding (DictionaryEncoding. dict-id false arrow-indices-type)
|
|
byte-data (dtype/make-list :int8)
|
|
;;offsets are int32 apparently
|
|
offsets (dtype/make-list :int32)]
|
|
(if (nil? prev-str-t)
|
|
(dotimes [str-idx (count int->str)]
|
|
(let [strdata (.get int->str str-idx)
|
|
_ (when (= strdata :failure)
|
|
(throw (Exception. "Invalid string table - missing entries.")))
|
|
str-bytes (.getBytes (str strdata))
|
|
soff (dtype/ecount byte-data)]
|
|
(.addAllReducible byte-data (ArrayLists/toList str-bytes))
|
|
(.add offsets soff)))
|
|
(let [prev-int->str (str-table/int->string prev-str-t)
|
|
start-offset (dtype/ecount prev-int->str)
|
|
n-extra (- (dtype/ecount int->str) (dtype/ecount prev-int->str))
|
|
_ (assert (>= n-extra 0) "Later string tables should only be larger")]
|
|
(dotimes [str-idx n-extra]
|
|
(let [strdata (int->str (+ str-idx start-offset))
|
|
_ (when (= strdata :failure)
|
|
(throw (Exception. "Invalid string table - missing entries.")))
|
|
str-bytes (.getBytes (str strdata))
|
|
soff (dtype/ecount byte-data)]
|
|
(.addAllReducible byte-data (ArrayLists/toList str-bytes))
|
|
(.add offsets soff)))))
|
|
;;Make everyone's life easier by adding an extra offset.
|
|
(.add offsets (dtype/ecount byte-data))
|
|
{:encoding encoding
|
|
:string-table str-t
|
|
:byte-data byte-data
|
|
:offsets (dtype-proto/->array-buffer offsets)
|
|
:is-delta? (boolean prev-str-t)}))
|
|
|
|
|
|
(defn- string-col->encoding
|
|
"Given a string column return a map of :dict-id :table-width. The dictionary
|
|
id is the hashcode of the column mame."
|
|
[^List dictionaries col]
|
|
(let [dict (string-column->dict col)]
|
|
(.add dictionaries dict)
|
|
{:encoding (:encoding dict)}))
|
|
|
|
|
|
(defn- make-field
|
|
^Field [^String name ^FieldType field-type]
|
|
(Field. name field-type nil))
|
|
|
|
|
|
(defn- field-type
|
|
^FieldType
|
|
([nullable? ^FieldType datatype ^DictionaryEncoding dict-encoding ^Map str-str-meta]
|
|
(FieldType. (boolean nullable?) datatype dict-encoding str-str-meta))
|
|
([nullable? datatype str-meta]
|
|
(field-type nullable? datatype nil str-meta))
|
|
([nullable? datatype]
|
|
(field-type nullable? datatype nil)))
|
|
|
|
|
|
(defn- ->str-str-meta
|
|
[metadata]
|
|
(->> metadata
|
|
(map (fn [[k v]] [(json/write-json-str k) (json/write-json-str v)]))
|
|
(into {})))
|
|
|
|
(def ^{:private true
|
|
:tag String} ARROW_EXTENSION_NAME "ARROW:extension:name")
|
|
(def ^{:private true
|
|
:tag String} ARROW_UUID_NAME "arrow.uuid")
|
|
|
|
(defn- datatype->field-type
|
|
(^FieldType [datatype & [nullable? metadata extra-data]]
|
|
(let [nullable? (or nullable? (= :object (casting/flatten-datatype datatype)))
|
|
metadata (->str-str-meta (dissoc metadata
|
|
:name :datatype :categorical?
|
|
::previous-string-table
|
|
::complex-datatype))
|
|
ft-fn (fn [arrow-type & [dict-encoding]]
|
|
(field-type nullable? arrow-type dict-encoding metadata))
|
|
complex-datatype datatype
|
|
datatype (if (map? complex-datatype)
|
|
(get complex-datatype :datatype)
|
|
datatype)
|
|
datatype (packing/unpack-datatype datatype)]
|
|
(case (if #{:epoch-microseconds :epoch-milliseconds
|
|
:epoch-days}
|
|
datatype
|
|
(casting/un-alias-datatype datatype))
|
|
:boolean (ft-fn (ArrowType$Bool.))
|
|
:uint8 (ft-fn (ArrowType$Int. 8 false))
|
|
:int8 (ft-fn (ArrowType$Int. 8 true))
|
|
:uint16 (ft-fn (ArrowType$Int. 16 false))
|
|
:int16 (ft-fn (ArrowType$Int. 16 true))
|
|
:uint32 (ft-fn (ArrowType$Int. 32 false))
|
|
:int32 (ft-fn (ArrowType$Int. 32 true))
|
|
:uint64 (ft-fn (ArrowType$Int. 64 false))
|
|
:int64 (ft-fn (ArrowType$Int. 64 true))
|
|
:float32 (ft-fn (ArrowType$FloatingPoint. FloatingPointPrecision/SINGLE))
|
|
:float64 (ft-fn (ArrowType$FloatingPoint. FloatingPointPrecision/DOUBLE))
|
|
:epoch-milliseconds (ft-fn (ArrowType$Timestamp. TimeUnit/MILLISECOND
|
|
(str (:timezone extra-data))))
|
|
:instant (ft-fn (ArrowType$Timestamp. TimeUnit/MICROSECOND
|
|
(str (:timezone extra-data))))
|
|
:epoch-microseconds (ft-fn (ArrowType$Timestamp. TimeUnit/MICROSECOND
|
|
(str (:timezone extra-data))))
|
|
:epoch-nanoseconds (ft-fn (ArrowType$Timestamp. TimeUnit/NANOSECOND
|
|
(str (:timezone extra-data))))
|
|
:epoch-days (ft-fn (ArrowType$Date. DateUnit/DAY))
|
|
:local-date (ft-fn (ArrowType$Date. DateUnit/DAY))
|
|
;;packed local time is 64bit microseconds since midnight
|
|
:local-time (ft-fn (ArrowType$Time. TimeUnit/MICROSECOND (int 64)))
|
|
:time-nanoseconds (ft-fn (ArrowType$Time. TimeUnit/NANOSECOND (int 64)))
|
|
:time-microseconds (ft-fn (ArrowType$Time. TimeUnit/MICROSECOND (int 64)))
|
|
:time-milliseconds (ft-fn (ArrowType$Time. TimeUnit/MILLISECOND (int 32)))
|
|
:time-seconds (ft-fn (ArrowType$Time. TimeUnit/SECOND (int 32)))
|
|
:duration (ft-fn (ArrowType$Duration. TimeUnit/MICROSECOND))
|
|
:string (if-let [^DictionaryEncoding encoding (:encoding extra-data)]
|
|
(ft-fn (ArrowType$Utf8.) encoding)
|
|
;;If no encoding is provided then just save the string as text
|
|
(ft-fn (ArrowType$Utf8.)))
|
|
:decimal (ft-fn (ArrowType$Decimal. (unchecked-int (get complex-datatype :precision))
|
|
(unchecked-int (get complex-datatype :scale))
|
|
(unchecked-int (get complex-datatype :bit-width))))
|
|
:uuid (ft-fn (ArrowType$FixedSizeBinary. 16))
|
|
:text (ft-fn (ArrowType$Utf8.))
|
|
:encoded-text (ft-fn (ArrowType$Utf8.))))))
|
|
|
|
|
|
(defn- col->field
|
|
^Field [dictionaries {strings-as-text? :strings-as-text?}
|
|
col]
|
|
(let [colmeta (meta col)
|
|
colmeta (if (identical? :uuid (get colmeta :datatype))
|
|
(assoc colmeta ARROW_EXTENSION_NAME ARROW_UUID_NAME)
|
|
colmeta)
|
|
nullable? (boolean
|
|
(or (:nullable? colmeta)
|
|
(not (empty? (ds-proto/missing col)))))
|
|
col-dtype (or (::complex-datatype colmeta) (:datatype colmeta))
|
|
colname (:name colmeta)
|
|
extra-data (merge (select-keys (meta col) [:timezone])
|
|
(when (and (not strings-as-text?)
|
|
(= :string col-dtype))
|
|
(string-col->encoding dictionaries col)))]
|
|
(try
|
|
(make-field
|
|
(ml-utils/column-safe-name colname)
|
|
(datatype->field-type col-dtype nullable? colmeta extra-data))
|
|
(catch Throwable e
|
|
(throw (Exception. (format "Column %s metadata conversion failure:\n%s"
|
|
colname e)
|
|
e))))))
|
|
|
|
(defn- ->timezone
|
|
(^ZoneId [& [item]]
|
|
(cond
|
|
(instance? ZoneId item)
|
|
item
|
|
(string? item)
|
|
(ZoneId/of ^String item)
|
|
:else
|
|
(dtype-dt/utc-zone-id))))
|
|
|
|
|
|
(defn- try-json-parse
|
|
[val]
|
|
(when val
|
|
(try (json/read-json val :key-fn keyword)
|
|
(catch Exception e
|
|
val))))
|
|
|
|
|
|
(defn- datafy-field
|
|
[^Field field]
|
|
(let [df (datafy (.. field getFieldType getType))]
|
|
(when-not (map? df)
|
|
(throw (Exception. (format "Failed to dataty datatype %s-%s"
|
|
field (type (.. field getFieldType getType))))))
|
|
(merge
|
|
{:name (.getName field)
|
|
:nullable? (.isNullable field)
|
|
:field-type df
|
|
:metadata (->> (.getMetadata field)
|
|
(map (fn [^Map$Entry entry]
|
|
[(try-json-parse (.getKey entry))
|
|
(try-json-parse (.getValue entry))]))
|
|
(into {}))}
|
|
(when-let [c (seq (.getChildren field))]
|
|
{:children (mapv datafy-field c)})
|
|
(when-let [encoding (.getDictionary field)]
|
|
{:dictionary-encoding (datafy encoding)}))))
|
|
|
|
|
|
(defn- read-schema
|
|
"returns a pair of offset-data and schema"
|
|
[{:keys [message _body _message-type]}]
|
|
(let [schema (MessageSerializer/deserializeSchema ^Message message)
|
|
;; _ (println schema)
|
|
fields
|
|
(->> (.getFields schema)
|
|
(mapv datafy-field))
|
|
metadata (when-let [metadata (.getCustomMetadata schema)]
|
|
(into {} metadata))
|
|
sparse-columns (when-let [sparse-columns (get metadata "sparse-columns")]
|
|
(->> (.split ^String sparse-columns ",")
|
|
(map #(Long/parseLong %))
|
|
(hamf/java-hashset)))]
|
|
{:fields fields
|
|
:encodings (->> (map :dictionary-encoding fields)
|
|
(remove nil?)
|
|
(map (juxt :id identity))
|
|
(into {}))
|
|
:metadata metadata
|
|
:sparse-columns sparse-columns}))
|
|
|
|
|
|
(defn- write-message-header
|
|
[^WriteChannel writer msg-header-data]
|
|
;;continuation so we have 8 byte lengths
|
|
(let [start (.getCurrentPosition writer)
|
|
meta-len (dtype/ecount msg-header-data)
|
|
padding (rem (+ start meta-len 8) 8)
|
|
;;pad so that start + meta-len ends on an 8 byte boundary
|
|
meta-len (long (if-not (== 0 padding)
|
|
(+ meta-len (- 8 padding))
|
|
meta-len))]
|
|
(.writeIntLittleEndian writer -1)
|
|
(.writeIntLittleEndian writer meta-len)
|
|
(.write writer (dtype/->byte-array msg-header-data))
|
|
(.align writer)))
|
|
|
|
|
|
(defn- write-schema
|
|
"Writes a schema to a message header."
|
|
[writer schema]
|
|
(write-message-header writer (MessageSerializer/serializeMetadata ^Schema schema)))
|
|
|
|
|
|
(defn- LE-wrap-data
|
|
^java.nio.ByteBuffer [buffer]
|
|
(let [^java.nio.ByteBuffer bbuf (nio-buffer/->nio-buffer buffer)]
|
|
(.order bbuf java.nio.ByteOrder/LITTLE_ENDIAN)))
|
|
|
|
(defn- read-long
|
|
^long [buffer]
|
|
(if (instance? NativeBuffer buffer)
|
|
(native-buffer/read-long buffer)
|
|
(-> (LE-wrap-data buffer)
|
|
(.getLong))))
|
|
|
|
|
|
(defn- decompress-buffers
|
|
[^BodyCompression compression buffers]
|
|
(if-not compression
|
|
buffers
|
|
(do
|
|
(when-not (== 0 (.method compression))
|
|
(throw (Exception. (format "Only buffer batch compression supported - got %d"
|
|
(.method compression)))))
|
|
(let [decompressor (create-decompressor (.codec compression))
|
|
buf-type (if (instance? NativeBuffer (hamf/first buffers))
|
|
:native-heap
|
|
:jvm-heap)
|
|
buffers (mapv (fn [buffer]
|
|
(if (> (dtype/ecount buffer) 8)
|
|
(let [orig-len (read-long buffer)]
|
|
{:orig-len orig-len
|
|
:buffer (dtype/sub-buffer buffer 8)})
|
|
{:orig-len (dtype/ecount buffer)
|
|
:buffer buffer}))
|
|
buffers)
|
|
;;All results get decompressed into one final buffer
|
|
decomp-buf-len (->> (lznc/map :orig-len buffers)
|
|
(lznc/remove #(== -1 (long %)))
|
|
(reduce + 0)
|
|
(long))
|
|
decomp-buf (dtype/make-container buf-type :int8 decomp-buf-len)]
|
|
(->> buffers
|
|
(reduce (fn [[res decomp-buf] {:keys [orig-len buffer]}]
|
|
;;-1 indicates the buffer isn't actually compressed.
|
|
;;And some buffers are just empty with size of 0
|
|
#_(println orig-len (dtype/ecount buffer)
|
|
(when (> (dtype/ecount buffer) 32)
|
|
(-> (native-buffer/set-native-datatype buffer :int32)
|
|
(dtype/sub-buffer 0 8)
|
|
(vec))))
|
|
(if (<= (long orig-len) 0)
|
|
[(conj res buffer) decomp-buf]
|
|
[(conj res (decompressor
|
|
buffer (dtype/sub-buffer decomp-buf 0 orig-len)))
|
|
(dtype/sub-buffer decomp-buf orig-len)]))
|
|
[[] decomp-buf])
|
|
(first))))))
|
|
|
|
|
|
(defn- read-record-batch
|
|
([^RecordBatch record-batch ^NativeBuffer data]
|
|
{:compression (.compression record-batch)
|
|
:nodes (->> (hamf/range (.nodesLength record-batch))
|
|
(mapv #(let [node (.nodes record-batch (unchecked-int %))]
|
|
{:n-elems (.length node)
|
|
:n-null-entries (.nullCount node)})))
|
|
:buffers (->> (hamf/range (.buffersLength record-batch))
|
|
(mapv #(let [buffer (.buffers record-batch (unchecked-int %))]
|
|
(dtype/sub-buffer data (.offset buffer)
|
|
(.length buffer)))))})
|
|
([{:keys [message body _message-type]}]
|
|
(read-record-batch (.header ^Message message (RecordBatch.)) body)))
|
|
|
|
|
|
(defn- write-record-batch-header
|
|
[^FlatBufferBuilder builder n-rows nodes buffers compression-type]
|
|
(let [_ (RecordBatch/startNodesVector builder (count nodes))
|
|
;;Apparently you have to reverse structs when writing to a vector...
|
|
_ (doseq [node (reverse nodes)]
|
|
(FieldNode/createFieldNode builder
|
|
(long (node :n-elems))
|
|
(long (node :n-null-entries))))
|
|
node-offset (.endVector builder)
|
|
_ (RecordBatch/startBuffersVector builder (count buffers))
|
|
_ (doseq [buffer (reverse buffers)]
|
|
(Buffer/createBuffer builder
|
|
(long (buffer :offset))
|
|
(long (buffer :length))))
|
|
buffers-offset (.endVector builder)
|
|
comp-offset (when compression-type
|
|
(BodyCompression/createBodyCompression
|
|
builder
|
|
(unchecked-byte compression-type)
|
|
(BodyCompressionMethod/BUFFER)))]
|
|
(RecordBatch/startRecordBatch builder)
|
|
(RecordBatch/addLength builder (long n-rows))
|
|
(when comp-offset (RecordBatch/addCompression builder (unchecked-int comp-offset)))
|
|
(RecordBatch/addNodes builder node-offset)
|
|
(RecordBatch/addBuffers builder buffers-offset)
|
|
(RecordBatch/endRecordBatch builder)))
|
|
|
|
|
|
(defn- len->bitwise-len
|
|
"Given a length of a boolean vector, return the length if
|
|
represented by a bitwise vector."
|
|
^long [^long n-elems]
|
|
(quot (+ n-elems 7) 8))
|
|
|
|
|
|
(defn- no-missing
|
|
[^long n-elems]
|
|
(let [n-bytes (len->bitwise-len n-elems)
|
|
c (dtype/make-container :int8 n-bytes)]
|
|
(dtype/set-constant! c -1)
|
|
c))
|
|
|
|
(defn- as-shorts
|
|
^shorts [abuf]
|
|
(let [bbuf (LE-wrap-data abuf)
|
|
rv (short-array (quot (dtype/ecount abuf) 2))]
|
|
(-> (.asShortBuffer bbuf)
|
|
(.get rv))
|
|
rv))
|
|
|
|
(defn- as-ints
|
|
^ints [abuf]
|
|
(let [bbuf (LE-wrap-data abuf)
|
|
rv (int-array (quot (dtype/ecount abuf) 4))]
|
|
(-> (.asIntBuffer bbuf)
|
|
(.get rv))
|
|
rv))
|
|
|
|
(defn- as-longs
|
|
^longs [abuf]
|
|
(let [bbuf (LE-wrap-data abuf)
|
|
rv (long-array (quot (dtype/ecount abuf) 8))]
|
|
(-> (.asLongBuffer bbuf)
|
|
(.get rv))
|
|
rv))
|
|
|
|
(defn- as-floats
|
|
^floats [abuf]
|
|
(let [bbuf (LE-wrap-data abuf)
|
|
rv (float-array (quot (dtype/ecount abuf) 4))]
|
|
(-> (.asFloatBuffer bbuf)
|
|
(.get rv))
|
|
rv))
|
|
|
|
(defn- as-doubles
|
|
^doubles [abuf]
|
|
(let [bbuf (LE-wrap-data abuf)
|
|
rv (double-array (quot (dtype/ecount abuf) 8))]
|
|
(-> (.asDoubleBuffer bbuf)
|
|
(.get rv))
|
|
rv))
|
|
|
|
|
|
(defn- set-buffer-datatype
|
|
[buffer dtype]
|
|
(if (instance? tech.v3.datatype.native_buffer.NativeBuffer buffer)
|
|
(native-buffer/set-native-datatype buffer dtype)
|
|
(let [abuf (dtype/->array-buffer buffer)]
|
|
(let [offset (.-offset abuf)
|
|
n-elems (.-n-elems abuf)
|
|
src-data (.-ary-data abuf)]
|
|
(case (casting/host-flatten dtype)
|
|
(:int8 :uint8) (ArrayBuffer. src-data offset n-elems dtype nil nil)
|
|
(:int16 :uint16) (ArrayBuffer. (as-shorts abuf) 0 (quot n-elems 2) dtype nil nil)
|
|
(:int32 :uint32) (ArrayBuffer. (as-ints abuf) 0 (quot n-elems 4) dtype nil nil)
|
|
(:int64 :uint64) (ArrayBuffer. (as-longs abuf) 0 (quot n-elems 8) dtype nil nil)
|
|
:float32 (ArrayBuffer. (as-floats abuf) 0 (quot n-elems 4) dtype nil nil)
|
|
:float64 (ArrayBuffer. (as-doubles abuf) 0 (quot n-elems 8) dtype nil nil))))))
|
|
|
|
(defn- ->jvm-array
|
|
[buffer ^long off ^long len]
|
|
(if (instance? NativeBuffer buffer)
|
|
(native-buffer/->jvm-array buffer off len)
|
|
(let [abuf (dtype/->array-buffer buffer)
|
|
ary-data (.-ary-data abuf)
|
|
offset (.-offset abuf)
|
|
n-elems (.-n-elems abuf)]
|
|
(if (and (== (.-offset abuf) off)
|
|
(== (.-n-elems abuf) len))
|
|
(.-ary-data abuf)
|
|
(let [^ArrayLists$ArrayOwner owner (ArrayLists/toList ary-data)]
|
|
(.copyOfRange owner offset (+ offset n-elems)))))))
|
|
|
|
(defn- byte-length
|
|
^long [container]
|
|
(* (dtype/ecount container) (casting/numeric-byte-width
|
|
(dtype/elemwise-datatype container))))
|
|
|
|
|
|
(defn- serialize-to-bytes
|
|
"Serialize a numeric buffer to a byte buffer."
|
|
^java.nio.Buffer [num-data]
|
|
(let [data-dt (casting/un-alias-datatype (dtype/elemwise-datatype num-data))
|
|
n-bytes (byte-length num-data)
|
|
backing-buf (byte-array n-bytes)
|
|
bbuf (-> (java.nio.ByteBuffer/wrap backing-buf)
|
|
(.order java.nio.ByteOrder/LITTLE_ENDIAN))
|
|
ary-data (dtype/->array (if (= data-dt :boolean)
|
|
:int8
|
|
data-dt)
|
|
num-data)]
|
|
(if (instance? java.nio.Buffer num-data)
|
|
(case (dtype/elemwise-datatype ary-data)
|
|
:int8 (.put bbuf ^java.nio.ByteBuffer num-data)
|
|
:int16 (-> (.asShortBuffer bbuf)
|
|
(.put ^java.nio.ShortBuffer num-data))
|
|
:int32 (-> (.asIntBuffer bbuf)
|
|
(.put ^java.nio.IntBuffer num-data))
|
|
:int64 (-> (.asLongBuffer bbuf)
|
|
(.put ^java.nio.LongBuffer num-data))
|
|
:float32 (-> (.asFloatBuffer bbuf)
|
|
(.put ^java.nio.FloatBuffer num-data))
|
|
:float64 (-> (.asDoubleBuffer bbuf)
|
|
(.put ^java.nio.DoubleBuffer num-data)))
|
|
(case (dtype/elemwise-datatype ary-data)
|
|
:int8 (.put bbuf ^bytes ary-data)
|
|
:int16 (-> (.asShortBuffer bbuf)
|
|
(.put ^shorts ary-data))
|
|
:int32 (-> (.asIntBuffer bbuf)
|
|
(.put ^ints ary-data))
|
|
:int64 (-> (.asLongBuffer bbuf)
|
|
(.put ^longs ary-data))
|
|
:float32 (-> (.asFloatBuffer bbuf)
|
|
(.put ^floats ary-data))
|
|
:float64 (-> (.asDoubleBuffer bbuf)
|
|
(.put ^doubles ary-data))))
|
|
backing-buf))
|
|
|
|
|
|
(defn- finish-builder
|
|
"Finish the flatbuffer builder returning a ByteBuffer"
|
|
^java.nio.ByteBuffer [^FlatBufferBuilder builder message-type header-off body-len]
|
|
(.finish builder
|
|
(Message/createMessage builder
|
|
(.toFlatbufID (MetadataVersion/DEFAULT))
|
|
message-type ;;dict message type
|
|
header-off
|
|
body-len
|
|
0 ;;custom metadata offset
|
|
))
|
|
(.dataBuffer builder))
|
|
|
|
|
|
(defn- compress-record-batch-buffers
|
|
[options buffers]
|
|
(if-let [comp-map (get options :compression)]
|
|
(let [buffers (vec buffers)
|
|
n-buffers (count buffers)]
|
|
{:compression-type (compression-kwd->file-type (comp-map :compression-type))
|
|
;;parallelize buffer compression
|
|
:buffers
|
|
(->> buffers
|
|
(hamf/pmap
|
|
(fn [buffer]
|
|
(let [buffers [buffer]
|
|
comp-fn (create-compressor comp-map)
|
|
os (ByteArrayOutputStream.)
|
|
data-len (byte-array 8)
|
|
^ByteBuffer nio-buf (nio-buffer/as-nio-buffer data-len)
|
|
data-len-buf (dtype/->buffer data-len)]
|
|
(-> (reduce (fn [[res writer-cache] buffer]
|
|
(let [buffer (serialize-to-bytes buffer)
|
|
uncomp-len (dtype/ecount buffer)
|
|
_ (ByteConversions/longToWriterLE uncomp-len data-len-buf 0)
|
|
{:keys [writer-cache dst-buffer]} (comp-fn buffer writer-cache)
|
|
dst-bytes (dtype/as-array-buffer dst-buffer)
|
|
_ (.write os data-len)
|
|
_ (.write os ^bytes (.ary-data dst-bytes)
|
|
(unchecked-int (.offset dst-bytes))
|
|
(unchecked-int (.n-elems dst-bytes)))
|
|
dst-buffer (.toByteArray os)]
|
|
(.reset os)
|
|
[(conj res (nio-buffer/as-nio-buffer dst-buffer))
|
|
writer-cache]))
|
|
[[] nil]
|
|
buffers)
|
|
(first)))))
|
|
(lznc/apply-concat)
|
|
(vec))})
|
|
{:buffers (vec (hamf/pmap #(-> (serialize-to-bytes %)
|
|
(nio-buffer/as-nio-buffer))
|
|
buffers))}))
|
|
|
|
(defn- buffers->buf-entries
|
|
[buffers]
|
|
(->
|
|
(reduce (fn [[res offset] buffer]
|
|
[(conj res {:offset offset
|
|
:length (dtype/ecount buffer)})
|
|
(pad (+ (long offset) (dtype/ecount buffer)))])
|
|
[[] 0]
|
|
buffers)
|
|
(first)))
|
|
|
|
|
|
(defn- block-data
|
|
[^long msg-start ^long data-start ^long data-end]
|
|
{:offset msg-start
|
|
:metadata-len (- data-start msg-start)
|
|
:data-len (- data-end data-start)})
|
|
|
|
|
|
(defn- write-block-data
|
|
[^FlatBufferBuilder builder block-data]
|
|
(Block/createBlock builder (long (block-data :offset))
|
|
(int (block-data :metadata-len))
|
|
(long (block-data :data-len))))
|
|
|
|
|
|
(defn- write-dictionary
|
|
"Write a dictionary to a dictionary batch. Returns enough information to construct
|
|
a Block entry in the footer."
|
|
[^WriteChannel writer {:keys [byte-data offsets encoding is-delta?]} options]
|
|
(let [n-elems (dec (count offsets))
|
|
missing (no-missing n-elems)
|
|
{:keys [compression-type buffers]}
|
|
(->> [missing offsets byte-data] (compress-record-batch-buffers options))
|
|
buffer-entries (buffers->buf-entries buffers)
|
|
enc-id (.getId ^DictionaryEncoding encoding)
|
|
offset-len (pad (byte-length offsets))
|
|
data-len (pad (count byte-data))
|
|
builder (FlatBufferBuilder.)
|
|
rbatch-off (write-record-batch-header
|
|
builder
|
|
n-elems
|
|
[{:n-elems n-elems
|
|
:n-null-entries 0}]
|
|
buffer-entries
|
|
compression-type)
|
|
last-entry (last buffer-entries)
|
|
body-len (pad (+ (long (last-entry :offset))
|
|
(long (last-entry :length))))
|
|
bbuf (finish-builder builder
|
|
(message-type->message-id :dictionary-batch)
|
|
(DictionaryBatch/createDictionaryBatch
|
|
builder enc-id rbatch-off (boolean is-delta?))
|
|
body-len)
|
|
msg-start (.getCurrentPosition writer)
|
|
_ (write-message-header writer bbuf)
|
|
data-start (.getCurrentPosition writer)
|
|
_ (doseq [buf buffers]
|
|
(.write writer ^ByteBuffer buf)
|
|
(.align writer))
|
|
data-end (.getCurrentPosition writer)]
|
|
(block-data msg-start data-start data-end)))
|
|
|
|
|
|
(defn- toggle-bit
|
|
^bytes [^bytes data ^long bit]
|
|
(let [idx (quot bit 8)
|
|
bit (rem bit 8)
|
|
existing (unchecked-int (aget data idx))]
|
|
(ArrayHelpers/aset data idx (unchecked-byte (bit-xor existing (bit-shift-left 1 bit)))))
|
|
data)
|
|
|
|
(defn validity-info
|
|
"returns [byte-array n-missing]"
|
|
[col ^bytes all-valid-buf]
|
|
(let [nbuf all-valid-buf
|
|
n-missing (ds-proto/missing-count col)]
|
|
[(if (== 0 n-missing)
|
|
nbuf
|
|
(if (sparse-col/is-sparse? col)
|
|
(reduce toggle-bit (byte-array (alength nbuf)) (first (ds-proto/column-buffer col)))
|
|
(reduce toggle-bit (Arrays/copyOf nbuf (alength nbuf)) (ds-proto/missing col))))
|
|
n-missing]))
|
|
|
|
(defn ^:no-doc boolean-bytes
|
|
^bytes [data]
|
|
(let [rdr (dtype/->reader data)
|
|
len (.lsize rdr)
|
|
retval (byte-array (len->bitwise-len len))]
|
|
(dotimes [idx len]
|
|
(when (.readObject rdr idx)
|
|
(toggle-bit retval idx)))
|
|
retval))
|
|
|
|
|
|
(defn col->buffers
|
|
[col ^long col-idx options]
|
|
(let [col-dt (casting/un-alias-datatype (dtype/elemwise-datatype col))
|
|
col-dt (if (and (:strings-as-text? options) (= col-dt :string))
|
|
:text
|
|
col-dt)
|
|
cbuf (tech.v3.datatype.protocols/->buffer col)
|
|
sparse? (contains? (get options :sparse-columns) col-idx)
|
|
^tech.v3.datatype.Buffer cbuf
|
|
(if sparse?
|
|
(if (sparse-col/is-sparse? col)
|
|
(tech.v3.datatype.protocols/->buffer (ds-proto/column-data col))
|
|
(let [c (col-base/make-container (dtype/elemwise-datatype col))
|
|
^RoaringBitmap missing (ds-proto/missing col)]
|
|
(dotimes [idx (dtype/ecount col)]
|
|
(when-not (.contains missing (unchecked-int idx))
|
|
(.add c (.readObject cbuf idx))))
|
|
(dtype/->buffer c)))
|
|
cbuf)]
|
|
;;Get the data as a datatype safe for conversion into a
|
|
;;nio buffer.
|
|
(if (casting/numeric-type? col-dt)
|
|
(let [cbuf (if (dtype/as-concrete-buffer cbuf)
|
|
cbuf
|
|
;;make buffer concrete
|
|
(dtype/clone cbuf))]
|
|
[(if-let [ary-buf (dtype/as-array-buffer cbuf)]
|
|
(-> (dtype/sub-buffer (.ary-data ary-buf)
|
|
(.offset ary-buf)
|
|
(.n-elems ary-buf))
|
|
(nio-buffer/as-nio-buffer))
|
|
(if-let [nbuf (dtype/as-native-buffer cbuf)]
|
|
(->
|
|
(native-buffer/set-native-datatype
|
|
nbuf (casting/datatype->host-datatype col-dt))
|
|
(nio-buffer/as-nio-buffer))
|
|
;;else data is not represented
|
|
(throw (Exception. "Numeric buffer missing concrete representation"))))])
|
|
(case col-dt
|
|
:boolean [(boolean-bytes cbuf)]
|
|
:uuid (let [data (byte-array (* 16 (dtype/ecount cbuf)))
|
|
wbuf (-> (java.nio.ByteBuffer/wrap data)
|
|
(.order java.nio.ByteOrder/BIG_ENDIAN))]
|
|
(reduce (fn [_ ^UUID v]
|
|
(if v
|
|
(do
|
|
(.putLong wbuf (.getMostSignificantBits v))
|
|
(.putLong wbuf (.getLeastSignificantBits v)))
|
|
(do
|
|
(.putLong wbuf 0) (.putLong wbuf 0))))
|
|
nil cbuf)
|
|
[(java.nio.ByteBuffer/wrap data)])
|
|
:decimal (let [colmeta (meta col)
|
|
{:keys [scale precision bit-width]} (get colmeta ::complex-datatype)
|
|
byte-width (quot (+ (long bit-width) 7) 8)
|
|
ne (.lsize cbuf)
|
|
byte-data (byte-array (* ne byte-width))
|
|
le? (identical? :little-endian (tech.v3.datatype.protocols/platform-endianness))]
|
|
(dotimes [idx ne]
|
|
(when-let [^BigDecimal d (.readObject cbuf idx)]
|
|
(let [^BigInteger bb (.unscaledValue d)
|
|
bb-bytes (.toByteArray bb)
|
|
offset (* idx byte-width)]
|
|
(if le?
|
|
(let [bb-len (alength bb-bytes)]
|
|
(dotimes [bidx bb-len]
|
|
(let [write-pos (+ offset (- bb-len bidx 1))]
|
|
(ArrayHelpers/aset byte-data write-pos (aget bb-bytes bidx)))))
|
|
(System/arraycopy bb-bytes 0 byte-data 0 (alength bb-bytes))))))
|
|
[(java.nio.ByteBuffer/wrap byte-data)])
|
|
:string (let [str-t (ds-base/ensure-column-string-table col)
|
|
indices (dtype-proto/->array-buffer (str-table/indices str-t))]
|
|
[(nio-buffer/as-nio-buffer indices)])
|
|
:text
|
|
(let [byte-data (dtype/make-list :int8)
|
|
offsets (dtype/make-list :int32)]
|
|
(reduce (fn [_ strdata]
|
|
(let [strdata (str (or strdata ""))]
|
|
(.add offsets (.size byte-data))
|
|
(.addAllReducible byte-data (ArrayLists/toList (.getBytes strdata))))) nil cbuf)
|
|
(.add offsets (.size byte-data))
|
|
[(nio-buffer/as-nio-buffer offsets)
|
|
(nio-buffer/as-nio-buffer byte-data)])))))
|
|
|
|
|
|
(defn- write-dataset
|
|
"Write a dataset-batch to a channel."
|
|
[^WriteChannel writer dataset options]
|
|
(let [n-rows (ds-base/row-count dataset)
|
|
;;Length of the byte valid buffer
|
|
valid-len (len->bitwise-len n-rows)
|
|
all-valid-buf (-> (doto (dtype/make-container :int8 valid-len)
|
|
(dtype/set-constant! -1))
|
|
(dtype/->byte-array))
|
|
nodes-buffs-lens
|
|
(->> (.values ^Map dataset)
|
|
(map-indexed vector)
|
|
(hamf/pmap (fn [[col-idx col]]
|
|
(let [[valid-buf n-missing] (validity-info col all-valid-buf)
|
|
buffers (hamf/concatv [valid-buf] (col->buffers col col-idx options))
|
|
lengths (hamf/mapv (comp pad byte-length) buffers)
|
|
col-len (long (reduce + 0 lengths))]
|
|
{:node {:n-elems n-rows
|
|
:n-null-entries n-missing}
|
|
:buffers buffers
|
|
:length col-len})))
|
|
(vec))
|
|
nodes (lznc/map :node nodes-buffs-lens)
|
|
{:keys [compression-type buffers]}
|
|
(->> (lznc/map :buffers nodes-buffs-lens)
|
|
(lznc/apply-concat)
|
|
(compress-record-batch-buffers options))
|
|
|
|
buf-entries (buffers->buf-entries buffers)
|
|
last-entry (last buf-entries)
|
|
body-len (if last-entry
|
|
(pad (+ (long (last-entry :offset)) (long (last-entry :length))))
|
|
0)
|
|
builder (FlatBufferBuilder.)
|
|
msg-start (.getCurrentPosition writer)
|
|
_ (write-message-header writer
|
|
(finish-builder
|
|
;;record-batch message type
|
|
builder (message-type->message-id :record-batch)
|
|
(write-record-batch-header builder n-rows nodes buf-entries
|
|
compression-type)
|
|
body-len))
|
|
data-start (.getCurrentPosition writer)
|
|
_ (doseq [buf buffers]
|
|
(.write writer ^ByteBuffer buf)
|
|
(.align writer))
|
|
data-end (.getCurrentPosition writer)]
|
|
(block-data msg-start data-start data-end)))
|
|
|
|
|
|
(defn- check-message-type
|
|
[expected-type actual-type]
|
|
(when-not (= actual-type expected-type)
|
|
(throw (Exception.
|
|
(format "Expected message type %s, got %s"
|
|
expected-type actual-type)))))
|
|
|
|
|
|
(defn- read-dictionary-batch
|
|
[{:keys [message body message-type]}]
|
|
(check-message-type :dictionary-batch message-type)
|
|
(let [^DictionaryBatch db (.header ^Message message (DictionaryBatch.))]
|
|
{:id (.id db)
|
|
:delta? (.isDelta db)
|
|
:records (read-record-batch (.data db) body)}))
|
|
|
|
|
|
(defmulti ^:private parse-message
|
|
"Given a message, parse it just a bit into a more interpretable datastructure."
|
|
:message-type)
|
|
|
|
|
|
(defmethod ^:private parse-message :schema
|
|
[msg]
|
|
(assoc (read-schema msg)
|
|
:message-type (:message-type msg)))
|
|
|
|
|
|
(defmethod ^:private parse-message :dictionary-batch
|
|
[msg]
|
|
(assoc (read-dictionary-batch msg)
|
|
:message-type (:message-type msg)))
|
|
|
|
|
|
(defmethod ^:private parse-message :record-batch
|
|
[msg]
|
|
(assoc (read-record-batch msg)
|
|
:message-type (:message-type msg)))
|
|
|
|
|
|
(defn ^:no-doc parse-message-printable
|
|
"Parse the message and return something that you can look at in the repl."
|
|
[msg]
|
|
(let [retval (parse-message msg)]
|
|
(cond
|
|
(contains? retval :records)
|
|
(update-in retval [:records :buffers]
|
|
#(mapv native-buffer/native-buffer->map %))
|
|
(contains? retval :buffers)
|
|
(update-in retval [:buffers]
|
|
#(mapv native-buffer/native-buffer->map %))
|
|
:else
|
|
retval)))
|
|
|
|
|
|
(defn- offsets-data->string-reader
|
|
^List [offsets data n-elems]
|
|
(let [n-elems (long n-elems)
|
|
offsets (dtype/->reader offsets)]
|
|
(dtype/make-reader-fn
|
|
:string :string n-elems
|
|
(if (instance? NativeBuffer data)
|
|
(fn [^long idx]
|
|
(let [start-off (long (offsets idx))
|
|
end-off (long (offsets (inc idx)))]
|
|
(native-buffer/native-buffer->string data start-off (- end-off start-off))))
|
|
(let [abuf (dtype/->array-buffer data)
|
|
^bytes src-data (.-ary-data abuf)
|
|
data-off (.-offset abuf)
|
|
]
|
|
(fn [^long idx]
|
|
(let [start-off (.readLong offsets idx)
|
|
end-off (.readLong offsets (inc idx))]
|
|
(String. src-data (+ start-off data-off) (- end-off start-off)))))))))
|
|
|
|
(defn- offsets-data->bytedata-reader
|
|
^List [offsets data n-elems]
|
|
(let [n-elems (long n-elems)
|
|
offsets (dtype/->reader offsets)]
|
|
(reify ObjectReader
|
|
(elemwiseDatatype [rdr] :object)
|
|
(lsize [rdr] n-elems)
|
|
(readObject [rdr idx]
|
|
(let [start-off (long (offsets idx))
|
|
end-off (long (offsets (inc idx)))]
|
|
(-> (dtype/sub-buffer data start-off
|
|
(- end-off start-off))
|
|
(dtype/->byte-array)))))))
|
|
|
|
|
|
(defn- dictionary->strings
|
|
"Returns a map of {:id :strings}"
|
|
[{:keys [id delta? records]}]
|
|
(let [nodes (:nodes records)
|
|
buffers (decompress-buffers (:compression records) (:buffers records))
|
|
_ (assert (== 1 (count nodes)))
|
|
_ (assert (== 3 (count buffers)))
|
|
node (first nodes)
|
|
[_bitwise offsets databuf] buffers
|
|
n-elems (long (:n-elems node))
|
|
offsets (-> (set-buffer-datatype offsets :int32)
|
|
(dtype/sub-buffer 0 (inc n-elems)))
|
|
data (set-buffer-datatype databuf :int8)
|
|
str-data (dtype/make-list :string (offsets-data->string-reader offsets data n-elems))]
|
|
{:id id
|
|
:delta? delta?
|
|
:strings str-data}))
|
|
|
|
|
|
(defn- string-reader->text-reader
|
|
^Buffer [item]
|
|
(let [data-buf (dtype/->reader item)]
|
|
(reify
|
|
ObjectReader
|
|
(elemwiseDatatype [rdr] :text)
|
|
(lsize [rdr] (.lsize data-buf))
|
|
(readObject [rdr idx]
|
|
(when-let [data (.readObject data-buf idx)]
|
|
(Text. (str data)))))))
|
|
|
|
|
|
(defn- string-data->column-data
|
|
[dict-map encoding offset-buf-dtype buffers n-elems options]
|
|
(if encoding
|
|
(StringTable. (-> (get dict-map (get encoding :id))
|
|
deref
|
|
(get :strings))
|
|
nil
|
|
(-> (first buffers)
|
|
(set-buffer-datatype (get-in encoding [:index-type :datatype]))
|
|
(->jvm-array 0 n-elems)
|
|
(ArrayLists/toList)))
|
|
(let [[offsets varchar-data] buffers
|
|
str-rdr (offsets-data->string-reader (set-buffer-datatype
|
|
offsets offset-buf-dtype)
|
|
varchar-data n-elems)]
|
|
(if-not (:text-as-strings? options)
|
|
(string-reader->text-reader str-rdr)
|
|
str-rdr))))
|
|
|
|
(defn validity->missing
|
|
^RoaringBitmap [validity ^long n-elems]
|
|
(hamf-rf/reduce-reducer
|
|
(hamf-rf/long-consumer-reducer
|
|
#(tech.v3.dataset.ByteValidity$MissingIndexReducer. n-elems (* 8 (dtype/ecount validity))))
|
|
validity))
|
|
|
|
(defn validity->indexes
|
|
^IMutList [validity ^long n-elems]
|
|
(->> validity
|
|
(hamf-rf/reduce-reducer
|
|
(hamf-rf/long-consumer-reducer
|
|
#(tech.v3.dataset.ByteValidity$ValidityIndexReducer. (cond
|
|
(< n-elems Byte/MAX_VALUE)
|
|
(hamf/byte-array-list)
|
|
(< n-elems Short/MAX_VALUE)
|
|
(hamf/short-array-list)
|
|
(< n-elems Integer/MAX_VALUE)
|
|
(hamf/int-array-list)
|
|
:else
|
|
(hamf/long-array-list))
|
|
n-elems)))))
|
|
|
|
(defn ^:no-doc byte-buffer->bitwise-boolean-buffer
|
|
^Buffer[bitbuffer ^long n-elems]
|
|
(let [buf (dtype/->buffer bitbuffer)]
|
|
(dtype/make-reader :boolean n-elems
|
|
(let [data (.readByte buf (quot idx 8))
|
|
shift-val (pmath/bit-shift-left 1 (rem idx 8))]
|
|
(if (pmath/== shift-val (pmath/bit-and data shift-val))
|
|
true
|
|
false)))))
|
|
|
|
(defn- field-node-count
|
|
[field]
|
|
(apply + 1 (map field-node-count
|
|
(get field :children))))
|
|
|
|
(defn- field-metadata
|
|
[field]
|
|
(-> (into (dissoc (:field-type field) :datatype) (:metadata field))
|
|
;;Fixes issue 330
|
|
(with-meta nil)
|
|
(assoc :name (:name field))))
|
|
|
|
(defmulti ^:private preparse-field
|
|
"Transform a field into a column."
|
|
(fn [field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(get-in field [:field-type :datatype])))
|
|
|
|
(defn construct-column
|
|
[sparse? node field buffers col-data-fn]
|
|
(let [rc (long (:n-elems node))
|
|
n-missing (long (:n-null-entries node))
|
|
metadata (field-metadata field)
|
|
validity-buf (nth buffers 0)
|
|
data-buffers (subvec buffers 1)]
|
|
(if sparse?
|
|
(let [^IMutList indexes (validity->indexes validity-buf rc)]
|
|
(sparse-col/construct-sparse-col indexes (col-data-fn (.size indexes) data-buffers) rc metadata))
|
|
(col-impl/construct-column (if (== 0 n-missing)
|
|
(bitmap/->bitmap)
|
|
(validity->missing validity-buf rc))
|
|
(col-data-fn rc data-buffers)
|
|
metadata))))
|
|
|
|
(defmethod ^:private preparse-field :string
|
|
[field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(assert (= 0 (count (:children field)))
|
|
(format "Field %s cannot be parsed with default parser" field))
|
|
(let [encoding (get field :dictionary-encoding)
|
|
buffers (if (nil? encoding)
|
|
[(.next buf-iter) (.next buf-iter) (.next buf-iter)]
|
|
[(.next buf-iter) (.next buf-iter)])
|
|
node (.next node-iter)
|
|
col-data-fn (fn [n-elems data-buffers]
|
|
(string-data->column-data
|
|
dict-map encoding
|
|
(get-in field [:field-type :offset-buffer-datatype])
|
|
data-buffers
|
|
n-elems
|
|
options))]
|
|
(fn parse-string-field
|
|
[decompressor]
|
|
(construct-column sparse? node field (decompressor buffers) col-data-fn))))
|
|
|
|
(defmethod ^:private preparse-field :boolean
|
|
[field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(assert (= 0 (count (:children field)))
|
|
(format "Field %s cannot be parsed with default parser" field))
|
|
(let [field-dtype (get-in field [:field-type :datatype])
|
|
node (.next node-iter)
|
|
nil-subtype? (identical? :null (get-in field [:field-type :subtype]))
|
|
buffers (when-not nil-subtype?
|
|
[(.next buf-iter) (.next buf-iter)])
|
|
col-data-fn (fn [n-elems data-buffers]
|
|
(byte-buffer->bitwise-boolean-buffer
|
|
(first data-buffers) n-elems))]
|
|
(fn parse-boolean-field
|
|
[decompressor]
|
|
;;nil subtype means null column
|
|
(if nil-subtype?
|
|
(let [n-elems (long (:n-elems node))]
|
|
(col-impl/new-column
|
|
(:name field)
|
|
(dtype/const-reader false n-elems)
|
|
(field-metadata field)
|
|
(bitmap/->bitmap (range n-elems))))
|
|
(construct-column sparse? node field (decompressor buffers) col-data-fn)))))
|
|
|
|
|
|
(defn- clone-downcast-text
|
|
[dbuf]
|
|
(if (= :text (dtype/elemwise-datatype dbuf))
|
|
(dtype/make-container :string (dtype/emap str :string dbuf))
|
|
(dtype/clone dbuf)))
|
|
|
|
|
|
(defmethod ^:private preparse-field :list
|
|
[field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(let [buffers [(.next buf-iter) (.next buf-iter)]
|
|
node (.next node-iter)
|
|
_ (assert (== 1 (count (:children field)))
|
|
(format "List types are expected to have exactly 1 child - found %d"
|
|
(count (:children field))))
|
|
;;L
|
|
sub-column-parse-fn (preparse-field (first (:children field)) false node-iter buf-iter dict-map options)]
|
|
(fn parse-list-field
|
|
[decompressor]
|
|
(let [sub-column (-> (sub-column-parse-fn decompressor)
|
|
;;copy to jvm memory. This is a quick insurance policy to ensure
|
|
;;if this buffer leaves the resource context it doesn't contain
|
|
;;native memory as only the parent buffer is cloned. A faster
|
|
;;but more involved option would be to make a custom reader with
|
|
;;an overloaded clone pathway.
|
|
(clone-downcast-text)
|
|
(dtype/->buffer))
|
|
col-data-fn (fn [^long n-elems data-buffers]
|
|
(let [offset-buf (first data-buffers)
|
|
offsets (-> (set-buffer-datatype
|
|
offset-buf :int32)
|
|
(dtype/sub-buffer 0 (inc n-elems))
|
|
(dtype/->buffer))]
|
|
(dtype/make-reader :object n-elems
|
|
(let [sidx (.readLong offsets idx)]
|
|
(dtype/sub-buffer sub-column
|
|
sidx
|
|
(- (.readLong offsets (unchecked-inc idx))
|
|
sidx))))))]
|
|
(construct-column sparse? node field (decompressor buffers) col-data-fn)))))
|
|
|
|
(defmethod ^:private preparse-field :binary
|
|
[field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(let [node (.next node-iter)
|
|
buffers [(.next buf-iter) (.next buf-iter) (.next buf-iter)]
|
|
col-data-fn (fn [^long n-elems data-buffers]
|
|
(let [[offset-buf data-buf] data-buffers
|
|
offsets (-> (native-buffer/set-native-datatype
|
|
offset-buf :int32)
|
|
(dtype/sub-buffer 0 (inc n-elems))
|
|
(dtype/->buffer))
|
|
data-buf (-> (dtype/clone data-buf)
|
|
(dtype/->buffer))]
|
|
(dtype/make-reader :object n-elems
|
|
(let [sidx (.readLong offsets idx)]
|
|
(dtype/sub-buffer data-buf
|
|
sidx
|
|
(- (.readLong offsets (unchecked-inc idx))
|
|
sidx))))))]
|
|
(fn parse-binary-field
|
|
[decompressor]
|
|
(construct-column sparse? node field (decompressor buffers) col-data-fn))))
|
|
|
|
(defmethod ^:private preparse-field :fixed-size-binary
|
|
[field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(let [node (.next node-iter)
|
|
buffers [(.next buf-iter) (.next buf-iter)]
|
|
field-width (long (get-in field [:field-type :byte-width]))
|
|
fm (field-metadata field)
|
|
col-data-fn (fn [n-elems data-buffers]
|
|
(let [data-buf (first data-buffers)
|
|
^bytes data-ary (if (instance? NativeBuffer data-buf)
|
|
(native-buffer/->jvm-array data-buf 0 (dtype/ecount data-buf))
|
|
(dtype/->array data-buf))]
|
|
(if (= ARROW_UUID_NAME (get fm ARROW_EXTENSION_NAME))
|
|
(let [longsdata (-> (java.nio.ByteBuffer/wrap data-ary)
|
|
(.order (java.nio.ByteOrder/BIG_ENDIAN)))]
|
|
(dtype/make-reader :uuid n-elems
|
|
(let [lidx (* idx 16)]
|
|
(java.util.UUID. (.getLong longsdata lidx)
|
|
(.getLong longsdata (+ lidx 8))))))
|
|
(let [ll (ArrayLists/toList data-ary)]
|
|
(dtype/make-reader :object n-elems
|
|
(let [lidx (* idx field-width)]
|
|
(.subList ll lidx (+ lidx field-width))))))))]
|
|
(fn parse-fixed-binary-field
|
|
[decompressor]
|
|
(construct-column sparse? node field (decompressor buffers) col-data-fn))))
|
|
|
|
(defn- copy-bytes
|
|
^bytes [^bytes data ^long sidx ^long eidx]
|
|
(Arrays/copyOfRange data sidx eidx))
|
|
|
|
(defn- copy-reverse-bytes
|
|
^bytes [^bytes data ^long sidx ^long eidx]
|
|
(let [ne (- eidx sidx)
|
|
rv (byte-array ne)]
|
|
(loop [idx 0]
|
|
(when (< idx ne)
|
|
(ArrayHelpers/aset rv idx (aget data (- eidx idx 1)))
|
|
(recur (inc idx))))
|
|
rv))
|
|
|
|
(defmethod ^:private preparse-field :decimal
|
|
[field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(let [node (.next node-iter)
|
|
buffers [(.next buf-iter) (.next buf-iter)]
|
|
{:keys [^long precision ^long scale ^long bit-width]} (get field :field-type)
|
|
byte-width (quot (+ bit-width 7) 8)
|
|
col-data-fn
|
|
(fn [n-elems data-buffers]
|
|
(let [data-buf (first data-buffers)
|
|
^bytes data-ary (if (instance? NativeBuffer data-buf)
|
|
(native-buffer/->jvm-array data-buf 0 (dtype/ecount data-buf))
|
|
(dtype/->array data-buf))
|
|
;;biginteger data is always stored big endian I guess...
|
|
;;https://github.com/apache/arrow-java/blob/main/vector/src/main/java/org/apache/arrow/vector/util/DecimalUtility.java#L53
|
|
array-copy (if (identical? :little-endian (tech.v3.datatype.protocols/platform-endianness))
|
|
copy-reverse-bytes
|
|
copy-bytes)]
|
|
(dtype/make-reader :decimal n-elems
|
|
(let [idx (* idx byte-width)]
|
|
(-> (BigInteger. ^bytes (array-copy data-ary idx (+ idx byte-width)))
|
|
(BigDecimal. scale))))))]
|
|
(fn parse-decimal
|
|
[decompressor]
|
|
(construct-column sparse? node field (decompressor buffers) col-data-fn))))
|
|
|
|
|
|
(defmethod ^:private preparse-field :default
|
|
[field sparse? ^Iterator node-iter ^Iterator buf-iter dict-map options]
|
|
(assert (= 0 (count (:children field)))
|
|
(format "Field %s cannot be parsed with default parser" field))
|
|
(let [field-dtype (get-in field [:field-type :datatype])
|
|
field-dtype (if-not (:integer-datetime-types? options)
|
|
(case field-dtype
|
|
:time-microseconds :packed-local-time
|
|
:epoch-microseconds :packed-instant
|
|
:epoch-days :packed-local-date
|
|
field-dtype)
|
|
field-dtype)
|
|
node (.next node-iter)
|
|
buffers [(.next buf-iter)
|
|
(.next buf-iter)]
|
|
col-data-fn
|
|
(fn [n-elems data-buffers]
|
|
(let [data-buf (first data-buffers)]
|
|
(-> (set-buffer-datatype data-buf field-dtype)
|
|
(dtype/sub-buffer 0 n-elems))))]
|
|
(fn parse-default-field
|
|
[decompressor]
|
|
(construct-column sparse? node field (decompressor buffers) col-data-fn))))
|
|
|
|
(defn- iter ^Iterator [^Iterable i] (when i (.iterator i)))
|
|
|
|
|
|
(defn- records->ds
|
|
[schema dict-map record-batch options]
|
|
(let [{:keys [nodes buffers]} record-batch
|
|
node-iter (iter (lznc/map-indexed #(assoc %2 :idx %1) nodes))
|
|
buf-iter (iter buffers)
|
|
fields (:fields schema)
|
|
keyfn (get options :key-fn identity)
|
|
keep-set (if-let [alist (get options :column-allowlist)]
|
|
(set alist)
|
|
(set (lznc/map (comp keyfn :name) fields)))
|
|
keep-set (if-let [blist (get options :column-blocklist)]
|
|
(set/difference keep-set (set blist))
|
|
keep-set)
|
|
decompressor (if-let [compression (get record-batch :compression)]
|
|
#(decompress-buffers compression %)
|
|
identity)]
|
|
(->> (:fields schema)
|
|
(lznc/map-indexed #(let [col-idx (long %1)
|
|
sparse? (contains? (get options :sparse-columns) col-idx)
|
|
parse-fn (preparse-field %2 sparse? node-iter buf-iter dict-map options)]
|
|
(when (.contains ^Set keep-set (keyfn (get %2 :name)))
|
|
parse-fn)))
|
|
(lznc/filter identity)
|
|
(lznc/map #(% decompressor))
|
|
(ds-impl/new-dataset options))))
|
|
|
|
|
|
(defn- bytes->short-array
|
|
^shorts [^bytes data nrows]
|
|
(let [retval (short-array nrows)]
|
|
(-> (ByteBuffer/wrap data)
|
|
(.order ByteOrder/LITTLE_ENDIAN)
|
|
(.asShortBuffer)
|
|
(.get retval))
|
|
retval))
|
|
|
|
(defn- bytes->int-array
|
|
^ints [^bytes data nrows]
|
|
(let [retval (int-array nrows)]
|
|
(-> (ByteBuffer/wrap data)
|
|
(.order ByteOrder/LITTLE_ENDIAN)
|
|
(.asIntBuffer)
|
|
(.get retval))
|
|
retval))
|
|
|
|
(defn- bytes->long-array
|
|
^longs [^bytes data nrows]
|
|
(let [retval (long-array nrows)]
|
|
(-> (ByteBuffer/wrap data)
|
|
(.order ByteOrder/LITTLE_ENDIAN)
|
|
(.asLongBuffer)
|
|
(.get retval))
|
|
retval))
|
|
|
|
(defn- bytes->float-array
|
|
^floats [^bytes data nrows]
|
|
(let [retval (float-array nrows)]
|
|
(-> (ByteBuffer/wrap data)
|
|
(.order ByteOrder/LITTLE_ENDIAN)
|
|
(.asFloatBuffer)
|
|
(.get retval))
|
|
retval))
|
|
|
|
(defn- bytes->double-array
|
|
^doubles [^bytes data nrows]
|
|
(let [retval (double-array nrows)]
|
|
(-> (ByteBuffer/wrap data)
|
|
(.order ByteOrder/LITTLE_ENDIAN)
|
|
(.asDoubleBuffer)
|
|
(.get retval))
|
|
retval))
|
|
|
|
|
|
(defn- stream->nbuf
|
|
^NativeBuffer [input]
|
|
(if (instance? InputStream input)
|
|
(let [^InputStream input input
|
|
dlist (ArrayList.)
|
|
total (long (loop [data-buf (byte-array 4096)
|
|
n-read (.read input data-buf (int 0) (int 4096))
|
|
total 0]
|
|
(if (> n-read 0)
|
|
(let [new-dbuf (byte-array 4096)]
|
|
(.add dlist (dtype/sub-buffer data-buf 0 n-read))
|
|
(recur new-dbuf (.read input new-dbuf (int 0)
|
|
(int 4096))
|
|
(+ total n-read)))
|
|
total)))
|
|
nbuf (dtype/make-container :native-heap :int8 total)]
|
|
(.close input)
|
|
(dtype/coalesce! nbuf dlist)
|
|
nbuf)
|
|
(do
|
|
(assert (instance? NativeBuffer input))
|
|
input)))
|
|
|
|
|
|
(defn- feather->ds
|
|
[input options]
|
|
(let [nbuf (stream->nbuf input)]
|
|
(let [leng (dtype/ecount nbuf)
|
|
m1 (native-buffer/read-int nbuf)
|
|
_ (when-not (== m1 FeatherTable/MAGIC)
|
|
(throw (Exception. "Initial magic number not found")))
|
|
m2 (native-buffer/read-int nbuf (- leng 4))
|
|
_ (when-not (== m2 FeatherTable/MAGIC)
|
|
(throw (Exception. "End magic number not found")))
|
|
meta-len (native-buffer/read-int nbuf (- leng 8))
|
|
metabytes (dtype/sub-buffer nbuf (- leng 8 meta-len) meta-len)
|
|
ctable (CTable/getRootAsCTable (nio-buffer/as-nio-buffer metabytes))
|
|
ncols (.columnsLength ctable)
|
|
nrows (.numRows ctable)
|
|
version (.version ctable)
|
|
int-dt-types? (get options :integer-datetime-types?)]
|
|
(->> (range ncols)
|
|
(map (fn [cidx]
|
|
(let [col (.columns ctable cidx)
|
|
cname (.name col)
|
|
primitive-ary (.values col)
|
|
fea-dt (.type primitive-ary)
|
|
n-missing (.nullCount primitive-ary)
|
|
usermeta (.userMetadata col)
|
|
data-off (.offset primitive-ary)
|
|
data-len (.totalBytes primitive-ary)
|
|
missing-len (if (== 0 n-missing)
|
|
0
|
|
(pad (quot (+ nrows 7) 8)))
|
|
missing
|
|
(if-not (== 0 n-missing)
|
|
(validity->missing (dtype/sub-buffer nbuf data-off missing-len)
|
|
nrows)
|
|
(bitmap/->bitmap))
|
|
buffers-len (- data-len missing-len)
|
|
data (dtype/sub-buffer nbuf (+ data-off missing-len) buffers-len)
|
|
as-native (fn ([dtype]
|
|
(-> (native-buffer/set-native-datatype data dtype)
|
|
(dtype/sub-buffer 0 nrows)))
|
|
([dtype nrows]
|
|
(-> (native-buffer/set-native-datatype data dtype)
|
|
(dtype/sub-buffer 0 nrows))))
|
|
buffer
|
|
(condp = fea-dt
|
|
Type/BOOL
|
|
(byte-buffer->bitwise-boolean-buffer data nrows)
|
|
Type/INT8
|
|
(as-native :int8)
|
|
Type/UINT8
|
|
(as-native :uint8)
|
|
Type/INT16
|
|
(as-native :int16)
|
|
Type/UINT16
|
|
(as-native :uint16)
|
|
Type/INT32
|
|
(as-native :int32)
|
|
Type/UINT32
|
|
(as-native :uint32)
|
|
Type/DATE
|
|
(if int-dt-types?
|
|
(as-native :epoch-days)
|
|
(as-native :packed-local-date))
|
|
Type/INT64
|
|
(as-native :int64)
|
|
Type/UINT64
|
|
(as-native :uint64)
|
|
Type/TIMESTAMP
|
|
(as-native :epoch-milliseconds)
|
|
Type/TIME
|
|
(as-native :milliseconds)
|
|
Type/FLOAT
|
|
(as-native :float32)
|
|
Type/DOUBLE
|
|
(as-native :float64)
|
|
Type/UTF8
|
|
(offsets-data->string-reader
|
|
(as-native :int32 (inc nrows))
|
|
(dtype/sub-buffer data (pad (* (inc nrows) 4)))
|
|
nrows)
|
|
Type/LARGE_UTF8
|
|
(offsets-data->string-reader
|
|
(as-native :int64 (inc nrows))
|
|
(dtype/sub-buffer data (* (inc nrows) 8))
|
|
data nrows)
|
|
Type/BINARY
|
|
(offsets-data->bytedata-reader
|
|
(as-native :int32 (inc nrows))
|
|
(dtype/sub-buffer data (pad (* (inc nrows) 4)))
|
|
nrows)
|
|
Type/LARGE_BINARY
|
|
(offsets-data->bytedata-reader
|
|
(as-native :int64 (inc nrows))
|
|
(dtype/sub-buffer data (* (inc nrows) 8))
|
|
data nrows))]
|
|
#:tech.v3.dataset{:name cname
|
|
:missing missing
|
|
:data buffer
|
|
:force-datatype? true})))
|
|
(ds-impl/new-dataset options)))))
|
|
|
|
(defn- file-tags
|
|
"Returns {:file-type :input} where input may be the original input
|
|
or a wrapper."
|
|
[input]
|
|
(if (instance? InputStream input)
|
|
(let [^InputStream input (if (.markSupported ^InputStream input)
|
|
input
|
|
(BufferedInputStream. input))
|
|
n-bytes 16
|
|
_ (.mark input n-bytes)
|
|
tagbuf (byte-array n-bytes)
|
|
_ (.read input tagbuf 0 n-bytes)
|
|
buf (int-array (quot n-bytes 4))
|
|
_ (-> (ByteBuffer/wrap tagbuf)
|
|
(.order ByteOrder/LITTLE_ENDIAN)
|
|
(.asIntBuffer)
|
|
(.get buf))]
|
|
(.reset input)
|
|
{:file-tags (vec buf)
|
|
:input input})
|
|
;;else native buffer
|
|
{:file-tags (-> (native-buffer/set-native-datatype input :int32)
|
|
(dtype/sub-buffer 0 4)
|
|
(vec))
|
|
:input input}))
|
|
|
|
|
|
(defn- file-type
|
|
[input]
|
|
(let [{:keys [file-tags input]} (file-tags input)]
|
|
{:file-type
|
|
(case (unchecked-int (file-tags 0))
|
|
1330795073 :arrow-file
|
|
826361158 :feather-v1
|
|
-1 :arrow-ipc)
|
|
:input input}))
|
|
|
|
(comment
|
|
(def files ["test/data/alldtypes.arrow-feather-v1"
|
|
"test/data/alldtypes.arrow-feather-compressed"
|
|
"test/data/alldtypes.arrow-ipc"
|
|
"test/data/alldtypes.arrow-feather"])
|
|
|
|
(doseq [file files]
|
|
(with-open [is (io/input-stream file)]
|
|
(println (str "file: " file " - type: " (:file-type (file-type is))))))
|
|
)
|
|
|
|
|
|
(defn- input->messages
|
|
^Iterable [input options]
|
|
(if (instance? InputStream input)
|
|
(stream-message-iterable input options)
|
|
(message-iterable input)))
|
|
|
|
|
|
(defn- maybe-next
|
|
[^Iterator iter] (when (and iter (.hasNext iter)) (.next iter)))
|
|
|
|
(deftype NextDatasetIter [schema ^Iterator messages fname
|
|
^{:unsynchronized-mutable true
|
|
:tag long} idx
|
|
^Map dict-map
|
|
options]
|
|
Iterator
|
|
(hasNext [this] (.hasNext messages))
|
|
(next [this]
|
|
(loop [msg (.next messages)]
|
|
(if (identical? :dictionary-batch (get msg :message-type))
|
|
(do
|
|
(.compute dict-map (get msg :id)
|
|
(hamf-fn/bi-function
|
|
k old-val
|
|
(delay
|
|
(let [new-val (dictionary->strings msg)]
|
|
(if (and old-val (new-val :delta?))
|
|
(let [old-strs (get @old-val :strings)
|
|
new-strs (get new-val :strings)
|
|
new-ec (+ (count new-strs) (count old-strs))
|
|
rv (hamf/wrap-array-growable
|
|
(make-array String new-ec)
|
|
0)]
|
|
(.addAllReducible rv old-strs)
|
|
(.addAllReducible rv new-strs)
|
|
(assoc new-val :strings rv))
|
|
new-val)))))
|
|
(recur (maybe-next messages)))
|
|
(let [cur-idx idx]
|
|
(set! idx (inc cur-idx))
|
|
(-> (records->ds schema dict-map msg options)
|
|
(ds-base/set-dataset-name (format "%s-%03d" fname cur-idx))))))))
|
|
|
|
(defn- discard
|
|
[input n-bytes]
|
|
(if (instance? InputStream input)
|
|
(do
|
|
(.read ^InputStream input (byte-array n-bytes) 0 n-bytes)
|
|
input)
|
|
(dtype/sub-buffer input 8)))
|
|
|
|
(defn- next-dataset-iter
|
|
^Iterator [input fname options]
|
|
(let [messages (->> (input->messages input options)
|
|
(lznc/map parse-message)
|
|
(iter))
|
|
schema (maybe-next messages)]
|
|
(when-not (= :schema (get schema :message-type))
|
|
(when (and (instance? InputStream input)
|
|
(get options :close-input-stream? true))
|
|
(.close ^InputStream input))
|
|
(throw (Exception. "Initial message is not a schema message.")))
|
|
(NextDatasetIter. schema messages fname 0 (hamf/java-hashmap)
|
|
(assoc options :sparse-columns (:sparse-columns schema)))))
|
|
|
|
(defn stream->dataset-iterable
|
|
"Loads data up to and including the first data record. Returns the a lazy
|
|
sequence of datasets. Datasets can be loaded using mmapped data and when that is true
|
|
realizing the entire sequence is usually safe, even for datasets that are larger than
|
|
available RAM.
|
|
The default resourc management pathway for this is :auto but you can override this
|
|
by explicity setting the option `:resource-type`. See documentation for
|
|
tech.v3.datatype.mmap/mmap-file.
|
|
|
|
Options:
|
|
|
|
* `:open-type` - Either `:mmap` or `:input-stream` defaulting to the slower but more robust
|
|
`:input-stream` pathway. When using `:mmap` resources will be released when the resource
|
|
system dictates - see documentation for [tech.v3.resource](https://techascent.github.io/tech.resource/tech.v3.resource.html).
|
|
When using `:input-stream` the stream will be closed when the lazy sequence is either
|
|
fully realized or an exception is thrown.
|
|
|
|
* `close-input-stream?` - When using `:input-stream` `:open-type`, close the input stream upon
|
|
exception or when stream is fully realized. Defaults to true.
|
|
|
|
* `:integer-datetime-types?` - when true arrow columns in the appropriate packed
|
|
datatypes will be represented as their integer types as opposed to their respective
|
|
packed types. For example columns of type `:epoch-days` will be returned to the user
|
|
as datatype `:epoch-days` as opposed to `:packed-local-date`. This means reading values
|
|
will return integers as opposed to `java.time.LocalDate`s.
|
|
|
|
* `:text-as-strings?` - Return strings instead of Text objects. This breaks automatic round-tripping
|
|
as it changes datatypes *but* can be useful when used with `:strings-as-text?` when writing data out.
|
|
When used like this uncompressed mmap pathways typically have the highest performance - roughly 100x
|
|
any other method."
|
|
^Iterable [fname & [options]]
|
|
(reify Iterable
|
|
(iterator [this]
|
|
(let [input (case (get options :open-type :input-stream)
|
|
:mmap (mmap/mmap-file fname options)
|
|
:input-stream (apply io/input-stream fname (apply concat (seq options))))
|
|
{:keys [file-type input]} (file-type input)]
|
|
(case file-type
|
|
:arrow-file
|
|
(next-dataset-iter (discard input 8) fname options)
|
|
:arrow-ipc
|
|
(next-dataset-iter input fname options)
|
|
:feather-v1
|
|
(iter (hamf/vector (feather->ds input options))))))))
|
|
|
|
(defn ^:no-doc stream->dataset-seq
|
|
"see docs for [[stream->dataset-iterable]]"
|
|
[fname & [options]]
|
|
(seq (stream->dataset-iterable fname options)))
|
|
|
|
|
|
(defn stream->dataset
|
|
"Reads data non-lazily in arrow streaming format expecting to find a single dataset.
|
|
|
|
Options:
|
|
|
|
* `:open-type` - Either `:mmap` or `:input-stream` defaulting to the slower but more robust
|
|
`:input-stream` pathway. When using `:mmap` resources will be released when the resource
|
|
system dictates - see documentation for [tech.v3.resource](https://techascent.github.io/tech.resource/tech.v3.resource.html).
|
|
When using `:input-stream` the stream will be closed when the lazy sequence is either fully realized or an
|
|
exception is thrown. Memory mapping is not supported on m-1 macs unless you are using JDK-17.
|
|
|
|
* `close-input-stream?` - When using `:input-stream` `:open-type`, close the input stream upon
|
|
exception or when stream is fully realized. Defaults to true.
|
|
|
|
* `:integer-datetime-types?` - when true arrow columns in the appropriate packed
|
|
datatypes will be represented as their integer types as opposed to their respective
|
|
packed types. For example columns of type `:epoch-days` will be returned to the user
|
|
as datatype `:epoch-days` as opposed to `:packed-local-date`. This means reading values
|
|
will return integers as opposed to `java.time.LocalDate`s."
|
|
([fname options]
|
|
(let [ds-iter (iter (stream->dataset-iterable fname options))]
|
|
(when-let [ds (maybe-next ds-iter)]
|
|
(when (.hasNext ds-iter)
|
|
(throw (Exception. "File contains multiple record batches.
|
|
Please use stream->dataset-seq.")))
|
|
(vary-meta ds assoc :name fname))))
|
|
([fname]
|
|
(stream->dataset fname nil)))
|
|
|
|
|
|
(defn- simplify-datatype
|
|
[datatype options]
|
|
(let [datatype (packing/unpack-datatype datatype)]
|
|
(if (and (= :string datatype) (get options :strings-as-text?))
|
|
:text
|
|
datatype)))
|
|
|
|
(defn decimal-column-metadata
|
|
[col]
|
|
(let [[scale precision bit-width]
|
|
(reduce (fn [[scale precision bit-width :as acc] ^BigDecimal dec]
|
|
(if dec
|
|
(let [ss (.scale dec)
|
|
pp (.precision dec)
|
|
bw (inc (.bitLength (.unscaledValue dec)))]
|
|
(when-not (nil? scale)
|
|
(when-not (== (long scale) ss)
|
|
(throw (RuntimeException. (str "column \"" (:name (meta col)) "\" has different scale than previous bigdecs
|
|
\texpected " scale " and got " ss)))))
|
|
;;smallest arrow java supports is 128 bit width
|
|
[ss
|
|
(max pp (long (or precision 1)))
|
|
(max (long (or bit-width 128)) bw)])
|
|
acc))
|
|
[nil 1 16]
|
|
col)
|
|
;;java.lang.IllegalArgumentException: Library only supports 128-bit and 256-bit decimal values
|
|
;;ArrowType.java: 1713 org.apache.arrow.vector.types.pojo.ArrowType/getTypeForField
|
|
;;Field.java: 124 org.apache.arrow.vector.types.pojo.Field/convertField
|
|
;;Schema.java: 116 org.apache.arrow.vector.types.pojo.Schema/convertSchema
|
|
;;MessageSerializer.java: 192 org.apache.arrow.vector.ipc.message.MessageSerializer/deserializeSchema
|
|
bit-width (-> (max 128 (ham_fisted.IntegerOps/nextPow2 (long bit-width)))
|
|
(min 256))]
|
|
{:scale scale
|
|
:bit-width bit-width
|
|
:precision precision
|
|
:datatype :decimal}))
|
|
|
|
|
|
(defn ^:no-doc prepare-dataset-for-write
|
|
"Normalize schemas and convert datatypes to datatypes appropriate for arrow
|
|
serialization."
|
|
;;prev ds was the dataset immediately previous to this one. We can assume its
|
|
;;schema is normalized already.
|
|
[ds prev-ds ds-schema options]
|
|
(let [ds (ds-base/select-columns ds (map :name ds-schema))
|
|
ds-meta (map meta (ds-base/columns ds))
|
|
;;Step one is normalize our schema
|
|
;;meaning upcast types when possible or error out.
|
|
ds (if prev-ds
|
|
(reduce (fn [ds col-meta]
|
|
(ds-base/update-column
|
|
ds (:name col-meta)
|
|
(fn [col]
|
|
(let [col-dt (simplify-datatype (dtype/elemwise-datatype col)
|
|
options)
|
|
src-dt (simplify-datatype (:datatype col-meta) options)
|
|
merged-dt (casting/widest-datatype src-dt col-dt)]
|
|
;;Do a datatype check to see if things match
|
|
(when-not (= src-dt merged-dt)
|
|
(throw (Exception.
|
|
(format "Datatypes differ in a downstream dataset. Expected '%s' got '%s'" src-dt col-dt))))
|
|
(if (= col-dt src-dt)
|
|
col
|
|
(dtype/elemwise-cast col src-dt))))))
|
|
ds
|
|
ds-schema)
|
|
ds)]
|
|
;;Second step, once the datatypes are set is to cast the types to arrow compatible
|
|
;;datatypes
|
|
(reduce
|
|
(fn [ds col]
|
|
(let [col-dt (dtype/elemwise-datatype col)]
|
|
(cond
|
|
(and (identical? :string col-dt)
|
|
(not (:strings-as-text? options)))
|
|
(let [missing (ds-proto/missing col)
|
|
metadata (meta col)
|
|
str-table-column-data
|
|
(fn [prev-ds data]
|
|
(if (nil? prev-ds)
|
|
(str-table/->string-table data)
|
|
(let [prev-col (ds-base/column prev-ds (:name metadata))
|
|
^StringTable prev-str-t (ds-proto/column-data prev-col)
|
|
container (str-table/fast-string-container (HashMap. ^Map (.str->int prev-str-t))
|
|
(ArrayList. ^List (.int->str prev-str-t)))]
|
|
(str-table/string-table-from-strings container data))))]
|
|
(assoc ds (metadata :name) (->> (ds-proto/column-data col)
|
|
(str-table-column-data prev-ds)
|
|
(ds-proto/with-column-data col))))
|
|
;;detect precision, scale and whether we need 128 or 256 bytes of accuracy
|
|
(identical? :decimal col-dt)
|
|
(assoc ds (ds-col/column-name col)
|
|
(vary-meta col assoc ::complex-datatype (decimal-column-metadata col)))
|
|
:else
|
|
ds)))
|
|
ds
|
|
(ds-base/columns ds))))
|
|
|
|
|
|
(deftype PrepDsIter [^:unsynchronized-mutable prev-ds
|
|
^:unsynchronized-mutable ds-schema
|
|
^Iterator ds-seq
|
|
options]
|
|
Iterator
|
|
(hasNext [this] (.hasNext ds-seq))
|
|
(next [this] (let [nds (.next ds-seq)
|
|
pds prev-ds
|
|
dsc (if ds-schema
|
|
ds-schema
|
|
(do
|
|
(set! ds-schema (mapv meta (vals nds)))
|
|
ds-schema))
|
|
prepared (prepare-dataset-for-write nds pds ds-schema options)]
|
|
(set! prev-ds prepared)
|
|
prepared)))
|
|
|
|
|
|
(defn- prepare-ds-seq-for-write
|
|
^Iterator [options ds-seq]
|
|
(PrepDsIter. nil nil (.iterator ^Iterable (hamf-proto/->iterable ds-seq)) options))
|
|
|
|
(defn ^:no-doc ds->schema
|
|
"Return a map of :schema, :dictionaries where :schema is an Arrow schema
|
|
and dictionaries is a list of {:byte-data :offsets}. Dataset must be prepared first
|
|
- [[prepare-dataset-for-write]] to ensure that columns have the appropriate datatypes."
|
|
([ds options]
|
|
(let [dictionaries (ArrayList.)
|
|
col-vals (.values ^Map ds)
|
|
sparse-columns (get options :sparse-columns)]
|
|
{:schema
|
|
(Schema. ^Iterable
|
|
(->> col-vals
|
|
(map (partial col->field dictionaries options)))
|
|
(if-not (empty? sparse-columns)
|
|
{"sparse-columns" (String/join "," ^"[Ljava.lang.String;" (into-array String (map str sparse-columns)))}
|
|
{}))
|
|
:dictionaries dictionaries}))
|
|
([ds]
|
|
(ds->schema ds {})))
|
|
|
|
|
|
(def ^:private compression-type
|
|
{:zstd CompressionType/ZSTD
|
|
:lz4 CompressionType/LZ4_FRAME})
|
|
|
|
|
|
(defn- ensure-valid-compression-keyword
|
|
[kwd]
|
|
(if (contains? compression-type kwd)
|
|
kwd
|
|
(throw (Exception. (format "Unrecognized compression type %s" kwd)))))
|
|
|
|
|
|
(defn- validate-compression-for-write
|
|
[compression]
|
|
(when compression
|
|
(->
|
|
(cond
|
|
(map? compression)
|
|
(do
|
|
(when-not (get compression :compression-type)
|
|
(throw (Exception. (format "Invalid compression map %s" compression))))
|
|
(update compression :compression-type ensure-valid-compression-keyword))
|
|
(keyword? compression)
|
|
{:compression-type (ensure-valid-compression-keyword compression)}
|
|
:else
|
|
(throw (Exception. "Unrecognized compression type")))
|
|
#_(create-compressor))))
|
|
|
|
|
|
(def ^{:tag 'bytes
|
|
:private true} arrow-file-begin-tag (byte-array [65 82 82 79 87 49 0 0]))
|
|
(def ^{:tag 'bytes
|
|
:private true} arrow-file-end-tag (byte-array [65 82 82 79 87 49]))
|
|
|
|
|
|
(defn- write-footer
|
|
[^WriteChannel writer ^Schema schema dict-blocks record-blocks]
|
|
;;IPC continuation
|
|
(.writeIntLittleEndian writer -1)
|
|
;;Empty integer for padding
|
|
(.writeIntLittleEndian writer 0)
|
|
(let [builder (FlatBufferBuilder.)
|
|
schema-offset (.getSchema schema builder)
|
|
_ (Footer/startDictionariesVector builder (count dict-blocks))
|
|
_ (doseq [dict (reverse dict-blocks)]
|
|
(write-block-data builder dict))
|
|
dict-offset (.endVector builder)
|
|
_ (Footer/startRecordBatchesVector builder (count record-blocks))
|
|
_ (doseq [rb (reverse record-blocks)]
|
|
(write-block-data builder rb))
|
|
rb-offset (.endVector builder)
|
|
_ (.finish builder (Footer/createFooter builder 4 schema-offset dict-offset rb-offset 0))
|
|
footer-data (.dataBuffer builder)
|
|
start (.getCurrentPosition writer)
|
|
_ (.write writer footer-data)
|
|
end (.getCurrentPosition writer)
|
|
metalen (- end start)]
|
|
(.writeIntLittleEndian writer metalen)
|
|
(.write writer (ByteBuffer/wrap arrow-file-end-tag))))
|
|
|
|
|
|
(defn dataset-seq->stream!
|
|
"Write a sequence of datasets as an arrow stream file. File will contain one record set
|
|
per dataset. Datasets in the sequence must have matching schemas or downstream schema
|
|
must be able to be safely widened to the first schema.
|
|
|
|
Options:
|
|
|
|
* `:strings-as-text?` - defaults to true - Save out strings into arrow files without
|
|
dictionaries. This works well if you want to load an arrow file in-place or if
|
|
you know the strings in your dataset are either really large or should not be in
|
|
string tables. **Saving multiple datasets with `{:strings-as-text false}` requires arrow
|
|
7.0.0+ support from your python or R code due to
|
|
[Arrow issue 13467](https://issues.apache.org/jira/browse/ARROW-13467). - the conservative
|
|
pathway for now is to set `:strings-as-text?` to true and only save text!!**.
|
|
|
|
* `:format` - one of `[:file :ipc]`, defaults to `:file`.
|
|
- `:file` - arrow file format, compatible with pyarrow's [open_file](https://arrow.apache.org/docs/python/generated/pyarrow.ipc.open_file.html#pyarrow.ipc.open_file). The suggested
|
|
suffix is `.arrow`.
|
|
- `:ipc` - arrow streaming format, compatible with pyarrow's [open_ipc](https://arrow.apache.org/docs/python/generated/pyarrow.ipc.open_file.html#pyarrow.ipc.open_ipc) pathway. The
|
|
suggested suffix is `.arrows`.
|
|
|
|
* `:compression` - Either `:zstd` or `:lz4`, defaults to no compression (nil).
|
|
Per-column compression of the data can result in some significant size savings
|
|
(2x+) and thus some significant time savings when loading over the network.
|
|
Using compression makes loading via mmap non-lazy - If you are going to use
|
|
compression mmap probably doesn't make sense and most likely will result in
|
|
slower loading times.
|
|
- `:lz4` - Decent and very fast compression.
|
|
- `:zstd` - Good compression, somewhat slower than `:lz4`. Can also have a
|
|
level parameter that ranges from 1-12 in which case compression is specified
|
|
in map form: `{:compression-type :zstd :level 5}`."
|
|
([path options ds-seq]
|
|
;;We use the first dataset to setup schema information the rest of the datasets
|
|
;;must follow. So the serialization of the first dataset differs from the serialization
|
|
;;of the subsequent datasets
|
|
(when (empty? ds-seq)
|
|
(throw (Exception. "Empty dataset sequence")))
|
|
(let [options (-> options
|
|
(update :compression validate-compression-for-write)
|
|
(update :strings-as-text? (fn [val]
|
|
(if (nil? val)
|
|
true
|
|
val))))
|
|
ds-seq-iter (prepare-ds-seq-for-write options ds-seq)
|
|
ds (when (.hasNext ds-seq-iter)
|
|
(.next ds-seq-iter))
|
|
file-tag? (= :file (get options :format :file))
|
|
dict-blocks (ArrayList.)
|
|
record-blocks (ArrayList.)
|
|
sparse-columns (if-let [sparse-col (get options :sparse-columns)]
|
|
(into #{} sparse-col)
|
|
#{})
|
|
colvec (.values ^Map ds)
|
|
sparse-columns (->> (range (ds-base/column-count ds))
|
|
(filter #(sparse-col/is-sparse? (colvec %)))
|
|
(reduce conj sparse-columns))
|
|
options (assoc options :sparse-columns sparse-columns)]
|
|
;;Any native mem allocated in this context will be released
|
|
(with-open [ostream (io/output-stream! path)]
|
|
(let [writer (arrow-output-stream-writer ostream)
|
|
_ (when file-tag?
|
|
(.write writer (ByteBuffer/wrap arrow-file-begin-tag)))
|
|
{:keys [schema dictionaries]} (ds->schema ds options)]
|
|
(write-schema writer schema)
|
|
(.addAll dict-blocks (lznc/map #(write-dictionary writer % options) dictionaries))
|
|
(.add record-blocks (resource/stack-resource-context
|
|
(write-dataset writer ds options)))
|
|
(loop [continue? (.hasNext ds-seq-iter)
|
|
options (assoc options :dictionaries dictionaries)]
|
|
(when continue?
|
|
(let [ds (.next ds-seq-iter)
|
|
;;Passing in previous dictionaries so we can chain them for differential
|
|
;;dictionary encoding
|
|
{:keys [dictionaries]} (ds->schema ds (assoc options :dictionaries dictionaries))]
|
|
(.addAll dict-blocks (lznc/map #(write-dictionary writer % options) dictionaries))
|
|
(.add record-blocks (resource/stack-resource-context (write-dataset writer ds options)))
|
|
(recur (.hasNext ds-seq-iter) (assoc options :dictionaries dictionaries)))))
|
|
|
|
(when file-tag?
|
|
(write-footer writer schema dict-blocks record-blocks))
|
|
(.getCurrentPosition writer)))))
|
|
([path ds-seq]
|
|
(dataset-seq->stream! path nil ds-seq)))
|
|
|
|
|
|
(defn dataset->stream!
|
|
"Write a dataset as an arrow file. File will contain one record set.
|
|
See documentation for [[dataset-seq->stream!]].
|
|
|
|
* `:strings-as-text?` defaults to false."
|
|
([ds path options]
|
|
(dataset-seq->stream! path (update options :strings-as-text?
|
|
(fn [val]
|
|
(if (nil? val)
|
|
false
|
|
val))) [ds]))
|
|
([ds path]
|
|
(dataset->stream! ds path {})))
|
|
|
|
|
|
(defn ^:no-doc read-stream-dataset-copying
|
|
"This method has been deprecated. Please use stream->dataset"
|
|
[path & [options]]
|
|
(stream->dataset path (assoc options :open-type :input-stream)))
|
|
|
|
|
|
(defn ^:no-doc read-stream-dataset-inplace
|
|
"This method has been deprecated. Please use stream->dataset"
|
|
[path & [options]]
|
|
(stream->dataset path (assoc options :open-type :mmap)))
|
|
|
|
|
|
(defn ^:no-doc write-dataset-to-stream!
|
|
"This method has been deprecated. Please use dataset->stream!"
|
|
[ds path & [options]]
|
|
(dataset->stream! ds path options))
|
|
|
|
|
|
(defmethod ds-io/data->dataset :arrow
|
|
[data options]
|
|
(stream->dataset data options))
|
|
|
|
|
|
(defmethod ds-io/dataset->data! :arrow
|
|
[ds output options]
|
|
(dataset->stream! ds output (update options :format
|
|
(fn [item]
|
|
(if (nil? item)
|
|
:file
|
|
item)))))
|
|
|
|
|
|
(defmethod ds-io/data->dataset :arrows
|
|
[data options]
|
|
(stream->dataset data options))
|
|
|
|
|
|
(defmethod ds-io/dataset->data! :arrows
|
|
[ds output options]
|
|
(dataset->stream! ds output (update options :format
|
|
(fn [item]
|
|
(if (nil? item)
|
|
:ipc
|
|
item)))))
|