sql

All things SQL and JDBC...
be9 2020-12-31T04:28:46.044700Z

Hi, is there a way to use seqs as sql parameters? I’m trying to

(jdbc/execute! ds ["SELECT COUNT(*) FROM transactions WHERE to_account_id IN (?)"  [1 2]])
with next.jdbc and get a weird error:
Execution error (NumberFormatException) at java.lang.NumberFormatException/forInputString (NumberFormatException.java:65).
For input string: "aced00057372001d636c6f6a7572652e6c616e672e50657273697374656e74566563746f72926bb181a556ad33020005490003636e7449000573686966744c00055f6d65746174001d4c636c6f6a7572652f6c616e672f4950657273697374656e744d61703b4c0004726f6f747400244c636c6f6a7572652f6c616e672f50657273697374656e74566563746f72244e6f64653b5b00047461696c7400135b4c6a6176612f6c616e672f4f626a6563743b7872001e636c6f6a7572652e6c616e672e4150657273697374656e74566563746f7240c68ede59abeb9b0200024900055f686173684900075f6861736865717870000000000000000000000002000000057073720022636c6f6a7572652e6c616e672e50657273697374656e74566563746f72244e6f6465893e0128132d606a0200015b0005617272617971007e00037870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000002070707070707070707070707070707070707070707070707070707070707070707571007e0008000000027372000e6a6176612e6c616e672e4c6f6e673b8be490cc8f23df0200014a000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000000000000017371007e000b0000000000000002"

seancorfield 2020-12-31T04:32:04.045600Z

@be9 a) depends on the database you're using and b) have you read the Tips & Tricks section of the next.jdbc docs?

seancorfield 2020-12-31T04:34:08.047500Z

For most databases, you need to unroll the collection and have a ? for every element:

