sql

All things SQL and JDBC...
adam 2020-06-24T00:28:19.333900Z

How do I set a Postgres type when inserting? I have some field that uses a type, e.g., CREATE TYPE language AS ENUM ('en', 'fr', 'es'); … the field is defined as lang language DEFAULT 'en', it asked me to cast the expression when using insert! and insert-multi!: > ERROR: column “lang” is of type language but expression is of type character varying > Hint: You will need to rewrite or cast the expression.

seancorfield 2020-06-24T00:38:04.334800Z

@somedude314 No idea what PG requires. In MySQL enum columns can have strings inserted into them and the JDBC driver handles the conversion automatically.

seancorfield 2020-06-24T00:39:42.335700Z

If you use execute! for the insert, you will be able to specify cast expressions which may be your best bet with PostgreSQL...?

2020-06-24T00:42:28.336400Z

☝️ Can confirm cast expressions work with postgres. That's what I use.

adam 2020-06-24T01:12:05.339300Z

I am not willing to give up the friendly functions, they are very clean and convenient, I’d rather turn the fields into varchar… I had it working with hugsql though by appending ::lang to the placeholder. How does it look in MySQL when the column name is a keyword?

seancorfield 2020-06-24T01:21:39.341200Z

@somedude314 Not sure what you mean re: MySQL? If I have some_col ENUM('blue','red','green') in MySQL, I can insert {:some_col "red"} and it just works. If you try to insert an invalid string, you get a value too big for column exception (something like that).

seancorfield 2020-06-24T01:22:48.342Z

If you use HoneySQL to generate the insert SQL (and then use execute! to run it), I think you can get casts into the inserted values reasonably cleanly.

adam 2020-06-24T02:06:59.342200Z

I see. I thought I could pass ::uuid or ::language after the value somehow. I will drop the enum usage for now.

seancorfield 2020-06-24T02:13:19.342400Z

Bear in mind that ::uuid might look like a keyword in Clojure but it's PostgreSQL-specific syntax in SQL (and ::uuid would auto-resolve to :current.namespace/uuid anyway).

seancorfield 2020-06-24T02:15:58.342600Z

I think in standard SQL, you'd say CAST( 'fr' AS language ) ?

seancorfield 2020-06-24T02:39:59.345Z

@somedude314 it looks like PostgreSQL would be happy with .setString() to assign a string version of the enum to the column. You can make that happen by providing a metadata implementation of SettableParameter as a wrapper around your string value.

seancorfield 2020-06-24T02:44:27.346500Z

I believe you could handle that with a function like this:

(defn enum [s] (with-meta [s] {`next.jdbc.prepare/set-parameter (fn [[v] ^java.sql.PreparedStatement s ^long i] (.setString s i v))}))
and then (enum "fr") should produce a value that next.jdbc.sql/insert! should be able to insert into any enum, if I'm understanding the PG docs correctly.

adam 2020-06-24T02:44:34.346600Z

I believe so, SELECT CAST( 'student' AS user_role ) from "user" works for me

seancorfield 2020-06-24T02:46:23.347300Z

(I'm not at my main machine so I can't verify that -- I will try tomorrow)

adam 2020-06-24T02:53:57.349500Z

Only wrapping with (enum) gives me: > Execution error (PSQLException) at org.postgresql.core.v3.QueryExecutorImpl/receiveErrorResponse (QueryExecutorImpl.java:2440). > ERROR: column “lang” is of type language but expression is of type character varying > Hint: You will need to rewrite or cast the expression. > Position: 93

seancorfield 2020-06-24T03:01:10.349700Z

Ah, that sucks.

seancorfield 2020-06-24T03:03:07.350700Z

The JDBC PG docs seemed to suggesting that would work. I'll try to find some time tomorrow to do some more research -- it's my day off so I won't be working.

seancorfield 2020-06-24T03:03:56.351200Z

Strange that it's the same error as you got originally tho'...

adam 2020-06-24T03:24:19.354800Z

I appreciate it. If it won't be doable with insert! I will be converting the fields and managing the types on the application level. I won't be leaving the insert! I'd die on that hill lol. The little functions cleaned my code so much

Gleb Posobin 2020-06-24T03:57:59.354900Z

I wanted to save a couple of million rows to a JSON, in the end I just made a lazy seq that did batched queries to the db.

Gleb Posobin 2020-06-24T03:58:32.355100Z

So basically pagination with offsets and limits.

seancorfield 2020-06-24T04:40:55.355300Z

Hmm, so streaming rows into a JSON file? You could do that with reducing over plan by appending the JSON of each row to a file, with trailing , added I guess? (and a leading [ and trailing ])

seancorfield 2020-06-24T05:53:12.356100Z

@somedude314 Figured it out! This works

(defn- enum [s]
  (with-meta [s]
    {`prep/set-parameter
     (fn [[v] ^java.sql.PreparedStatement s ^long i]
       (.setObject s i v java.sql.Types/OTHER))}))

