sql

onetom 2025-11-11T09:02:38.381059Z

is there a way to determine, whether the :multi-rs option is supported for a specific jdbc implementation? the tips&tricks next-jdbc docs don't say it explicitly and it seems to only return a single result, since sqlite's begin/end doesn't seem to document multiple return values:

(jdbc/execute!
    (jdbc/get-datasource "jdbc:sqlite::memory:")
    ["begin; pragma table_list; pragma table_info(sqlite_schema); end"]
    {:multi-rs true})
=> [[{:next.jdbc/update-count 0}]]

onetom 2025-11-11T09:04:02.740439Z

maybe i should try it with batching? since it doesn't seem to be a valid use to provide multiple statements within a jdbc/execute! call anyway...

seancorfield 2025-11-11T15:45:48.831579Z

That's definitely all very DB-specific, unfortunately. Happy to add more notes to Tips & Tricks if you want to create an issue with details, or submit a PR.

onetom 2025-11-12T01:09:57.118899Z

good to know, but i don't think im qualified enough yet to make such calls. in the meantime, i leave some of my experiments here, in case it's useful for others too. This is supposed to return the id values (or the implicit ROWID column, if we https://www.sqlite.org/c3ref/last_insert_rowid.html), but it returns an empty map instead:

(require '[next.jdbc :as jdbc])
(require '[next.jdbc.result-set :as rs])

  (jdbc/with-transaction [tx "jdbc:sqlite::memory:"]
    (jdbc/execute-one! tx ["create table t (id integer primary key autoincrement, c)"])
    (with-open
      [insert (jdbc/prepare tx ["insert into t (c) values (?)"] {:return-keys [:id]})]

      [(jdbc/execute-batch! insert [[10] [20] [30]]
                            {:batch-size 2
                             :return-generated-keys true})
       (jdbc/execute! tx ["select c from t"] {:builder-fn rs/as-arrays})]))
=> [[] [[:t/c] [10] [20] [30]]]
if we call it with :return-generated-keys false, it does return 1 for every inserted row:
=> [[1 1 1] [[:t/c] [10] [20] [30]]]
(i was kinda expecting counts for the batches, not the individual statements, but it makes sense, that the batching wouldn't affect the structure of the return value) Meanwhile, if we don’t use prepared statements (which one would need for high performance batch inserts), then we can obtain the inserted rows IDs, though it ignores which fields are we requesting:
(jdbc/with-transaction [tx "jdbc:sqlite::memory:"]
    (jdbc/execute-one! tx ["create table t (id integer primary key autoincrement, c)"])
    [(jdbc/execute! tx ["insert into t (c) values (?), (?)" 10 20] {:return-keys false})
     (jdbc/execute! tx ["insert into t (c) values (?), (?)" 30 40] {:return-keys [:id]})
     (jdbc/execute! tx ["insert into t (c) values (?), (?) returning id,c" 50 60])])
=> [[{:next.jdbc/update-count 2}]
    [{:last_insert_rowid() 4}]
    [{:t/id 5, :t/c 50} {:t/id 6, :t/c 60}]]
Using the RETURNING (https://sqlite.org/lang_returning.html) clause works, but that can't be used in a batch, because we get an error about the returned values:
(jdbc/with-transaction [tx "jdbc:sqlite::memory:"]
    (jdbc/execute-one! tx ["create table t (c)"])
    (with-open
      [insert (jdbc/prepare tx ["insert into t (c) values (?) returning *"]
                            {:return-keys true})]
      (jdbc/execute-batch! insert [[10] [20] [30]] {:return-generated-keys true})))

Execution error (BatchUpdateException) at org.sqlite.core.DB/executeBatch (DB.java:944).
batch entry 0: query returns results

onetom 2025-11-12T01:16:06.749659Z

someone already "complained" about it over in the sqlite jdbc driver: https://github.com/xerial/sqlite-jdbc/issues/1186 while i agree that referenced the docs are misleading, i also agree, it would be a little too much magic trying to fiddle with sql statement itself and inject the RETURNING clause, if the generated keys are requested... πŸ˜•

Mark Wardle 2025-11-11T20:02:12.859929Z

I'm using integrant, and not component, and have need to allow a service to be configured optionally with a connection pool and pool specific configuration options. I can do that via connection/->pool obviously, but the issue then is in closing the pool afterwards. As this is itself a library, then I'd rather give the client the control over pool configuration as they know best. There is an internal private function attempt-close that is used by the component implementation. Would there be interest in a small PR to make attempt-close a) do the check to see if the handle implements http://java.io.Closeable, and then falls back to that reflective approach looking for a 'close' method? and b) remove the :private metadata? I can obviously implement this myself manually if there is not interest in widening the API at this point?

Mark Wardle 2025-11-12T15:12:21.446839Z

I just checked and it looks as if c3p0 added Autocloseable to its datasource as of Jan 24. java.io.Closeable implements Autocloseable, so arguably all of this could be replaced by a check for Autocloseable. See https://github.com/swaldman/c3p0/commit/a3906341a86c610327aa95c9429e82529ebca9c4.

seancorfield 2025-11-12T18:24:10.179679Z

That's good to know. Can you create an issue on gh so I remember to look into this and make changes? I'm on the road to Conj right now.

πŸ‘ 1
Mark Wardle 2025-11-12T18:24:36.320219Z

Of course. Enjoy!

Mark Wardle 2025-11-12T20:40:47.771729Z

Crux of the matter is that it would be ill-advised to make the fallback close mechanics public, given that any user nowadays can just call .close without reflection themselves. Sorry for all of the noise.

Mark Wardle 2025-11-12T20:41:05.883209Z

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

seancorfield 2025-11-12T22:28:58.413129Z

Just to be clear, that whole attempt close machinery right now is specific to Component and allowing "any" connection pooling library. But realistically there are only Hikari and c3p0

πŸ‘ 1
seancorfield 2025-11-11T20:23:17.298409Z

That (private) function is currently used in only one place, like so:

([clazz db-spec]
   (component clazz db-spec #(if (isa? clazz java.io.Closeable)
                               (.close ^java.io.Closeable %)
                               (attempt-close %))))
It would probably be fine to remove that conditional and have an instance? check inside attempt-close, but it needs a better name if it becomes public, and then it's part of the documented API of next.jdbc.connection...

seancorfield 2025-11-11T20:25:21.027449Z

So, yeah, feel free to submit a PR and come up with a better name...

πŸ‘ 1
jarohen 2025-11-11T20:40:49.123179Z

we have a https://github.com/xtdb/xtdb/blob/main/core/src/main/clojure/xtdb/util.clj#L65-L144 of these in XT's utils file, feel free to pinch whatever you find useful πŸ™‚

πŸ‘ 1
p-himik 2025-11-11T20:56:09.762489Z

Maybe I'm not getting something, but when you control the creation of something, why is there any need for introspection? Just use the way to dispose of that something.

p-himik 2025-11-11T21:05:37.755209Z

As an example:

(defmethod ig/init-key ::src
  [_ options]
  (next.jdbc.connection/->pool HikariDataSource options))

(defmethod ig/halt-key! ::src
  [_ ^Closeable db-src]
  (try
    (.close db-src)
    (catch Exception _
      ;; Just swallow the exception - there's nothing reasonable that we can do here.
      nil)))
An instance of HikariDataSource is closable, so you know for sure that your ::src is closable, and you can just treat it as such, with no checks.

jarohen 2025-11-11T21:12:05.533759Z

it's more if it's not created in the first place, IIUC

p-himik 2025-11-11T21:15:58.667969Z

Yeah, but if a client of the library controls the creation of something, it's their responsibility to dispose of it. At least that's how I'd do it.

πŸ‘ 1
Mark Wardle 2025-11-11T21:48:12.625679Z

Example: service that provides access to a SQLite database. I don't want to have to have another service to manage the connection pool; I simply want my config to look something like {:f "wibble.sql" :pool {:class 'HikariDataSource :pool-size 5 ...} and the 'service' manages that datasource. Hoisting the responsibility for the pooling to the caller in this case doesn't work. next.jdbc.connection/->pool provides that configuration, but the 'close' part is harder without reflection. One option is to hardcode two options e.g. :hikari :c3po and manage the differences internally, but as next.jdbc exposes this hook for the creation of the pool, it feels fairly appropriate to also provide a hook for safe closing?

Mark Wardle 2025-11-11T21:49:25.525569Z

I could hardcode to use Hikari and then things are easier of course.

p-himik 2025-11-11T21:54:58.774859Z

Mm yeah, to me that definitely smells. Like probably 90% of introspection out there. With things being general, I try to think in general terms. What if the :class that a user provides doesn't have anything that can be detected as closable? What if it is detected as such, but a proper disposal relies on something else, or on some particular procedure? That could be not a concern at all in reality. But I myself have simply seen too many cases of "not a concern right now" turning into snafus down the line.

Mark Wardle 2025-11-11T21:56:44.076089Z

Yes. You might be right. Hardcoding would be fine in the integrant init, and then the library just uses whatever it is given, rather than taking responsibility for open/close. In fact maybe that is the better option here anyway.

Mark Wardle 2025-11-11T21:58:00.480519Z

My library has an 'open' function that just takes a file and options, so hardcoding pooling there would be wrong. But hardcoding a pool at the integrant system level ie. in init method, would be absolutely fine.

Mark Wardle 2025-11-11T21:58:33.198689Z

Thanks all. Perhaps can avoid after all.

p-himik 2025-11-11T21:58:43.504989Z

Could be something like :class + :closing-fn :closable (maybe by default) for most cases. :class + :closing-fn some.fqn for the rest.

seancorfield 2025-11-11T22:16:51.540889Z

The reason next.jdbc does this is because, while Hikari's pooled datasource is Closeable, c3p0's is not and casting it to Closeable to call .close on it doesn't work (as I recall). Calling .close on it without the type hint does work, but omitting the type hint produces a reflection warning (again, as I recall) and I didn't want to be that annoying library maintainer that has reflection warnings in his project πŸ™‚ So, yeah, this machinery is kind of ugly and it's weird to have explicit Java reflection stuff going on but...

seancorfield 2025-11-11T22:17:50.209579Z

(I do recall that this was all a PITA to get working without reflection warnings)