diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b589f..70978e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Only accretive/fixative changes will be made from now on. The following changes have been committed to the **master** branch since the 1.0.9 release: +* Address #70 by adding `next.jdbc.result-set/clob-column-reader` and `next.jdbc.result-set/clob->string` helper to make it easier to deal with `CLOB` column data. * Update `org.clojure/java.data` to `"0.1.4"` (0.1.2 fixes a number of reflection warnings). ## Stable Builds diff --git a/doc/result-set-builders.md b/doc/result-set-builders.md index 4bc526d..b61ba18 100644 --- a/doc/result-set-builders.md +++ b/doc/result-set-builders.md @@ -37,6 +37,14 @@ And finally there are adapters for the existing builders that let you override t * `as-maps-adapter` -- adapts an existing map builder function with a new column reader, * `as-arrays-adapter` -- adapts an existing array builder function with a new column reader. +An example column reader is provided -- `clob-column-reader` -- that still uses `.getObject` but will expand `java.sql.Clob` values into string (using the `clob->string` helper function): + +```clojure + {:builder-fn (result-set/as-maps-adapter + result-set/as-maps + result-set/clob-column-reader)} +``` + ## RowBuilder Protocol This protocol defines four functions and is used whenever `next.jdbc` needs to materialize a row from a `ResultSet` as a Clojure data structure: diff --git a/src/next/jdbc/result_set.clj b/src/next/jdbc/result_set.clj index e13f8c6..a983e47 100644 --- a/src/next/jdbc/result_set.clj +++ b/src/next/jdbc/result_set.clj @@ -14,9 +14,11 @@ Also provides the default implemenations for `Executable` and the default `datafy`/`nav` behavior for rows from a result set." (:require [clojure.core.protocols :as core-p] + [clojure.java.io :as io] [next.jdbc.prepare :as prepare] [next.jdbc.protocols :as p]) - (:import (java.sql PreparedStatement + (:import (java.sql Clob + PreparedStatement ResultSet ResultSetMetaData SQLException) (java.util Locale))) @@ -236,6 +238,21 @@ (with-row [this mrs row] (with-row mrsb mrs row)) (rs! [this mrs] (rs! mrsb mrs)))))) +(defn clob->string + "Given a CLOB column value, read it as a string." + [^Clob clob] + (with-open [rdr (io/reader (.getCharacterStream clob))] + (slurp rdr))) + +(defn clob-column-reader + "An example column-reader that still uses `.getObject` but expands CLOB + columns into strings." + [^ResultSet rs ^ResultSetMetaData _ ^Integer i] + (when-let [value (.getObject rs i)] + (cond-> value + (instance? Clob value) + (clob->string)))) + (defrecord ArrayResultSetBuilder [^ResultSet rs rsmeta cols] RowBuilder (->row [this] (transient [])) diff --git a/test/next/jdbc/result_set_test.clj b/test/next/jdbc/result_set_test.clj index 9b127d9..a8620e9 100644 --- a/test/next/jdbc/result_set_test.clj +++ b/test/next/jdbc/result_set_test.clj @@ -289,3 +289,30 @@ (= "fruit" (-> % val name str/lower-case))) row)) metadata)))) + +(deftest clob-reading + (when-not (postgres?) ; embedded postgres does not support clob + (with-open [con (p/get-connection (ds) {})] + (try + (p/-execute-one con ["DROP TABLE CLOBBER"] {}) + (catch Exception _)) + (p/-execute-one con [(str " +CREATE TABLE CLOBBER ( + ID INTEGER, + STUFF CLOB +)")] + {}) + (p/-execute-one con + [(str "insert into clobber (id, stuff)" + "values (?,?), (?,?)") + 1 "This is some long string" + 2 "This is another long string"] + {}) + (is (= "This is some long string" + (-> (p/-execute-all con + ["select * from clobber where id = ?" 1] + {:builder-fn (rs/as-maps-adapter + rs/as-unqualified-lower-maps + rs/clob-column-reader)}) + (first) + :stuff)))))) diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 1184a10..a6eac84 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -2,8 +2,7 @@ (ns next.jdbc-test "Not exactly a test suite -- more a series of examples." - (:require [clojure.java.io :as io] - [clojure.string :as str] + (:require [clojure.string :as str] [clojure.test :refer [deftest is testing use-fixtures]] [next.jdbc :as jdbc] [next.jdbc.connection :as c] @@ -12,7 +11,7 @@ [next.jdbc.prepare :as prep] [next.jdbc.result-set :as rs] [next.jdbc.specs :as specs]) - (:import (java.sql Clob ResultSet ResultSetMetaData))) + (:import (java.sql ResultSet ResultSetMetaData))) (set! *warn-on-reflection* true) @@ -213,37 +212,3 @@ VALUES ('Pear', 'green', 49, 47) (into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"])))) (is (thrown? IllegalArgumentException (doall (take 3 (jdbc/plan (ds) ["select * from fruit"])))))) - -(defn- clob-reader - [^ResultSet rs ^ResultSetMetaData _ ^Integer i] - (let [obj (.getObject rs i)] - (cond (instance? Clob obj) - (with-open [rdr (io/reader (.getCharacterStream ^Clob obj))] - (slurp rdr)) - :default - obj))) - -(deftest clob-reading - (when-not (postgres?) - (with-open [con (jdbc/get-connection (ds))] - (try - (jdbc/execute-one! con ["DROP TABLE CLOBBER"]) - (catch Exception _)) - (jdbc/execute-one! con [(str " -CREATE TABLE CLOBBER ( - ID INTEGER, - STUFF CLOB -)")]) - (jdbc/execute-one! con - [(str "insert into clobber (id, stuff)" - "values (?,?), (?,?)") - 1 "This is some long string" - 2 "This is another long string"]) - (is (= "This is some long string" - (-> (jdbc/execute! con - ["select * from clobber where id = ?" 1] - {:builder-fn (rs/as-maps-adapter - rs/as-unqualified-lower-maps - clob-reader)}) - (first) - :stuff))))))