(let [coll [1 2]]
  (jdbc/execute! ds (into [(str "SELECT COUNT(*) FROM transaction WHERE to_account IN {" (str/join "," (repeat (count coll) "?")) ")"] coll)))
(where str/join is from clojure.string)

seancorfield 2020-12-31T04:34:48.048400Z

Another option is to use HoneySQL to build your SQL -- it knows how to deal with IN and collections (and does essentially the above code to generate the SQL string).

seancorfield 2020-12-31T04:36:35.048900Z

user=> (require '[honeysql.core :as sql])
nil
user=> (sql/format {:select [(sql/call :count :*)] :from [:transactions] :where [:in :to_account [1 2]]})
["SELECT count(*) FROM transactions WHERE (to_account in (?, ?))" 1 2]
user=>

be9 2020-12-31T04:40:37.049600Z

@seancorfield I use H2

be9 2020-12-31T04:42:05.050100Z

found this ANY(?) trick for postgres, but I will unroll, thx

seancorfield 2020-12-31T04:44:30.050600Z

Yeah, I'm not sure if ANY(?) works with any other database than PG.

emccue 2020-12-31T14:56:45.051100Z

What happens if you "nest" with-transaction?

emccue 2020-12-31T14:57:31.051800Z

so if I have a function that does a "find or create" and so I know that part needs to be in a transaction

emccue 2020-12-31T14:58:04.052400Z

but the outside scope might be doing a transaction of its own and I don't know

emccue 2020-12-31T14:59:33.052700Z

(defn find-or-create! [db participating-page-ids]
  (jdbc/with-transaction [transaction db]
    (or (find-by-participants transaction participating-page-ids)
        (create! transaction participating-page-ids))))

emccue 2020-12-31T15:00:42.054100Z

(jdbc/with-transaction [transaction db]
  (find-or-create! transaction [1 2 3])
  (other-stuff! transaction))

emccue 2020-12-31T15:00:45.054300Z

basically this

emccue 2020-12-31T15:01:03.054600Z

i know that it doesn't seem to crash immediately

emccue 2020-12-31T15:01:14.054900Z

but i don't know for sure what the behavior is

borkdude 2020-12-31T17:00:16.056Z

ANN: the babashka sqlite3 pod https://github.com/babashka/pod-babashka-sqlite3 This is a self-contained pod for interacting with sqlite3. It works well together with HoneySQL.

❤️ 1
seancorfield 2020-12-31T18:05:49.056700Z

@emccue The answer is different between clojure.java.jdbc and next.jdbc

seancorfield 2020-12-31T18:08:13.058800Z

In c.j.j, nested TX are quietly ignored so only the TX in effect is the outermost one. In next.jdbc, the default is to go ahead and actually try to stand up the nested TX anyway -- assuming you "know what you're doing" -- but there is a dynamic var you can bind to either ignore nested TX (like c.j.j) or prohibit nested TX (attempts will throw an exception).

seancorfield 2020-12-31T18:10:49.059200Z

See https://cljdoc.org/d/seancorfield/next.jdbc/1.1.613/api/next.jdbc.transaction#*nested-tx* (this behavior is otherwise undocumented I think).

seancorfield 2020-12-31T18:11:53.059800Z

(well, it is mentioned briefly in the change log for version 1.1.547)

emccue 2020-12-31T20:04:20.060300Z

when you say "you know what you are doing"

emccue 2020-12-31T20:04:30.060600Z

where would I go to learn what i should be doing

emccue 2020-12-31T20:04:56.061200Z

and what are the footguns here

emccue 2020-12-31T20:05:40.062200Z

because performance aside i would guess that a nested transaction would be maybe less performant but probably do the "correct" thing with regards to function boundaries

seancorfield 2020-12-31T20:55:39.064300Z

On most databases, if you attempt to set up two TX on the same connection, you'll get start TX1 ..(a).. start TX2 ..(b).. end TX2 (which will commit both (a) and (b)) ..©.. end TX1 (will commit © if it wasn't already auto-committed after the "end" of TX2).

seancorfield 2020-12-31T20:56:20.065100Z

That's why c.j.j silently ignored the start/end of TX2 in that case (but you would only then get all of (a), (b), and (c) committed or all rolled back).

seancorfield 2020-12-31T20:59:24.068200Z

It's because TX aren't really a "thing" on their own: they are just settings on a connection. By default, connections are auto-commit (every SQL operation is committed as it is performed). Setting up a TX puts the connection into manual commit mode and then "ending" that TX commits the changes (since the last commit) or rolls back the changes (to the last commit), and also changes the connection back to auto-commit. So any SQL operations done on that connection after a TX "ends" are auto-committed again.

seancorfield 2020-12-31T21:00:19.069100Z

That doesn't matter if your TX are fairly short-lived but it starts to matter if you start a TX and then call a bunch of Clojure functions that might also want to start a TX.

seancorfield 2020-12-31T21:02:41.071600Z

Where people tend to run into this is test fixtures: it can seem like a good idea for a test fixture to run the test inside a TX and explicitly roll it back at the end. And that will seem to work in "most" cases. But if the test itself tries to do TX stuff, you'll shoot yourself in the foot (probably). Also, DDL is often non-transactional (depends on the DB) so if your test does any DDL (as opposed to regular SQL) those operations won't be rolled back anyway and you can also get in a mess.

seancorfield 2020-12-31T21:03:19.072300Z

The TL;DR is really to use save points instead of TX if you think you might want more granular commit/rollback.

seancorfield 2020-12-31T21:03:38.072700Z

And then on some DBs, you actually can nested TX 😐

seancorfield 2020-12-31T21:04:36.073600Z

(I'm not fully aware of how they do that though... none of the DBs I have worked with allow nested TX, but it was something folks complained about with c.j.j)

seancorfield 2020-12-31T21:05:00.073800Z

Does that help @emccue?

emccue 2020-12-31T21:07:02.074600Z

Yeah, so basically I probably just want to set the old default for my app

seancorfield 2020-12-31T21:08:14.075800Z

If you were using c.j.j and "just happened" to have what look like nested TX and the behavior "worked" for you, then binding the *nested-tx* to :ignore should give you the same (wrong) behavior 🙂

seancorfield 2020-12-31T21:09:57.077700Z

For test fixtures, where you run the test inside a rollback-only TX, specifying :ignore will at least ensure none of the TX inside your test will do anything -- modulo the DDL caveat above on DBs where DDL is auto-committed regardless of the connection state.

seancorfield 2020-12-31T21:11:32.079Z

(this is why I use a scratch DB for testing and don't try to run tests inside a rollback-only TX -- and it's also why I only use TX very, very occasionally, in very small blocks of code where I specifically want a narrow commit/rollback scope)