├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── boot.properties ├── build.boot ├── project.clj ├── src └── mpg │ ├── core.clj │ ├── data.clj │ ├── datetime.clj │ └── util.clj └── test └── mpg └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-* 10 | .hgignore 11 | .hg/ 12 | /.idea 13 | *.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | sudo: false 3 | script: ./boot travis 4 | branches: 5 | only: 6 | - master 7 | addons: 8 | postgresql: "9.4" 9 | services: 10 | - postgresql 11 | before_script: 12 | - psql -c 'create database mpg_test;' -U postgres 13 | - psql -d mpg_test -c 'create extension hstore;' -U postgres 14 | - psql -d mpg_test -c 'create extension citext;' -U postgres 15 | jdk: 16 | - oraclejdk8 17 | # travis does not support any of these (yet): 18 | # - oraclejdk9 19 | # - openjdk8 20 | # - openjdk9 21 | install: 22 | - curl -fsSLo boot https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh 23 | - chmod 755 boot 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [1.2.0] - 2016-07-25 5 | ### Fixed 6 | - Issue with extension of protocol ISQLParameter where if the datatype could not be determined when resolving an IPersistentMap parameter (#19) 7 | - Tests ensure extensions are present before running (#20) 8 | 9 | ## [1.1.0] - 2016-06-19 10 | ### Fixed 11 | - Queries involving array type work consistently accross calls (#17) 12 | 13 | ## [1.0.0] - 2016-05-10 14 | ### Changed 15 | - Better handling of `citext` type data (#13) 16 | - Improved handling of `jdbc/insert!` operations (#14) 17 | 18 | ## [0.3.0] - 2016-04-28 19 | ### Changed 20 | - Republish as `mpg` 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Shane Kilkelly 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpg - more modern postgres to the gallon 2 | 3 | Eases interoperability between clojure/java datatypes and postgres 4 | datatypes. No more boilerplate! 5 | 6 | Handles the following: 7 | 8 | - `DATE` <-> `java.time.LocalDate` 9 | - `TIMESTAMP/TIMESTAMPTZ` <-> `java.time.ZonedDateTime` 10 | - `JSON/JSONB` <-> clojure map/vector 11 | - `ARRAY` (e.g. `int[]`) <-> clojure vector 12 | - `BYTEA` <-> byte array 13 | - `HSTORE` <-> clojure map (limited support - jdbc stringifies all contents) 14 | 15 | Can also insert (but not retrieve) the following types: 16 | 17 | - `java.util.Date` -> `DATE/TIMESTAMP/TIMESTAMPTZ` 18 | - `java.sql.Timestamp` -> `DATE/TIMESTAMP/TIMESTAMPTZ` 19 | - `java.nio.ByteBuffer` -> `BYTEA` 20 | 21 | 22 | [![Build Status](https://travis-ci.org/mpg-project/mpg.svg?branch=master)](https://travis-ci.org/mpg-project/mpg) 23 | 24 | Note: this library was once called [jdbc-pg-sanity](https://clojars.org/jdbc-pg-sanity), `mpg` is the new version. 25 | 26 | ## Installation 27 | 28 | Add `mpg` as a leiningen or boot dependency: 29 | 30 | ```clojure 31 | [mpg "1.3.0"] 32 | ``` 33 | 34 | [![Clojars Project](https://img.shields.io/clojars/v/mpg.svg)](https://clojars.org/mpg) 35 | 36 | 37 | ## Usage 38 | 39 | Just require the `mpg.core` namespace and call `patch` 40 | 41 | ```clojure 42 | (ns whatever.db 43 | (require [clojure.java.jdbc :as j] 44 | [mpg.core :as mpg]])) 45 | (mpg/patch) ;; take the default settings 46 | (mpg/patch {:default-map :hstore}) ;; custom settings are merged with the defaults 47 | ;; valid settings: 48 | :data - boolean, default true. auto-map maps and vectors? 49 | :datetime - boolean, default true. auto-map java.time.{LocalDate, ZonedDateTime} ? 50 | :default-map - keyword. one of :json, :jsonb, :hstore. Default :jsonb 51 | ``` 52 | 53 | ## Limitations 54 | 55 | The current clojure.java.jdbc interface imposes some limitations on us. 56 | 57 | 1. You only get the autoconversion when using clojure.java.jdbc or something built on it 58 | 2. When using unbound statements, we cannot save a vector as an array type (we therefore use json) 59 | 3. When using unbound statements, you must choose between storing maps as json or hstore (default: json) 60 | 4. All applications that have written to the database are assumed to have correctly saved timestamps in UTC. If you only use this library, you won't have to worry about that. Most applications can be configured with the TZ environment variable 61 | 62 | ## Running tests 63 | 64 | You need a database and a user on it with which we can run tests. 65 | 66 | You can provide information about these with environment variables: 67 | ```bash 68 | MPG_TEST_DB_URI # default is '//127.0.0.1:5432/mpg_test' 69 | MPG_TEST_DB_USER 70 | MPG_TEST_DB_PASS 71 | ``` 72 | 73 | You can create a postgres database to test with `createdb` and give your 74 | user permissions with `GRANT` as per normal postgres. 75 | 76 | Running the tests is the same as any leiningen-based project: 77 | 78 | ```shell 79 | boot test 80 | ``` 81 | 82 | ## Contributing 83 | 84 | Contributions and improvements welcome, just open an issue! :) 85 | 86 | If this library should do something, and it doesn't currently do it, please fork 87 | and open a pull request. All reasonable contributions will be accepted. 88 | 89 | Please run the tests before opening a pull request :) 90 | 91 | ## Acknowledgements 92 | 93 | This library was originally extracted from luminus boilerplate which 94 | hails from code floating around the internet 95 | generally. [James Laver](https://github.com/jjl) basically rewrote it. 96 | 97 | ## License 98 | 99 | Copyright © 2016 Shane Kilkelly, James Laver 100 | 101 | Distributed under the MIT license. 102 | -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_CLOJURE_VERSION=1.8.0 2 | BOOT_WARN_TARGET=no -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (set-env! 2 | :version "1.3.0" 3 | :dependencies '[[org.clojure/clojure "1.8.0"] 4 | [cheshire "5.6.1"] 5 | [org.clojure/java.jdbc "0.6.0-alpha2"] 6 | [org.postgresql/postgresql "9.4.1208"] 7 | [tolitius/boot-check "0.1.3" :scope "test"] 8 | [adzerk/boot-test "1.1.0" :scope "test"] 9 | [environ "1.0.2" :scope "test"]] 10 | :resource-paths #{"src"} 11 | :source-paths #{"src"}) 12 | 13 | (require '[adzerk.boot-test :as t]) 14 | 15 | (task-options! 16 | pom {:project 'mpg 17 | :version (get-env :version) 18 | :description "More modern Postgres to the gallon. Transparently maps clojure <-> postgresql data" 19 | :url "https://github.com/mpg-project/mpg" 20 | :scm {:url "https://github.com/irresponsible/oolong.git"} 21 | :license {:name "MIT" :url "https://opensource.org/licenses/MIT"} 22 | target {:dir #{"target"}}}) 23 | 24 | (deftask testing [] 25 | (alter-var-root #'*warn-on-reflection* (constantly true)) 26 | (set-env! :source-paths #(conj % "test") 27 | :resource-paths #(conj % "test")) 28 | identity) 29 | 30 | (deftask test [] 31 | (comp (testing) (speak) (t/test))) 32 | 33 | (deftask autotest [] 34 | (comp (watch) (test))) 35 | 36 | (deftask make-jar [] 37 | (comp (pom) (jar))) 38 | 39 | (deftask travis [] 40 | (testing) 41 | (t/test)) 42 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject mpg "1.3.0" 2 | :description "More modern Postgres to the gallon. Transparently maps clojure <-> postgresql data" 3 | :url "https://github.com/mpg-project/mpg" 4 | :license {:name "MIT" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"] 7 | [cheshire "5.6.1"] 8 | [org.clojure/java.jdbc "0.6.0-alpha2"] 9 | [org.postgresql/postgresql "9.4.1208"]] 10 | :profiles {:dev {:dependencies [[environ "1.0.2"]]}} 11 | :global-vars {*warn-on-reflection* true}) 12 | -------------------------------------------------------------------------------- /src/mpg/core.clj: -------------------------------------------------------------------------------- 1 | (ns mpg.core 2 | (:require [mpg.data :as data] 3 | [mpg.datetime :as datetime])) 4 | 5 | (defn patch 6 | "Smooths integration with postgres such as by mapping between JSON and vectors/maps. 7 | Args: [& [opts]] 8 | opts is a map of options valid keys: 9 | :data - boolean. auto-map maps and vectors? 10 | :datetime - boolean. auto-map java.time.{LocalDate, Instant, ZonedDateTime} ? 11 | :default-map - keyword. one of :json, :jsonb, :hstore. Controls the default 12 | output format for maps in unprepared statements 13 | opts if provided are merged against the default settings: 14 | {:data true :datetime true :default-map :jsonb}" 15 | ([& [{:keys [data datetime default-map] 16 | :or {data true datetime true 17 | default-map :jsonb}}]] 18 | (when data 19 | (data/patch default-map)) 20 | (when datetime 21 | (datetime/patch)))) 22 | -------------------------------------------------------------------------------- /src/mpg/data.clj: -------------------------------------------------------------------------------- 1 | (ns mpg.data 2 | (:require [cheshire.core :as c] 3 | [clojure.java.jdbc :as j] 4 | [mpg.util :as u :refer [fatal pg-param-type pg-json pg-jsonb]]) 5 | (:import [org.postgresql.util PGobject] 6 | [org.postgresql.jdbc PgArray] 7 | [clojure.lang IPersistentMap IPersistentVector ExceptionInfo] 8 | [java.nio ByteBuffer] 9 | [java.sql Date Timestamp PreparedStatement] 10 | [java.time Instant LocalDateTime] 11 | [java.util HashMap])) 12 | 13 | (defn bytebuf->array [^ByteBuffer buf] 14 | (let [a (byte-array (.remaining buf))] 15 | (.get buf a) 16 | a)) 17 | 18 | (defn patch 19 | "Installs conversion hooks: 20 | map <-> json/b OR hstore 21 | vector <-> json/b 22 | args: [default-map] 23 | default-map controls whether a map will be treated as hstore or json 24 | with unprepared statements (as we cannot just read the type). 25 | value is one of: :json, :jsonb :hstore" 26 | [default-map] 27 | (extend-protocol j/IResultSetReadColumn 28 | java.util.HashMap ;; hstore 29 | (result-set-read-column [v _ _] (into {} v)) 30 | PgArray 31 | (result-set-read-column [v _ _] (vec (.getArray v))) 32 | PGobject 33 | (result-set-read-column [pgobj _metadata _index] 34 | (let [type (.getType pgobj) 35 | value (.getValue pgobj)] 36 | (cond (#{"json" "jsonb"} type) 37 | (c/parse-string value true) 38 | 39 | (#{"varchar" "citext"} type) 40 | (str value) 41 | 42 | :else value)))) 43 | (extend-protocol j/ISQLValue 44 | IPersistentMap 45 | (sql-value [value] 46 | (case default-map 47 | :json (pg-json value) 48 | :jsonb (pg-jsonb value) 49 | :hstore (HashMap. ^clojure.lang.PersistentHashMap value))) 50 | IPersistentVector 51 | (sql-value [value] 52 | (pg-json value)) 53 | ByteBuffer 54 | (sql-value [value] 55 | (bytebuf->array value))) 56 | (extend-protocol j/ISQLParameter 57 | IPersistentMap 58 | (set-parameter [v ^java.sql.PreparedStatement stmt ^long idx] 59 | (let [type (try (pg-param-type stmt idx) (catch ExceptionInfo e (name default-map)))] 60 | (case type 61 | "citext" (.setObject stmt idx (str v)) 62 | "hstore" (.setObject stmt idx (java.util.HashMap. ^clojure.lang.PersistentHashMap v)) 63 | "json" (.setObject stmt idx (pg-json v)) 64 | "jsonb" (.setObject stmt idx (pg-jsonb v)) 65 | (cond (#{"smallint" "integer" "int2" "int4" "serial" "serial4"} type) 66 | (if (integer? v) 67 | (.setInt stmt idx v) 68 | (fatal "Expected integer" {:type type :got v :col-index idx})) 69 | 70 | (#{"bigint" "int8" "serial8" "bigserial"} type) 71 | (if (integer? v) 72 | (.setLong stmt idx v) 73 | (fatal "Expected integer" {:type type :got v :col-index idx})) 74 | 75 | (#{"decimal" "numeric" "real" "double precision"} type) 76 | (if (float? v) 77 | (.setDouble stmt idx v) 78 | (fatal "Expected float" {:type type :got v :col-index idx})) 79 | 80 | :else (fatal "Unknown data type in map" {:v v :idx idx :stmt stmt}))))) 81 | IPersistentVector 82 | (set-parameter [v ^java.sql.PreparedStatement stmt ^long idx] 83 | (let [conn (.getConnection stmt) 84 | meta (.getParameterMetaData stmt) 85 | type-name (.getParameterTypeName meta idx)] 86 | (if-let [elem-type (when (= (first type-name) \_) (apply str (rest type-name)))] 87 | (.setObject stmt idx (.createArrayOf conn elem-type (to-array v))) 88 | (if-let [array-type (-> (re-matches #"(.*)\[\]" type-name) 89 | (nth 1))] 90 | (.setObject stmt idx (.createArrayOf conn array-type (to-array v))) 91 | (.setObject stmt idx (pg-json v)))))) 92 | ByteBuffer 93 | (set-parameter [v ^java.sql.PreparedStatement stmt ^long idx] 94 | (.setBytes stmt idx (bytebuf->array v))))) 95 | -------------------------------------------------------------------------------- /src/mpg/datetime.clj: -------------------------------------------------------------------------------- 1 | (ns mpg.datetime 2 | (:require [clojure.java.jdbc :as j] 3 | [mpg.util :as u :refer [pg-param-type]]) 4 | (:import [java.time Instant LocalDate LocalDateTime ZonedDateTime ZoneId] 5 | [java.time.temporal ChronoField] 6 | [java.sql Date Timestamp PreparedStatement] 7 | [java.util Calendar])) 8 | 9 | (defn get-zone 10 | "Returns the zone named, which may be a keyword or string, e.g. :UTC" 11 | [zone] 12 | (cond 13 | (string? zone) (ZoneId/of zone) 14 | (keyword? zone) (ZoneId/of (name zone)) 15 | :else (throw (ex-info "Invalid timezone" {:got zone})))) 16 | 17 | (def ^ZoneId utc (get-zone :UTC)) 18 | (def ^ZoneId local (ZoneId/systemDefault)) 19 | 20 | (defn zoneddatetime->timestamp [^ZonedDateTime zdt] 21 | (-> zdt 22 | (.withZoneSameInstant local) 23 | .toLocalDateTime 24 | Timestamp/valueOf)) 25 | 26 | (defn truncate-date [^java.util.Date d] 27 | (-> (doto (Calendar/getInstance) 28 | (.setTime d) 29 | (.set Calendar/HOUR_OF_DAY 0) 30 | (.set Calendar/MINUTE 0) 31 | (.set Calendar/SECOND 0) 32 | (.set Calendar/MILLISECOND 0)) 33 | .getTimeInMillis 34 | Date.)) 35 | 36 | 37 | (defn localdate->date [^LocalDate ld] 38 | (-> ld 39 | (.atStartOfDay (ZoneId/systemDefault)) 40 | .toInstant 41 | Date/from)) 42 | 43 | (defn patch 44 | "Installs conversion hooks for various java.time types 45 | args: []" 46 | [] 47 | (extend-protocol j/IResultSetReadColumn 48 | Date 49 | (result-set-read-column [^Date v _ _] 50 | (.toLocalDate v)) 51 | Timestamp 52 | (result-set-read-column [^Timestamp v _ _] 53 | (-> v .toInstant (.atZone utc)))) 54 | (extend-protocol j/ISQLValue 55 | java.util.Date 56 | (sql-value [value] 57 | (Date. (.getTime value))) 58 | LocalDate 59 | (sql-value [value] 60 | (localdate->date value)) 61 | ZonedDateTime 62 | (sql-value [value] 63 | (zoneddatetime->timestamp value))) 64 | (extend-protocol j/ISQLParameter 65 | java.util.Date 66 | (set-parameter [^java.util.Date v ^PreparedStatement stmt ^long idx] 67 | (.setObject stmt idx 68 | (case (pg-param-type stmt idx) 69 | "date" (Date. (.getTime v)) 70 | "timestamp" (Timestamp. (.getTime v)) 71 | "timestamptz" (Timestamp. (.getTime v))))) 72 | LocalDate 73 | (set-parameter [^LocalDate v ^PreparedStatement stmt ^long idx] 74 | (.setObject stmt idx 75 | (case (pg-param-type stmt idx) 76 | "date" (Date/valueOf v) 77 | "timestamp" (Date/valueOf v) 78 | "timestamptz" (Date/valueOf v)))) 79 | ZonedDateTime 80 | (set-parameter [^ZonedDateTime v ^PreparedStatement stmt ^long idx] 81 | (let [t (pg-param-type stmt idx)] 82 | (if (#{"timestamp" "timestamptz"} t) 83 | (->> v zoneddatetime->timestamp (.setTimestamp stmt idx)) 84 | (throw (ex-info (str "Invalid conversion from ZonedDateTime. expected " t) {}))))))) 85 | -------------------------------------------------------------------------------- /src/mpg/util.clj: -------------------------------------------------------------------------------- 1 | (ns mpg.util 2 | (:require [cheshire.core :as c]) 3 | (:import [org.postgresql.util PGobject] 4 | [java.sql PreparedStatement])) 5 | 6 | (defn pg-param-type 7 | [^PreparedStatement s ^long idx] 8 | (if-let [md (.getParameterMetaData s)] 9 | (or (.getParameterTypeName md idx) 10 | (throw (ex-info "We could not obtain the column type name" {:got s :meta md}))) 11 | (throw (ex-info "We could not obtain metadata from the prepared statement" {:got s})))) 12 | 13 | (defn pg-result-type 14 | [^PreparedStatement s ^long idx] 15 | (if-let [md (.getMetaData s)] 16 | (or (.getColumnTypeName md idx) 17 | (throw (ex-info "We could not obtain the column type name" {:got s :meta md}))) 18 | (throw (ex-info "We could not obtain metadata from the prepared statement" {:got s})))) 19 | 20 | (defn param-meta 21 | [s] 22 | (let [m (.getParameterMetaData s) 23 | c (.getParameterCount m)] 24 | (into [] (map (fn [i] 25 | {:class (.getParameterClassName m i) 26 | :type (.getParameterTypeName m i)})) 27 | (range 1 (inc c))))) 28 | 29 | (defn result-meta [s] 30 | (let [m (.getMetaData s) 31 | c (.getColumnCount m)] 32 | (into [] (map (fn [i] 33 | {:class (.getColumnClassName m i) 34 | :type (.getColumnTypeName m i) 35 | :label (.getColumnLabel m i) 36 | :name (.getColumnName m i)})) 37 | (range 1 (inc c))))) 38 | 39 | (defn pg-json 40 | "Converts the given value to a PG JSON object" 41 | [value] 42 | (doto (PGobject.) 43 | (.setType "json") 44 | (.setValue (c/generate-string value)))) 45 | 46 | (defn pg-jsonb 47 | "" 48 | [value] 49 | (doto (PGobject.) 50 | (.setType "jsonb") 51 | (.setValue (c/generate-string value)))) 52 | 53 | 54 | (defn fatal [msg map] 55 | (throw (ex-info (str msg ": " (pr-str map)) map))) 56 | -------------------------------------------------------------------------------- /test/mpg/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns mpg.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.jdbc :as sql] 4 | [environ.core :refer [env]] 5 | [mpg.core :as mpg] 6 | [mpg.data :as d] 7 | [mpg.datetime :as dt]) 8 | (:import [java.nio ByteBuffer] 9 | [java.sql Timestamp] 10 | [java.time LocalDate ZonedDateTime ZoneId] 11 | [org.postgresql.util PGobject])) 12 | 13 | (mpg/patch) 14 | 15 | (defn to-byte-string [thing] 16 | (cond (instance? (Class/forName "[B") thing) (String. ^bytes thing) 17 | (instance? ByteBuffer thing) (String. ^bytes (d/bytebuf->array thing)) 18 | :else (-> (str "WTF is this? " thing " " (type thing)) 19 | (ex-info {:got thing}) 20 | throw))) 21 | 22 | (defn to-ts [thing] 23 | (cond (instance? Timestamp thing) thing 24 | (instance? java.util.Date thing) (Timestamp. (.getTime ^java.util.Date thing)) 25 | (instance? ZonedDateTime thing) (dt/zoneddatetime->timestamp thing) 26 | :else (-> (str "WTF is this? " thing " " (type thing)) 27 | (ex-info {:got thing}) 28 | throw))) 29 | 30 | (defn to-date [thing] 31 | (cond (instance? java.util.Date thing) (dt/truncate-date thing) 32 | (instance? LocalDate thing) (dt/localdate->date thing) 33 | :else (-> (str "WTF is this? " thing " " (type thing)) 34 | (ex-info {:got thing}) 35 | throw))) 36 | 37 | ;; Allow the user to specify connection parameters on the command line 38 | (def pg 39 | (as-> {:subprotocol "postgresql" 40 | :subname (or (env :mpg-test-db-uri) 41 | "//127.0.0.1:5432/mpg_test")} $ 42 | (if-let [user (env :mpg-test-db-user)] 43 | (assoc $ :user user) 44 | $) 45 | (if-let [password (env :mpg-test-db-pass)] 46 | (assoc $ :password password) 47 | $))) 48 | 49 | (def conn (sql/get-connection pg)) 50 | 51 | (defn prepare-db [] 52 | (sql/execute! {:connection conn} "create extension if not exists hstore") 53 | (sql/execute! {:connection conn} "create extension if not exists citext") 54 | (sql/execute! {:connection conn} "drop table if exists insert_test") 55 | (sql/execute! {:connection conn} 56 | "create temporary table insert_test( 57 | a smallint, b integer, c bigint, d serial, e bigserial, 58 | f int2, g int4, h int8, i serial4, j serial8, 59 | k decimal, l numeric, m real, n double precision, 60 | o varchar, p citext, q jsonb, r json)")) 61 | 62 | (defn random-byte-array [size] 63 | (let [a (byte-array size)] 64 | (->> a .nextBytes (doto (java.util.Random.))) 65 | a)) 66 | 67 | (defn select-roundtrip-prepared [type val] 68 | (-> (as-> (str "select (? :: " type ") as result") $ 69 | (sql/prepare-statement conn $) 70 | (sql/query conn [$ val])) 71 | first :result)) 72 | 73 | (defn select-roundtrip-unprepared [type val] 74 | (-> (as-> (str "select (? :: " type ") as result") sql 75 | (sql/query {:connection conn} [sql val])) 76 | first :result)) 77 | 78 | (defn select-roundtrip-test 79 | ([title v types] 80 | (select-roundtrip-test title v types identity)) 81 | ([title v types printer] 82 | (testing title 83 | (let [v2 (printer v)] 84 | (doseq [t types] 85 | (is (= v2 86 | (printer (select-roundtrip-unprepared t v)) 87 | (printer (select-roundtrip-prepared t v)) 88 | ;; repeat because of issue #17 89 | (printer (select-roundtrip-unprepared t v)) 90 | (printer (select-roundtrip-prepared t v))))))))) 91 | 92 | (defn keywordize [m] 93 | (into {} (map (fn [[k v]] [(keyword (str k)) v])) m)) 94 | 95 | (deftest insert-test 96 | (let [orig {:a 123 :b 123 :c 123 :d 123 :e 123 :f 123 :g 123 :h 123 :i 123 :j 123 97 | :k 1.23M :l 1.23M :m (float 1.23) :n 1.23 :o "foo" :p "foo" 98 | :q {123 123 1.23 1.23 "a" "b"} :r {123 123 1.23 1.23 "a" "b"}} 99 | one (-> orig (update :q keywordize) (update :r keywordize)) 100 | two (first (sql/insert! {:connection conn} "insert_test" orig)) 101 | three (first (sql/query {:connection conn} "select * from insert_test as result"))] 102 | (is (= one two three)))) 103 | 104 | (deftest select-data-bidirectional 105 | (let [v1 {:a [{:b "c"}]} 106 | v2 [{:b [4 5 6]}] 107 | v3 {"a" "123" "b" "456"} ; jdbc+hstore cannot has such rich delights as "numbers" 108 | v4 [2 4 8] 109 | v5 "example text" 110 | v6 (random-byte-array 32)] 111 | (select-roundtrip-test "maps <-> json" v1 ["json" "jsonb"]) 112 | (select-roundtrip-test "vector <-> json" v2 ["json" "jsonb"]) 113 | (select-roundtrip-test "map <-> hstore" v3 ["hstore"]) 114 | (select-roundtrip-test "vector <-> array" v4 ["int[]"]) 115 | (select-roundtrip-test "string <-> citext" v5 ["citext"]) 116 | (select-roundtrip-test "byte-array <-> bytea" v6 ["bytea"] #(String. ^bytes %)))) 117 | 118 | (deftest select-data-unidirectional 119 | (let [bytes (random-byte-array 32) 120 | bb (doto (ByteBuffer/allocate 32) 121 | (.put ^bytes bytes))] 122 | (select-roundtrip-test "Bytebuffer -> bytea" bb ["bytea"] to-byte-string))) 123 | 124 | (deftest select-datetime-bidirectional 125 | (let [now-loc (LocalDate/now) 126 | now-utc (ZonedDateTime/now (ZoneId/of "UTC"))] 127 | (select-roundtrip-test "LocalDate <-> date" now-loc ["date"]) 128 | (select-roundtrip-test "ZonedDateTime <-> timestamp" now-utc ["timestamp" "timestamptz"]))) 129 | 130 | (deftest select-datetime-unidirectional 131 | (let [now-jud (java.util.Date.) 132 | now-jsts (java.sql.Timestamp. (.getTime now-jud))] 133 | (select-roundtrip-test "j.u.Date -> date" now-jud ["date"] to-date) 134 | (select-roundtrip-test "j.u.Date -> timestamp" now-jud ["timestamp" "timestamptz"] to-ts) 135 | (select-roundtrip-test "j.s.Timestamp -> date" now-jsts ["date"] to-date) 136 | (select-roundtrip-test "j.s.Timestamp -> timestamp" now-jsts ["timestamp" "timestamptz"] to-ts))) 137 | 138 | (defn create-test-table [] 139 | (sql/execute! {:connection conn} "create table insert_test")) 140 | 141 | (defn delete-test-table [] 142 | (sql/execute! {:connection conn} "drop table insert_test")) 143 | 144 | (prepare-db) 145 | --------------------------------------------------------------------------------