(deftest enum-pg
  (when (postgres?)
    (let [r (sql/insert! (ds) :lang_test {:lang (enum "fr")})]
      (println 'enum-pg r))))
Deduced from https://stackoverflow.com/questions/851758/java-enums-jpa-and-postgres-enums-how-do-i-make-them-work-together

seancorfield 2020-06-24T05:54:19.357Z

So I'm probably going to add some helpers, based on java.sql.Types, that allow easy translation on a per-object basis.

dharrigan 2020-06-24T06:08:55.357200Z

That's pretty fantastic!

Gleb Posobin 2020-06-24T11:45:03.357300Z

Hmmm, from that SO answer I thought that reduce is eager: https://stackoverflow.com/questions/58068904/how-to-adapt-the-ireduceinit-from-next-jdbc-to-stream-json-using-cheshire-to-a-h

adam 2020-06-24T15:24:56.360300Z

Hmm, it’s not working for me. I get this error on insert! and insert-multi!: Error: Execution error (PSQLException) at org.postgresql.jdbc.PgPreparedStatement/setObject (PgPreparedStatement.java:949). Can't infer the SQL type to use for an instance of clojure.lang.PersistentVector. Use setObject() with an explicit Types value to specify the type to use. -- (enum "fr") => ["fr"] (type (enum "fr")) => clojure.lang.PersistentVector postgres --version postgres (PostgreSQL) 12.3 Am I supposed to be passing some special options?

seancorfield 2020-06-24T15:48:32.362300Z

@somedude314 make sure you exactly copy what I pasted -- in particular the first arg to the anonymous fn is a destructured [v]

seancorfield 2020-06-24T15:49:34.363200Z

The above code comes from the test suite, so it definitely works 😁

seancorfield 2020-06-24T15:53:17.364200Z

Reduce is eager. The "lazy" part refers to how the rows are read from the database

Gleb Posobin 2020-06-24T15:58:46.364400Z

So the bottleneck for me is in ram, I didn't want to have the whole result set in memory.

adam 2020-06-24T16:04:04.366Z

[~/Desktop] ⤑ lein repl nREPL server started on port 51669 on host 127.0.0.1 - <nrepl://127.0.0.1:51669> REPL-y 0.4.4, nREPL 0.6.0 Clojure 1.10.0 Java HotSpot(TM) 64-Bit Server VM 11.0.7+8-LTS Docs: (doc function-name-here) (find-doc "part-of-name-here") Source: (source function-name-here) Javadoc: (javadoc java-object-or-class-here) Exit: Control+D or (exit) or (quit) Results: Stored in vars *1, *2, *3, an exception in *e user=&gt; (defn- enum [s] #_=&gt; (with-meta [s] #_=&gt; {prep/set-parameter` #_=&gt; (fn [[v] ^java.sql.PreparedStatement s ^long i] #_=&gt; (.setObject s i v java.sql.Types/OTHER))})) #'user/enum user=&gt; (enum "fr") ["fr"] user=&gt; (type (enum "fr")) clojure.lang.PersistentVector

adam 2020-06-24T16:04:16.366300Z

It’s outputting a plain vector

seancorfield 2020-06-24T16:08:42.366700Z

It's a vector with metadata. That's correct.

seancorfield 2020-06-24T16:09:35.367700Z

You haven't required next.jdbc.prepare :as prep -- required for the `prep/set-parameter symbol to resolve correctly.

seancorfield 2020-06-24T16:10:08.368300Z

or you could use 'next.jdbc.prepare/set-parameter as the metadata key.

seancorfield 2020-06-24T16:10:51.369200Z

BTW, you can use triple backticks around blocks of code to make them more readable.

like this
block

1👌
seancorfield 2020-06-24T16:12:04.369300Z

Right, so you must "lazily stream" the result sets from the database, but you can eagerly reduce them.

seancorfield 2020-06-24T16:13:08.369500Z

Getting the driver to lazily stream results is database-dependent -- some suggestions are given in the next.jdbc docs, on the Tips &amp; Tricks page as I recall.

Gleb Posobin 2020-06-24T16:21:48.369700Z

Ah, that's great, I'll take a look, thank you!

adam 2020-06-24T16:24:06.370600Z

Got it working now with that! Thanks.

seancorfield 2020-06-24T16:30:12.371900Z

Cool. In the next version of next.jdbc (probably later today), there will be several built-in helpers to make this a lot easier.

seancorfield 2020-06-24T18:50:05.373Z

Just released 1.0.478 with next.jdbc.types/as-other so you no longer need to define that enum function!

seancorfield 2020-06-24T18:51:03.373500Z

See usage in the test for inserting a string into a PostgreSQL enum https://github.com/seancorfield/next-jdbc/blob/develop/test/next/jdbc/sql_test.clj#L162-L165 @somedude314

seancorfield 2020-06-24T18:51:47.374400Z

(these as-xxx functions are auto-generated from java.sql.Types via Java Reflection when the next.jdbc.types namespace is required so it's opt-in)

seancorfield 2020-06-24T19:02:53.375700Z

(also, develop has been merged into nested-tx and multi-rs in case anyone is testing against those features and wants the latest released features too)

dharrigan 2020-06-24T19:27:42.376Z

awesome sauce!

1💯
seancorfield 2020-06-24T19:57:24.376600Z

Multiple result set support will probably get merged to develop and released later today. I just have to write up all the documentation around it.

seancorfield 2020-06-24T19:58:26.377400Z

Belay that... I still haven't figured out how multiple result sets should work with plan...

seancorfield 2020-06-24T19:59:04.378100Z

It all works for execute! (but not for execute-one! since that is specifically designed to return one row of one result set).

adam 2020-06-24T20:14:54.378200Z

Amazing, thank you. Working like a charm. I am still defining enum in my simple db wrapper to avoid importing the types all over the place :)

(def enum types/as-other)

1
seancorfield 2020-06-24T21:18:46.379700Z

I'm going back and forth on how multiple result sets should work with plan but I'm thinking the most intuitive approach is to reduce over each result set in sequence and still conj the results into a vector.

seancorfield 2020-06-24T21:19:45.380800Z

But some databases return an update count as well as actual result sets (at least, based on the stored procs I've created for testing), and some don't (and HSQLDB and MySQL return the update count in different places, just to make this more fun).

seancorfield 2020-06-24T21:21:07.381800Z

Anyone reducing over plan with multiple result sets has got to know what they're doing. Is it common to have different types of result sets coming back, so a single reducing function could not be used?

seancorfield 2020-06-24T21:21:47.382600Z

(perhaps this is too niche of a concern to even warrant spending cycles on it at all? How many people actually use multiple result sets in the first place?)

seancorfield 2020-06-24T21:23:31.384Z

If you're using stored procs already, I suspect you'd lean toward having the proc compute "everything" and just get fairly simple result sets back -- so maybe execute! is sufficient? But what about large result sets or other in-memory computation you might need to account for?

seancorfield 2020-06-24T21:26:19.384200Z

I guess you could just have a multi-method as the reducing function? Or a function that did stuff conditionally depending on what columns the row has in it? Because of the MySQL/HSQLDB results, you'd already need to look for :next.jdbc/update-count to spot an update count "result set" instead of a "real" one...

adam 2020-06-24T23:45:43.387Z

Not a really a bug but a funny behaviour in next.jdbc: (sql/query datasource [(str "SELECT * FROM \"user\"")]) gets me the users from my user table (sql/query datasource ["SELECT * FROM user"]) gets me Postgres users from its user table

adam 2020-06-25T19:41:36.418100Z

got it, thanks

2020-06-24T23:48:37.387900Z

same behavior from psql

2020-06-24T23:48:50.388200Z

kevin=# select * from user;
 user  
-------
 kevin
(1 row)

kevin=# select * from "user";
--
(0 rows)

kevin=# 

2020-06-24T23:52:08.390500Z

user isn't actually postgres's table of users

2020-06-24T23:52:26.390900Z

user is, maybe a session variable? that always contains the current user name

2020-06-24T23:52:42.391200Z

kevin=# select 'kevin' = user;
 ?column? 
----------
 t
(1 row)

kevin=# 

adam 2020-06-24T23:53:57.392500Z

Ah I see. Maybe I should have used plural forms for my table names. Singular names seem more correct but not sure it the troubles are worth it

2020-06-24T23:54:08.392600Z

my postgres won't let you create a table named user unless you quote it too

2020-06-24T23:54:47.393300Z

but if you used the ddl stuff to generate your create table commands it might just quote everything