sql

All things SQL and JDBC...
2020-07-21T04:52:12.269300Z

Hi, I am using next.jdbc and am doing a batch insert using next.jdbc.prepare/execute-batch! It is working as expected but then following the instructions here https://cljdoc.org/d/seancorfield/next.jdbc/1.1.569/doc/getting-started/prepared-statements#batched-parameters I and trying to use .getGeneratedKeys to get the results set back and it only returns the last batch to be inserted not the entire set. Is this expected behaviour?

2020-07-21T04:59:48.270600Z

I sort of understand why, it is stateful and .getGeneratedKeys would need to be called in between each call to .executeBatch?

seancorfield 2020-07-21T05:05:16.271200Z

@sofra Yeah, it's a bit of an edge case. Different databases behave differently on this, unfortunately.

2020-07-21T05:05:28.271500Z

:thumbsup:

seancorfield 2020-07-21T05:05:51.272100Z

Some databases will return all generated keys (from all batches executed), some don't return any keys at all.

2020-07-21T05:06:13.272400Z

Thanks @seancorfield, makes sense

seancorfield 2020-07-21T05:07:08.272700Z

(hence all the caveats on that page 🙂 )

seancorfield 2020-07-21T05:10:30.273Z

How big is the batch you're inserting @sofra?

seancorfield 2020-07-21T05:16:44.276200Z

If it's large enough to cause multiple .executeBatch calls, I can understand you only getting one set of keys back. I guess I could modify execute-batch! so that if :return-keys is truthy, it could attempt to call .getGeneratedKeys each time internally and return the result sets joined together. It seems like a reasonable enhancement, although I'd need to ensure it doesn't break existing behavior (so, probably another option as well as :return-keys I suspect).

seancorfield 2020-07-21T05:18:47.276400Z

https://github.com/seancorfield/next-jdbc/issues/133

2020-07-21T05:25:28.278200Z

@seancorfield it is a few thousand rows. Thanks for the info. I can work around it now I understand. Sounds like a reasonable enhancement to me too.

seancorfield 2020-07-21T05:35:42.278800Z

@sofra Are you specifying the :batch-size option to execute-batch! or just letting it do its thing?

2020-07-21T05:36:11.279200Z

yeah I was specifying it

seancorfield 2020-07-21T05:36:58.280100Z

If you don't specify it, .executeBatch will only be called once and .getGeneratedKeys should return all the keys -- but that may too much data for a single batch.

seancorfield 2020-07-21T05:37:32.280700Z

Anyway, I'll add that enhancement probably this week and put out a new release by the end of next weekend at the latest.

2020-07-21T05:42:01.282700Z

Thanks @seancorfield I was batching since it was too much for a regular mutli-insert! and the db was throwing errors. That is why I figured I would need to set :batch-size but I didn’t actually test that assumption.

2020-07-21T05:43:06.283900Z

Thanks for your help, much appreciated! Happy to give it a try if you need when you have something ready.

g7s 2020-07-21T08:17:25.287Z

I don't know if this has been asked before but here it goes. Is there a way to make next.jdbc 's :builder-fn work with CTEs? For some context: I am using WITH RECURSIVE to build a result set but it seems that the :builder-fn configuration option is not used

Eric Ihli 2020-07-21T15:30:20.292900Z

I'm trying to get Clojure booleans from SQLite booleans (which are stored as integers). I'm close, but there's at least one thing I don't understand and I'm curious if there's something I don't know and a better way to do this.

