Files
df-research/tech.ml.dataset/src/tech/v3/libs/arrow.clj
2026-02-08 11:20:43 -10:00

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)))))