15

For example, here is a product table in PostgreSQL with status as an enum:

create type product_status as enum ('InStock', 'OutOfStock');

create table product (
    pid            int primary key default nextval('product_pid_seq'),
    sku            text not null unique,
    name           text not null,
    description    text not null,
    quantity       int not null,
    cost           numeric(10,2) not null,
    price          numeric(10,2) not null,
    weight         numeric(10,2),
    status         product_status not null
);

Typical Clojure code to insert a product would be:

(def prod-12345 {:sku "12345"
                 :name "My Product"
                 :description "yada yada yada"
                 :quantity 100
                 :cost 42.00
                 :price 59.00
                 :weight 0.3
                 :status "InStock"})

(sql/with-connection db-spec
   (sql/insert-record :product prod-12345))

However, status is an enum so you can't insert it as a normal string without casting it to an enum:

'InStock'::product_status

I know you can do it with a prepared statement, such as:

INSERT INTO product (name, status) VALUES (?, ?::product_status)

But is there a way to do it without using a prepared statement?

5 Answers 5

4

I got this working today using the stringtype=unspecified hack workaround.

You can add this parameter to your db-spec as follows:

(def db-spec {:classname "org.postgresql.Driver"
              :subprotocol "postgresql"
              :subname "//myserver:5432/mydatabase"
              :user "myuser"
              :password "mypassword"
              :stringtype "unspecified"}) ; HACK to support enums

Then just use insert! as usual.

It would be good to have a solution that doesn't weaken type safety so much.

Sign up to request clarification or add additional context in comments.

1 Comment

I stumbled across this as an alternative. a bit more verbose but might be worth looking into.
1

Kris Jurka replied to the discussion Mike Sherrill cited above with a workaround:

use the url parameter stringtype=unspecified [in the JDBC connection URL] to have setString always bind to unknown instead of varchar, which then shouldn't require any code changes.

I tried this in Java, and it seems to work fine.

Comments

1

This blog post addresses the problem nicely. jdbc provides the ISQLValue protocol, that has only one method, sql-value that converts a clojure value into an sql value, represented by a PGObject. The blog post suggest representing enums with keywords in the form :type/value, so ISQLValue can be implemented as follow:

(defn kw->pgenum [kw]
  (let [type (-> (namespace kw)
                 (s/replace "-" "_"))
        value (name kw)]
    (doto (PGobject.)
      (.setType type)
      (.setValue value))))

(extend-type clojure.lang.Keyword
  jdbc/ISQLValue
  (sql-value [kw]
    (kw->pgenum kw)))

In your example, you would insert your product with:

(def prod-12345 {:sku "12345"
                 :name "My Product"
                 :description "yada yada yada"
                 :quantity 100
                 :cost 42.00
                 :price 59.00
                 :weight 0.3
                 ;; magic happens here
                 :status :product_status/InStock})

(sql/with-connection db-spec
   (sql/insert-record :product prod-12345))

The problem is that when querying the db, the enum is a simple string instead of a keyword. This can be solved in a similar fashion by implementing the IResultSetReadColumn protocol:

(def +schema-enums+
  "A set of all PostgreSQL enums in schema.sql. Used to convert
  enum-values back into Clojure keywords."
  ;; add your other enums here
  #{"product_status"})

(extend-type java.lang.String
  jdbc/IResultSetReadColumn
  (result-set-read-column [val rsmeta idx]
    (let [type (.getColumnTypeName rsmeta idx)]
      (if (contains? +schema-enums+ type)
        (keyword (s/replace type "_" "-") val)
        val))))

Comments

1

If anyone references this question while using the successor to clojure.java.jdbc, jdbc.next, the code for inserting enums is something like:

(ns whatever
 (:require
  [next.jdbc.sql :as jdbc.sql]
  [next.jdbc.types :as jdbc.types]
 ))

;; ...define your database connection and data source...    

(def prod-12345 {:sku "12345"
                 :name "My Product"
                 :description "yada yada yada"
                 :quantity 100
                 :cost 42.00
                 :price 59.00
                 :weight 0.3
                 :status (jdbc.types/as-other "InStock")})

(jdbc.sql/insert! ds :product prod-12345)

As documented https://github.com/seancorfield/next-jdbc/blob/develop/doc/tips-and-tricks.md under the "Working with Enumerated Types" heading.

Comments

0

Unless you pass plain SQL to the back end, you'll have to use a cast. (The SQL statement INSERT INTO product (name, status) VALUES ('SomeName', 'InStock'); should work fine.)

Tom Lane addressed this issue on pgsql-hackers a week after you asked your question.

AFAIK this is just business as usual with JDBC: setString() implies that the parameter is of a string type. It'll fall over if the type actually required is anything but a string. (I'm no Java expert, but I seem to recall that using setObject instead is the standard workaround.)

Enums are not suffering any special hardship here, and I'd be against weakening the type system to give them a special pass.

Our own @CraigRinger participated in that discussion, and might have found something relevant by now.

1 Comment

I think @espeed is asking about the use of the Clojure library clojure.java.jdbc, not Java's JDBC.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.