(require '[next.jdbc :as jdbc])

  (def db-spec {:dbtype "sqlite" :dbname "example-db"})

  (jdbc/execute!
   db-spec
   ["create table if not exists users (name text, is_active bool)"])

  (jdbc/execute!
   db-spec
   ["insert into users (name, is_active) values (?, ?)" "alice" true])

  (jdbc/execute!
   db-spec
   ["select * from users"])
  ;; => [#:users{:name "alice", :is_active 1}]
I see that next.jdbc lets you extend the ReadableColumn protocol for different types. That lead me to get the following code that I works for at least a simple example.
(extend-protocol result-set/ReadableColumn
  Integer
  (read-column-by-index [x mrs i]
    (if (re-find #"(?i)bool" (.getColumnTypeName mrs i))
      (if (= 1 x) true false)
      x)))
But I see that there's also a read-column-by-label that doesn't receive a ResultSetMetaData object which is what I need in order to get the column type name and check to see if it's boolean. I haven't been able to determine how/where read-column-by-label is used and how/if this solution is insufficient. I imagine there's a better solution than keeping a set of which column names refer to boolean columns and doing something like (read-column-by-label [x name] (if (set-of-boolean-columns name) ,,,)

seancorfield 2020-07-21T16:08:44.294600Z

@ericihli Can you create an issue on next.jdbc with details of that? I use MySQL and it stores booleans as BIT(1) and those do convert into true/`false` as expected. I haven't specifically tried SQLite in that situation.

Eric Ihli 2020-07-21T16:09:25.294900Z

Sure thing.

seancorfield 2020-07-21T16:10:10.295600Z

next.jdbc tries to canonicalize Boolean values if it sees them but that assumes the JDBC driver actually produces Boolean values 🙂

g7s 2020-07-21T16:33:52.295800Z

Sure. I am using honeysql so this is the query (as data) that I am running

{:with-recursive [[:starting-record
                   {:select [:*]
                    :from   [:records]
                    :limit  1}]
                  [:records-path
                   {:union-all [{:select [:*]
                                 :from   [:starting-record]}
                                {:select [:recs.*]
                                 :from   [[:records-path :rp]]
                                 :join   [[:records :r]
                                          [:= :rp.previous-id :recs.id]]}]}]]
 :select         [:*]
 :from           [:records-path]}
The options I pass to the query include a :builder-fn with value the following function
(defn- as-entities [rs opts]
  (as-modified-maps rs (assoc opts :qualifier-fn entity-namespace :label-fn kebab)))
Now when I execute the query I am certain that the :qualifier-fn is not getting called while the :label-fn is getting called.

seancorfield 2020-07-21T16:37:15.296Z

What database are you using?

g7s 2020-07-21T16:37:23.296200Z

PostgreSQL

g7s 2020-07-21T16:37:59.296400Z

For other queries works as expected though

seancorfield 2020-07-21T16:38:58.296600Z

:qualifier-fn is not called if a given column has no associated table name -- and that's up to the JDBC driver.

seancorfield 2020-07-21T16:39:39.296800Z

(mapv (fn [^Integer i]
            (if-let [q (some-> (.getTableName rsmeta i) (qf) (not-empty))]
              (keyword q (-> (.getColumnLabel rsmeta i) (lf)))
              (keyword (-> (.getColumnLabel rsmeta i) (lf)))))
          (range 1 (inc (.getColumnCount rsmeta))))))
^ if .getTableName() returns nil, we have no table name to process.

g7s 2020-07-21T16:39:49.297Z

Ah OK it makes sense!

seancorfield 2020-07-21T16:40:11.297200Z

Computed columns often have no table name associated with them.

g7s 2020-07-21T16:41:25.297400Z

I see. Is there anything I can do to pass somehow a function that even with nil table name to return something? (constantly "foo")

g7s 2020-07-21T16:42:08.297600Z

Right now I am mapping the keys manually which is a bit ugly

seancorfield 2020-07-21T16:44:30.297800Z

At some point, I could have sworn I'd made a change to always call :qualifier-fn so that you could get behavior compatible with clojure.java.jdbc... I'll have to go digging. Can you open an issue on next.jdbc with details of the above, so I don't forget?

g7s 2020-07-21T16:49:39.298Z

Sure!

seancorfield 2020-07-21T16:51:06.298200Z

Hmm, interesting. Normally, .getTableName() would return "" for a table name not being available and there's actually a test for this and it does work. So this is a case where .getTableName() seems to be returning nil instead which is somewhat unexpected...

g7s 2020-07-21T16:51:46.298400Z

Lemme check for the driver version I am running

seancorfield 2020-07-21T16:52:04.298600Z

See https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/ResultSetMetaData.html#getTableName(int) -- it is explicitly supposed to return "" if the table name is not available. Not nil.

g7s 2020-07-21T16:53:08.298800Z

I am using [org.postgresql/postgresql "42.2.9"]

g7s 2020-07-21T16:53:27.299Z

Yes I can see that it should return ""

seancorfield 2020-07-21T16:53:35.299200Z

I can certainly workaround that by adding (or "") into that threading expression above but I'd certainly like to confirm that PG really is returning nil in this case...

seancorfield 2020-07-21T16:54:10.299400Z

I test against 42.2.10 just FYI.

g7s 2020-07-21T16:54:20.299600Z

will do just a sec

g7s 2020-07-21T16:54:59.299800Z

Should I test against the latest 42.2.14?

g7s 2020-07-21T16:58:57.300Z

Hey it worked!

seancorfield 2020-07-21T16:58:59.300200Z

Sure. Add a note into the issue you create saying what versions you tested against. I just looked over the PG (developers) mailing list and it appears they had it hardcoded to return "" at one point and then changed it to return the base table name... so I'm wondering if this is essentially a bug in PG that the base table name is returning null instead of "" in this case?

seancorfield 2020-07-21T16:59:11.300400Z

Ah! So they fixed their bug 🙂

g7s 2020-07-21T16:59:21.300600Z

haha seems like it!

g7s 2020-07-21T16:59:34.300800Z

Thanks a lot Sean

seancorfield 2020-07-21T17:04:37.301Z

I don't see that change listed anywhere here https://jdbc.postgresql.org/documentation/changelog.html#version_42.2.14 but I will run my test suite with 42.2.9 and up to see if the test case I have is affected by any of those versions...

g7s 2020-07-21T17:06:36.301300Z

I checked again with 42.2.9 and it seems that it is getting called with "" but I guess that is what you get when you debug with println 😒 I somehow missed all the empty strings printed in my REPL. Sorry for wasting your time

g7s 2020-07-21T17:08:20.301500Z

I was expecting to see a table name there (didn't know the computed columns do not have a table name attached)

seancorfield 2020-07-21T17:09:42.301700Z

OK, so :qualifier-fn (constantly "foo") should work, even on 42.2.9.

g7s 2020-07-21T17:10:56.301900Z

Yes it should.. Now I have to figure out how I can pass it from the query definition 🙂

seancorfield 2020-07-21T17:12:19.302100Z

Well, you have :qualifier-fn entity-namespace so that function is called 🙂

g7s 2020-07-21T17:16:59.302300Z

Yeah this entity-namespace actually maps a table name to an entity namespace and the mapping is pretty hardcoded. Since I am defining my queries separately from where I am executing them I have to find a way to pass to the query data the table name to be used (for queries that lack the table name)

g7s 2020-07-21T17:17:28.302500Z

(maybe as metadata to the honeysql datastructure)

g7s 2020-07-21T17:18:01.302700Z

Anyway I don't want to waste more of your time 😳

Eric Ihli 2020-07-21T23:45:18.307100Z

Trying to accomplish the below in SQLite. Curious how to do this in all SQL rather than writing Clojure code to do the looping and conjing. Anyone have a pointer, even it it's just a hint, like "Look into CTE and recursive" or something.

WITH table_names AS (SELECT name FROM sqlite_master) SELECT * FROM table_names;
-- For each table_name above, join it to each of its column names and types, like in the select below.
SELECT name, type FROM pragma_table_info("<table_name>");
-- Union everything so the end result is a table of all tables, columns, and column types.
-- |table_name|column_name|column_type|

Eric Ihli 2020-07-22T13:48:34.311100Z

Ah. This was it.

SELECT p.name, p.type, m.name
FROM sqlite_master AS m
JOIN pragma_table_info(m.name) AS p;