   [node]
   (-> (doto (ClientConfig. "" "")
         (.setReconnectOnConnectionLoss true))
-      (ClientFactory/createClient)
-      (doto
-        (.createConnection (name node)))))
+        (ClientFactory/createClient)
+        (doto
+          (.createConnection (name node)))))
+
+(defn volt-table->map
+  "Converts a VoltDB table to a data structure like
+
+  {:status status-code
+   :schema [{:column_name VoltType, ...}]
+   :rows [{:k1 v1, :k2 v2}, ...]}"
+  [^VoltTable t]
+  (let [column-count (.getColumnCount t)
+        column-names (loop [i     0
+                            cols  (transient [])]
+                       (if (= i column-count)
+                         (persistent! cols)
+                         (recur (inc i)
+                                (conj! cols (keyword (.getColumnName t i))))))
+        basis        (apply create-struct column-names)
+        column-types (loop [i 0
+                            types (transient [])]
+                       (if (= i column-count)
+                         (persistent! types)
+                         (recur (inc i)
+                                (conj! types (.getColumnType t i)))))
+        row          (doto (.cloneRow t)
+                       (.resetRowPosition))]
+  {:status (.getStatusCode t)
+   :schema (apply struct basis column-types)
+   :rows (loop [rows (transient [])]
+           (if (.advanceRow row)
+             (let [cols (object-array column-count)]
+               (loop [j 0]
+                 (when (< j column-count)
+                   (aset cols j (.get row j (nth column-types j)))
+                   (recur (inc j))))
+               (recur (conj! rows (clojure.lang.PersistentStructMap/construct
+                                    basis
+                                    (seq cols)))))
+             ; Done
+             (persistent! rows)))}))
+
+(defn call!
+  "Call a stored procedure and returns a seq of VoltTable results."
+  [client procedure & args]
+  (let [res (.callProcedure client procedure (into-array Object args))]
+    ; Docs claim callProcedure will throw, but tutorial checks anyway so ???
+    (assert (= (.getStatus res) ClientResponse/SUCCESS))
+    (map volt-table->map (.getResults res))))
+
+(defn ad-hoc!
+  "Run an ad-hoc SQL stored procedure."
+  [client & args]
+  (apply call! client "@AdHoc" args))
 
 (defn client
   "A single-register client."

         :name    "voltdb"
         :os      debian/os
         :client  (client)
         :db      (db url)))

   (let [initialized? (promise)]
     (reify client/Client
       (setup! [_ test node]
         (let [conn (connect node)]
           (c/on node
                 (when (deliver initialized? true)
                   ; Create table
                   (sql-cmd! "CREATE TABLE registers (
                             id          INTEGER UNIQUE NOT NULL,
                             value       INTEGER NOT NULL,
                             PRIMARY KEY (id)
                             );
                             PARTITION TABLE registers ON COLUMN id;")
                   (info node "table created")

                   ; Install stored procedures
                   (c/upload "procedures/jepsen-procedures.jar"
                             (str base-dir "/jepsen-procedures.jar"))
                   (sql-cmd! "load classes jepsen-procedures.jar")
                   (sql-cmd! "CREATE PROCEDURE FROM CLASS jepsen.procedures.ReadAll")
                   (call! conn "REGISTERS.upsert" 0 0)))

           (client conn)))

       (invoke! [this test op]
         (case (:f op)
           :read   (assoc op
                          :type :ok
                          :value (-> conn
                                     (call! "REGISTERS.select" 0)
                                     first
                                     :rows
                                     first
                                     :VALUE))
           :write (do (call! conn "REGISTERS.upsert" 0 (:value op))
                      (assoc op :type :ok))
           :cas   (let [[v v'] (:value op)
                        res (-> conn
                                (ad-hoc! "UPDATE registers SET value = ?
                                         WHERE id = ? AND value = ?"
                                         v' 0 v)
                                first
                                :rows
                                first
                                :modified_tuples)]
                    (assert (#{0 1} res))
                    (assoc op :type (if (zero? res) :fail :ok)))))

       (teardown! [_ test]
         (.close conn))))))

(defn r   [_ _] {:type :invoke, :f :read, :value nil})
(defn w   [_ _] {:type :invoke, :f :write, :value (rand-int 5)})
(defn cas [_ _] {:type :invoke, :f :cas, :value [(rand-int 5) (rand-int 5)]})

(defn voltdb-test
  "Takes a tarball URL"
