Compare commits

...

147 commits

Author SHA1 Message Date
Sean Corfield
e3b80e6128
auto-add :jdk21 for xtdb
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-24 18:13:02 -04:00
Sean Corfield
150a4aa605
only run xtdb tests on jdk21+
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-24 18:04:14 -04:00
Sean Corfield
2a30c5cffd
back to xtdb beta 7
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-24 09:17:00 -04:00
Sean Corfield
0c385c36a6
temporarily drop back to xtdb beta 6 to debug CI
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-24 09:11:47 -04:00
Sean Corfield
af827c0200
update xtdb to beta 7
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-24 08:49:56 -04:00
Sean Corfield
0ff09dca21
note #299 / #300 in changelog
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-22 20:16:33 -04:00
Sean Corfield
fd3c61b1e6
Merge pull request #300 from GAumala/false-column-clob-builder
Allow false column values in clob-column-reader
2025-04-17 23:52:02 -04:00
Gabriel Aumala
a3fba32605 Allow false column values in clob-column-reader 2025-04-17 19:41:15 -05:00
Sean Corfield
f39f6b3d24
re-enable xtdb testing using beta6
instead of nightly

Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-27 10:30:55 -07:00
Sean Corfield
99075cd408
disable xtdb in ci for now
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-25 21:01:08 -07:00
Sean Corfield
14b4c3435f
move jdk24 jvm-opts to separate
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-25 20:48:46 -07:00
Sean Corfield
a40abc285e
update dev/test/build deps
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-25 17:35:16 -07:00
Sean Corfield
16c1faeaf6
more test cleanup
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-17 17:49:46 -07:00
Sean Corfield
d1115c8c42
switch quoted tests to core lazytest dsl
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-17 17:23:12 -07:00
Sean Corfield
60503dac03
put the refers back in! oops!
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 19:09:16 -07:00
Sean Corfield
a829074b99
clean up some tests
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 18:52:51 -07:00
Sean Corfield
532d77f372
note lazytest in changelog
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 18:45:39 -07:00
Sean Corfield
e650137f07
Merge pull request #297 from seancorfield/lazytest
migrate test suite to lazytest
2025-03-12 18:02:34 -07:00
Sean Corfield
9fdc24eab7
remove prep lib; restore gitlibs cache
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-10 10:15:42 -07:00
Sean Corfield
1642cc04ca
update to lazytest 1.6.1
revert throws? back to thrown?

Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-10 10:13:33 -07:00
Sean Corfield
9d1c35b86c
fix the slf4j nudge warning
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-08 14:21:05 -08:00
Sean Corfield
ed8ba5402a
reduce a couple of gratuitous diffs
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-08 13:41:16 -08:00
Sean Corfield
403d156331
don't cache gitlibs
avoid prep lib java version issue

Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-08 12:43:22 -08:00
Sean Corfield
78894f8ba4
and remove 1.11 override
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-08 12:38:23 -08:00
Sean Corfield
0d7b3b60ed
restore clojure 1.10 testing
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-08 12:34:28 -08:00
Sean Corfield
2214e06cfa
restore ns require layout
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-07 17:43:44 -08:00
Sean Corfield
0d7d58fece
migrate test suite to lazytest
no longer tests against clojure 1.10 (issue created)

Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-07 17:31:13 -08:00
Sean Corfield
c2b64dbd54
fix snapshot version ref
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-06 13:01:15 -08:00
Sean Corfield
24085c459f
prep for 1.3.1002
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-06 12:18:19 -08:00
Sean Corfield
1062f572d5
clarify #295 changelog
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-02 11:44:05 -08:00
Sean Corfield
220957a69f
note #296 in changes
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-22 15:03:07 -08:00
Sean Corfield
c89744b05f
fixes #296 by adding sql-params validation
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-22 15:02:05 -08:00
Sean Corfield
75dab5d843
start to prep for more than 1,000 commits
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-21 21:33:37 -08:00
Sean Corfield
22e96dcb84
fixes #181 for logging
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-14 13:38:03 -08:00
Sean Corfield
9ed335dc8d
address #181 (again!)
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-13 17:41:27 -08:00
Sean Corfield
e6d1abf3ec
addresses #295
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-13 16:44:56 -08:00
Sean Corfield
946d160409
prep for 1.3.994
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-28 12:52:11 -08:00
Sean Corfield
1294e35b98
fixes #293
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-28 10:11:25 -08:00
Sean Corfield
fa6f9040d7 Merge branch 'develop' of github.com:seancorfield/next-jdbc into develop 2025-01-22 16:26:39 -08:00
Sean Corfield
3de04761cc
update java.data
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-22 16:26:36 -08:00
Sean Corfield
08c08e7f7b
Delete .joker 2025-01-20 14:36:57 -08:00
Sean Corfield
56bd2356ac
Add Zulip logo to badge 2024-12-22 10:39:12 -08:00
Sean Corfield
c6f86b042e
an xtdb kondo update
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-20 21:18:24 -08:00
Sean Corfield
a592b7ebae
note pr #292 for doc fix
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-20 12:15:51 -08:00
Sean Corfield
d71a3b6423
Merge pull request #292 from devurandom/ds/fix-3ed1f4b99c975b556327d1abb9b387db8a6d93a6
Fix documentation of next.jdbc/execute-batch!
2024-12-20 12:13:54 -08:00
Dennis Schridde
295fd5ddf5
Fix documentation of next.jdbc/execute-batch!
Searching for documentation for `next.jdbc/execute-batch!`, I tried to
execute the "equivalent to" examples of the `next.jdbc.sql/insert-multi!`
documentation, but encountered:
```
java.lang.ClassCastException: class clojure.lang.PersistentArrayMap cannot be cast to class java.sql.PreparedStatement
```

Fixes: 3ed1f4b99c
2024-12-20 20:26:10 +01:00
Sean Corfield
f9bfb1248a
Add Zulip badge 2024-12-19 20:43:22 -08:00
Sean Corfield
d5f284871b
update change log for bit/boolean test work
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-14 12:17:25 -08:00
Sean Corfield
2dc72d9254
beef up boolean tests; add xtdb into them
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-14 12:16:19 -08:00
Sean Corfield
b981357d47
prep for 1.3.981; test against xtdb nightly
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-13 22:57:40 -08:00
Sean Corfield
801e6c923b
fixes #291 by adding XTDB tips & tricks
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-11 17:36:53 -08:00
Sean Corfield
151b7d8d0b
note xtdb as supported in changelog
still need to add more notes about it elsewhere!

Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-11 09:57:51 -08:00
Sean Corfield
87f4224c22
Merge pull request #290 from seancorfield/xtdb-testing
enable xtdb testing automatically
2024-12-11 09:54:07 -08:00
Sean Corfield
4db63f6122
breaking change in beta 4: :dbname "xtdb" now required
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-10 15:54:58 -08:00
Sean Corfield
0282df7629
integrate xtdb testing
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-02 19:08:57 -08:00
Sean Corfield
80748a9593 Merge branch 'develop' into xtdb-testing 2024-12-02 19:01:43 -08:00
Sean Corfield
9196b34249
update dev/test deps
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-02 19:01:20 -08:00
Sean Corfield
b7858c937d
prep for 1.3.967
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-02 18:43:33 -08:00
Sean Corfield
d1c3291224
note assertion change
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-01 13:45:38 -08:00
Sean Corfield
585a7120a9
switch asserts to validation / exceptions
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-01 13:44:38 -08:00
Sean Corfield
f43e5490ac
clean up clj-kondo
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-24 09:39:42 -08:00
Sean Corfield
cad6462c53
clean up nses
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-24 09:31:34 -08:00
Sean Corfield
05cfe1f3fa
XTDB 0 failures!
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-24 09:27:49 -08:00
Sean Corfield
564c43bc79
78 failures, 1 errors.
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-24 00:52:05 -08:00
Sean Corfield
5f0c93642a
more xtdb test progress
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-24 00:26:41 -08:00
Sean Corfield
0c50cf28b5
wip xtdb testing
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 23:56:44 -08:00
Sean Corfield
ecd950d009
remove experimental name-fn option
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 19:43:54 -08:00
Sean Corfield
bcc6ad85dd
clean up clj-kondo imports
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 15:26:53 -08:00
Sean Corfield
224333abe6
remove auto-start/auto-connect
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 11:17:41 -08:00
Sean Corfield
beaaea8f48
addresses #288 by adding xtdb as a :dbtype
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-20 22:03:44 -08:00
Sean Corfield
b2da0156f7
#282 abstract method error? really, jTDS?
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-20 17:16:40 -08:00
Sean Corfield
f52897adea
#282 allow for unsupported wrappers in drivers
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-20 17:13:45 -08:00
Sean Corfield
b0a640a101
fixes #282 by tracking raw Connection objects for TXs.
this no longer checks TX nesting for DataSource-based TXs, but instead uses the Connection-based implementation directly.

raw Connection objects are tracked in a dynamic set.

thanks to [mbezjak](https://github.com/mbezjak) for the core of the implementation.

Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-20 16:49:39 -08:00
Sean Corfield
a75468105f
fixes #287 by merging instead of assoc'ing
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-15 18:19:32 -08:00
Sean Corfield
03117d1160
add date of last release; update copyright years
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-09 16:38:25 -08:00
Sean Corfield
218cf82637
prep for 1.3.955
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-10-06 14:51:05 -07:00
Sean Corfield
64c1f3fefd
update embedded pg, sqlite
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-09-28 20:11:51 -07:00
Sean Corfield
a9058cde18
fixes #285
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-09-28 11:51:51 -07:00
Sean Corfield
a6eb05ea30
update sql server to 2022 (from 2019)
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-09-25 14:40:38 -07:00
Sean Corfield
b2656120b4
handle "bug fix" in PG 4.7.4
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-09-25 14:29:31 -07:00
Sean Corfield
4ea3bcf4a3
update dev/test/ci deps
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-09-25 13:49:04 -07:00
Sean Corfield
a6ecdee5ac
update test deps to 1.12.0
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-09-05 22:44:36 -07:00
Sean Corfield
fb3188320d
docker-compose => docker compose
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-08-03 12:42:00 -07:00
Sean Corfield
31ad33dc85
clojure 1.11.4 & 1.12.0 rc 1
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-08-03 12:26:50 -07:00
Sean Corfield
26e642096b
Note PR #284 2024-07-25 22:37:53 -07:00
Sean Corfield
1fed35adc0
Merge pull request #284 from ExNexu/patch-1
Fix NPE in <-pgobject
2024-07-25 22:35:18 -07:00
Stefan Bleibinhaus
d6d0edb7a6
Fixes NPE in <-pgobject 2024-07-25 15:48:32 -07:00
Sean Corfield
005ec2ac83
fixes #283
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-07-23 19:06:19 -07:00
Sean Corfield
1bd4bdedce
fixes #269 by adding :name-fn option
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-06-29 13:09:13 -07:00
Sean Corfield
aa7c358cde
auto-start repl
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-06-28 11:11:43 -07:00
Sean Corfield
5c9d4795e3
clojure 1.12 alpha 12
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-23 19:26:21 -07:00
Sean Corfield
212f31d9aa
prep for 1.3.939
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-17 18:39:43 -07:00
Sean Corfield
35805f1235
fix #278 by correcting link
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-17 18:25:22 -07:00
Sean Corfield
49a22db52e
fix #279 by adding missing docs
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-17 18:20:37 -07:00
Sean Corfield
c77f7539c6
fix #280 by changing the regex used for fk resolution
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-17 18:07:07 -07:00
Sean Corfield
9e914bc5e1
split test/thinking code into src/test
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-09 12:33:10 -07:00
Sean Corfield
87ea23271e
continuing deferred thought experiment
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-09 00:44:34 -07:00
Sean Corfield
eb7dafbe58
deferred thought experiment
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-08 23:39:34 -07:00
Sean Corfield
e2f21e4845
remove confusing mention of c.j.j.-only option
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-08 12:03:47 -07:00
Sean Corfield
05553eee0e
update dev deps
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-05-06 19:49:12 -07:00
Sean Corfield
f03b1ba316 ignore new calva repl path and sort .gitignore 2024-03-24 21:16:34 -07:00
Sean Corfield
4c3e0129bf
reflect 1.11.2 is most recent stable clojure
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 18:19:49 -07:00
Sean Corfield
ed95235b7e
bump dependencies
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 18:13:23 -07:00
Sean Corfield
7b32faa7d1
fix CI badges
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 18:07:55 -07:00
Sean Corfield
3249f31062
fix mariadb env var in bb script
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 18:05:47 -07:00
Sean Corfield
39018404ab
prep for 1.3.925
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 17:57:53 -07:00
Sean Corfield
3042079138
fix #274 by adding aggregate-by-keys
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 17:54:03 -07:00
Sean Corfield
10fd00a756
fix #273 by linking to PG2
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 16:27:06 -07:00
Sean Corfield
581df5839f
fix #275 by noting the potential performance overhead of qualified column names with postgres
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-15 16:18:09 -07:00
Sean Corfield
1476c35413
update dependencies
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-03-08 09:45:36 -08:00
Sean Corfield
e21a7ab590
lint: "call" of static field
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-02-12 14:07:11 -08:00
Sean Corfield
d644ec4a95
test against clojure 1.12 alpha 7
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-02-09 17:18:50 -08:00
Sean Corfield
ee1511f097
Merge pull request #272 from harrigan/develop
Fix link
2024-01-21 09:45:41 -08:00
Martin Harrigan
0a51334dbf
Fix link 2024-01-21 15:08:17 +00:00
Sean Corfield
a5be7d00cb
linting
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-01-14 17:08:47 -08:00
Sean Corfield
44b3cc206f
fix #268 by clarifying insert-multi! docs
Signed-off-by: Sean Corfield <sean@corfield.org>
2023-12-24 12:26:46 -08:00
Sean Corfield
da2bb35ff0 fix typo 2023-12-23 12:32:11 -08:00
Sean Corfield
6a8c40c998 enable multi-version testing 2023-12-17 11:40:26 -08:00
Sean Corfield
23ae537625 ensure run-test.clj exits with non-zero on test failure 2023-12-17 11:32:27 -08:00
Sean Corfield
60d67d875e suppress logging during testing 2023-12-17 11:32:00 -08:00
Sean Corfield
902eebe536 clean up logging deps
these were only provided to override java.data's default and clean up test output a bit
2023-12-16 16:34:20 -08:00
Sean Corfield
b35d531705 prep for 1.3.909 2023-12-16 16:20:47 -08:00
Sean Corfield
81ebccc490 minor doc updates 2023-12-15 20:35:30 -08:00
Sean Corfield
bc92cc027d clarify reduce-over-plan requires init-value 2023-12-11 09:25:11 -08:00
Sean Corfield
6de1175bd8 wordsmith per @dharrigan 2023-12-08 13:23:45 -08:00
Sean Corfield
4910030ad3 fix #267 by adding :schema-opts 2023-12-06 21:32:50 -08:00
Sean Corfield
d6fe4c1577 fix #264 2023-11-10 13:26:21 -08:00
Sean Corfield
2b7f25ac78 expand find-by-keys examples 2023-10-29 10:31:18 -07:00
Sean Corfield
06bdbd8139
Merge pull request #259 from dancek/patch-1
Mention HikariCP use of :username more prominently
2023-10-09 12:34:58 -07:00
Hannu Hartikainen
c148d6ae81
Mention HikariCP use of :username more prominently
I've been using the wrong keyword in two separate projects now. Clarifying the keyword reference for my future self and maybe others too.
2023-10-09 15:10:46 +03:00
Sean Corfield
bea53cb15b minor scm tweak 2023-10-06 15:44:38 -07:00
Sean Corfield
35eb931877 update tools.build; drop template pom.xml 2023-10-06 15:05:46 -07:00
Sean Corfield
115fe1507d more doc updates about returning keys 2023-09-29 09:59:47 -07:00
Sean Corfield
d688bfaf85 correct changelog to reflect next version 2023-09-29 09:01:52 -07:00
Sean Corfield
02637c9c6c fix #258 by updating versions in getting started 2023-09-29 09:01:00 -07:00
Sean Corfield
e3ff1b7218 update changelog to match release notes
adds the database driver update note
2023-09-24 17:03:11 -07:00
Sean Corfield
d8447b66fd prep for 1.3.894 2023-09-24 16:54:04 -07:00
Sean Corfield
04588e8ef2 update tests for more recent sqlite
in particular, adding RETURNING * to insert, to get keys back
2023-09-24 16:20:49 -07:00
Sean Corfield
c6b4587408 bump CI versions; streamline JDK matrix 2023-09-24 15:57:35 -07:00
Sean Corfield
190958b74e fix #256 by documenting the new macros 2023-09-24 14:07:46 -07:00
Sean Corfield
c8cf8c7d2f fix #257 2023-09-15 15:58:18 -07:00
Sean Corfield
abd926cde3 actually change version! 2023-08-11 10:57:50 -07:00
Sean Corfield
7155ca39c3 bump tools.build to 0.9.5 2023-08-11 10:56:19 -07:00
Sean Corfield
9ea5b172bb add on-connection+options #256 2023-08-09 22:47:39 -07:00
Sean Corfield
cd214cb17e remove incorrect type hint #256 2023-08-09 22:12:38 -07:00
Sean Corfield
044de70b49 address #256 by adding with-transaction+options 2023-08-09 19:35:25 -07:00
Sean Corfield
8ad1110fcb fix test failure handling 2023-08-09 19:19:51 -07:00
73 changed files with 3136 additions and 1363 deletions

View file

@ -0,0 +1,8 @@
{:hooks
{:analyze-call
{next.jdbc/with-transaction
hooks.com.github.seancorfield.next-jdbc/with-transaction
next.jdbc/with-transaction+options
hooks.com.github.seancorfield.next-jdbc/with-transaction+options}}
:lint-as {next.jdbc/on-connection clojure.core/with-open
next.jdbc/on-connection+options clojure.core/with-open}}

View file

@ -0,0 +1,34 @@
(ns hooks.com.github.seancorfield.next-jdbc
(:require [clj-kondo.hooks-api :as api]))
(defn with-transaction
"Expands (with-transaction [tx expr opts] body)
to (let [tx expr] opts body) per clj-kondo examples."
[{:keys [:node]}]
(let [[binding-vec & body] (rest (:children node))
[sym val opts] (:children binding-vec)]
(when-not (and sym val)
(throw (ex-info "No sym and val provided" {})))
(let [new-node (api/list-node
(list*
(api/token-node 'let)
(api/vector-node [sym val])
opts
body))]
{:node new-node})))
(defn with-transaction+options
"Expands (with-transaction+options [tx expr opts] body)
to (let [tx expr] opts body) per clj-kondo examples."
[{:keys [:node]}]
(let [[binding-vec & body] (rest (:children node))
[sym val opts] (:children binding-vec)]
(when-not (and sym val)
(throw (ex-info "No sym and val provided" {})))
(let [new-node (api/list-node
(list*
(api/token-node 'let)
(api/vector-node [sym val])
opts
body))]
{:node new-node})))

View file

@ -0,0 +1,8 @@
{:linters {:xtql/redundant-pipeline {:level :warning}
:xtql/redundant-unify {:level :warning}
:xtql/unrecognized-operation {:level :error}
:xtql/unrecognized-parameter {:level :warning}
:xtql/missing-parameter {:level :error}
:xtql/type-mismatch {:level :error}
:xtql/invalid-arity {:level :error}}
:hooks {:analyze-call {xtdb.api/q hooks.xtql/q}}}

View file

@ -0,0 +1,567 @@
(ns ^:no-doc hooks.xtql
(:require [clj-kondo.hooks-api :as api]))
(def source-op?
#{'from 'rel 'unify})
(def tail-op?
#{'aggregate
'limit 'offset
'where
'order-by
'with 'without 'return
'unnest})
(def unify-clause?
#{'from 'rel
'join 'left-join
'unnest
'where
'with})
(defn node-map? [node]
(contains? #{:map :namespaced-map}
(:tag node)))
(defn node-namespaced-map? [node]
(= :namespaced-map (:tag node)))
(defn map-children [node]
(->> (if (node-namespaced-map? node)
(-> node :children first)
node)
:children
(partition-all 2)))
(defn node-vector? [node]
(= :vector (:tag node)))
(defn node-list? [node]
(= :list (:tag node)))
(defn node-symbol? [node]
(symbol? (:value node)))
(defn node-symbol [node]
(:value node))
(defn node-keyword? [node]
(keyword? (:k node)))
(defn node-keyword [node]
(:k node))
(defn node-quote? [node]
(= :quote (:tag node)))
(defn node-op [node]
(-> node :children first))
(declare lint-query)
(defmulti lint-unify-clause #(-> % node-op node-symbol))
(defmulti lint-source-op #(-> % node-op node-symbol))
(defmulti lint-tail-op #(-> % node-op node-symbol))
(defn lint-not-arg-symbol [node]
(when (= \$ (-> node node-symbol str first))
(api/reg-finding!
(assoc (meta node)
:message "unexpected parameter in binding"
:type :xtql/unrecognized-parameter))))
(defn lint-bind [node]
(cond
(node-symbol? node)
;; TODO: Make own type, should really be a warning
(lint-not-arg-symbol node)
(node-map? node)
(doseq [[k _v] (map-children node)]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "all keys in binding maps must be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta node)
:message "expected a symbol or map"
:type :xtql/type-mismatch))))
;; TODO: Lint more unify clauses
(defmethod lint-unify-clause :default [node]
(when-not (unify-clause? (-> node node-op node-symbol))
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "unrecognized unify clause"
:type :xtql/unrecognized-operation))))
(defmethod lint-unify-clause 'from [node]
(lint-source-op node))
(defmethod lint-unify-clause 'rel [node]
(lint-source-op node))
(defmethod lint-unify-clause 'with [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(if (node-map? opt)
(let [ks (->> opt
map-children
(map first)
(remove node-symbol?))]
(doseq [k ks]
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be symbols in a unify"
:type :xtql/type-mismatch))))
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a map"
:type :xtql/type-mismatch))))))
(defn lint-join-clause [node]
(let [args (-> node :children rest)]
(if-not (= (count args) 2)
(api/reg-finding!
(assoc (meta node)
:message "expected at exactly two arguments"
:type :xtql/invalid-arity))
(let [[query opts] args]
(lint-query query)
(cond
(node-vector? opts)
(->> opts :children (run! lint-bind))
(node-map? opts)
(let [kvs (map-children opts)
ks (->> kvs
(map first)
(map node-keyword)
(remove nil?)
(into #{}))]
(when-not (contains? ks :bind)
(api/reg-finding!
(assoc (meta opts)
:message "Missing :bind parameter"
:type :xtql/missing-parameter)))
(doseq [[k v] kvs]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "All keys in 'opts' must be keywords"
:type :xtql/type-mismatch)))
(case (node-keyword k)
:bind (if (node-vector? v)
(->> v :children (run! lint-bind))
(api/reg-finding!
(assoc (meta v)
:message "expected :bind value to be a vector"
:type :xtql/type-mismatch)))
:args (if (node-vector? v)
;; TODO: Make args specific
(->> v :children (run! lint-bind))
(api/reg-finding!
(assoc (meta v)
:message "expected :args value to be a vector"
:type :xtql/type-mismatch)))
; else
(api/reg-finding!
(assoc (meta k)
:message "unrecognized parameter"
:type :xtql/unrecognized-parameter)))))
:else
(api/reg-finding!
(assoc (meta node)
:message "opts must be a map or vector"
:type :xtql/type-mismatch)))))))
(defmethod lint-unify-clause 'join [node]
(lint-join-clause node))
(defmethod lint-unify-clause 'inner-join [node]
(lint-join-clause node))
(defmethod lint-unify-clause 'unnest [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected at exactly one argument"
:type :xtql/invalid-arity)))
(let [opt (first opts)]
(if (node-map? opt)
(doseq [[k _v] (map-children opt)]
(when-not (node-symbol? k)
(api/reg-finding!
(assoc (meta k)
:message "expected all columns to be symbols"
:type :xtql/type-mismatch))))
(api/reg-finding!
(assoc (meta opt)
:message "expected opt to be a map"
:type :xtql/type-mismatch))))))
(defmethod lint-source-op :default [node]
(let [op (-> node node-op node-symbol)]
(if (tail-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "tail op in source position"
:type :xtql/unrecognized-operation))
(when-not (source-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "unrecognized source operation"
:type :xtql/unrecognized-operation))))))
(defmethod lint-source-op 'from [node]
(let [[_ table opts] (some-> node :children)]
(when-not (node-keyword? table)
(api/reg-finding!
(assoc (meta table)
:message "expected 'table' to be a keyword"
:type :xtql/type-mismatch)))
(case (:tag opts)
:vector (->> opts :children (run! lint-bind))
:map
(let [kvs (map-children opts)
ks (->> kvs
(map first)
(map node-keyword)
(remove nil?)
(into #{}))]
(when-not (contains? ks :bind)
(api/reg-finding!
(assoc (meta opts)
:message "Missing :bind parameter"
:type :xtql/missing-parameter)))
(doseq [[k v] kvs]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "All keys in 'opts' must be keywords"
:type :xtql/type-mismatch)))
(case (node-keyword k)
:bind (if (node-vector? v)
(->> v :children (run! lint-bind))
(api/reg-finding!
(assoc (meta opts)
:message "expected :bind value to be a vector"
:type :xtql/type-mismatch)))
;; TODO
:for-valid-time nil
;; TODO
:for-system-time nil
; else
(api/reg-finding!
(assoc (meta k)
:message "unrecognized parameter"
:type :xtql/unrecognized-parameter)))))
(api/reg-finding!
(assoc (meta opts)
:message "expected 'opts' to be either a map or vector"
:type :xtql/type-mismatch)))))
(defmethod lint-source-op 'unify [node]
(let [[_ & clauses] (some-> node :children)]
(doseq [bad-op (remove node-list? clauses)]
(api/reg-finding!
(assoc (meta bad-op)
:message "all operations in a unify must be lists"
:type :xtql/type-mismatch)))
(when (= (count clauses) 1)
(let [clause (first clauses)
clause-op (-> clause node-op node-symbol)
unify-node (some-> node :children first)]
(case clause-op
from (api/reg-finding!
(assoc (meta unify-node)
:message "redundant unify"
:type :xtql/redundant-unify))
rel (api/reg-finding!
(assoc (meta unify-node)
:message "redundant unify"
:type :xtql/redundant-unify))
;; TODO: Cover other operators
nil)))
(->> clauses
(filter node-list?)
(run! lint-unify-clause))))
(defmethod lint-source-op 'rel [node]
(let [[_ _expr binds] (some-> node :children)]
(if (node-vector? binds)
(->> binds :children (run! lint-bind))
(api/reg-finding!
(assoc (meta binds)
:message "expected rel binding to be a vector"
:type :xtql/type-mismatch)))))
;; TODO: Lint more tail ops
(defmethod lint-tail-op :default [node]
(let [op (-> node node-op node-symbol)]
(if (source-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "source op in tail position"
:type :xtql/unrecognized-operation))
(when-not (tail-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "unrecognized tail operation"
:type :xtql/unrecognized-operation))))))
(defn lint-keyword [node name]
(when-not (node-keyword? node)
(api/reg-finding!
(assoc (meta node)
:message (str "expected '" name "' to be a keyword")
:type :xtql/type-mismatch))))
(defn lint-enum [node name values]
;; TODO: Expand to more than just keywords?
;; Maybe a `node-value` function?
(when-not (contains? values (node-keyword node))
(api/reg-finding!
(assoc (meta node)
:message (str "expected '" name "' to be one of " values)
;; TODO: change to different type?
:type :xtql/type-mismatch))))
(defmethod lint-tail-op 'limit [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected exactly one argument"
:type :xtql/invalid-arity)))
(when-let [opt (first opts)]
(when-not (some-> opt :value int?)
(api/reg-finding!
(assoc (meta opt)
:message "expected limit to be an integer"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'offset [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected exactly one argument"
:type :xtql/invalid-arity)))
(when-let [opt (first opts)]
(when-not (some-> opt :value int?)
(api/reg-finding!
(assoc (meta opt)
:message "expected offset to be an integer"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'with [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(let [ks (->> opt
map-children
(map first)
(remove node-keyword?))]
(doseq [k ks]
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a symbol or map"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'return [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(let [ks (->> opt
map-children
(map first)
(remove node-keyword?))]
(doseq [k ks]
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a symbol or map"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'order-by [node]
(doseq [opt (-> node :children rest)]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(let [kvs (map-children opt)
ks (->> kvs
(map first)
(map node-keyword)
(remove nil?)
(into #{}))]
(when-not (contains? ks :val)
(api/reg-finding!
(assoc (meta opt)
:message "Missing :val parameter"
:type :xtql/missing-parameter)))
(doseq [[k v] kvs]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "All keys in 'opts' must be keywords"
:type :xtql/type-mismatch)))
(case (node-keyword k)
:val
(cond
(node-symbol? v)
(lint-not-arg-symbol v)
(node-keyword? v)
(api/reg-finding!
(assoc (meta v)
:message "expected :val value to be a symbol or an expression"
:type :xtql/type-mismatch)))
; else do nothing
:dir
(if (node-keyword? v)
(lint-enum v :dir #{:asc :desc})
(lint-keyword v ":dir value"))
:nulls
(if (node-keyword? v)
(lint-enum v :nulls #{:first :last})
(lint-keyword v ":nulls value"))
; else
(api/reg-finding!
(assoc (meta k)
:message "unrecognized parameter"
:type :xtql/unrecognized-parameter)))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a symbol or map"
:type :xtql/type-mismatch)))))
(defmethod lint-tail-op 'without [node]
(let [columns (-> node :children rest)]
(when-not (>= (count columns) 1)
;; TODO: Should be a warning really
(api/reg-finding!
(assoc (meta node)
:message "expected at least one column"
:type :xtql/invalid-arity)))
(doseq [column columns]
(when-not (node-keyword? column)
(api/reg-finding!
(assoc (meta column)
:message "expected column to be a keyword"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'aggregate [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(doseq [[k _v] (map-children opt)]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "expected opts to be a symbol or map"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'unnest [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected at exactly one argument"
:type :xtql/invalid-arity)))
(let [opt (first opts)]
(if (node-map? opt)
(doseq [[k _v] (map-children opt)]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "expected all columns to be keywords"
:type :xtql/type-mismatch))))
(api/reg-finding!
(assoc (meta opt)
:message "expected opt to be a map"
:type :xtql/type-mismatch))))))
(defn lint-pipeline [node]
(let [[_ & ops] (some-> node :children)]
(doseq [bad-op (remove node-list? ops)]
(api/reg-finding!
(assoc (meta bad-op)
:message "all operations in a pipeline must be lists"
:type :xtql/type-mismatch)))
(when (= 1 (count ops))
(api/reg-finding!
(assoc (-> node :children first meta)
:message "redundant pipeline"
:type :xtql/redundant-pipeline)))
(let [first-op (first ops)]
(when (node-list? first-op)
(lint-source-op (first ops))))
(->> ops
(drop 1)
(filter node-list?)
(run! lint-tail-op))))
(defn lint-query [node]
(if (= '-> (node-symbol (-> node :children first)))
(lint-pipeline node)
(lint-source-op node)))
;; TODO: Lint other functions that take queries
(defn q [{:keys [node]}]
(let [[_ _node quoted-query] (some-> node :children)]
(when (node-quote? quoted-query)
(let [query (-> quoted-query :children first)]
(lint-query query)))))

View file

@ -0,0 +1,6 @@
{:linters {:cond-plus/empty-else {:level :error}
:cond-plus/missing-fn {:level :error}
:cond-plus/non-final-else {:level :error}
:cond-plus/sequence {:level :error}
:unresolved-symbol {:exclude [(cond-plus.core/cond+ [=> else])]}}
:hooks {:analyze-call {cond-plus.core/cond+ hooks.cond-plus-hook/cond+}}}

View file

@ -0,0 +1,65 @@
(ns hooks.cond-plus-hook
(:require [clj-kondo.hooks-api :as api]))
(defn analyze-clauses [clauses]
(reduce
(fn [found-else? clause]
;; non-sequence clause
(if (not (or (api/list-node? clause)
(api/vector-node? clause)))
(let [{:keys [row col]} (meta clause)]
(api/reg-finding!
{:message "must be sequence"
:type :cond-plus/sequence
:row row
:col col})
found-else?)
(let [[sym arrow fn-expr] (api/sexpr clause)]
(cond
;; non-final else
found-else?
(do (api/reg-finding!
(merge
{:message ":else must be in final position"
:type :cond-plus/non-final-else}
found-else?))
(reduced nil))
;; check fn-exprs
(and (or (= :> arrow)
(= '=> arrow))
(nil? fn-expr))
(let [{:keys [row col]} (meta clause)]
(api/reg-finding!
{:message "fn-expr must have third position symbol"
:type :cond-plus/missing-fn
:row row
:col col})
found-else?)
;; else handling
(or (= :else sym)
(= 'else sym))
(if found-else?
(let [{:keys [row col]} (meta clause)]
(api/reg-finding!
{:message "only one :else clause allowed"
:type :cond-plus/empty-else
:row row
:col col})
;; early exit cuz not worth analyzing the rest
(reduced nil))
(do (when-not arrow
(let [{:keys [row col]} (meta clause)]
(api/reg-finding!
{:message ":else must have a body"
:type :cond-plus/empty-else
:row row
:col col})))
;; Store row and col from existing else as we don't throw until
;; we've seen a following clause
(select-keys (meta clause) [:row :col])))))))
nil
clauses))
(defn cond+ [{:keys [node]}]
(analyze-clauses (rest (:children node)))
node)

View file

@ -0,0 +1,23 @@
{:lint-as {lazytest.core/given clojure.core/let
lazytest.core/around clojure.core/fn
lazytest.core/defdescribe clojure.core/def
;; clojure.test interface
lazytest.experimental.interfaces.clojure-test/deftest clojure.test/deftest
lazytest.experimental.interfaces.clojure-test/testing clojure.test/testing
lazytest.experimental.interfaces.clojure-test/is clojure.test/is
lazytest.experimental.interfaces.clojure-test/are clojure.test/are
;; xunit interface
lazytest.experimental.interfaces.xunit/defsuite clojure.core/def
;; Expectations v2
lazytest.extensions.expectations/defexpect clojure.core/def
lazytest.extensions.expectations/from-each clojure.core/for
lazytest.extensions.expectations/=? clojure.core/=
}
:hooks {:analyze-call {;; Expectations v2
lazytest.extensions.expectations/more-> hooks.lazytest.expectations/more->
lazytest.extensions.expectations/more-of hooks.lazytest.expectations/more-of
}}
:linters {:clojure-lsp/unused-public-var
{:exclude-when-defined-by #{lazytest.core/defdescribe
lazytest.experimental.interfaces.xunit/defsuite
lazytest.experimental.interfaces.clojure-test/deftest}}}}

View file

@ -0,0 +1,31 @@
;; Copied from https://github.com/clojure-expectations/clojure-test/blob/b90ed5b24924238b3b16b0bbaaee4c3b05a1268a
(ns hooks.lazytest.expectations
(:require [clj-kondo.hooks-api :as api]))
(defn more-> [{:keys [node]}]
(let [tail (rest (:children node))
rewritten
(api/list-node
(list*
(api/token-node 'cond->)
(api/token-node 'nil)
tail))]
{:node rewritten}))
(defn more-of [{:keys [node]}]
(let [bindings (fnext (:children node))
pairs (partition 2 (nnext (:children node)))
rewritten
(api/list-node
(list*
(api/token-node 'fn)
(api/vector-node (vector bindings))
(map (fn [[e a]]
(api/list-node
(list
(api/token-node 'lazytest.core/expect)
e
a)))
pairs)))]
{:node rewritten}))

View file

@ -0,0 +1,4 @@
{:linters
{:unresolved-symbol
{:exclude [(cljs.test/is [match? thrown-match?])
(clojure.test/is [match? thrown-match?])]}}}

View file

@ -0,0 +1,5 @@
{:lint-as
{rewrite-clj.zip/subedit-> clojure.core/->
rewrite-clj.zip/subedit->> clojure.core/->>
rewrite-clj.zip/edit-> clojure.core/->
rewrite-clj.zip/edit->> clojure.core/->>}}

View file

@ -0,0 +1 @@
{:config-in-call {xtdb.api/template {:ignore [:unresolved-symbol :unresolved-namespace]}}}

View file

@ -9,19 +9,19 @@ jobs:
build-and-release: build-and-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-java@v3 - uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '11' java-version: '21'
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.11.1.1347' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository
@ -30,11 +30,11 @@ jobs:
~/.cpcache ~/.cpcache
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }} key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
- name: Setup Databases - name: Setup Databases
run: docker-compose up -d run: docker compose up -d
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
- name: Run MariaDB Tests - name: Run MariaDB Tests
run: clojure -X:test run: clojure -M:test:runner
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
@ -44,6 +44,7 @@ jobs:
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
NEXT_JDBC_TEST_XTDB: yes
NEXT_JDBC_TEST_MSSQL: yes NEXT_JDBC_TEST_MSSQL: yes
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
- name: Deploy Release - name: Deploy Release

View file

@ -9,17 +9,17 @@ jobs:
build-and-snapshot: build-and-snapshot:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-java@v3 - uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '11' java-version: '21'
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.11.1.1347' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository
@ -28,11 +28,11 @@ jobs:
~/.cpcache ~/.cpcache
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }} key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
- name: Setup Databases - name: Setup Databases
run: docker-compose up -d run: docker compose up -d
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
- name: Run MariaDB Tests - name: Run MariaDB Tests
run: clojure -X:test run: clojure -M:test:runner
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
@ -42,6 +42,7 @@ jobs:
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
NEXT_JDBC_TEST_XTDB: yes
NEXT_JDBC_TEST_MSSQL: yes NEXT_JDBC_TEST_MSSQL: yes
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
- name: Deploy Snapshot - name: Deploy Snapshot
@ -54,54 +55,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: [ '11', '17' ] java: [ '11', '17', '21' ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-java@v3 - uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.11.1.1347' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository
~/.gitlibs
~/.clojure ~/.clojure
~/.cpcache ~/.cpcache
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }} key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
- name: Run Tests - name: Run Tests
run: clojure -T:build test run: clojure -T:build:jdk${{ matrix.java }} test
build-graalvm-new:
runs-on: ubuntu-latest
strategy:
matrix:
graalvm: [ '21.1.0' ]
base: [ 'java11', 'java16' ]
steps:
- uses: actions/checkout@v3
- name: Setup GraalVM
uses: DeLaGuardo/setup-graalvm@5.0
with:
graalvm: ${{ matrix.graalvm }}
java: ${{ matrix.base }}
- name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master
with:
cli: '1.11.1.1347'
- name: Cache All The Things
uses: actions/cache@v3
with:
path: |
~/.m2/repository
~/.gitlibs
~/.clojure
~/.cpcache
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
- name: Run Tests
run: clojure -T:build test

View file

@ -7,19 +7,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: [ '11', '16', '17', '18' ] java: [ '11', '17', '21' ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-java@v3 - uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.11.1.1347' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository
@ -28,48 +28,20 @@ jobs:
~/.cpcache ~/.cpcache
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }} key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
- name: Setup Databases - name: Setup Databases
run: docker-compose up -d run: docker compose up -d
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
- name: Run MariaDB Tests - name: Run MariaDB Tests
run: clojure -X:test run: clojure -M:test:runner
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
NEXT_JDBC_TEST_MARIADB: yes NEXT_JDBC_TEST_MARIADB: yes
- name: Run All Tests - name: Run All Tests
run: clojure -X:test run: clojure -M:test:runner:jdk${{ matrix.java }}
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
NEXT_JDBC_TEST_XTDB: yes
NEXT_JDBC_TEST_MSSQL: yes NEXT_JDBC_TEST_MSSQL: yes
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
build-graalvm-new:
runs-on: ubuntu-latest
strategy:
matrix:
graalvm: [ '21.1.0' ]
base: [ 'java11', 'java16' ]
steps:
- uses: actions/checkout@v3
- name: Setup GraalVM
uses: DeLaGuardo/setup-graalvm@5.0
with:
graalvm: ${{ matrix.graalvm }}
java: ${{ matrix.base }}
- name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master
with:
cli: '1.11.1.1347'
- name: Cache All The Things
uses: actions/cache@v3
with:
path: |
~/.m2/repository
~/.gitlibs
~/.clojure
~/.cpcache
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
- name: Run Tests
run: clojure -T:build test

3
.gitignore vendored
View file

@ -1,7 +1,8 @@
.calva/output-window/ .calva/output-window/
.calva/repl.calva-repl
.classpath .classpath
.clj-kondo/.cache .clj-kondo/.cache
.clj-kondo/com.github.seancorfield/next.jdbc .clj-kondo/.lock
.cpcache .cpcache
.eastwood .eastwood
.factorypath .factorypath

View file

@ -1,3 +0,0 @@
FROM gitpod/workspace-full
RUN brew install clojure/tools/clojure@1.10.3.933

View file

@ -1,23 +0,0 @@
image:
file: .gitpod.dockerfile
vscode:
extensions:
- betterthantomorrow.calva
- mauricioszabo.clover
tasks:
- name: Prepare deps/clover
init: |
clojure -A:test -P
echo 50505 > .socket-repl-port
mkdir ~/.config/clover
cp .clover/config.cljs ~/.config/clover/
- name: Start REPL
command: clojure -J-Dclojure.server.repl="{:address \"0.0.0.0\" :port 50505 :accept clojure.core.server/repl}" -A:test
- name: See Changes
command: code CHANGELOG.md
github:
prebuilds:
develop: true

6
.joker
View file

@ -1,6 +0,0 @@
{:known-macros [next.jdbc/with-transaction]
:ignored-unused-namespaces [next.jdbc.connection
next.jdbc.date-time
next.jdbc.prepare
next.jdbc.result-set
next.jdbc.transaction]}

18
.vscode/settings.json vendored
View file

@ -1,18 +0,0 @@
{
"calva.replConnectSequences": [
{
"name": "next.jdbc (Jack-In)",
"projectType": "deps.edn",
"autoSelectForJackIn": true,
"menuSelections": {
"cljAliases": ["1.12", "dev/repl", "portal", "test"]
}
},
{
"name": "next.jdbc (Connect)",
"projectType": "deps.edn",
"autoSelectForConnect": true
}
],
"calva.autoConnectRepl": true
}

View file

@ -2,6 +2,70 @@
Only accretive/fixative changes will be made from now on. Only accretive/fixative changes will be made from now on.
* 1.3.next in progress
* Fix handling of `false` in `clob-column-reader` [#299](https://github.com/seancorfield/next-jdbc/issues/299) via PR [#300](https://github.com/seancorfield/next-jdbc/pull/300) from [@GAumala](https://github.com/GAumala)
* Switch tests to LazyTest via PR [#297](https://github.com/seancorfield/next-jdbc/pull/297).
* Update dev/test/build deps.
* 1.3.1002 -- 2025-03-06
* Address [#296](https://github.com/seancorfield/next-jdbc/issues/296) by adding an explicit check (and `throw`) for `sql-params` in `next.jdbc` functions.
* Address [#295](https://github.com/seancorfield/next-jdbc/issues/295) by providing a way to tell `next.jdbc` that certain options should be passed "as-is" in the `Properties` object when creating a `Connection` -- `:next.jdbc/as-is-properties` accepts a sequence (or set) of keywords, identifying properties that should not be converted to strings.
* Fix [#181](https://github.com/seancorfield/next-jdbc/issues/181) (again!) by adding `Wrapped` protocol as a way for `DefaultOptions` and `SQLLogging` to consistently expose the underlying connectable, even when nested.
* 1.3.994 -- 2025-01-28
* Fix [#293](https://github.com/seancorfield/next-jdbc/issues/293) by no longer `locking` on the `Connection` retrieved from a `DataSource`.
* Fix documentation examples of `execute-batch!` via PR [#292](https://github.com/seancorfield/next-jdbc/pull/292) from [@devurandom](https://github.com/devurandom).
* Update `java.data` to 1.3.113.
* Beef up bit/boolean tests and enable them for XTDB.
* 1.3.981 -- 2024-12-13
* Address [#291](https://github.com/seancorfield/next-jdbc/issues/291) by adding an XTDB section to **Tips & Tricks**.
* Added XTDB as a supported database for testing via PR [#290](https://github.com/seancorfield/next-jdbc/pull/290). _Note: not all features are tested against XTDB due to several fundamental differences in architecture, mostly around primary key/generated keys and lack of DDL operations (since XTDB is schemaless)._
* Update dev/test dependencies.
* 1.3.967 -- 2024-12-02
* Address [#288](https://github.com/seancorfield/next-jdbc/issues/288) by adding speculative support for `:dbtype "xtdb"`.
* Fix [#287](https://github.com/seancorfield/next-jdbc/issues/287) by merging user-supplied options over `:return-keys true`.
* Fix [#282](https://github.com/seancorfield/next-jdbc/issues/282) by tracking raw `Connection` objects for active TXs, which relaxes several of the conditions around nested transactions.
* Replace `assert` calls with proper validation, throwing `IllegalArgumentException` on failure.
* Removed (experimental) `:name-fn` option since the driver for it no longer exists (qualified columns names in XTDB).
* 1.3.955 -- 2024-10-06
* Address [#285](https://github.com/seancorfield/next-jdbc/issues/285) by setting the default Clojure version to the earliest supported (1.10.3) to give a better hint to users.
* Update PostgreSQL **Tips & Tricks** example code to fix possible NPE. PR [#284](https://github.com/seancorfield/next-jdbc/pull/284) from [@ExNexu](https://github.com/ExNexu).
* Address [#283](https://github.com/seancorfield/next-jdbc/issues/283) by adding a note in the documentation, linking to the PostgreSQL bug report about `ANY(array)`.
* ~Address [#269](https://github.com/seancorfield/next-jdbc/issues/269) by adding `:name-fn` as an option (primarily for the SQL builder functions, but also for result set processing); the default is `clojure.core/name` but you can now use `next.jdbc.sql.builder/qualified-name` to preserve the qualifier.~ _[This was removed in 1.3.967 since XTDB no longer supports qualified column names]_
* Update testing deps; `docker-compose` => `docker compose`.
* 1.3.939 -- 2024-05-17
* Fix [#280](https://github.com/seancorfield/next-jdbc/issues/280) by allowing `-` as well as `_` in `nav` foreign key names.
* Address [#279](https://github.com/seancorfield/next-jdbc/issues/279) by adding the missing documentation.
* Address [#278](https://github.com/seancorfield/next-jdbc/issues/278) by fixing link in options page.
* Update dev dependencies, including testing against Clojure 1.12 Alpha 11.
* 1.3.925 -- 2024-03-15
* Address [#275](https://github.com/seancorfield/next-jdbc/issues/275) by noting that PostgreSQL may perform additional SQL queries to produce table names used in qualified result set builders.
* Address [#274](https://github.com/seancorfield/next-jdbc/issues/274) by adding `next.jdbc.sql/aggregate-by-keys` as a convenient wrapper around `find-by-keys` when you want just a single aggregate value back (such as `count`, `max`, etc).
* Address [#273](https://github.com/seancorfield/next-jdbc/issues/273) by linking to [PG2](https://github.com/igrishaev/pg2) in the PostgreSQL **Tips & Tricks** section.
* Address [#268](https://github.com/seancorfield/next-jdbc/issues/268) by expanding the documentation around `insert-multi!` and `insert!`.
* Update dependency versions (including Clojure).
* Code cleanup per `clj-kondo`.
* 1.3.909 -- 2023-12-16
* Address [#267](https://github.com/seancorfield/next-jdbc/issues/267) by adding the `:schema-opts` option to override the default conventions for identifying foreign keys in columns.
* Address [#264](https://github.com/seancorfield/next-jdbc/issues/264) by letting `insert-multi!` accept empty rows (and producing an empty result vector). This improves compatibility with `clojure.java.jdbc`.
* Address [#258](https://github.com/seancorfield/next-jdbc/issues/258) by updating all the library (driver) versions in Getting Started to match the latest versions being tested (from `deps.edn`).
* Update `java.data` to 1.1.103 so that `next.jdbc` no longer has a transitive dependency on `org.clojure/tools.logging`!
* Attempt to clarify that when calling `reduce` on the result of `plan`, you must provide an initial value.
* Expand examples for calling `next.jdbc.sql/find-by-keys` to show `LIKE` and `IN` clauses.
* Update `tools.build` to 0.9.6 (and get rid of `template/pom.xml` in favor of new `:pom-data` option to `b/write-pom`).
* 1.3.894 -- 2023-09-24
* Fix [#257](https://github.com/seancorfield/next-jdbc/issues/257) by making the `fdef` spec for `with-transaction` more permissive. Also add specs for `on-connection` and the `+options` variants of both macros.
* Address [#256](https://github.com/seancorfield/next-jdbc/issues/256) by adding `with-transaction+options` and `on-connection+options`.
* Updates most of the JDBC drivers used for testing, including SQLite 3.43.0.0 which now throws an exception when `.getGeneratedKeys()` is called so you cannot use `:return-generated-keys true` with it but you can add `RETURNING *` to your SQL statements instead (the tests have been updated to reflect this).
* Update `tools.build` to 0.9.5 (and remove `:java-opts` from `build/test`)
* 1.3.883 -- 2023-06-25 * 1.3.883 -- 2023-06-25
* Address [#254](https://github.com/seancorfield/next-jdbc/issues/254) by adding `next.jdbc/active-tx?` and adding more explanation to [**Transactions**](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/transactions) about the conventions behind transactions and the limitations of thread-local tracking of active transactions in `next.jdbc`. * Address [#254](https://github.com/seancorfield/next-jdbc/issues/254) by adding `next.jdbc/active-tx?` and adding more explanation to [**Transactions**](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/transactions) about the conventions behind transactions and the limitations of thread-local tracking of active transactions in `next.jdbc`.
* Address [#251](https://github.com/seancorfield/next-jdbc/issues/251) by updating `next.jdbc/with-logging` docstring. * Address [#251](https://github.com/seancorfield/next-jdbc/issues/251) by updating `next.jdbc/with-logging` docstring.

View file

@ -1,4 +1,4 @@
# next.jdbc [![Clojure CI](https://github.com/seancorfield/next-jdbc/actions/workflows/test.yml/badge.svg)](https://github.com/seancorfield/next-jdbc/actions/workflows/test.yml) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/seancorfield/next-jdbc) # next.jdbc [![Clojure CI Release](https://github.com/seancorfield/next-jdbc/actions/workflows/test-and-release.yml/badge.svg)](https://github.com/seancorfield/next-jdbc/actions/workflows/test-and-release.yml) [![Clojure CI Develop](https://github.com/seancorfield/next-jdbc/actions/workflows/test-and-snapshot.yml/badge.svg)](https://github.com/seancorfield/next-jdbc/actions/workflows/test-and-snapshot.yml) [![Clojure CI Pull Request](https://github.com/seancorfield/next-jdbc/actions/workflows/test.yml/badge.svg)](https://github.com/seancorfield/next-jdbc/actions/workflows/test.yml)
The next generation of `clojure.java.jdbc`: a new low-level Clojure wrapper for JDBC-based access to databases. The next generation of `clojure.java.jdbc`: a new low-level Clojure wrapper for JDBC-based access to databases.
@ -8,10 +8,11 @@ The next generation of `clojure.java.jdbc`: a new low-level Clojure wrapper for
The latest versions on Clojars and on cljdoc: The latest versions on Clojars and on cljdoc:
[![Clojars](https://img.shields.io/badge/clojars-com.github.seancorfield/next.jdbc_1.3.883-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABjFBMVEUAAAAdCh0qDikdChwAAAAnDSY0EjM2FjUnDiYnDSYnDSYpDigyEDEEAQRGNUb///////8mDSYAAAAAAAAAAAAFAgUqEyoAAAAAAAAAAAAFAgUAAABXU1c2FjVMx+dQx+f///////9Nx+b////4/f6y4vRPt+RQtOT///9Qt+P///8oDSey4vRQr9/////3/P5hzelNx+dNx+dNx+f///8AAAAuDy0zETIAAAAoDScAAAAAAAARBREAAAAvDy40ETMwEC9gSF+Ne42ilKKuoK6Rg5B5ZXlaP1o4Gzf///9nTWZ4YncyEDF/bn/8/Pz9/P339/c1FTUlDCRRM1AbCRtlS2QyEDEuDy1gRWAxEDAzETIwEC/g4OAvDy40EjOaiZorDiq9sbzNyM3UzdQyEDE0ETMzETKflZ/UzdQ5Fzmu4fNYyuhNx+dPt+RLu9xQyOhBbo81GTuW2vCo4PJNx+c4MFE5N1lHiLFEhKQyEDGDboMzETI5Fjh5bXje2d57aHrIw8jc2NyWhJUrDioxe9o4AAAAPnRSTlMAkf+IAQj9+e7n6e31RtqAD/QAAAED+A0ZEQ8DwvkLBsmcR4aG8+cdAD6C8/MC94eP+qoTrgH+/wj1HA8eEvpXOCUAAAABYktHRA8YugDZAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wcHFjou4Z/shwAAAUpJREFUOMul0/VTwzAUB/AAwyW4y3B3h8EDNuTh7u6UDHcd8I+TbHSjWdrjju/1h77kc+3Lu5aQvyakF/r6B5wu1+DQMEBomLRtG0EpozYDCEccA4iIjIqOiY0bB5iYxHgZ4FQCpYneKmmal0aQPMOXZnUAvJhLkbpInf8NFtKCTrGImK6DJcTlDGl/BXGV6oCsrSNIYAM3aQDwl2xJYBtBB5lZAuyYgWzY3YMcNcjN2wc4EGMEFTg8+hlyfgEenygAj71Q9FBExH0wKC4p1bRTJlJWXqEAVNM05ovbXfkPAHBmAUQPAGaAsXMBLiwA8z3h0gRcsWsObuAWLJu8Awb3ZoB5T8EvS/CgBo9Y5Z8TPwXBJwlUI9Ia/yRrEZ8lID71Olrf0MiamkkL4kurDEjba+C/e2sninR0wrsH8eMTvrqIWbodjh7jyjdtCY3Aniz4jwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wNy0wN1QyMjo1ODo0NiswMjowMCgWtSoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDctMDdUMjI6NTg6NDYrMDI6MDBZSw2WAAAAAElFTkSuQmCC)](https://clojars.org/com.github.seancorfield/next.jdbc) [![Clojars](https://img.shields.io/badge/clojars-com.github.seancorfield/next.jdbc_1.3.1002-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABjFBMVEUAAAAdCh0qDikdChwAAAAnDSY0EjM2FjUnDiYnDSYnDSYpDigyEDEEAQRGNUb///////8mDSYAAAAAAAAAAAAFAgUqEyoAAAAAAAAAAAAFAgUAAABXU1c2FjVMx+dQx+f///////9Nx+b////4/f6y4vRPt+RQtOT///9Qt+P///8oDSey4vRQr9/////3/P5hzelNx+dNx+dNx+f///8AAAAuDy0zETIAAAAoDScAAAAAAAARBREAAAAvDy40ETMwEC9gSF+Ne42ilKKuoK6Rg5B5ZXlaP1o4Gzf///9nTWZ4YncyEDF/bn/8/Pz9/P339/c1FTUlDCRRM1AbCRtlS2QyEDEuDy1gRWAxEDAzETIwEC/g4OAvDy40EjOaiZorDiq9sbzNyM3UzdQyEDE0ETMzETKflZ/UzdQ5Fzmu4fNYyuhNx+dPt+RLu9xQyOhBbo81GTuW2vCo4PJNx+c4MFE5N1lHiLFEhKQyEDGDboMzETI5Fjh5bXje2d57aHrIw8jc2NyWhJUrDioxe9o4AAAAPnRSTlMAkf+IAQj9+e7n6e31RtqAD/QAAAED+A0ZEQ8DwvkLBsmcR4aG8+cdAD6C8/MC94eP+qoTrgH+/wj1HA8eEvpXOCUAAAABYktHRA8YugDZAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wcHFjou4Z/shwAAAUpJREFUOMul0/VTwzAUB/AAwyW4y3B3h8EDNuTh7u6UDHcd8I+TbHSjWdrjju/1h77kc+3Lu5aQvyakF/r6B5wu1+DQMEBomLRtG0EpozYDCEccA4iIjIqOiY0bB5iYxHgZ4FQCpYneKmmal0aQPMOXZnUAvJhLkbpInf8NFtKCTrGImK6DJcTlDGl/BXGV6oCsrSNIYAM3aQDwl2xJYBtBB5lZAuyYgWzY3YMcNcjN2wc4EGMEFTg8+hlyfgEenygAj71Q9FBExH0wKC4p1bRTJlJWXqEAVNM05ovbXfkPAHBmAUQPAGaAsXMBLiwA8z3h0gRcsWsObuAWLJu8Awb3ZoB5T8EvS/CgBo9Y5Z8TPwXBJwlUI9Ia/yRrEZ8lID71Olrf0MiamkkL4kurDEjba+C/e2sninR0wrsH8eMTvrqIWbodjh7jyjdtCY3Aniz4jwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wNy0wN1QyMjo1ODo0NiswMjowMCgWtSoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDctMDdUMjI6NTg6NDYrMDI6MDBZSw2WAAAAAElFTkSuQmCC)](https://clojars.org/com.github.seancorfield/next.jdbc)
[![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/next.jdbc?1.3.883)](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT) [![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/next.jdbc?1.3.1002)](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT)
[![Slack](https://img.shields.io/badge/slack-next.jdbc-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=sql) [![Slack](https://img.shields.io/badge/slack-next.jdbc-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=sql)
[![Join Slack](https://img.shields.io/badge/slack-join_clojurians-orange.svg?logo=slack)](http://clojurians.net) [![Join Slack](https://img.shields.io/badge/slack-join_clojurians-orange.svg?logo=slack)](http://clojurians.net)
[![Zulip](https://img.shields.io/badge/zulip-next.jdbc-orange.svg?logo=zulip)](https://clojurians.zulipchat.com/#narrow/channel/152063-sql)
The documentation on [cljdoc.org](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT) is for the current version of `next.jdbc`: The documentation on [cljdoc.org](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT) is for the current version of `next.jdbc`:
@ -20,11 +21,11 @@ The documentation on [cljdoc.org](https://cljdoc.org/d/com.github.seancorfield/n
* [Migrating from `clojure.java.jdbc`](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/migration-from-clojure-java-jdbc) * [Migrating from `clojure.java.jdbc`](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/migration-from-clojure-java-jdbc)
* Feedback via [issues](https://github.com/seancorfield/next-jdbc/issues) or in the [`#sql` channel on the Clojurians Slack](https://clojurians.slack.com/messages/C1Q164V29/) or the [`#sql` stream on the Clojurians Zulip](https://clojurians.zulipchat.com/#narrow/stream/152063-sql). * Feedback via [issues](https://github.com/seancorfield/next-jdbc/issues) or in the [`#sql` channel on the Clojurians Slack](https://clojurians.slack.com/messages/C1Q164V29/) or the [`#sql` stream on the Clojurians Zulip](https://clojurians.zulipchat.com/#narrow/stream/152063-sql).
The documentation on GitHub is for **develop** since the 1.3.883 release -- [see the CHANGELOG](https://github.com/seancorfield/next-jdbc/blob/develop/CHANGELOG.md) and then read the [corresponding updated documentation](https://github.com/seancorfield/next-jdbc/tree/develop/doc) on GitHub if you want. Older versions of `next.jdbc` were published under the `seancorfield` group ID and you can find [older seancorfield/next.jdbc documentation on cljdoc.org](https://cljdoc.org/versions/seancorfield/next.jdbc). The documentation on GitHub is for **develop** since the 1.3.1002 release -- [see the CHANGELOG](https://github.com/seancorfield/next-jdbc/blob/develop/CHANGELOG.md) and then read the [corresponding updated documentation](https://github.com/seancorfield/next-jdbc/tree/develop/doc) on GitHub if you want. Older versions of `next.jdbc` were published under the `seancorfield` group ID and you can find [older seancorfield/next.jdbc documentation on cljdoc.org](https://cljdoc.org/versions/seancorfield/next.jdbc).
This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository. This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
> Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.999-SNAPSHOT build to Clojars so the very latest version of `next.jdbc` is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/next.jdbc) or via a git dependency on the latest SHA. > Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.9999-SNAPSHOT build to Clojars so the very latest version of `next.jdbc` is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/next.jdbc) or via a git dependency on the latest SHA.
## Motivation ## Motivation
@ -49,8 +50,8 @@ The primary concepts behind `next.jdbc` are that you start by producing a `javax
From a `DataSource`, either you or `next.jdbc` can create a `java.sql.Connection` via the `get-connection` function. You can specify an options hash map to `get-connection` to modify the connection that is created: `:read-only`, `:auto-commit`. From a `DataSource`, either you or `next.jdbc` can create a `java.sql.Connection` via the `get-connection` function. You can specify an options hash map to `get-connection` to modify the connection that is created: `:read-only`, `:auto-commit`.
The primary SQL execution API in `next.jdbc` is: The primary SQL execution API in `next.jdbc` is:
* `plan` -- yields an `IReduceInit` that, when reduced, executes the SQL statement and then reduces over the `ResultSet` with as little overhead as possible. * `plan` -- yields an `IReduceInit` that, when reduced with an initial value, executes the SQL statement and then reduces over the `ResultSet` with as little overhead as possible.
* `execute!` -- executes the SQL statement and produces a vector of realized hash maps, that use qualified keywords for the column names, of the form `:<table>/<column>`. If you join across multiple tables, the qualified keywords will reflect the originating tables for each of the columns. If the SQL produces named values that do not come from an associated table, a simple, unqualified keyword will be used. The realized hash maps returned by `execute!` are `Datafiable` and thus `Navigable` (see Clojure 1.10's `datafy` and `nav` functions, and tools like [Portal](https://github.com/djblue/portal), [Reveal](https://github.com/vlaaad/reveal), and Cognitect's REBL). Alternatively, you can specify `{:builder-fn rs/as-arrays}` and produce a vector with column names followed by vectors of row values. `rs/as-maps` is the default for `:builder-fn` but there are also `rs/as-unqualified-maps` and `rs/as-unqualified-arrays` if you want unqualified `:<column>` column names (and there are also lower-case variants of all of these). * `execute!` -- executes the SQL statement and produces a vector of realized hash maps, that use qualified keywords for the column names, of the form `:<table>/<column>`. If you join across multiple tables, the qualified keywords will reflect the originating tables for each of the columns. If the SQL produces named values that do not come from an associated table, a simple, unqualified keyword will be used. The realized hash maps returned by `execute!` are `Datafiable` and thus `Navigable` (see Clojure 1.10's `datafy` and `nav` functions, and tools like [Portal](https://github.com/djblue/portal), [Reveal](https://github.com/vlaaad/reveal), and Nubank's Morse -- formerly Cognitect's REBL). Alternatively, you can specify `{:builder-fn rs/as-arrays}` and produce a vector with column names followed by vectors of row values. `rs/as-maps` is the default for `:builder-fn` but there are also `rs/as-unqualified-maps` and `rs/as-unqualified-arrays` if you want unqualified `:<column>` column names (and there are also lower-case variants of all of these).
* `execute-one!` -- executes the SQL or DDL statement and produces a single realized hash map. The realized hash map returned by `execute-one!` is `Datafiable` and thus `Navigable`. * `execute-one!` -- executes the SQL or DDL statement and produces a single realized hash map. The realized hash map returned by `execute-one!` is `Datafiable` and thus `Navigable`.
In addition, there are API functions to create `PreparedStatement`s (`prepare`) from `Connection`s, which can be passed to `plan`, `execute!`, or `execute-one!`, and to run code inside a transaction (the `transact` function and the `with-transaction` macro). In addition, there are API functions to create `PreparedStatement`s (`prepare`) from `Connection`s, which can be passed to `plan`, `execute!`, or `execute-one!`, and to run code inside a transaction (the `transact` function and the `with-transaction` macro).
@ -88,6 +89,6 @@ In addition, convenience functions -- "syntactic sugar" -- are provided to inser
## License ## License
Copyright © 2018-2021 Sean Corfield Copyright © 2018-2024 Sean Corfield
Distributed under the Eclipse Public License version 1.0. Distributed under the Eclipse Public License version 1.0.

View file

@ -5,47 +5,63 @@
clojure -T:build deploy clojure -T:build deploy
Run tests via: Run tests via:
clojure -X:test clojure -M:test:runner
For more information, run: For more information, run:
clojure -A:deps -T:build help/doc" clojure -A:deps -T:build help/doc"
(:refer-clojure :exclude [test]) (:refer-clojure :exclude [test])
(:require [clojure.tools.build.api :as b] (:require [clojure.tools.build.api :as b]
[clojure.tools.deps :as t] [deps-deploy.deps-deploy :as dd]
[deps-deploy.deps-deploy :as dd])) [clojure.string :as str]))
(def lib 'com.github.seancorfield/next.jdbc) (def lib 'com.github.seancorfield/next.jdbc)
(defn- the-version [patch] (format "1.3.%s" patch)) (defn- the-version [patch] (format "1.3.%s" patch))
(def version (the-version (b/git-count-revs nil))) (def version (the-version (b/git-count-revs nil)))
(def snapshot (the-version "999-SNAPSHOT")) (def snapshot (the-version "9999-SNAPSHOT"))
(def class-dir "target/classes") (def class-dir "target/classes")
(defn test "Run all the tests." [opts] (defn test "Run all the tests." [opts]
(doseq [alias [:1.10 :1.11 :master]] (doseq [alias [:1.10 :1.11 :1.12]]
(println "\nRunning tests for Clojure" (name alias)) (println "\nRunning tests for Clojure" (name alias))
(let [basis (b/create-basis {:aliases [:test alias]}) (let [basis (b/create-basis
combined (t/combine-aliases basis [:test alias]) {:aliases (cond-> [:test alias]
(str/starts-with? (System/getProperty "java.version") "21")
(conj :jdk21))})
cmds (b/java-command cmds (b/java-command
{:basis basis {:basis basis
:java-opts (:jvm-opts combined)
:main 'clojure.main :main 'clojure.main
:main-args ["-m" "cognitect.test-runner"]}) :main-args ["-m" "lazytest.main"]})
{:keys [exit]} (b/process cmds)] {:keys [exit]} (b/process cmds)]
(when-not (zero? exit) (throw "Tests failed")))) (when-not (zero? exit) (throw (ex-info "Tests failed" {})))))
opts) opts)
(defn- pom-template [version]
[[:description "The next generation of clojure.java.jdbc: a new low-level Clojure wrapper for JDBC-based access to databases."]
[:url "https://github.com/seancorfield/next-jdbc"]
[:licenses
[:license
[:name "Eclipse Public License"]
[:url "http://www.eclipse.org/legal/epl-v10.html"]]]
[:developers
[:developer
[:name "Sean Corfield"]]]
[:scm
[:url "https://github.com/seancorfield/next-jdbc"]
[:connection "scm:git:https://github.com/seancorfield/next-jdbc.git"]
[:developerConnection "scm:git:ssh:git@github.com:seancorfield/next-jdbc.git"]
[:tag (str "v" version)]]])
(defn- jar-opts [opts] (defn- jar-opts [opts]
(let [version (if (:snapshot opts) snapshot version)] (let [version (if (:snapshot opts) snapshot version)]
(assoc opts (assoc opts
:lib lib :version version :lib lib :version version
:jar-file (format "target/%s-%s.jar" lib version) :jar-file (format "target/%s-%s.jar" lib version)
:scm {:tag (str "v" version)}
:basis (b/create-basis {}) :basis (b/create-basis {})
:class-dir class-dir :class-dir class-dir
:target "target" :target "target"
:src-dirs ["src"] :src-dirs ["src"]
:src-pom "template/pom.xml"))) :pom-data (pom-template version))))
(defn ci "Run the CI pipeline of tests (and build the JAR)." [opts] (defn ci "Run the CI pipeline of tests (and build the JAR)." [opts]
(test opts) (test opts)

View file

@ -1,52 +1,58 @@
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}} {:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}
"ossrh-snapshots" {:url "https://s01.oss.sonatype.org/content/repositories/snapshots"}}
:paths ["src" "resources"] :paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.11.1"} :deps {org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/java.data {:mvn/version "1.0.95"} org.clojure/java.data {:mvn/version "1.3.113"}
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}} camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}
:aliases :aliases
{;; for help: clojure -A:deps -T:build help/doc {;; for help: clojure -A:deps -T:build help/doc
:build {:deps {io.github.clojure/tools.build :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.7"}
{:git/tag "v0.9.4" :git/sha "76b78fe"} slipset/deps-deploy {:mvn/version "0.2.2"}}
slipset/deps-deploy {:mvn/version "0.2.1"}}
:ns-default build} :ns-default build}
;; versions to test against: ;; versions to test against:
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}} :1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
:1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.1"}}} :1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
:master {:override-deps {org.clojure/clojure {:mvn/version "1.12.0-master-SNAPSHOT"}}} :1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
;; running tests/checks of various kinds: ;; running tests/checks of various kinds:
:test {:extra-paths ["test"] ; can also run clojure -X:test :test {:extra-paths ["test"]
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"} :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
io.github.cognitect-labs/test-runner io.github.noahtheduke/lazytest {:mvn/version "1.6.1"}
{:git/tag "v0.5.1" :git/sha "dfb30dd"}
;; connection pooling ;; connection pooling
com.zaxxer/HikariCP {:mvn/version "5.0.1"} com.zaxxer/HikariCP {:mvn/version "6.3.0"}
com.mchange/c3p0 {:mvn/version "0.9.5.5"} com.mchange/c3p0 {:mvn/version "0.10.1"}
;; JDBC drivers ;; JDBC drivers
;; 10.16.x is JDK17+ ;; 10.16.x is JDK17+
org.apache.derby/derby {:mvn/version "10.15.2.0"} org.apache.derby/derby {:mvn/version "10.15.2.0"}
org.apache.derby/derbyshared {:mvn/version "10.15.2.0"} org.apache.derby/derbyshared {:mvn/version "10.15.2.0"}
org.hsqldb/hsqldb {:mvn/version "2.7.2"} org.hsqldb/hsqldb {:mvn/version "2.7.4"}
com.h2database/h2 {:mvn/version "2.1.214"} com.h2database/h2 {:mvn/version "2.3.232"}
net.sourceforge.jtds/jtds {:mvn/version "1.3.1"} net.sourceforge.jtds/jtds {:mvn/version "1.3.1"}
org.mariadb.jdbc/mariadb-java-client {:mvn/version "3.1.4"} org.mariadb.jdbc/mariadb-java-client {:mvn/version "3.5.2"}
com.mysql/mysql-connector-j {:mvn/version "8.0.33"} com.mysql/mysql-connector-j {:mvn/version "9.2.0"}
org.postgresql/postgresql {:mvn/version "42.6.0"} ;; 42.7.4 changes update count (to -1) for stored procs:
io.zonky.test/embedded-postgres {:mvn/version "2.0.4"} org.postgresql/postgresql {:mvn/version "42.7.5"}
io.zonky.test.postgres/embedded-postgres-binaries-darwin-amd64 {:mvn/version "15.3.0"} io.zonky.test/embedded-postgres {:mvn/version "2.1.0"}
io.zonky.test.postgres/embedded-postgres-binaries-linux-amd64 {:mvn/version "15.3.0"} io.zonky.test.postgres/embedded-postgres-binaries-darwin-amd64 {:mvn/version "17.4.0"}
io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "15.3.0"} io.zonky.test.postgres/embedded-postgres-binaries-linux-amd64 {:mvn/version "17.4.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.42.0.0"} io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "17.4.0"}
com.microsoft.sqlserver/mssql-jdbc {:mvn/version "11.2.0.jre11"} org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
;; supplementary test stuff com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.10.0.jre11"}
;; use log4j 2.x: ;; use log4j2 to reduce log noise during testing:
org.apache.logging.log4j/log4j-api {:mvn/version "2.20.0"} org.apache.logging.log4j/log4j-api {:mvn/version "2.24.3"}
;; bridge into log4j: ;; bridge everything into log4j:
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.20.0"} org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.20.0"} org.apache.logging.log4j/log4j-jcl {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.20.0"} org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.20.0"}} org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.24.3"}
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-info.properties"] org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}}
:exec-fn cognitect.test-runner.api/test}}} :jvm-opts ["-Dlog4j2.configurationFile=log4j2-info.properties"]}
:runner {:main-opts ["-m" "lazytest.main"]}
:jdk11 {}
:jdk17 {}
:jdk21 {:extra-deps {;; only need the XTDB JDBC module:
com.xtdb/xtdb-jdbc {:mvn/version "2.0.0-beta7"}}}
:jdk24 {:jvm-opts [;; for SQLite on JDK 24 locally
"--enable-native-access=ALL-UNNAMED"]}}}

View file

@ -16,7 +16,7 @@ Although `get-datasource` does not accept options, the "db spec" hash map passed
* `:port` -- an optional integer that identifies the port on which the database is running; for common database types, `next.jdbc` knows the default so this should only be needed for non-standard setups or "exotic" database types; if `:none` is specified, `next.jdbc` will omit the port segment of the JDBC URL, * `:port` -- an optional integer that identifies the port on which the database is running; for common database types, `next.jdbc` knows the default so this should only be needed for non-standard setups or "exotic" database types; if `:none` is specified, `next.jdbc` will omit the port segment of the JDBC URL,
* `:property-separator` -- an optional string that can be used to override the separators used in `next.jdbc.connection/jdbc-url` for the properties (after the initial JDBC URL portion); by default `?` and `&` are used to build JDBC URLs with properties; for SQL Server drivers (both MS and jTDS) `:property-separator ";"` is used, so this option should only be necessary when you are specifying "unusual" databases that `next.jdbc` does not already know about, * `:property-separator` -- an optional string that can be used to override the separators used in `next.jdbc.connection/jdbc-url` for the properties (after the initial JDBC URL portion); by default `?` and `&` are used to build JDBC URLs with properties; for SQL Server drivers (both MS and jTDS) `:property-separator ";"` is used, so this option should only be necessary when you are specifying "unusual" databases that `next.jdbc` does not already know about,
* `:classname` -- an optional string that identifies the name of the JDBC driver class to be used for the connection; for common database types, `next.jdbc` knows the default so this should only be needed for "exotic" database types, * `:classname` -- an optional string that identifies the name of the JDBC driver class to be used for the connection; for common database types, `next.jdbc` knows the default so this should only be needed for "exotic" database types,
* `:user` -- an optional string that identifies the database username to be used when authenticating, * `:user` -- an optional string that identifies the database username to be used when authenticating (NOTE: HikariCP needs `:username` instead see below),
* `:password` -- an optional string that identifies the database password to be used when authenticating. * `:password` -- an optional string that identifies the database password to be used when authenticating.
If you already have a JDBC URL, you can either specify that string _instead_ of a "db spec" hash map or, if you need additional properties passed to the JDBC driver, you can use a hash map containing `:jdbcUrl`, specifying the JDBC URL, and any properties you need as additional keys in the hash map. If you already have a JDBC URL, you can either specify that string _instead_ of a "db spec" hash map or, if you need additional properties passed to the JDBC driver, you can use a hash map containing `:jdbcUrl`, specifying the JDBC URL, and any properties you need as additional keys in the hash map.
@ -33,6 +33,9 @@ Any path that calls `get-connection` will accept the following options:
If you need additional options set on a connection, you can either use Java interop to set them directly, or provide them as part of the "db spec" hash map passed to `get-datasource` (although then they will apply to _all_ connections obtained from that datasource). If you need additional options set on a connection, you can either use Java interop to set them directly, or provide them as part of the "db spec" hash map passed to `get-datasource` (although then they will apply to _all_ connections obtained from that datasource).
Additional options passed are set as `java.util.Properties` and, by default, are coerced to strings.
If you are working with a driver that requires a non-string value for a property (such as the Snowflake driver), you can provide a `:next.jdbc/as-is-properties` option containing a sequence of options that should be added as-is, rather than coerced to strings.
> Note: If `plan`, `execute!`, or `execute-one!` are passed a `DataSource`, a "db spec" hash map, or a JDBC URL string, they will call `get-connection`, so they will accept the above options in those cases. > Note: If `plan`, `execute!`, or `execute-one!` are passed a `DataSource`, a "db spec" hash map, or a JDBC URL string, they will call `get-connection`, so they will accept the above options in those cases.
## Generating SQL ## Generating SQL
@ -42,7 +45,11 @@ Except for `query` (which is simply an alias for `execute!`), all the "friendly"
* `:table-fn` -- the quoting function to be used on the string that identifies the table name, if provided; this also applies to assumed table names when `nav`igating schemas, * `:table-fn` -- the quoting function to be used on the string that identifies the table name, if provided; this also applies to assumed table names when `nav`igating schemas,
* `:column-fn` -- the quoting function to be used on any string that identifies a column name, if provided; this also applies to the reducing function context over `plan` and to assumed foreign key column names when `nav`igating schemas. * `:column-fn` -- the quoting function to be used on any string that identifies a column name, if provided; this also applies to the reducing function context over `plan` and to assumed foreign key column names when `nav`igating schemas.
They also support a `:suffix` argument which can be used to specify a SQL string that should be appended to the generated SQL string before executing it, e.g., `:suffix "FOR UPDATE"`. They also support a `:suffix` argument which can be used to specify a SQL string that should be appended to the generated SQL string before executing it, e.g., `:suffix "FOR UPDATE"` or, for an `insert!` call `:suffix "RETURNING *"`.
The latter is particularly useful for databases, such as SQLite these days,
which do not support calling `.getGeneratedKeys()` on `PreparedStatement` objects,
so you cannot use `:return-generated-keys` to get back the keys -- you must
use `RETURNING *`.
In addition, `find-by-keys` accepts the following options (see its docstring for more details): In addition, `find-by-keys` accepts the following options (see its docstring for more details):
@ -54,6 +61,10 @@ In the simple case, the `:columns` option expects a vector of keywords and each
> Note: `get-by-id` accepts the same options as `find-by-keys` but it will only ever produce one row, as a hash map, so sort order and pagination are less applicable, although `:columns` may be useful. > Note: `get-by-id` accepts the same options as `find-by-keys` but it will only ever produce one row, as a hash map, so sort order and pagination are less applicable, although `:columns` may be useful.
As of 1.3.925, `aggregate-by-keys` exists as a wrapper around `find-by-keys`
that accepts the same options as `find-by-keys` except that `:columns` may not
be specified (since it is used to add the aggregate to the query).
## Generating Rows and Result Sets ## Generating Rows and Result Sets
Any function that might realize a row or a result set will accept: Any function that might realize a row or a result set will accept:
@ -61,11 +72,23 @@ Any function that might realize a row or a result set will accept:
* `:builder-fn` -- a function that implements the `RowBuilder` and `ResultSetBuilder` protocols; strictly speaking, `plan` and `execute-one!` only need `RowBuilder` to be implemented (and `plan` only needs that if it actually has to realize a row) but most generation functions will implement both for ease of use. * `:builder-fn` -- a function that implements the `RowBuilder` and `ResultSetBuilder` protocols; strictly speaking, `plan` and `execute-one!` only need `RowBuilder` to be implemented (and `plan` only needs that if it actually has to realize a row) but most generation functions will implement both for ease of use.
* `:label-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option must be present and should specify a string-to-string transformation that will be applied to the column label for each returned column name. * `:label-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option must be present and should specify a string-to-string transformation that will be applied to the column label for each returned column name.
* `:qualifier-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option should specify a string-to-string transformation that will be applied to the table name for each returned column name. It will be called with an empty string if the table name is not available. It can be omitted for the `as-unqualified-modified-*` variants. * `:qualifier-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option should specify a string-to-string transformation that will be applied to the table name for each returned column name. It will be called with an empty string if the table name is not available. It can be omitted for the `as-unqualified-modified-*` variants.
* `:column-fn` -- if present, applied to each column name before looking up the column in the `ResultSet` to get that column's value.
In addition, `execute!` accepts the `:multi-rs true` option to return multiple result sets -- as a vector of result sets. In addition, `execute!` accepts the `:multi-rs true` option to return multiple result sets -- as a vector of result sets.
> Note: Subject to the caveats above about `:builder-fn`, that means that `plan`, `execute!`, `execute-one!`, and the "friendly" SQL functions will all accept these options for generating rows and result sets. > Note: Subject to the caveats above about `:builder-fn`, that means that `plan`, `execute!`, `execute-one!`, and the "friendly" SQL functions will all accept these options for generating rows and result sets.
## Datafying & Navigating Rows and Result Sets
Any function that produces a result set will accept the following options
that modify the behavior of `datafy` and `nav` applied to the rows in that
result set:
* `:schema` -- override the conventions for identifying foreign keys and the related (primary) keys in the tables to which they refer, on a per table/column basis; can also be used to indicate a fk relationship is one-to-many or many-to-many rather than one-to-one or one-to-many,
* `:schema-opts` -- override the default conventions for identifying foreign keys and the related (primary) keys in the tables to which they refer, as a whole.
See [`datafy`, `nav`, and `:schema`](/doc/datafy-nav-and-schema.md) for more details.
## Statements & Prepared Statements ## Statements & Prepared Statements
Any function that creates a `Statement` or a `PreparedStatement` will accept the following options (see below for additional options for `PreparedStatement`): Any function that creates a `Statement` or a `PreparedStatement` will accept the following options (see below for additional options for `PreparedStatement`):
@ -87,6 +110,7 @@ Any function that creates a `PreparedStatement` will additionally accept the fol
* `:return-keys` -- a truthy value asks that the JDBC driver to return any generated keys created by the operation; it can be `true` or it can be a vector of keywords identifying column names that should be returned. * `:return-keys` -- a truthy value asks that the JDBC driver to return any generated keys created by the operation; it can be `true` or it can be a vector of keywords identifying column names that should be returned.
Not all databases or drivers support all of these options, or all values for any given option. If `:return-keys` is a vector of column names and that is not supported, `next.jdbc` will attempt a generic "return generated keys" option instead. If that is not supported, `next.jdbc` will fall back to a regular SQL operation. If other options are not supported, you may get a `SQLException`. Not all databases or drivers support all of these options, or all values for any given option. If `:return-keys` is a vector of column names and that is not supported, `next.jdbc` will attempt a generic "return generated keys" option instead. If that is not supported, `next.jdbc` will fall back to a regular SQL operation. If other options are not supported, you may get a `SQLException`.
You may need to use `RETURNING *` on `INSERT` statements instead of using `:return-keys` with some database drivers.
> Note: If `plan`, `execute!`, or `execute-one!` are passed a `DataSource`, a "db spec" hash map, or a JDBC URL string, they will call `prepare` to create a `PreparedStatement`, so they will accept the above options in those cases. > Note: If `plan`, `execute!`, or `execute-one!` are passed a `DataSource`, a "db spec" hash map, or a JDBC URL string, they will call `prepare` to create a `PreparedStatement`, so they will accept the above options in those cases.
@ -94,11 +118,11 @@ In addition to the above, `next.jdbc/execute-batch!` (which may create a `Prepar
* `:batch-size` -- an integer that determines how to partition the parameter groups for submitting to the database in batches, * `:batch-size` -- an integer that determines how to partition the parameter groups for submitting to the database in batches,
* `:large` -- a Boolean flag that indicates whether the batch will produce large update counts (`long` rather than `int` values), * `:large` -- a Boolean flag that indicates whether the batch will produce large update counts (`long` rather than `int` values),
* `:return-generated-keys` -- a Boolean flag that indicates whether `.getGeneratedKeys` should be called on the `PreparedStatement` after each batch is executed (if `true`, `execute-batch!` will return a vector of hash maps containing generated keys). * `:return-generated-keys` -- a Boolean flag that indicates whether `.getGeneratedKeys` should be called on the `PreparedStatement` after each batch is executed (if `true`, `execute-batch!` will return a vector of hash maps containing generated keys). Some databases do not support this and you need to use `RETURNING *` on `INSERT` statements instead.
## Transactions ## Transactions
The `transact` function and `with-transaction` macro accept the following options: The `transact` function and `with-transaction` (`+options`) macro accept the following options:
* `:isolation` -- a keyword that identifies the isolation to be used for this transaction: `:none`, `:read-committed`, `:read-uncommitted`, `:repeatable-read`, or `:serializable`; these represent increasingly strict levels of transaction isolation and may not all be available depending on the database and/or JDBC driver being used, * `:isolation` -- a keyword that identifies the isolation to be used for this transaction: `:none`, `:read-committed`, `:read-uncommitted`, `:repeatable-read`, or `:serializable`; these represent increasingly strict levels of transaction isolation and may not all be available depending on the database and/or JDBC driver being used,
* `:read-only` -- a `Boolean` that indicates whether the transaction should be read-only or not (the default), * `:read-only` -- a `Boolean` that indicates whether the transaction should be read-only or not (the default),

View file

@ -1,6 +1,6 @@
# `datafy`, `nav`, and the `:schema` option # `datafy`, `nav`, and the `:schema` option
Clojure 1.10 introduced a new namespace, [`clojure.datafy`](http://clojure.github.io/clojure/clojure.datafy-api.html), and two new protocols (`Datafiable` and `Navigable`) that allow for generalized, lazy navigation around data structures. Cognitect also released [REBL](http://rebl.cognitect.com/) -- a graphical, interactive tool for browsing Clojure data structures, based on the new `datafy` and `nav` functions. Clojure 1.10 introduced a new namespace, [`clojure.datafy`](http://clojure.github.io/clojure/clojure.datafy-api.html), and two new protocols (`Datafiable` and `Navigable`) that allow for generalized, lazy navigation around data structures. Cognitect also released REBL (now Nubank's [Morse](https://github.com/nubank/morse)) -- a graphical, interactive tool for browsing Clojure data structures, based on the new `datafy` and `nav` functions.
Shortly after REBL's release, I added experimental support to `clojure.java.jdbc` for `datafy` and `nav` that supported lazy navigation through result sets into foreign key relationships and connected rows and tables. `next.jdbc` bakes that support into result sets produced by `execute!` and `execute-one!`. Shortly after REBL's release, I added experimental support to `clojure.java.jdbc` for `datafy` and `nav` that supported lazy navigation through result sets into foreign key relationships and connected rows and tables. `next.jdbc` bakes that support into result sets produced by `execute!` and `execute-one!`.
@ -13,8 +13,8 @@ Additional tools that understand `datafy` and `nav` include [Portal](https://git
Here's how the process works, for result sets produced by `next.jdbc`: Here's how the process works, for result sets produced by `next.jdbc`:
* `execute!` and `execute-one!` produce result sets containing rows that are `Datafiable`, * `execute!` and `execute-one!` produce result sets containing rows that are `Datafiable`,
* Tools like Portal, Reveal, and REBL can call `datafy` on result sets to render them as "pure data" (which they already are, but this makes them also `Navigable`), * Tools like Portal, Reveal, and Morse can call `datafy` on result sets to render them as "pure data" (which they already are, but this makes them also `Navigable`),
* Tools like Portal, Reveal, and REBL allow users to "drill down" into elements of rows in the "pure data" result set, using `nav`, * Tools like Portal, Reveal, and Morse allow users to "drill down" into elements of rows in the "pure data" result set, using `nav`,
* If a column in a row represents a foreign key into another table, calling `nav` will fetch the related row(s), * If a column in a row represents a foreign key into another table, calling `nav` will fetch the related row(s),
* Those can in turn be `datafy`'d and `nav`'d to continue drilling down through connected data in the database. * Those can in turn be `datafy`'d and `nav`'d to continue drilling down through connected data in the database.
@ -26,6 +26,29 @@ By default, `next.jdbc` assumes that a column named `<something>id` or `<somethi
You can override this default behavior for any column in any table by providing a `:schema` option that is a hash map whose keys are column names (usually the table-qualified keywords that `next.jdbc` produces by default) and whose values are table-qualified keywords, optionally wrapped in vectors, that identity the name of the table to which that column is a foreign key and the name of the key column within that table. You can override this default behavior for any column in any table by providing a `:schema` option that is a hash map whose keys are column names (usually the table-qualified keywords that `next.jdbc` produces by default) and whose values are table-qualified keywords, optionally wrapped in vectors, that identity the name of the table to which that column is a foreign key and the name of the key column within that table.
As of 1.3.909, you can also override this behavior via the `:schema-opts`
option. This is a hash map whose keys can be:
* `:fk-suffix` -- a string used instead of `"id"` to identify foreign keys,
* `:pk` -- a string used instead of `"id"` for the primary key column in the target table,
* `:pk-fn` -- a function that takes the table name and the value of `:pk` and returns the name of the primary key column in the target table, instead of just using the value of `:pk` (the default is effectively `(constantly <pk>)`).
For `:fk-suffix`, the `_` is still permitted and optional in the column name,
so if you specified `:schema-opts {:fk-suffix "fk"}` then `addressfk` and
`address_fk` would both be treated as foreign keys into the `address` table.
_Note: as of 1.3.939, `-` is permitted in key names (in addition to `_`) so that kebab result set builders work as expected._
The `:pk-fn` can use the table name to determine the primary key column name
for exceptions to the `:pk` value. For example, if you have a table `address`
with a primary key column `address_id` instead of `id`, you could use:
```clojure
:pk-fn (fn [table pk]
(if (= "address" table)
"address_id"
pk))
```
The default behavior in the example above is equivalent to this `:schema` value: The default behavior in the example above is equivalent to this `:schema` value:
```clojure ```clojure
@ -35,6 +58,16 @@ The default behavior in the example above is equivalent to this `:schema` value:
{:schema {:contact/addressid :address/id}}) {:schema {:contact/addressid :address/id}})
``` ```
or these `:schema-opts` values:
```clojure
(jdbc/execute! ds
["select * from contact where city = ?" "San Francisco"]
;; a one-to-one or many-to-one relationship
{:schema-opts {:fk-suffix "id" :pk "id"
:pk-fn (constantly "id")}})
```
If you had a table to track the valid/bouncing status of email addresses over time, `:deliverability`, where `email` is the non-unique key, you could provide automatic navigation into that using: If you had a table to track the valid/bouncing status of email addresses over time, `:deliverability`, where `email` is the non-unique key, you could provide automatic navigation into that using:
```clojure ```clojure
@ -45,9 +78,14 @@ If you had a table to track the valid/bouncing status of email addresses over ti
:address/email [:deliverability/email]}}) :address/email [:deliverability/email]}})
``` ```
Since this relies on a foreign key that does not follow a standard suffix
pattern, there is no comparable `:schema-opts` version. In addition, the
`:schema-opts` approach cannot designate a one-to-many or many-to-many
relationship.
When you indicate a `*-to-many` relationship, by wrapping the foreign table/key in a vector, `next.jdbc`'s implementation of `nav` will fetch a multi-row result set from the target table. When you indicate a `*-to-many` relationship, by wrapping the foreign table/key in a vector, `next.jdbc`'s implementation of `nav` will fetch a multi-row result set from the target table.
If you use foreign key constraints in your database, you could probably generate this `:schema` data structure automatically from the metadata in your database. Similarly, if you use a library that depends on an entity relationship map (such as [seql](https://exoscale.github.io/seql/) or [walkable](https://walkable.gitlab.io/)), then you could probably generate this `:schema` data structure from that entity map. If you use foreign key constraints in your database, you could probably generate this `:schema` data structure automatically from the metadata in your database. Similarly, if you use a library that depends on an entity relationship map (such as [seql](https://github.com/exoscale/seql) or [walkable](https://walkable.gitlab.io/)), then you could probably generate this `:schema` data structure from that entity map.
### Behind The Scenes ### Behind The Scenes
@ -55,7 +93,11 @@ Making rows datafiable is implemented by adding metadata to each row with a key
When called (`datafy` on a row), it adds metadata to the row with a key of `clojure.core.protocols/nav` and another function as the value. That function also closes over the connectable and options passed in. When called (`datafy` on a row), it adds metadata to the row with a key of `clojure.core.protocols/nav` and another function as the value. That function also closes over the connectable and options passed in.
When that is called (`nav` on a row, column name, and column value), if a `:schema` entry exists for that column or it matches the default convention described above, then it will fetch row(s) using `next.jdbc`'s `Executable` functions `-execute-one` or `-execute-all`, passing in the connectable and options closed over. When that is called (`nav` on a row, column name, and column value), if a
`:schema` entry exists for that column or it matches the convention described
above (either by default or via `:schema-opts`), then it will fetch row(s)
using `next.jdbc`'s `Executable` functions `-execute-one` or `-execute-all`,
passing in the connectable and options closed over.
The protocol `next.jdbc.result-set/DatafiableRow` has a default implementation of `datafiable-row` for `clojure.lang.IObj` that just adds the metadata to support `datafy`. There is also an implementation baked into the result set handling behind `plan` so that you can call `datafiable-row` directly during reduction and get a fully-realized row that can be `datafy`'d (and then `nav`igated). The protocol `next.jdbc.result-set/DatafiableRow` has a default implementation of `datafiable-row` for `clojure.lang.IObj` that just adds the metadata to support `datafy`. There is also an implementation baked into the result set handling behind `plan` so that you can call `datafiable-row` directly during reduction and get a fully-realized row that can be `datafy`'d (and then `nav`igated).

View file

@ -25,6 +25,12 @@ These functions are described in more detail below. They are deliberately simple
If you prefer to write your SQL separately from your code, take a look at [HugSQL](https://github.com/layerware/hugsql) -- [HugSQL documentation](https://www.hugsql.org/) -- which has a `next.jdbc` adapter, as of version 0.5.1. See below for a "[quick start](#hugsql-quick-start)" for using HugSQL with `next.jdbc`. If you prefer to write your SQL separately from your code, take a look at [HugSQL](https://github.com/layerware/hugsql) -- [HugSQL documentation](https://www.hugsql.org/) -- which has a `next.jdbc` adapter, as of version 0.5.1. See below for a "[quick start](#hugsql-quick-start)" for using HugSQL with `next.jdbc`.
As of 1.3.925, `aggregate-by-keys` exists as a wrapper around `find-by-keys`
that accepts the same options as `find-by-keys` and an aggregate SQL expression
and it returns a single value (the aggregate). `aggregate-by-keys` accepts the
same options as `find-by-keys` except that `:columns` may not be specified
(since it is used to add the aggregate to the query).
## `insert!` ## `insert!`
Given a table name (as a keyword) and a hash map of column names and values, this performs a single row insertion into the database: Given a table name (as a keyword) and a hash map of column names and values, this performs a single row insertion into the database:
@ -34,11 +40,22 @@ Given a table name (as a keyword) and a hash map of column names and values, thi
;; equivalent to ;; equivalent to
(jdbc/execute-one! ds ["INSERT INTO address (name,email) VALUES (?,?)" (jdbc/execute-one! ds ["INSERT INTO address (name,email) VALUES (?,?)"
"A.Person" "albert@person.org"] {:return-keys true}) "A.Person" "albert@person.org"] {:return-keys true})
;; some databases may require this instead
(jdbc/execute-one! ds ["INSERT INTO address (name,email) VALUES (?,?) RETURNING *"
"A.Person" "albert@person.org"])
;; which you can achieve with the :suffix option
(sql/insert! ds :address {:name "A. Person" :email "albert@person.org"}
{:suffix "RETURNING *"})
``` ```
If you have multiple rows (hash maps) to insert and they all have the same
set of keys, you can use `insert-multi!` instead (see below), which will
perform a single multi-row insertion, which will generally be faster.
## `insert-multi!` ## `insert-multi!`
Given a table name (as a keyword), a vector of column names, and a vector of row value vectors, this performs a multi-row insertion into the database: Given a table name (as a keyword), a vector of column names, and a vector of
row value vectors, this performs a single multi-row insertion into the database:
```clojure ```clojure
(sql/insert-multi! ds :address (sql/insert-multi! ds :address
@ -53,7 +70,11 @@ Given a table name (as a keyword), a vector of column names, and a vector of row
"Aunt Sally" "sour@lagunitas.beer"] {:return-keys true}) "Aunt Sally" "sour@lagunitas.beer"] {:return-keys true})
``` ```
Given a table name (as a keyword) and a vector of hash maps, this performs a multi-row insertion into the database: All the row vectors must be the same length, and must match the number of
columns specified.
Given a table name (as a keyword) and a vector of hash maps, this performs a
single multi-row insertion into the database:
```clojure ```clojure
(sql/insert-multi! ds :address (sql/insert-multi! ds :address
@ -67,7 +88,15 @@ Given a table name (as a keyword) and a vector of hash maps, this performs a mul
"Aunt Sally" "sour@lagunitas.beer"] {:return-keys true}) "Aunt Sally" "sour@lagunitas.beer"] {:return-keys true})
``` ```
> Note: this expands to a single SQL statement with placeholders for every All the hash maps must have the same set of keys, so that the vector of hash
maps can be converted to a vector of columns names and a vector of row value
vectors, as above, so a single multi-row insertion can be performed.
If you wish to insert multiple hash maps that do not have identical keys, you
need to iterate over `insert!` and insert one row at a time, which will
generally be much slower.
> Note: both of these expand to a single SQL statement with placeholders for every
value being inserted -- for large sets of rows, this may exceed the limits value being inserted -- for large sets of rows, this may exceed the limits
on SQL string size and/or number of parameters for your JDBC driver or your on SQL string size and/or number of parameters for your JDBC driver or your
database. Several databases have a limit of 1,000 parameter placeholders. database. Several databases have a limit of 1,000 parameter placeholders.
@ -88,8 +117,8 @@ will use `execute-batch!` under the hood, instead of `execute!`, as follows:
{:batch true}) {:batch true})
;; equivalent to ;; equivalent to
(jdbc/execute-batch! ds (jdbc/execute-batch! ds
["INSERT INTO address (name,email) VALUES (?,?)" "INSERT INTO address (name,email) VALUES (?,?)"
["Stella" "stella@artois.beer"] [["Stella" "stella@artois.beer"]
["Waldo" "waldo@lagunitas.beer"] ["Waldo" "waldo@lagunitas.beer"]
["Aunt Sally" "sour@lagunitas.beer"]] ["Aunt Sally" "sour@lagunitas.beer"]]
{:return-keys true :return-generated-keys true}) {:return-keys true :return-generated-keys true})
@ -102,14 +131,14 @@ will use `execute-batch!` under the hood, instead of `execute!`, as follows:
{:batch true}) {:batch true})
;; equivalent to ;; equivalent to
(jdbc/execute-batch! ds (jdbc/execute-batch! ds
["INSERT INTO address (name,email) VALUES (?,?)" "INSERT INTO address (name,email) VALUES (?,?)"
["Stella" "stella@artois.beer"] [["Stella" "stella@artois.beer"]
["Waldo" "waldo@lagunitas.beer"] ["Waldo" "waldo@lagunitas.beer"]
["Aunt Sally" "sour@lagunitas.beer"]] ["Aunt Sally" "sour@lagunitas.beer"]]
{:return-keys true :return-generated-keys true}) {:return-keys true :return-generated-keys true})
``` ```
See [**Batched Parameters**](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/prepared-statements#caveats) for caveats and possible database-specific behaviors. > Note: not all databases or drivers support returning generated keys like this -- see [**Batched Parameters**](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/prepared-statements#caveats) for caveats and possible database-specific behaviors. You may need `RETURNING *` in your SQL instead.
## `query` ## `query`
@ -162,6 +191,25 @@ Given a table name (as a keyword) and either a hash map of column names and valu
"Stella" "stella@artois.beer"]) "Stella" "stella@artois.beer"])
``` ```
While the hash map approach -- "query by example" -- is great for equality
comparisons, sometimes you need other types of comparisons. For example, you
might want to find all the rows where the email address ends in `.beer`:
```clojure
(sql/find-by-keys ds :address ["email LIKE ?" "%.beer"])
;; equivalent to
(jdbc/execute! ds ["SELECT * FROM address WHERE email LIKE ?" "%.beer"])
```
Or you may want to find all the rows where the name is one of a specific
set of values:
```clojure
(sql/find-by-keys ds :address ["name IN (?,?)" "Stella" "Waldo"])
;; equivalent to
(jdbc/execute! ds ["SELECT * FROM address WHERE name IN (?,?)" "Stella" "Waldo"])
```
The default behavior is to return all the columns in each row. You can specify a subset of columns to return using the `:columns` option. It takes a vector and each element of the vector can be: The default behavior is to return all the columns in each row. You can specify a subset of columns to return using the `:columns` option. It takes a vector and each element of the vector can be:
* a simple keyword representing the column name (`:column-fn` will be applied, if provided), * a simple keyword representing the column name (`:column-fn` will be applied, if provided),
@ -205,6 +253,26 @@ If you want to match all rows in a table -- perhaps with the pagination options
If no rows match, `find-by-keys` returns `[]`, just like `execute!`. If no rows match, `find-by-keys` returns `[]`, just like `execute!`.
## `aggregate-by-keys`
Added in 1.3.925, this is a wrapper around `find-by-keys` that makes it easier
to perform aggregate queries::
```clojure
(sql/aggregate-by-keys ds :address "count(*)" {:name "Stella"
:email "stella@artois.beer"})
;; is roughly equivalent to
(-> (sql/find-by-keys ds :address {:name "Stella" :email "stella@artois.beer"}
{:columns [["count(*)" :next_jdbc_aggregate_123]]})
(first)
(get :next_jdbc_aggregate_123))
```
(where `:next_jdbc_aggregate_123` is a unique alias generated by `next.jdbc`,
derived from the aggregate expression string).
> Note: the SQL string provided for the aggregate is copied exactly as-is into the generated SQL -- you are responsible for ensuring it is legal SQL!
## `get-by-id` ## `get-by-id`
Given a table name (as a keyword) and a primary key value, with an optional primary key column name, execute a query on the database: Given a table name (as a keyword) and a primary key value, with an optional primary key column name, execute a query on the database:
@ -261,8 +329,8 @@ These quoting functions can be provided to any of the friendly SQL functions abo
Here's how to get up and running quickly with `next.jdbc` and HugSQL. For more detail, consult the [HugSQL documentation](https://www.hugsql.org/). Add the following dependencies to your project (in addition to `com.github.seancorfield/next.jdbc` and whichever JDBC drivers you need): Here's how to get up and running quickly with `next.jdbc` and HugSQL. For more detail, consult the [HugSQL documentation](https://www.hugsql.org/). Add the following dependencies to your project (in addition to `com.github.seancorfield/next.jdbc` and whichever JDBC drivers you need):
```clojure ```clojure
com.layerware/hugsql-core {:mvn/version "0.5.1"} com.layerware/hugsql-core {:mvn/version "0.5.3"}
com.layerware/hugsql-adapter-next-jdbc {:mvn/version "0.5.1"} com.layerware/hugsql-adapter-next-jdbc {:mvn/version "0.5.3"}
``` ```
_Check the HugSQL documentation for the latest versions to use!_ _Check the HugSQL documentation for the latest versions to use!_

View file

@ -6,26 +6,26 @@ It is designed to work with Clojure 1.10 or later, supports `datafy`/`nav`, and
## Installation ## Installation
**You must be using Clojure 1.10 or later.** 1.10.3 is the most recent stable version of Clojure (as of March 4th, 2021). **You must be using Clojure 1.10 or later.** 1.12.0 is the most recent stable version of Clojure (as of March 15th, 2024).
You can add `next.jdbc` to your project with either: You can add `next.jdbc` to your project with either:
```clojure ```clojure
com.github.seancorfield/next.jdbc {:mvn/version "1.3.883"} com.github.seancorfield/next.jdbc {:mvn/version "1.3.1002"}
``` ```
for `deps.edn` or: for `deps.edn` or:
```clojure ```clojure
[com.github.seancorfield/next.jdbc "1.3.883"] [com.github.seancorfield/next.jdbc "1.3.1002"]
``` ```
for `project.clj` or `build.boot`. for `project.clj` or `build.boot`.
**In addition, you will need to add dependencies for the JDBC drivers you wish to use for whatever databases you are using. For example:** **In addition, you will need to add dependencies for the JDBC drivers you wish to use for whatever databases you are using. For example:**
* MySQL: `mysql/mysql-connector-java {:mvn/version "8.0.19"}` ([search for latest version](https://search.maven.org/artifact/mysql/mysql-connector-java)) * MySQL: `com.mysql/mysql-connector-j {:mvn/version "9.1.0"}` ([search for latest version](https://search.maven.org/artifact/com.mysql/mysql-connector-j))
* PostgreSQL: `org.postgresql/postgresql {:mvn/version "42.2.10"}` ([search for latest version](https://search.maven.org/artifact/org.postgresql/postgresql)) * PostgreSQL: `org.postgresql/postgresql {:mvn/version "42.7.4"}` ([search for latest version](https://search.maven.org/artifact/org.postgresql/postgresql))
* Microsoft SQL Server: `com.microsoft.sqlserver/mssql-jdbc {:mvn/version "8.2.1.jre8"}` ([search for latest version](https://search.maven.org/artifact/com.microsoft.sqlserver/mssql-jdbc)) * Microsoft SQL Server: `com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.8.1.jre11"}` ([search for latest version](https://search.maven.org/artifact/com.microsoft.sqlserver/mssql-jdbc))
* Sqlite: `org.xerial/sqlite-jdbc {:mvn/version "3.39.2.1"}` ([search for latest version](https://search.maven.org/artifact/org.xerial/sqlite-jdbc)) * Sqlite: `org.xerial/sqlite-jdbc {:mvn/version "3.47.1.0"}` ([search for latest version](https://search.maven.org/artifact/org.xerial/sqlite-jdbc))
> Note: these are the versions that `next.jdbc` is tested against but there may be more recent versions and those should generally work too -- click the "search for latest version" link to see all available versions of those drivers on Maven Central. You can see the full list of drivers and versions that `next.jdbc` is tested against in [the project's `deps.edn` file](https://github.com/seancorfield/next-jdbc/blob/develop/deps.edn#L10-L27), but many other JDBC drivers for other databases should also work (e.g., Oracle, Red Shift). > Note: these are the versions that `next.jdbc` is tested against but there may be more recent versions and those should generally work too -- click the "search for latest version" link to see all available versions of those drivers on Maven Central. You can see the full list of drivers and versions that `next.jdbc` is tested against in [the project's `deps.edn` file](https://github.com/seancorfield/next-jdbc/blob/develop/deps.edn#L10-L27), but many other JDBC drivers for other databases should also work (e.g., Oracle, Red Shift).
@ -37,9 +37,9 @@ For the examples in this documentation, we will use a local H2 database on disk,
```clojure ```clojure
;; deps.edn ;; deps.edn
{:deps {org.clojure/clojure {:mvn/version "1.10.3"} {:deps {org.clojure/clojure {:mvn/version "1.12.0"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.883"} com.github.seancorfield/next.jdbc {:mvn/version "1.3.1002"}
com.h2database/h2 {:mvn/version "1.4.199"}}} com.h2database/h2 {:mvn/version "2.3.232"}}}
``` ```
### Create & Populate a Database ### Create & Populate a Database
@ -48,7 +48,7 @@ In this REPL session, we'll define an H2 datasource, create a database with a si
```clojure ```clojure
> clj > clj
Clojure 1.10.3 Clojure 1.12.0
user=> (require '[next.jdbc :as jdbc]) user=> (require '[next.jdbc :as jdbc])
nil nil
user=> (def db {:dbtype "h2" :dbname "example"}) user=> (def db {:dbtype "h2" :dbname "example"})
@ -77,6 +77,11 @@ We described the database with just `:dbtype` and `:dbname` because it is create
> Note: You can see the full list of `:dbtype` values supported in [next.jdbc/get-datasource](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc#get-datasource)'s docstring. If you need this programmatically, you can get it from the [next.jdbc.connection/dbtypes](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc.connection#dbtypes) hash map. If those lists differ, the hash map is the definitive list (and I'll need to fix the docstring!). The docstring of that Var explains how to tell `next.jdbc` about additional databases. > Note: You can see the full list of `:dbtype` values supported in [next.jdbc/get-datasource](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc#get-datasource)'s docstring. If you need this programmatically, you can get it from the [next.jdbc.connection/dbtypes](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc.connection#dbtypes) hash map. If those lists differ, the hash map is the definitive list (and I'll need to fix the docstring!). The docstring of that Var explains how to tell `next.jdbc` about additional databases.
The hash map can contain arbitrary keys and values: any keys not specifically
recognized by `next.jdbc` will be passed through to the JDBC driver as part
of the connection string. For example, if you specify `:useSSL false`, then
the connection string will have `&useSSL=false` appended to it.
If you already have a JDBC URL (string), you can use that as-is instead of the db-spec hash map. If you have a JDBC URL and still need additional options passed into the JDBC driver, you can use a hash map with the `:jdbcUrl` key specifying the string and whatever additional options you need. If you already have a JDBC URL (string), you can use that as-is instead of the db-spec hash map. If you have a JDBC URL and still need additional options passed into the JDBC driver, you can use a hash map with the `:jdbcUrl` key specifying the string and whatever additional options you need.
### `execute!` & `execute-one!` ### `execute!` & `execute-one!`
@ -129,6 +134,7 @@ user=>
Relying on the default result set builder -- and table-qualified column names -- is the recommended approach to take, if possible, with a few caveats: Relying on the default result set builder -- and table-qualified column names -- is the recommended approach to take, if possible, with a few caveats:
* MS SQL Server produces unqualified column names by default (see [**Tips & Tricks**](/doc/tips-and-tricks.md) for how to get table names back from MS SQL Server), * MS SQL Server produces unqualified column names by default (see [**Tips & Tricks**](/doc/tips-and-tricks.md) for how to get table names back from MS SQL Server),
* Oracle's JDBC driver doesn't support `.getTableName()` so it will only produce unqualified column names (also mentioned in **Tips & Tricks**), * Oracle's JDBC driver doesn't support `.getTableName()` so it will only produce unqualified column names (also mentioned in **Tips & Tricks**),
* PostgreSQL's JDBC driver performs an extra SQL query to get the necessary metadata, so there is some overhead to using qualified column names (also mentioned in **Tips & Tricks**),
* If your SQL query joins tables in a way that produces duplicate column names, and you use unqualified column names, then those duplicated column names will conflict and you will get only one of them in your result -- use aliases in SQL (`as`) to make the column names distinct, * If your SQL query joins tables in a way that produces duplicate column names, and you use unqualified column names, then those duplicated column names will conflict and you will get only one of them in your result -- use aliases in SQL (`as`) to make the column names distinct,
* If your SQL query joins a table to itself under different aliases, the _qualified_ column names will conflict because they are based on the underlying table name provided by the JDBC driver rather the alias you used in your query -- again, use aliases in SQL to make those column names distinct. * If your SQL query joins a table to itself under different aliases, the _qualified_ column names will conflict because they are based on the underlying table name provided by the JDBC driver rather the alias you used in your query -- again, use aliases in SQL to make those column names distinct.
@ -193,7 +199,8 @@ user=> (reduce
14.67M 14.67M
``` ```
The call to `jdbc/plan` returns an `IReduceInit` object but does not actually run the SQL. Only when the returned object is reduced is the connection obtained from the data source, the SQL executed, and the computation performed. The connection is closed automatically when the reduction is complete. The `row` in the reduction is an abstraction over the underlying (mutable) `ResultSet` object -- it is not a Clojure data structure. Because of that, you can simply access the columns via their SQL labels as shown -- you do not need to use the column-qualified name, and you do not need to worry about the database returning uppercase column names (SQL labels are not case sensitive). The call to `jdbc/plan` returns an `IReduceInit` object (a "reducible collection" that requires an initial value) but does not actually run the SQL.
Only when the returned object is reduced is the connection obtained from the data source, the SQL executed, and the computation performed. The connection is closed automatically when the reduction is complete. The `row` in the reduction is an abstraction over the underlying (mutable) `ResultSet` object -- it is not a Clojure data structure. Because of that, you can simply access the columns via their SQL labels as shown -- you do not need to use the column-qualified name, and you do not need to worry about the database returning uppercase column names (SQL labels are not case sensitive).
> Note: if you want a column name transformation to be applied here, specify `:column-fn` as an option to the `plan` call. > Note: if you want a column name transformation to be applied here, specify `:column-fn` as an option to the `plan` call.
@ -306,6 +313,8 @@ As of 1.1.588, two helper functions are available to make some `plan` operations
* `next.jdbc.plan/select-one!` -- reduces over `plan` and returns part of just the first row, * `next.jdbc.plan/select-one!` -- reduces over `plan` and returns part of just the first row,
* `next.jdbc.plan/select!` -- reduces over `plan` and returns a sequence of parts of each row. * `next.jdbc.plan/select!` -- reduces over `plan` and returns a sequence of parts of each row.
> Note: in both those cases, an appropriate initial value is supplied to the `reduce` (since `plan` returns an `IReduceInit` object).
`select!` accepts a vector of column names to extract or a function to apply to each row. It is equivalent to the following: `select!` accepts a vector of column names to extract or a function to apply to each row. It is equivalent to the following:
```clojure ```clojure
@ -445,7 +454,7 @@ You can read more about [working with transactions](/doc/transactions.md) furthe
(jdbc/execute! con-opts ...) ; auto-committed (jdbc/execute! con-opts ...) ; auto-committed
(jdbc/with-transaction [tx con-opts] ; will commit or rollback this group: (jdbc/with-transaction [tx con-opts] ; will commit or rollback this group:
(let [tx-opts (jdbc/with-options tx (:options con-opts)] (let [tx-opts (jdbc/with-options tx (:options con-opts))]
(jdbc/execute! tx-opts ...) (jdbc/execute! tx-opts ...)
(jdbc/execute! tx-opts ...) (jdbc/execute! tx-opts ...)
(into [] (map :column) (jdbc/plan tx-opts ...)))) (into [] (map :column) (jdbc/plan tx-opts ...))))
@ -453,6 +462,11 @@ You can read more about [working with transactions](/doc/transactions.md) furthe
(jdbc/execute! con-opts ...))) ; auto-committed (jdbc/execute! con-opts ...))) ; auto-committed
``` ```
As of 1.3.894, you can use `next.jdbc/with-transaction+options` instead,
which will automatically rewrap the `Connection` with the options from the
initial transactable. Be aware that means you cannot use Java interop on the
new connectable because it is no longer a plain Java `java.sql.Connection` object.
### Prepared Statement Caveat ### Prepared Statement Caveat
Not all databases support using a `PreparedStatement` for every type of SQL operation. You might have to create a `java.sql.Statement` instead, directly from a `java.sql.Connection` and use that, without parameters, in `plan`, `execute!`, or `execute-one!`. See the following example: Not all databases support using a `PreparedStatement` for every type of SQL operation. You might have to create a `java.sql.Statement` instead, directly from a `java.sql.Connection` and use that, without parameters, in `plan`, `execute!`, or `execute-one!`. See the following example:
@ -473,9 +487,9 @@ Not all databases support using a `PreparedStatement` for every type of SQL oper
First, you need to add the connection pooling library as a dependency, e.g., First, you need to add the connection pooling library as a dependency, e.g.,
```clojure ```clojure
com.zaxxer/HikariCP {:mvn/version "3.3.1"} com.zaxxer/HikariCP {:mvn/version "6.2.1"}
;; or: ;; or:
com.mchange/c3p0 {:mvn/version "0.9.5.4"} com.mchange/c3p0 {:mvn/version "0.10.1"}
``` ```
_Check those libraries' documentation for the latest version to use!_ _Check those libraries' documentation for the latest version to use!_
@ -629,6 +643,14 @@ if one is passed or create a new one if needed (and automatically close it after
> Note: to avoid confusion and/or incorrect usage, you cannot pass options to `on-connection` because they would be ignored in some cases (existing `Connection` or a wrapped `Connection`). > Note: to avoid confusion and/or incorrect usage, you cannot pass options to `on-connection` because they would be ignored in some cases (existing `Connection` or a wrapped `Connection`).
As of 1.3.894, if you want the options from a wrapped connectable to flow
through to the new connectable inside `on-connection`, you can use the
`on-connection+options` variant of the macro. This will automatically rewrap
the connectable produced with the options from the initial connectable.
Be aware that means you cannot
use plain Java interop inside the body of the macro because the connectable
is no longer a plain Java `java.sql.Connection` object.
## Logging ## Logging
Sometimes it is convenient to have database operations logged automatically. `next.jdbc/with-logging` Sometimes it is convenient to have database operations logged automatically. `next.jdbc/with-logging`

View file

@ -24,14 +24,14 @@ Although both libraries support transactions -- via `clojure.java.jdbc/with-db-t
via `next.jdbc/with-transaction` -- there are some important considerations when you are migrating: via `next.jdbc/with-transaction` -- there are some important considerations when you are migrating:
* `clojure.java.jdbc/with-db-transaction` allows nested calls to be present but it tracks the "depth" of such calls and "nested" calls are simply ignored (because transactions do not actually nest in JDBC). * `clojure.java.jdbc/with-db-transaction` allows nested calls to be present but it tracks the "depth" of such calls and "nested" calls are simply ignored (because transactions do not actually nest in JDBC).
* `next.jdbc/with-transaction` will attempt to set up a transaction on an existing `Connection` if that is what it is passed (otherwise a new `Connection` is created and a new transaction set up on that). That means that if you have nested calls, the inner transaction will commit (or rollback) all the way to the outermost transaction. `next.jdbc` "trusts" the programmer to know what they are doing. You can bind `next.jdbc.transaction/*nested-tx*` to `:ignore` if you want the same behavior as `clojure.java.jdbc` where all nested calls are ignored and the outermost transaction is in full control. _Note that this is a **global** setting and not related to just a single connection, so you can't use this setting if you are working with multiple databases._ * `next.jdbc/with-transaction` will attempt to set up a transaction on an existing `Connection` if that is what it is passed (otherwise a new `Connection` is created and a new transaction set up on that). That means that if you have nested calls, the inner transaction will commit (or rollback) all the way to the outermost transaction. `next.jdbc` "trusts" the programmer to know what they are doing. You can bind `next.jdbc.transaction/*nested-tx*` to `:ignore` if you want the same behavior as `clojure.java.jdbc` where all nested calls are ignored and the outermost transaction is in full control. _Note that this is a per-thread "global" setting and not related to just a single connection, so you can't use this setting if you are working with multiple databases in the same dynamic thread context (`binding`)._
* Every operation in `clojure.java.jdbc` attempts to create its own transaction, which is a no-op inside an `with-db-transaction` so it is safe; transactions are _implicit_ in `clojure.java.jdbc`. However, if you have migrated that `with-db-transaction` call over to `next.jdbc/with-transaction` then any `clojure.java.jdbc` operations invoked inside the body of that migrated transaction _will still try to create their own transactions_ and `with-db-transaction` won't know about the outer `with-transaction` call. That means you will effectively get the "overlapping" behavior of `next.jdbc` since the `clojure.java.jdbc` operation will cause the outermost transaction to be committed or rolled back. * Every operation in `clojure.java.jdbc` attempts to create its own transaction, which is a no-op inside an `with-db-transaction` so it is safe; transactions are _implicit_ in `clojure.java.jdbc`. However, if you have migrated that `with-db-transaction` call over to `next.jdbc/with-transaction` then any `clojure.java.jdbc` operations invoked inside the body of that migrated transaction _will still try to create their own transactions_ and `with-db-transaction` won't know about the outer `with-transaction` call. That means you will effectively get the "overlapping" behavior of `next.jdbc` since the `clojure.java.jdbc` operation will cause the outermost transaction to be committed or rolled back.
* None of the operations in `next.jdbc` try to create transactions -- exception `with-transaction`. All `Connection`s are auto-commit by default so it doesn't need the local transactions that `clojure.java.jdbc` tries to create; transactions are _explicit_ in `next.jdbc`. * None of the operations in `next.jdbc` try to create transactions -- exception `with-transaction`. All `Connection`s are auto-commit by default so it doesn't need the local transactions that `clojure.java.jdbc` tries to create; transactions are _explicit_ in `next.jdbc`.
There are some strategies you can take to mitigate these differences: There are some strategies you can take to mitigate these differences:
1. Migrate code bottom-up so that you don't end up with calls to `clojure.java.jdbc` operations inside `next.jdbc/with-transaction` calls. 1. Migrate code bottom-up so that you don't end up with calls to `clojure.java.jdbc` operations inside `next.jdbc/with-transaction` calls.
2. When you migrate a `with-db-transaction` call, think carefully about whether it could be a nested call (in which case simply remove it) or a conditionally nested call which you'll need to be much more careful about migrating. 2. When you migrate a `with-db-transaction` call, think carefully about whether it could be a nested call (in which case simply remove it) or a conditionally nested call which you'll need to be much more careful about migrating.
3. You can bind `next.jdbc.transaction/*nested-tx*` to `:prohibit` which will throw exceptions if you accidentally nest calls to `next.jdbc/with-transaction`. Although you can bind it to `:ignore` in order to mimic the behavior of `clojure.java.jdbc`, that should be considered a last resort for dealing with complex conditional nesting of transaction calls. _Note that this is a **global** setting and not related to just a single connection, so you can't use this setting if you are working with multiple databases._ 3. You can bind `next.jdbc.transaction/*nested-tx*` to `:prohibit` which will throw exceptions if you accidentally nest calls to `next.jdbc/with-transaction`. Although you can bind it to `:ignore` in order to mimic the behavior of `clojure.java.jdbc`, that should be considered a last resort for dealing with complex conditional nesting of transaction calls. _Note that this is a per-thread "global" setting and not related to just a single connection, so you can't use this setting if you are working with multiple databases in the same dynamic thread context (`binding`)._
### Option Handling ### Option Handling
@ -64,7 +64,7 @@ If you were using other forms of the `db-spec` hash map, you'll need to adjust t
The `next.jdbc.sql` namespace contains several functions with similarities to `clojure.java.jdbc`'s core API: The `next.jdbc.sql` namespace contains several functions with similarities to `clojure.java.jdbc`'s core API:
* `insert!` -- similar to `clojure.java.jdbc/insert!` but only supports inserting a single map, * `insert!` -- similar to `clojure.java.jdbc/insert!` but only supports inserting a single map,
* `insert-multi!` -- similar to `clojure.java.jdbc/insert-multi!` but only supports inserting columns and a vector of row values, * `insert-multi!` -- similar to `clojure.java.jdbc/insert-multi!` but only supports inserting columns and a vector of row values, or a sequence of hash maps _that all have the same keys_ -- unlike `clojure.java.jdbc/insert-multi!`, you should always get a single multi-row insertion,
* `query` -- similar to `clojure.java.jdbc/query`, * `query` -- similar to `clojure.java.jdbc/query`,
* `find-by-keys` -- similar to `clojure.java.jdbc/find-by-keys` but will also accept a partial where clause (vector) instead of a hash map of column name/value pairs, * `find-by-keys` -- similar to `clojure.java.jdbc/find-by-keys` but will also accept a partial where clause (vector) instead of a hash map of column name/value pairs,
* `get-by-id` -- similar to `clojure.java.jdbc/get-by-id`, * `get-by-id` -- similar to `clojure.java.jdbc/get-by-id`,
@ -118,7 +118,7 @@ Several methods on `DatabaseMetaData` return a `ResultSet` object. All of those
These are mostly drawn from [Issue #5](https://github.com/seancorfield/next-jdbc/issues/5) although most of the bullets in that issue are described in more detail above. These are mostly drawn from [Issue #5](https://github.com/seancorfield/next-jdbc/issues/5) although most of the bullets in that issue are described in more detail above.
* Keyword options no longer end in `?` -- for consistency (in `clojure.java.jdbc`, some flag options ended in `?` and some did not; also some options that ended in `?` accepted non-`Boolean` values, e.g., `:as-arrays?` and `:explain?`), * Keyword options no longer end in `?` -- for consistency (in `clojure.java.jdbc`, some flag options ended in `?` and some did not; also some options that ended in `?` accepted non-`Boolean` values),
* `with-db-connection` has been replaced by just `with-open` containing a call to `get-connection`, * `with-db-connection` has been replaced by just `with-open` containing a call to `get-connection`,
* `with-transaction` can take a `:rollback-only` option, but there is no built-in way to change a transaction to rollback _dynamically_; either throw an exception (all transactions roll back on an exception) or call `.rollback` directly on the `java.sql.Connection` object (see [Manual Rollback Inside a Transactions](/doc/transactions.md#manual-rollback-inside-a-transaction) and the following section about save points), * `with-transaction` can take a `:rollback-only` option, but there is no built-in way to change a transaction to rollback _dynamically_; either throw an exception (all transactions roll back on an exception) or call `.rollback` directly on the `java.sql.Connection` object (see [Manual Rollback Inside a Transactions](/doc/transactions.md#manual-rollback-inside-a-transaction) and the following section about save points),
* `clojure.java.jdbc` implicitly allowed transactions to nest and just silently ignored the inner, nested transactions (so you only really had the top-level, outermost transaction); `next.jdbc` by default assumes you know what you are doing and so an inner (nested) transaction will commit or rollback the work done so far in outer transaction (and then when that outer transaction ends, the remaining work is rolled back or committed); `next.jdbc.transaction/*nested-tx*` is a dynamic var that can be bound to `:ignore` to get similar behavior to `clojure.java.jdbc`. * `clojure.java.jdbc` implicitly allowed transactions to nest and just silently ignored the inner, nested transactions (so you only really had the top-level, outermost transaction); `next.jdbc` by default assumes you know what you are doing and so an inner (nested) transaction will commit or rollback the work done so far in outer transaction (and then when that outer transaction ends, the remaining work is rolled back or committed); `next.jdbc.transaction/*nested-tx*` is a dynamic var that can be bound to `:ignore` to get similar behavior to `clojure.java.jdbc`.

View file

@ -133,7 +133,7 @@ If you want to get the generated keys from an `insert` done via `execute-batch!`
This calls `rs/datafiable-result-set` behind the scenes so you can also pass a `:builder-fn` option to `execute-batch!` if you want something other than qualified as-is hash maps. This calls `rs/datafiable-result-set` behind the scenes so you can also pass a `:builder-fn` option to `execute-batch!` if you want something other than qualified as-is hash maps.
> Note: not all databases support calling `.getGeneratedKeys` here (everything I test against seems to, except MS SQL Server). Some databases will only return one generated key per batch, rather than a generated key for every row inserted. > Note: not all databases support calling `.getGeneratedKeys` here (everything I test against seems to, except MS SQL Server and SQLite). Some databases will only return one generated key per batch, rather than a generated key for every row inserted. You may need to add `RETURNING *` to your `INSERT` statements instead.
### Caveats ### Caveats
@ -141,4 +141,4 @@ There are several caveats around using batched parameters. Some JDBC drivers nee
In addition, if the batch operation fails for a group of parameters, it is database-specific whether the remaining groups of parameters are used, i.e., whether the operation is performed for any further groups of parameters after the one that failed. The result of calling `execute-batch!` is a vector of integers. Each element of the vector is the number of rows affected by the operation for each group of parameters. `execute-batch!` may throw a `BatchUpdateException` and calling `.getUpdateCounts` (or `.getLargeUpdateCounts`) on the exception may return an array containing a mix of update counts and error values (a Java `int[]` or `long[]`). Some databases don't always return an update count but instead a value indicating the number of rows is not known (but sometimes you can still get the update counts). In addition, if the batch operation fails for a group of parameters, it is database-specific whether the remaining groups of parameters are used, i.e., whether the operation is performed for any further groups of parameters after the one that failed. The result of calling `execute-batch!` is a vector of integers. Each element of the vector is the number of rows affected by the operation for each group of parameters. `execute-batch!` may throw a `BatchUpdateException` and calling `.getUpdateCounts` (or `.getLargeUpdateCounts`) on the exception may return an array containing a mix of update counts and error values (a Java `int[]` or `long[]`). Some databases don't always return an update count but instead a value indicating the number of rows is not known (but sometimes you can still get the update counts).
Finally, some database drivers don't do batched operations at all -- they accept `.executeBatch` but they run the operation as separate commands for the database rather than a single batched command. Finally, some database drivers don't do batched operations at all -- they accept `.executeBatch` but they run the operation as separate commands for the database rather than a single batched command. Some database drivers do not support `.getGeneratedKeys` (e.g., MS SQL Server and SQLite) so you cannot use `:return-generated-keys` and you need to use `RETURNING *` in your `INSERT` statements instead.

View file

@ -55,7 +55,7 @@ be very database-specific. Some database drivers **don't** use the hierarchy
above -- notably PostgreSQL, which has a generic `PSQLException` type above -- notably PostgreSQL, which has a generic `PSQLException` type
with its own subclasses and semantics. See [PostgreSQL JDBC issue #963](https://github.com/pgjdbc/pgjdbc/issues/963) with its own subclasses and semantics. See [PostgreSQL JDBC issue #963](https://github.com/pgjdbc/pgjdbc/issues/963)
for a discussion of the difficulty in adopting the standard JDBC hierarchy for a discussion of the difficulty in adopting the standard JDBC hierarchy
(dating back five years). (dating back to 2017!).
The `java.sql.SQLException` class provides `.getErrorCode()` and The `java.sql.SQLException` class provides `.getErrorCode()` and
`.getSQLState()` methods but the values returned by those are `.getSQLState()` methods but the values returned by those are
@ -124,7 +124,7 @@ you can use `run!` instead of `reduce`:
``` ```
`run!` is based on `reduce` and `process-row` here takes just one argument -- `run!` is based on `reduce` and `process-row` here takes just one argument --
the row -- rather than the usual reducing function that takes two the row -- rather than the usual reducing function that takes two.
The result of `plan` is also foldable in the [clojure.core.reducers](https://clojure.org/reference/reducers) sense. While you could use `execute!` to produce a vector of fully-realized rows as hash maps and then fold that vector (Clojure's vectors support fork-join parallel reduce-combine), that wouldn't be possible for very large result sets. If you fold the result of `plan`, the result set will be partitioned and processed using fork-join parallel reduce-combine. Unlike reducing over `plan`, each row **is** realized into a Clojure data structure and each batch is forked for reduction as soon as that many rows have been realized. By default, `fold`'s batch size is 512 but you can specify a different value in the 4-arity call. Once the entire result set has been read, the last (partial) batch is forked for reduction. The combining operations are forked and interleaved with the reducing operations, so the order (of forked tasks) is batch-1, batch-2, combine-1-2, batch-3, combine-1&2-3, batch-4, combine-1&2&3-4, etc. The amount of parallelization you get will depend on many factors including the number of processors, the speed of your reducing function, the speed of your combining function, and the speed with which result sets can actually be streamed from your database. The result of `plan` is also foldable in the [clojure.core.reducers](https://clojure.org/reference/reducers) sense. While you could use `execute!` to produce a vector of fully-realized rows as hash maps and then fold that vector (Clojure's vectors support fork-join parallel reduce-combine), that wouldn't be possible for very large result sets. If you fold the result of `plan`, the result set will be partitioned and processed using fork-join parallel reduce-combine. Unlike reducing over `plan`, each row **is** realized into a Clojure data structure and each batch is forked for reduction as soon as that many rows have been realized. By default, `fold`'s batch size is 512 but you can specify a different value in the 4-arity call. Once the entire result set has been read, the last (partial) batch is forked for reduction. The combining operations are forked and interleaved with the reducing operations, so the order (of forked tasks) is batch-1, batch-2, combine-1-2, batch-3, combine-1&2-3, batch-4, combine-1&2&3-4, etc. The amount of parallelization you get will depend on many factors including the number of processors, the speed of your reducing function, the speed of your combining function, and the speed with which result sets can actually be streamed from your database.
@ -227,9 +227,32 @@ is the best way to ensure the statement is properly closed after use.
## PostgreSQL ## PostgreSQL
When you use `:return-keys true` with `execute!` or `execute-one!` (or you use `insert!`), PostgreSQL returns the entire inserted row (unlike nearly every other database that just returns any generated keys!). As you can see in this section (and elsewhere in this documentation), the
PostgreSQL JDBC driver has a number of interesting quirks and behaviors that
you need to be aware of. Although accessing PostgreSQL via JDBC is the most
common approach, there is also a non-JDBC Clojure/Java driver for PostgreSQL called
[PG2](https://github.com/igrishaev/pg2) which supports JSON operations natively
(see below for what's required for JDBC), as well as supporting Java Time natively
(see the section above about **Times, Dates, and Timezones**), and it also
quite a bit faster than using JDBC.
If you have a query where you want to select where a column is `IN` a sequence of values, you can use `col = ANY(?)` with a native array of the values instead of `IN (?,?,?,,,?)` and a sequence of values. When you use `:return-keys true` with `execute!` or `execute-one!` (or you use `insert!`), PostgreSQL returns the entire inserted row (unlike nearly every other database that just returns any generated keys!).
_[It seems to achieve this by the equivalent of automatically appending `RETURNING *` to your SQL, if necessary.]_
The default result set builder for `next.jdbc` is `as-qualified-maps` which
uses the `.getTableName()` method on `ResultSetMetaData` to qualify the
columns in the result set. While some database drivers have this information
on hand from the original SQL operation, PostgreSQL's JDBC driver does not
and it performs an extra SQL query to fetch table names the first time this
method is called for each query. If you want to avoid those extra queries,
and you can live with unqualified column names, you can use `as-unqualified-maps`
as the result set builder instead.
If you have a query where you want to select where a column is `IN` a sequence of values, you can use `col = ANY(?)` with a native array of the values instead of `IN (?,?,?,,,?)` and a sequence of values. **Be aware of
[PostgreSQL bug 17822](https://www.postgresql.org/message-id/flat/17922-1e2e0aeedd294424%40postgresql.org)
which can cause pathological performance when the array has a single element!**
If you think you might have a single-element array, consider using `UNNEST` and
`IN` instead.
What does this mean for your use of `next.jdbc`? In `plan`, `execute!`, and `execute-one!`, you can use `col = ANY(?)` in the SQL string and a single primitive array parameter, such as `(int-array [1 2 3 4])`. That means that in `next.jdbc.sql`'s functions that take a where clause (`find-by-keys`, `update!`, and `delete!`) you can specify `["col = ANY(?)" (int-array data)]` for what would be a `col IN (?,?,?,,,?)` where clause for other databases and require multiple values. What does this mean for your use of `next.jdbc`? In `plan`, `execute!`, and `execute-one!`, you can use `col = ANY(?)` in the SQL string and a single primitive array parameter, such as `(int-array [1 2 3 4])`. That means that in `next.jdbc.sql`'s functions that take a where clause (`find-by-keys`, `update!`, and `delete!`) you can specify `["col = ANY(?)" (int-array data)]` for what would be a `col IN (?,?,?,,,?)` where clause for other databases and require multiple values.
@ -395,14 +418,12 @@ containing JSON:
(.setValue (->json x))))) (.setValue (->json x)))))
(defn <-pgobject (defn <-pgobject
"Transform PGobject containing `json` or `jsonb` value to Clojure "Transform PGobject containing `json` or `jsonb` value to Clojure data."
data." [^PGobject v]
[^org.postgresql.util.PGobject v]
(let [type (.getType v) (let [type (.getType v)
value (.getValue v)] value (.getValue v)]
(if (#{"jsonb" "json"} type) (if (#{"jsonb" "json"} type)
(when value (some-> value <-json (with-meta {:pgtype type}))
(with-meta (<-json value) {:pgtype type}))
value))) value)))
``` ```
@ -551,3 +572,29 @@ If you are using `plan`, you'll most likely be accessing columns by just the lab
[] []
(jdbc/plan ds ["select * from some_table"])) (jdbc/plan ds ["select * from some_table"]))
``` ```
See also [`datafy`, `nav`, and `:schema` > **SQLite**](/doc/datafy-nav-and-schema.md#sqlite)
for additional caveats on the `next.jdbc.datafy` namespace when using SQLite.
## XTDB
XTDB is a bitemporal, schemaless, document-oriented database that presents
itself as a PostgreSQL-compatible database, in terms of JDBC. It has a number
of SQL extensions, and some differences from common JDBC behavior. See
its documentation for details:
* [SQL Overview](https://docs.xtdb.com/quickstart/sql-overview.html)
* [SQL Queries](https://docs.xtdb.com/reference/main/sql/queries.html)
* [SQL Transactions/DML](https://docs.xtdb.com/reference/main/sql/txs.html)
`next.jdbc` officially supports XTDB as of 1.3.981 but there are some caveats:
* You can use `:dbtype "xtdb"` to identify XTDB as the database type.
* You must specify `:dbname "xtdb"` in the db-spec hash map or JDBC URL.
* XTDB does not support `.getTableName()` so you always get unqualified column names in result sets.
* The primary key on all tables is `_id` and it must be specified in all `INSERT` operations (no auto-generated keys).
* That means that `next.jdbc.sql/get-by-id` requires the 5-argument call, so that you can specify the `pk-name` as `:_id` and provide an options map.
* If you want to use `next.jdbc`'s built-in `datafy` / `nav` functionality, you need to explicitly specify `:schema-opts {:pk "_id"}` to override the default assumption of `id` as the primary key.
* DML operations (`INSERT`, `UPDATE`, and `DELETE`) are essentially asynchronous in XTDB and therefore can not return an accurate `next.jdbc/update-count` (so it is always 0).
* `INSERT` operations do not return the inserted row (like PostgreSQL does) nor even the provided `_id` primary key.
* That means that the `next.jdbc.defer` namespace functions do not work well with XTDB.
* `next.jdbc.sql/insert-multi!` returns an empty vector for XTDB (since `INSERT` operations do not return keys or update counts).
* The `next.jdbc.result-set/*-kebab-maps` functions (and associated `next.jdbc/*-kebab-opts` option maps) cause leading `_` to be stripped from column names and cannot be used with XTDB (this is inherent in the underlying library that `next.jdbc` relies on -- you can of course write your own custom result set builder function to handle this).

View file

@ -45,7 +45,7 @@ you can call `next.jdbc/active-tx?` to determine that, in your own code, in
case you want to write code that behaves differently inside or outside a case you want to write code that behaves differently inside or outside a
transaction. transaction.
> Note: `active-tx?` only knows about `next.jdbc` transactions -- it cannot track any transactions that you create yourself using the underlying JDBC `Connection`. In addition, this is a **global** state (per thread) and not related to just a single connection, so you can't use this predicate if you are working with multiple databases in the same context. > Note: `active-tx?` only knows about `next.jdbc` transactions -- it cannot track any transactions that you create yourself using the underlying JDBC `Connection`. In addition, this is a per-thread "global" setting and not related to just a single connection, so you can't use this setting if you are working with multiple databases in the same dynamic thread context (`binding`).
## Manual Rollback Inside a Transaction ## Manual Rollback Inside a Transaction
@ -107,4 +107,16 @@ transactions in the code under test.
* `(binding [next.jdbc.transaction/*nested-tx* :ignore] ...)` provides the same behavior as `clojure.java.jdbc` where nested calls are essentially ignored and only the outermost transaction takes effect, * `(binding [next.jdbc.transaction/*nested-tx* :ignore] ...)` provides the same behavior as `clojure.java.jdbc` where nested calls are essentially ignored and only the outermost transaction takes effect,
* `(binding [next.jdbc.transaction/*nested-tx* :prohibit] ...)` will cause any attempt to start a nested transaction to throw an exception instead; this could be a useful way to detect the potentially buggy behavior described above (for either `:allow` or `:ignore`). * `(binding [next.jdbc.transaction/*nested-tx* :prohibit] ...)` will cause any attempt to start a nested transaction to throw an exception instead; this could be a useful way to detect the potentially buggy behavior described above (for either `:allow` or `:ignore`).
> Note: this is a **global** setting (per thread) and not related to just a single connection, so you can't use this setting if you are working with multiple databases in the same context. > Note: this is a per-thread "global" setting and not related to just a single connection, so you can't use this setting if you are working with multiple databases in the same dynamic thread context (`binding`).
### `with-options`
If you are using `with-options` to produce wrapped connectables / transactables,
it's important to be aware that `with-transaction` produces a bare Java
`java.sql.Connection` object that cannot have options -- but does allow direct
interop. If you want to use `with-options` with `with-transaction`, you must
either rewrap the `Connection` with a nested call to `with-options` or,
as of 1.3.894, you can use `with-transaction+options` which will automatically
rewrap the `Connection` in a new connectable along with the options from the
original transactable. Be aware that you cannot use Java interop on this
wrapped connectable.

View file

@ -1,4 +1,3 @@
version: '2'
services: services:
mysql: mysql:
image: percona:5.7 image: percona:5.7
@ -9,9 +8,14 @@ services:
command: command:
[--character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci] [--character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci]
sqlserver: sqlserver:
image: mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04 image: mcr.microsoft.com/mssql/server:2022-latest
environment: environment:
ACCEPT_EULA: Y ACCEPT_EULA: Y
SA_PASSWORD: Str0ngP4ssw0rd MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
ports: ports:
- "1433:1433" - "1433:1433"
xtdb:
image: ghcr.io/xtdb/xtdb:latest
# pull_policy: always
ports:
- "5432:5432"

View file

@ -1,5 +1,8 @@
{:hooks {:hooks
{:analyze-call {:analyze-call
{next.jdbc/with-transaction {next.jdbc/with-transaction
hooks.com.github.seancorfield.next-jdbc/with-transaction}} hooks.com.github.seancorfield.next-jdbc/with-transaction
:lint-as {next.jdbc/on-connection clojure.core/with-open}} next.jdbc/with-transaction+options
hooks.com.github.seancorfield.next-jdbc/with-transaction+options}}
:lint-as {next.jdbc/on-connection clojure.core/with-open
next.jdbc/on-connection+options clojure.core/with-open}}

View file

@ -16,3 +16,19 @@
opts opts
body))] body))]
{:node new-node}))) {:node new-node})))
(defn with-transaction+options
"Expands (with-transaction+options [tx expr opts] body)
to (let [tx expr] opts body) per clj-kondo examples."
[{:keys [:node]}]
(let [[binding-vec & body] (rest (:children node))
[sym val opts] (:children binding-vec)]
(when-not (and sym val)
(throw (ex-info "No sym and val provided" {})))
(let [new-node (api/list-node
(list*
(api/token-node 'let)
(api/vector-node [sym val])
opts
body))]
{:node new-node})))

View file

@ -2,11 +2,33 @@
(require '[babashka.process :as p]) (require '[babashka.process :as p])
(let [maria? (= "maria" (first *command-line-args*)) (defn- run-tests [env v]
(when v (println "\nTesting Clojure" v))
(let [{:keys [exit]}
(p/shell {:extra-env env}
"clojure"
(str "-M"
(when v (str ":" v))
":test:runner"
;; jdk21+ adds xtdb:
(when (System/getenv "NEXT_JDBC_TEST_XTDB")
":jdk21")
;; to suppress native access warnings on JDK24:
":jdk24")
"--output" "dots")]
(when-not (zero? exit)
(System/exit exit))))
(let [maria? (some #(= "maria" %) *command-line-args*)
xtdb? (some #(= "xtdb" %) *command-line-args*)
all? (some #(= "all" %) *command-line-args*)
env env
(cond-> {"NEXT_JDBC_TEST_MSSQL" "yes" (cond-> {"NEXT_JDBC_TEST_MSSQL" "yes"
"NEXT_JDBC_TEST_MYSQL" "yes" "NEXT_JDBC_TEST_MYSQL" "yes"
"MSSQL_SA_PASSWORD" "Str0ngP4ssw0rd"} "MSSQL_SA_PASSWORD" "Str0ngP4ssw0rd"}
maria? maria?
(assoc "NEXT_JDBC_TEST_MARIA" "yes"))] (assoc "NEXT_JDBC_TEST_MARIADB" "yes")
(p/shell {:extra-env env} "clojure" "-X:test")) xtdb?
(assoc "NEXT_JDBC_TEST_XTDB" "yes"))]
(doseq [v (if all? ["1.10" "1.11" "1.12"] [nil])]
(run-tests env v)))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2023 Sean Corfield, all rights reserved ;; copyright (c) 2018-2025 Sean Corfield, all rights reserved
(ns next.jdbc (ns next.jdbc
"The public API of the next generation java.jdbc library. "The public API of the next generation java.jdbc library.
@ -14,8 +14,8 @@
* `get-connection` -- given a connectable, obtain a new `java.sql.Connection` * `get-connection` -- given a connectable, obtain a new `java.sql.Connection`
from it and return that, from it and return that,
* `plan` -- given a connectable and SQL + parameters or a statement, * `plan` -- given a connectable and SQL + parameters or a statement,
return a reducible that, when reduced will execute the SQL and consume return a reducible that, when reduced (with an initial value) will
the `ResultSet` produced, execute the SQL and consume the `ResultSet` produced,
* `execute!` -- given a connectable and SQL + parameters or a statement, * `execute!` -- given a connectable and SQL + parameters or a statement,
execute the SQL, consume the `ResultSet` produced, and return a vector execute the SQL, consume the `ResultSet` produced, and return a vector
of hash maps representing the rows (@1); this can be datafied to allow of hash maps representing the rows (@1); this can be datafied to allow
@ -137,6 +137,7 @@
* `sqlserver`, `mssql` -- `com.microsoft.sqlserver.jdbc.SQLServerDriver` -- `1433` * `sqlserver`, `mssql` -- `com.microsoft.sqlserver.jdbc.SQLServerDriver` -- `1433`
* `timesten:client` -- `com.timesten.jdbc.TimesTenClientDriver` * `timesten:client` -- `com.timesten.jdbc.TimesTenClientDriver`
* `timesten:direct` -- `com.timesten.jdbc.TimesTenDriver` * `timesten:direct` -- `com.timesten.jdbc.TimesTenDriver`
* `xtdb` -- `xtdb.jdbc.Driver` -- an XTDB wrapper around `postgresql`
For more details about `:dbtype` and `:classname` values, see: For more details about `:dbtype` and `:classname` values, see:
https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc.connection#dbtypes" https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc.connection#dbtypes"
@ -176,6 +177,14 @@
[spec user password opts] [spec user password opts]
(p/get-connection spec (assoc opts :user user :password password)))) (p/get-connection spec (assoc opts :user user :password password))))
(defn- ensure-sql-params [sql-params]
(when-not (or (nil? sql-params)
(and (seqable? sql-params)
(or (empty? sql-params)
(string? (first sql-params)))))
(throw (ex-info "sql-params should be a vector containing a SQL string and any parameters"
{:sql-params sql-params}))))
(defn prepare (defn prepare
"Given a connection to a database, and a vector containing SQL and any "Given a connection to a database, and a vector containing SQL and any
parameters it needs, return a new `PreparedStatement`. parameters it needs, return a new `PreparedStatement`.
@ -191,15 +200,20 @@
be passed to prepare." be passed to prepare."
(^java.sql.PreparedStatement (^java.sql.PreparedStatement
[connection sql-params] [connection sql-params]
(ensure-sql-params sql-params)
(p/prepare connection sql-params {})) (p/prepare connection sql-params {}))
(^java.sql.PreparedStatement (^java.sql.PreparedStatement
[connection sql-params opts] [connection sql-params opts]
(ensure-sql-params sql-params)
(p/prepare connection sql-params opts))) (p/prepare connection sql-params opts)))
(defn plan (defn plan
"General SQL execution function (for working with result sets). "General SQL execution function (for working with result sets).
Returns a reducible that, when reduced, runs the SQL and yields the result. Returns a reducible that, when reduced (with an initial value), runs the
SQL and yields the result. `plan` returns an `IReduceInit` object so you
must provide an initial value when calling `reduce` on it.
The reducible is also foldable (in the `clojure.core.reducers` sense) but The reducible is also foldable (in the `clojure.core.reducers` sense) but
see the **Tips & Tricks** section of the documentation for some important see the **Tips & Tricks** section of the documentation for some important
caveats about that. caveats about that.
@ -228,10 +242,12 @@
(p/-execute stmt [] {})) (p/-execute stmt [] {}))
(^clojure.lang.IReduceInit (^clojure.lang.IReduceInit
[connectable sql-params] [connectable sql-params]
(ensure-sql-params sql-params)
(p/-execute connectable sql-params (p/-execute connectable sql-params
{:next.jdbc/sql-params sql-params})) {:next.jdbc/sql-params sql-params}))
(^clojure.lang.IReduceInit (^clojure.lang.IReduceInit
[connectable sql-params opts] [connectable sql-params opts]
(ensure-sql-params sql-params)
(p/-execute connectable sql-params (p/-execute connectable sql-params
(assoc opts :next.jdbc/sql-params sql-params)))) (assoc opts :next.jdbc/sql-params sql-params))))
@ -248,9 +264,11 @@
([stmt] ([stmt]
(p/-execute-all stmt [] {})) (p/-execute-all stmt [] {}))
([connectable sql-params] ([connectable sql-params]
(ensure-sql-params sql-params)
(p/-execute-all connectable sql-params (p/-execute-all connectable sql-params
{:next.jdbc/sql-params sql-params})) {:next.jdbc/sql-params sql-params}))
([connectable sql-params opts] ([connectable sql-params opts]
(ensure-sql-params sql-params)
(p/-execute-all connectable sql-params (p/-execute-all connectable sql-params
(assoc opts :next.jdbc/sql-params sql-params)))) (assoc opts :next.jdbc/sql-params sql-params))))
@ -267,9 +285,11 @@
([stmt] ([stmt]
(p/-execute-one stmt [] {})) (p/-execute-one stmt [] {}))
([connectable sql-params] ([connectable sql-params]
(ensure-sql-params sql-params)
(p/-execute-one connectable sql-params (p/-execute-one connectable sql-params
{:next.jdbc/sql-params sql-params})) {:next.jdbc/sql-params sql-params}))
([connectable sql-params opts] ([connectable sql-params opts]
(ensure-sql-params sql-params)
(p/-execute-one connectable sql-params (p/-execute-one connectable sql-params
(assoc opts :next.jdbc/sql-params sql-params)))) (assoc opts :next.jdbc/sql-params sql-params))))
@ -332,9 +352,9 @@
result)))) result))))
params))) params)))
([connectable sql param-groups opts] ([connectable sql param-groups opts]
(if (or (instance? java.sql.Connection connectable) (when-not (string? sql)
(and (satisfies? p/Connectable connectable) (throw (IllegalArgumentException. "execute-batch! requires a SQL string")))
(instance? java.sql.Connection (:connectable connectable)))) (if (instance? java.sql.Connection (p/unwrap connectable))
(with-open [ps (prepare connectable [sql] opts)] (with-open [ps (prepare connectable [sql] opts)]
(execute-batch! ps param-groups opts)) (execute-batch! ps param-groups opts))
(with-open [con (get-connection connectable)] (with-open [con (get-connection connectable)]
@ -360,16 +380,47 @@
Otherwise, creates a new `Connection` object from the connectable, Otherwise, creates a new `Connection` object from the connectable,
executes the body, and automatically closes it for you." executes the body, and automatically closes it for you."
[[sym connectable] & body] [[sym connectable] & body]
(let [con-sym (vary-meta sym assoc :tag 'java.sql.Connection) (let [con-sym (vary-meta sym assoc :tag 'java.sql.Connection)]
con-obj connectable] `(let [con-obj# ~connectable
`(cond (instance? java.sql.Connection ~con-obj) bare-con# (p/unwrap con-obj#)]
((^{:once true} fn* [~con-sym] ~@body) ~con-obj) (if (instance? java.sql.Connection bare-con#)
(and (satisfies? p/Connectable ~con-obj) ((^{:once true} fn* [~con-sym] ~@body) bare-con#)
(instance? java.sql.Connection (:connectable ~con-obj))) (with-open [con# (get-connection con-obj#)]
((^{:once true} fn* [~con-sym] ~@body) (:connectable ~con-obj)) ((^{:once true} fn* [~con-sym] ~@body) con#))))))
:else
(with-open [con# (get-connection ~con-obj)] (defmacro on-connection+options
((^{:once true} fn* [~con-sym] ~@body) con#))))) "Given a connectable object, assumed to be wrapped with options, gets
a connection, rewraps it with those options, and binds it to `sym`,
then executes the `body` in that context.
This allows you to write generic, **wrapped** connectable code without
needing to know the exact type of an incoming datasource:
```clojure
(on-connection+options [conn datasource]
(execute! conn some-insert-sql)
(execute! conn some-update-sql))
```
If passed a `Connection` then that `Connection` is used as-is.
If passed a `Connectable` that wraps a `Connection`, then that
`Connectable` is used as-is.
Otherwise, creates a new `Connection` object from the connectable,
wraps that with options, executes the body, and automatically closes
the new `Connection` for you.
Note: the bound `sym` will be a **wrapped** connectable and not a plain
Java object, so you cannot call JDBC methods directly on it like you can
with `on-connection`."
[[sym connectable] & body]
`(let [con-obj# ~connectable]
(if (instance? java.sql.Connection (p/unwrap con-obj#))
((^{:once true} fn* [~sym] ~@body) con-obj#)
(with-open [con# (get-connection con-obj#)]
((^{:once true} fn* [~sym] ~@body)
(with-options con# (:options con-obj# {})))))))
(defn transact (defn transact
"Given a transactable object and a function (taking a `Connection`), "Given a transactable object and a function (taking a `Connection`),
@ -389,6 +440,9 @@
Like `with-open`, if `with-transaction` creates a new `Connection` object, Like `with-open`, if `with-transaction` creates a new `Connection` object,
it will automatically close it for you. it will automatically close it for you.
If you are working with default options via `with-options`, you might want
to use `with-transaction+options` instead.
The options map supports: The options map supports:
* `:isolation` -- `:none`, `:read-committed`, `:read-uncommitted`, * `:isolation` -- `:none`, `:read-committed`, `:read-uncommitted`,
`:repeatable-read`, `:serializable`, `:repeatable-read`, `:serializable`,
@ -403,12 +457,19 @@
"Returns true if `next.jdbc` has a currently active transaction in the "Returns true if `next.jdbc` has a currently active transaction in the
current thread, else false. current thread, else false.
With no arguments, tells you if any transaction is currently active.
With a `Connection` argument, tells you if a transaction is currently
active on that specific connection.
Note: transactions are a convention of operations on a `Connection` so Note: transactions are a convention of operations on a `Connection` so
this predicate only reflects `next.jdbc/transact` and `next.jdbc/with-transaction` this predicate only reflects `next.jdbc/transact` and `next.jdbc/with-transaction`
operations -- it does not reflect any other operations on a `Connection`, operations -- it does not reflect any other operations on a `Connection`,
performed via JDBC interop directly." performed via JDBC interop directly."
[] ([]
@#'tx/*active-tx*) (boolean (seq @#'tx/*active-tx*)))
([con]
(contains? @#'tx/*active-tx* con)))
(defn with-options (defn with-options
"Given a connectable/transactable object and a set of (default) options "Given a connectable/transactable object and a set of (default) options
@ -419,9 +480,44 @@
return plain Java objects, so if you call any of those on this wrapped return plain Java objects, so if you call any of those on this wrapped
object, you'll need to re-wrap the Java object `with-options` again. See object, you'll need to re-wrap the Java object `with-options` again. See
the Datasources, Connections & Transactions section of Getting Started for the Datasources, Connections & Transactions section of Getting Started for
more details, and some examples of use with these functions." more details, and some examples of use with these functions.
`with-transaction+options` exists to automatically rewrap a `Connection`
with the options from a `with-options` wrapper."
[connectable opts] [connectable opts]
(opts/->DefaultOptions connectable opts)) (let [c (:connectable connectable)
o (:options connectable)]
(if (and c o)
(opts/->DefaultOptions c (merge o opts))
(opts/->DefaultOptions connectable opts))))
(defmacro with-transaction+options
"Given a transactable object, assumed to be wrapped with options, gets a
connection, rewraps it with those options, and binds it to `sym`, then
executes the `body` in that context, committing any changes if the body
completes successfully, otherwise rolling back any changes made.
Like `with-open`, if `with-transaction+options` creates a new `Connection`
object, it will automatically close it for you.
Note: the bound `sym` will be a **wrapped** connectable and not a plain
Java object, so you cannot call JDBC methods directly on it like you can
with `with-transaction`.
The options map supports:
* `:isolation` -- `:none`, `:read-committed`, `:read-uncommitted`,
`:repeatable-read`, `:serializable`,
* `:read-only` -- `true` / `false` (`true` will make the `Connection` readonly),
* `:rollback-only` -- `true` / `false` (`true` will make the transaction
rollback, even if it would otherwise succeed)."
[[sym transactable opts] & body]
`(let [tx# ~transactable]
(transact tx#
(^{:once true} fn*
[con#] ; this is the unwrapped java.sql.connection
(let [~sym (with-options con# (:options tx# {}))]
~@body))
~(or opts {}))))
(defn with-logging (defn with-logging
"Given a connectable/transactable object and a sql/params logging "Given a connectable/transactable object and a sql/params logging

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2023 Sean Corfield, all rights reserved ;; copyright (c) 2018-2025 Sean Corfield, all rights reserved
(ns next.jdbc.connection (ns next.jdbc.connection
"Standard implementations of `get-datasource` and `get-connection`. "Standard implementations of `get-datasource` and `get-connection`.
@ -130,7 +130,9 @@
:host :none} :host :none}
"timesten:direct" {:classname "com.timesten.jdbc.TimesTenDriver" "timesten:direct" {:classname "com.timesten.jdbc.TimesTenDriver"
:dbname-separator ":dsn=" :dbname-separator ":dsn="
:host :none}}) :host :none}
"xtdb" {:classname "xtdb.jdbc.Driver"
:port 5432}})
(def ^:private driver-cache (def ^:private driver-cache
"An optimization for repeated calls to get-datasource, or for get-connection "An optimization for repeated calls to get-datasource, or for get-connection
@ -330,7 +332,8 @@
(component clazz db-spec close-fn))})))}))) (component clazz db-spec close-fn))})))})))
(comment (comment
(require '[com.stuartsierra.component :as component]) (require '[com.stuartsierra.component :as component]
'[next.jdbc.sql :as sql])
(import '(com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource) (import '(com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)
'(com.zaxxer.hikari HikariDataSource)) '(com.zaxxer.hikari HikariDataSource))
(isa? PooledDataSource java.io.Closeable) ;=> false (isa? PooledDataSource java.io.Closeable) ;=> false
@ -358,7 +361,7 @@
;; start the chosen datasource component: ;; start the chosen datasource component:
(def ds (component/start dbc)) (def ds (component/start dbc))
;; invoke datasource component to get the underlying javax.sql.DataSource: ;; invoke datasource component to get the underlying javax.sql.DataSource:
(next.jdbc.sql/get-by-id (ds) :fruit 1) (sql/get-by-id (ds) :fruit 1)
;; stop the component and close the pooled datasource: ;; stop the component and close the pooled datasource:
(component/stop ds) (component/stop ds)
) )
@ -368,12 +371,15 @@
[s] [s]
[s {}]) [s {}])
(defn- ^Properties as-properties (defn- as-properties
"Convert any seq of pairs to a `java.util.Properties` instance." "Convert any seq of pairs to a `java.util.Properties` instance."
[m] ^Properties [m]
(let [p (Properties.)] (let [p (Properties.)
(doseq [[k v] m] as-is (set (:next.jdbc/as-is-properties m))]
(.setProperty p (name k) (str v))) (doseq [[k v] (dissoc m :next.jdbc/as-is-properties)]
(if (contains? as-is k)
(.put p (name k) v)
(.setProperty p (name k) (str v))))
p)) p))
(defn uri->db-spec (defn uri->db-spec

View file

@ -1,4 +1,4 @@
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved ;; copyright (c) 2020-2024 Sean Corfield, all rights reserved
(ns next.jdbc.datafy (ns next.jdbc.datafy
"This namespace provides datafication of several JDBC object types, "This namespace provides datafication of several JDBC object types,

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
(ns next.jdbc.date-time (ns next.jdbc.date-time
"Optional namespace that extends `next.jdbc.prepare/SettableParameter` "Optional namespace that extends `next.jdbc.prepare/SettableParameter`

View file

@ -1,4 +1,4 @@
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved ;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
(ns ^:no-doc next.jdbc.default-options (ns ^:no-doc next.jdbc.default-options
"Implementation of default options logic." "Implementation of default options logic."
@ -8,6 +8,10 @@
(defrecord DefaultOptions [connectable options]) (defrecord DefaultOptions [connectable options])
(extend-protocol p/Wrapped
DefaultOptions
(unwrap [this] (p/unwrap (:connectable this))))
(extend-protocol p/Sourceable (extend-protocol p/Sourceable
DefaultOptions DefaultOptions
(get-datasource [this] (get-datasource [this]

94
src/next/jdbc/defer.clj Normal file
View file

@ -0,0 +1,94 @@
;; copyright (c) 2024 Sean Corfield, all rights reserved
(ns next.jdbc.defer
"The idea behind the next.jdbc.defer namespace is to provide a
way to defer the execution of a series of SQL statements until
a later time, but still provide a way for inserted keys to be
used in later SQL statements.
The principle is to provide a core subset of the next.jdbc
and next.jdbc.sql API that produces a data structure that
describes a series of SQL operations to be performed, that
are held in a dynamic var, and that can be executed at a
later time, in a transaction."
(:require [next.jdbc :as jdbc]
[next.jdbc.sql.builder :refer [for-delete for-insert for-update]]))
(set! *warn-on-reflection* true)
(def ^:private ^:dynamic *deferred* nil)
(defn execute-one!
"Given a vector containing a SQL statement and parameters, defer
execution of that statement."
([sql-p]
(execute-one! sql-p {}))
([sql-p opts]
(swap! *deferred* conj
{:sql-p sql-p
:key-fn (or (:key-fn opts) (comp first vals))
:key (:key opts)
:opts opts})))
(defn insert!
"Given a table name, and a data hash map, defer an insertion of the
data as a single row in the database."
([table key-map]
(insert! table key-map {}))
([table key-map opts]
(swap! *deferred* conj
{:sql-p (for-insert table key-map opts)
:key-fn (or (:key-fn opts) (comp first vals))
:key (:key opts)
:opts opts})))
(defn update!
"Given a table name, a hash map of columns and values to set, and
either a hash map of columns and values to search on or a vector
of a SQL where clause and parameters, defer an update on the table."
([table key-map where-params]
(update! table key-map where-params {}))
([table key-map where-params opts]
(swap! *deferred* conj
{:sql-p (for-update table key-map where-params opts)
:opts opts})))
(defn delete!
"Given a table name, and either a hash map of columns and values
to search on or a vector of a SQL where clause and parameters,
defer a delete on the table."
([table where-params]
(delete! table where-params {}))
([table where-params opts]
(swap! *deferred* conj
{:sql-p (for-delete table where-params opts)
:opts opts})))
(defn deferrable [transactable stmts]
(reify clojure.lang.IDeref
(deref [_]
(let [keys (atom {})]
(jdbc/with-transaction [conn transactable]
(doseq [{:keys [sql-p key-fn key opts]} @stmts]
(let [sql-p
(mapv (fn [v]
(if (keyword? v)
(if (contains? @keys v)
(get @keys v)
(throw (ex-info (str "Deferred key not found " v)
{:key v})))
v))
sql-p)
result (jdbc/execute-one! conn sql-p opts)]
(when key
(swap! keys assoc key (key-fn result))))))
@keys))))
(defn defer-ops [f]
(binding [*deferred* (atom [])]
(f)
*deferred*))
(defmacro with-deferred [connectable & body]
`(let [conn# ~connectable]
(deferrable conn# (defer-ops (^{:once true} fn* [] ~@body)))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
(ns next.jdbc.optional (ns next.jdbc.optional
"Builders that treat NULL SQL values as 'optional' and omit the "Builders that treat NULL SQL values as 'optional' and omit the
@ -11,8 +11,8 @@
(defrecord MapResultSetOptionalBuilder [^ResultSet rs rsmeta cols] (defrecord MapResultSetOptionalBuilder [^ResultSet rs rsmeta cols]
rs/RowBuilder rs/RowBuilder
(->row [this] (transient {})) (->row [_this] (transient {}))
(column-count [this] (count cols)) (column-count [_this] (count cols))
(with-column [this row i] (with-column [this row i]
;; short-circuit on null to avoid column reading logic ;; short-circuit on null to avoid column reading logic
(let [v (.getObject rs ^Integer i)] (let [v (.getObject rs ^Integer i)]
@ -20,17 +20,17 @@
row row
(rs/with-column-value this row (nth cols (dec i)) (rs/with-column-value this row (nth cols (dec i))
(rs/read-column-by-index v rsmeta i))))) (rs/read-column-by-index v rsmeta i)))))
(with-column-value [this row col v] (with-column-value [_this row col v]
;; ensure that even if this is adapted, we omit null columns ;; ensure that even if this is adapted, we omit null columns
(if (nil? v) (if (nil? v)
row row
(assoc! row col v))) (assoc! row col v)))
(row! [this row] (persistent! row)) (row! [_this row] (persistent! row))
rs/ResultSetBuilder rs/ResultSetBuilder
(->rs [this] (transient [])) (->rs [_this] (transient []))
(with-row [this mrs row] (with-row [_this mrs row]
(conj! mrs row)) (conj! mrs row))
(rs! [this mrs] (persistent! mrs))) (rs! [_this mrs] (persistent! mrs)))
(defn as-maps (defn as-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder` "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
@ -76,7 +76,7 @@
locales where the lower case version of a character is not a valid SQL locales where the lower case version of a character is not a valid SQL
entity name (e.g., Turkish)." entity name (e.g., Turkish)."
[^String s] [^String s]
(.toLowerCase s (Locale/US))) (.toLowerCase s Locale/US))
(defn as-lower-maps (defn as-lower-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder` "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
@ -117,25 +117,25 @@
(let [mrsb (builder-fn rs opts)] (let [mrsb (builder-fn rs opts)]
(reify (reify
rs/RowBuilder rs/RowBuilder
(->row [this] (rs/->row mrsb)) (->row [_this] (rs/->row mrsb))
(column-count [this] (rs/column-count mrsb)) (column-count [_this] (rs/column-count mrsb))
(with-column [this row i] (with-column [_this row i]
;; short-circuit on null to avoid column reading logic ;; short-circuit on null to avoid column reading logic
(let [v (column-reader rs (:rsmeta mrsb) i)] (let [v (column-reader rs (:rsmeta mrsb) i)]
(if (nil? v) (if (nil? v)
row row
(rs/with-column-value mrsb row (nth (:cols mrsb) (dec i)) (rs/with-column-value mrsb row (nth (:cols mrsb) (dec i))
(rs/read-column-by-index v (:rsmeta mrsb) i))))) (rs/read-column-by-index v (:rsmeta mrsb) i)))))
(with-column-value [this row col v] (with-column-value [_this row col v]
;; ensure that even if this is adapted, we omit null columns ;; ensure that even if this is adapted, we omit null columns
(if (nil? v) (if (nil? v)
row row
(rs/with-column-value mrsb row col v))) (rs/with-column-value mrsb row col v)))
(row! [this row] (rs/row! mrsb row)) (row! [_this row] (rs/row! mrsb row))
rs/ResultSetBuilder rs/ResultSetBuilder
(->rs [this] (rs/->rs mrsb)) (->rs [_this] (rs/->rs mrsb))
(with-row [this mrs row] (rs/with-row mrsb mrs row)) (with-row [_this mrs row] (rs/with-row mrsb mrs row))
(rs! [this mrs] (rs/rs! mrsb mrs)) (rs! [_this mrs] (rs/rs! mrsb mrs))
clojure.lang.ILookup clojure.lang.ILookup
(valAt [this k] (get mrsb k)) (valAt [_this k] (get mrsb k))
(valAt [this k not-found] (get mrsb k not-found)))))) (valAt [_this k not-found] (get mrsb k not-found))))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved ;; copyright (c) 2020-2024 Sean Corfield, all rights reserved
(ns next.jdbc.plan (ns next.jdbc.plan
"Some helper functions that make common operations with `next.jdbc/plan` "Some helper functions that make common operations with `next.jdbc/plan`

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved ;; copyright (c) 2018-2024 Sean Corfield, all rights reserved
(ns next.jdbc.prepare (ns next.jdbc.prepare
"Mostly an implementation namespace for how `PreparedStatement` objects are "Mostly an implementation namespace for how `PreparedStatement` objects are

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved ;; copyright (c) 2018-2025 Sean Corfield, all rights reserved
(ns next.jdbc.protocols (ns next.jdbc.protocols
"This is the extensible core of the next generation java.jdbc library. "This is the extensible core of the next generation java.jdbc library.
@ -38,8 +38,8 @@
`PreparedStatement`, and `Object`, on the assumption that an `Object` can be `PreparedStatement`, and `Object`, on the assumption that an `Object` can be
turned into a `DataSource` and therefore used to get a `Connection`." turned into a `DataSource` and therefore used to get a `Connection`."
(-execute ^clojure.lang.IReduceInit [this sql-params opts] (-execute ^clojure.lang.IReduceInit [this sql-params opts]
"Produce a 'reducible' that, when reduced, executes the SQL and "Produce a 'reducible' that, when reduced (with an initial value), executes
processes the rows of the `ResultSet` directly.") the SQL and processes the rows of the `ResultSet` directly.")
(-execute-one [this sql-params opts] (-execute-one [this sql-params opts]
"Executes the SQL or DDL and produces the first row of the `ResultSet` "Executes the SQL or DDL and produces the first row of the `ResultSet`
as a fully-realized, datafiable hash map (by default).") as a fully-realized, datafiable hash map (by default).")
@ -63,3 +63,15 @@
:extend-via-metadata true :extend-via-metadata true
(-transact [this body-fn opts] (-transact [this body-fn opts]
"Run the `body-fn` inside a transaction.")) "Run the `body-fn` inside a transaction."))
(defprotocol Wrapped
"Protocol for (un)wrapping a `next.jdbc` connectable.
Implementations are provided for `Object` (identity) and `DefaultOptions`
and SQLLogging."
(unwrap [this]
"Unwrap the connectable to get the underlying connectable."))
(extend-protocol Wrapped
Object
(unwrap [this] this))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2023 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
(ns next.jdbc.quoted (ns next.jdbc.quoted
"Provides functions for use with the `:table-fn` and `:column-fn` options "Provides functions for use with the `:table-fn` and `:column-fn` options

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved ;; copyright (c) 2018-2024 Sean Corfield, all rights reserved
(ns next.jdbc.result-set (ns next.jdbc.result-set
"An implementation of `ResultSet` handling functions. "An implementation of `ResultSet` handling functions.
@ -58,6 +58,9 @@
(mapv (fn [^Integer i] (keyword (.getColumnLabel rsmeta i))) (mapv (fn [^Integer i] (keyword (.getColumnLabel rsmeta i)))
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0))))) (range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0)))))
(defn- validate [expr ^String msg]
(when-not expr (throw (IllegalArgumentException. msg))))
(defn get-modified-column-names (defn get-modified-column-names
"Given `ResultSetMetaData`, return a vector of modified column names, each "Given `ResultSetMetaData`, return a vector of modified column names, each
qualified by the table from which it came. qualified by the table from which it came.
@ -66,8 +69,8 @@
[^ResultSetMetaData rsmeta opts] [^ResultSetMetaData rsmeta opts]
(let [qf (:qualifier-fn opts) (let [qf (:qualifier-fn opts)
lf (:label-fn opts)] lf (:label-fn opts)]
(assert qf ":qualifier-fn is required") (validate qf ":qualifier-fn is required")
(assert lf ":label-fn is required") (validate lf ":label-fn is required")
(mapv (fn [^Integer i] (mapv (fn [^Integer i]
(if-let [q (some-> (get-table-name rsmeta i) (qf) (not-empty))] (if-let [q (some-> (get-table-name rsmeta i) (qf) (not-empty))]
(keyword q (-> (.getColumnLabel rsmeta i) (lf))) (keyword q (-> (.getColumnLabel rsmeta i) (lf)))
@ -81,7 +84,7 @@
Requires the `:label-fn` option." Requires the `:label-fn` option."
[^ResultSetMetaData rsmeta opts] [^ResultSetMetaData rsmeta opts]
(let [lf (:label-fn opts)] (let [lf (:label-fn opts)]
(assert lf ":label-fn is required") (validate lf ":label-fn is required")
(mapv (fn [^Integer i] (keyword (lf (.getColumnLabel rsmeta i)))) (mapv (fn [^Integer i] (keyword (lf (.getColumnLabel rsmeta i))))
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0)))))) (range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0))))))
@ -90,7 +93,7 @@
locales where the lower case version of a character is not a valid SQL locales where the lower case version of a character is not a valid SQL
entity name (e.g., Turkish)." entity name (e.g., Turkish)."
[^String s] [^String s]
(.toLowerCase s (Locale/US))) (.toLowerCase s Locale/US))
(defn get-lower-column-names (defn get-lower-column-names
"Given `ResultSetMetaData`, return a vector of lower-case column names, each "Given `ResultSetMetaData`, return a vector of lower-case column names, each
@ -183,37 +186,37 @@
(let [builder (builder-fn rs opts)] (let [builder (builder-fn rs opts)]
(reify (reify
RowBuilder RowBuilder
(->row [this] (->row builder)) (->row [_this] (->row builder))
(column-count [this] (column-count builder)) (column-count [_this] (column-count builder))
(with-column [this row i] (with-column [this row i]
(with-column-value this row (nth (:cols builder) (dec i)) (with-column-value this row (nth (:cols builder) (dec i))
(column-by-index-fn builder rs i))) (column-by-index-fn builder rs i)))
(with-column-value [this row col v] (with-column-value [_this row col v]
(with-column-value builder row col v)) (with-column-value builder row col v))
(row! [this row] (row! builder row)) (row! [_this row] (row! builder row))
ResultSetBuilder ResultSetBuilder
(->rs [this] (->rs builder)) (->rs [_this] (->rs builder))
(with-row [this mrs row] (with-row builder mrs row)) (with-row [_this mrs row] (with-row builder mrs row))
(rs! [this mrs] (rs! builder mrs)) (rs! [_this mrs] (rs! builder mrs))
clojure.lang.ILookup clojure.lang.ILookup
(valAt [this k] (get builder k)) (valAt [_this k] (get builder k))
(valAt [this k not-found] (get builder k not-found)))))) (valAt [_this k not-found] (get builder k not-found))))))
(defrecord MapResultSetBuilder [^ResultSet rs rsmeta cols] (defrecord MapResultSetBuilder [^ResultSet rs rsmeta cols]
RowBuilder RowBuilder
(->row [this] (transient {})) (->row [_this] (transient {}))
(column-count [this] (count cols)) (column-count [_this] (count cols))
(with-column [this row i] (with-column [this row i]
(with-column-value this row (nth cols (dec i)) (with-column-value this row (nth cols (dec i))
(read-column-by-index (.getObject rs ^Integer i) rsmeta i))) (read-column-by-index (.getObject rs ^Integer i) rsmeta i)))
(with-column-value [this row col v] (with-column-value [_this row col v]
(assoc! row col v)) (assoc! row col v))
(row! [this row] (persistent! row)) (row! [_this row] (persistent! row))
ResultSetBuilder ResultSetBuilder
(->rs [this] (transient [])) (->rs [_this] (transient []))
(with-row [this mrs row] (with-row [_this mrs row]
(conj! mrs row)) (conj! mrs row))
(rs! [this mrs] (persistent! mrs))) (rs! [_this mrs] (persistent! mrs)))
(defn as-maps (defn as-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder` "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
@ -275,6 +278,10 @@
:qualifier-fn ->kebab-case :qualifier-fn ->kebab-case
:label-fn ->kebab-case))) :label-fn ->kebab-case)))
(comment
(->kebab-case "_id") ;;=> "id"!!
)
(defn as-unqualified-kebab-maps (defn as-unqualified-kebab-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder` "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
that produces bare vectors of hash map rows, with simple, kebab-case keys." that produces bare vectors of hash map rows, with simple, kebab-case keys."
@ -316,26 +323,26 @@
"An example column-reader that still uses `.getObject` but expands CLOB "An example column-reader that still uses `.getObject` but expands CLOB
columns into strings." columns into strings."
[^ResultSet rs ^ResultSetMetaData _ ^Integer i] [^ResultSet rs ^ResultSetMetaData _ ^Integer i]
(when-let [value (.getObject rs i)] (let [value (.getObject rs i)]
(cond-> value (cond-> value
(instance? Clob value) (instance? Clob value)
(clob->string)))) (clob->string))))
(defrecord ArrayResultSetBuilder [^ResultSet rs rsmeta cols] (defrecord ArrayResultSetBuilder [^ResultSet rs rsmeta cols]
RowBuilder RowBuilder
(->row [this] (transient [])) (->row [_this] (transient []))
(column-count [this] (count cols)) (column-count [_this] (count cols))
(with-column [this row i] (with-column [this row i]
(with-column-value this row nil (with-column-value this row nil
(read-column-by-index (.getObject rs ^Integer i) rsmeta i))) (read-column-by-index (.getObject rs ^Integer i) rsmeta i)))
(with-column-value [this row _ v] (with-column-value [_this row _ v]
(conj! row v)) (conj! row v))
(row! [this row] (persistent! row)) (row! [_this row] (persistent! row))
ResultSetBuilder ResultSetBuilder
(->rs [this] (transient [cols])) (->rs [_this] (transient [cols]))
(with-row [this ars row] (with-row [_this ars row]
(conj! ars row)) (conj! ars row))
(rs! [this ars] (persistent! ars))) (rs! [_this ars] (persistent! ars)))
(defn as-arrays (defn as-arrays
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder` "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
@ -495,30 +502,30 @@
;; marker, just for printing resolution ;; marker, just for printing resolution
InspectableMapifiedResultSet InspectableMapifiedResultSet
(row-number [this] (.getRow rs)) (row-number [_this] (.getRow rs))
(column-names [this] (:cols @builder)) (column-names [_this] (:cols @builder))
(metadata [this] (d/datafy (.getMetaData rs))) (metadata [_this] (d/datafy (.getMetaData rs)))
clojure.lang.IPersistentMap clojure.lang.IPersistentMap
(assoc [this k v] (assoc [_this k v]
(assoc (row-builder @builder) k v)) (assoc (row-builder @builder) k v))
(assocEx [this k v] (assocEx [_this k v]
(.assocEx ^clojure.lang.IPersistentMap (row-builder @builder) k v)) (.assocEx ^clojure.lang.IPersistentMap (row-builder @builder) k v))
(without [this k] (without [_this k]
(dissoc (row-builder @builder) k)) (dissoc (row-builder @builder) k))
java.lang.Iterable ; Java 7 compatible: no forEach / spliterator java.lang.Iterable ; Java 7 compatible: no forEach / spliterator
(iterator [this] (iterator [_this]
(.iterator ^java.lang.Iterable (row-builder @builder))) (.iterator ^java.lang.Iterable (row-builder @builder)))
clojure.lang.Associative clojure.lang.Associative
(containsKey [this k] (containsKey [_this k]
(try (try
(.getObject rs ^String (name-fn k)) (.getObject rs ^String (name-fn k))
true true
(catch SQLException _ (catch SQLException _
false))) false)))
(entryAt [this k] (entryAt [_this k]
(try (try
(clojure.lang.MapEntry. k (read-column-by-label (clojure.lang.MapEntry. k (read-column-by-label
(.getObject rs ^String (name-fn k)) (.getObject rs ^String (name-fn k))
@ -526,28 +533,28 @@
(catch SQLException _))) (catch SQLException _)))
clojure.lang.Counted clojure.lang.Counted
(count [this] (count [_this]
(column-count @builder)) (column-count @builder))
clojure.lang.IPersistentCollection clojure.lang.IPersistentCollection
(cons [this obj] (cons [_this obj]
(let [row (row-builder @builder)] (let [row (row-builder @builder)]
(conj row obj))) (conj row obj)))
(empty [this] (empty [_this]
{}) {})
(equiv [this obj] (equiv [_this obj]
(.equiv ^clojure.lang.IPersistentCollection (row-builder @builder) obj)) (.equiv ^clojure.lang.IPersistentCollection (row-builder @builder) obj))
;; we support get with a numeric key for array-based builders: ;; we support get with a numeric key for array-based builders:
clojure.lang.ILookup clojure.lang.ILookup
(valAt [this k] (valAt [_this k]
(try (try
(if (number? k) (if (number? k)
(let [^Integer i (inc k)] (let [^Integer i (inc k)]
(read-column-by-index (.getObject rs i) (:rsmeta @builder) i)) (read-column-by-index (.getObject rs i) (:rsmeta @builder) i))
(read-column-by-label (.getObject rs ^String (name-fn k)) ^String (name-fn k))) (read-column-by-label (.getObject rs ^String (name-fn k)) ^String (name-fn k)))
(catch SQLException _))) (catch SQLException _)))
(valAt [this k not-found] (valAt [_this k not-found]
(try (try
(if (number? k) (if (number? k)
(let [^Integer i (inc k)] (let [^Integer i (inc k)]
@ -558,12 +565,12 @@
;; we support nth for array-based builders (i is primitive int here!): ;; we support nth for array-based builders (i is primitive int here!):
clojure.lang.Indexed clojure.lang.Indexed
(nth [this i] (nth [_this i]
(try (try
(let [i (inc i)] (let [i (inc i)]
(read-column-by-index (.getObject rs i) (:rsmeta @builder) i)) (read-column-by-index (.getObject rs i) (:rsmeta @builder) i))
(catch SQLException _))) (catch SQLException _)))
(nth [this i not-found] (nth [_this i not-found]
(try (try
(let [i (inc i)] (let [i (inc i)]
(read-column-by-index (.getObject rs i) (:rsmeta @builder) i)) (read-column-by-index (.getObject rs i) (:rsmeta @builder) i))
@ -571,11 +578,11 @@
not-found))) not-found)))
clojure.lang.Seqable clojure.lang.Seqable
(seq [this] (seq [_this]
(seq (row-builder @builder))) (seq (row-builder @builder)))
DatafiableRow DatafiableRow
(datafiable-row [this connectable opts] (datafiable-row [_this connectable opts]
;; since we have to call these eagerly, we trap any exceptions so ;; since we have to call these eagerly, we trap any exceptions so
;; that they can be thrown when the actual functions are called ;; that they can be thrown when the actual functions are called
(let [row (try (.getRow rs) (catch Throwable t t)) (let [row (try (.getRow rs) (catch Throwable t t))
@ -624,9 +631,7 @@
;; in reality, this is going to be over-optimistic and will like cause `nav` ;; in reality, this is going to be over-optimistic and will like cause `nav`
;; to fail on attempts to navigate into result sets that are not hash maps ;; to fail on attempts to navigate into result sets that are not hash maps
(datafiable-row [this connectable opts] (datafiable-row [this connectable opts]
(vary-meta (vary-meta this assoc
this
assoc
`core-p/datafy (navize-row connectable opts) `core-p/datafy (navize-row connectable opts)
`core-p/nav (navable-row connectable opts)))) `core-p/nav (navable-row connectable opts))))
@ -953,14 +958,14 @@
(reify (reify
clojure.lang.IReduceInit clojure.lang.IReduceInit
(reduce [_ f init] (reduce [_ f init]
(reduce-stmt this f init (assoc opts :return-keys true))) (reduce-stmt this f init (merge {:return-keys true} opts)))
r/CollFold r/CollFold
(coll-fold [_ n combinef reducef] (coll-fold [_ n combinef reducef]
(fold-stmt this n combinef reducef (.getConnection this) (fold-stmt this n combinef reducef (.getConnection this)
(assoc opts :return-keys true))) (merge {:return-keys true} opts)))
(toString [_] "`IReduceInit` from `plan` -- missing reduction?"))) (toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
(-execute-one [this _ opts] (-execute-one [this _ opts]
(if-let [rs (stmt->result-set this (assoc opts :return-keys true))] (if-let [rs (stmt->result-set this (merge {:return-keys true} opts))]
(let [builder-fn (get opts :builder-fn as-maps) (let [builder-fn (get opts :builder-fn as-maps)
builder (builder-fn rs opts)] builder (builder-fn rs opts)]
(when (.next rs) (when (.next rs)
@ -971,16 +976,16 @@
(if (:multi-rs opts) (if (:multi-rs opts)
(loop [go (.execute this) acc []] (loop [go (.execute this) acc []]
(if-let [rs (stmt->result-set-update-count (if-let [rs (stmt->result-set-update-count
(.getConnection this) this go (assoc opts :return-keys true))] (.getConnection this) this go (merge {:return-keys true} opts))]
(recur (.getMoreResults this) (conj acc rs)) (recur (.getMoreResults this) (conj acc rs))
acc)) acc))
(if-let [rs (stmt->result-set this (assoc opts :return-keys true))] (if-let [rs (stmt->result-set this (merge {:return-keys true} opts))]
(datafiable-result-set rs (.getConnection this) opts) (datafiable-result-set rs (.getConnection this) opts)
[{:next.jdbc/update-count (.getUpdateCount this)}]))) [{:next.jdbc/update-count (.getUpdateCount this)}])))
java.sql.Statement java.sql.Statement
(-execute [this sql-params opts] (-execute [this sql-params opts]
(assert (= 1 (count sql-params)) (validate (= 1 (count sql-params))
"Parameters cannot be provided when executing a non-prepared Statement") "Parameters cannot be provided when executing a non-prepared Statement")
(reify (reify
clojure.lang.IReduceInit clojure.lang.IReduceInit
@ -992,7 +997,7 @@
(.getConnection this) opts)) (.getConnection this) opts))
(toString [_] "`IReduceInit` from `plan` -- missing reduction?"))) (toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
(-execute-one [this sql-params opts] (-execute-one [this sql-params opts]
(assert (= 1 (count sql-params)) (validate (= 1 (count sql-params))
"Parameters cannot be provided when executing a non-prepared Statement") "Parameters cannot be provided when executing a non-prepared Statement")
(if-let [rs (stmt-sql->result-set this (first sql-params))] (if-let [rs (stmt-sql->result-set this (first sql-params))]
(let [builder-fn (get opts :builder-fn as-maps) (let [builder-fn (get opts :builder-fn as-maps)
@ -1002,12 +1007,12 @@
(.getConnection this) opts))) (.getConnection this) opts)))
{:next.jdbc/update-count (.getUpdateCount this)})) {:next.jdbc/update-count (.getUpdateCount this)}))
(-execute-all [this sql-params opts] (-execute-all [this sql-params opts]
(assert (= 1 (count sql-params)) (validate (= 1 (count sql-params))
"Parameters cannot be provided when executing a non-prepared Statement") "Parameters cannot be provided when executing a non-prepared Statement")
(if (:multi-rs opts) (if (:multi-rs opts)
(loop [go (.execute this (first sql-params)) acc []] (loop [go (.execute this (first sql-params)) acc []]
(if-let [rs (stmt->result-set-update-count (if-let [rs (stmt->result-set-update-count
(.getConnection this) this go (assoc opts :return-keys true))] (.getConnection this) this go (merge {:return-keys true} opts))]
(recur (.getMoreResults this) (conj acc rs)) (recur (.getMoreResults this) (conj acc rs))
acc)) acc))
(if-let [rs (stmt-sql->result-set this (first sql-params))] (if-let [rs (stmt-sql->result-set this (first sql-params))]
@ -1025,12 +1030,61 @@
(defn- default-schema (defn- default-schema
"The default schema lookup rule for column names. "The default schema lookup rule for column names.
If a column name ends with `_id` or `id`, it is assumed to be a foreign key We have a foreign key column suffix convention of `<table><fk>` or
into the table identified by the first part of the column name." `<table>_<fk>`, which maps to a (primary) key in the `<table` called
[col] `<pk>`.
(let [[_ table] (re-find #"(?i)^(.+?)_?id$" (name col))]
By default, both `<fk>` and `<pk>` are assumed to be `id`. That can be
overridden by the `:schema-opts` hash map in the options:
* `:fk-suffix` -- the suffix for foreign key columns, default `id`
* `:pk` -- the (primary) key column name, default `id`
* `:pk-fn` -- a function to apply to the table name and the value of `:pk`
to get the (primary) key column name, default `(constantly <pk>)`."
[opts col]
(let [fk-suffix (get-in opts [:schema-opts :fk-suffix] "id")
pk (get-in opts [:schema-opts :pk] "id")
pk-fn (get-in opts [:schema-opts :pk-fn] (constantly (name pk)))
[_ table] (re-find (re-pattern (str "(?i)^(.+?)[-_]?"
(name fk-suffix)
"$"))
(name col))]
(when table (when table
[(keyword table) :id]))) [(keyword table) (keyword (pk-fn table pk))])))
(comment
(default-schema {} :userstatusid)
(default-schema {} :userstatus_id)
(default-schema {} :user_statusid)
(default-schema {:schema-opts {:fk-suffix "did"}} :user_id)
(default-schema {:schema-opts {:fk-suffix "did"}} :user_did)
(default-schema {:schema-opts {:fk-suffix "did"}} :user-did)
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user_id)
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user_did)
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user-did)
(default-schema {:schema-opts {:fk-suffix "(did|id)"
:pk :did}} :user_did)
(default-schema {:schema-opts {:fk-suffix "(did|id)"
:pk :did
:pk-fn (fn [table pk]
(if (= "user" table)
"id"
pk))}}
:user_did)
(default-schema {:schema-opts {:fk-suffix "(did|id)"
:pk :did
:pk-fn (fn [table pk]
(if (= "user" table)
"id"
pk))}}
:user-did)
(default-schema {:schema-opts {:fk-suffix "(did|id)"
:pk :did
:pk-fn (fn [table pk]
(if (= "user" table)
"id"
pk))}}
:book_did)
)
(defn- expand-schema (defn- expand-schema
"Given a (possibly nil) schema entry, return it expanded to a triple of: "Given a (possibly nil) schema entry, return it expanded to a triple of:
@ -1074,48 +1128,10 @@
"Given a connectable object, return a function that knows how to turn a row "Given a connectable object, return a function that knows how to turn a row
into a `nav`igable object. into a `nav`igable object.
A `:schema` option can provide a map from qualified column names See navable-row below for more details."
(`:<table>/<column>`) to tuples that indicate for which table they are a
foreign key, the name of the key within that table, and (optionality) the
cardinality of that relationship (`:many`, `:one`).
If no `:schema` item is provided for a column, the convention of `<table>id` or
`<table>_id` is used, and the assumption is that such columns are foreign keys
in the `<table>` portion of their name, the key is called `id`, and the
cardinality is `:one`.
Rows are looked up using `-execute-all` or `-execute-one`, and the `:table-fn`
option, if provided, is applied to the assumed table name and `:column-fn` if
provided to the assumed foreign key column name."
[connectable opts] [connectable opts]
(fn [row] (fn [row]
(vary-meta (vary-meta row assoc `core-p/nav (navable-row connectable opts))))
row
assoc
`core-p/nav (fn [_ k v]
(try
(let [[table fk cardinality]
(expand-schema k (or (get-in opts [:schema k])
(default-schema k)))]
(if (and fk connectable)
(let [table-fn (:table-fn opts identity)
column-fn (:column-fn opts identity)
exec-fn! (if (= :many cardinality)
p/-execute-all
p/-execute-one)]
(exec-fn! connectable
[(str "SELECT * FROM "
(table-fn (name table))
" WHERE "
(column-fn (name fk))
" = ?")
v]
opts))
v))
(catch Exception _
;; assume an exception means we just cannot
;; navigate anywhere, so return just the value
v))))))
(defn- navable-row (defn- navable-row
"Given a connectable object, return a function that knows how to `nav` "Given a connectable object, return a function that knows how to `nav`
@ -1131,6 +1147,8 @@
in the `<table>` portion of their name, the key is called `id`, and the in the `<table>` portion of their name, the key is called `id`, and the
cardinality is `:one`. cardinality is `:one`.
That convention can in turn be modified via the `:schema-opts` option.
Rows are looked up using `-execute-all` or `-execute-one`, and the `:table-fn` Rows are looked up using `-execute-all` or `-execute-one`, and the `:table-fn`
option, if provided, is applied to the assumed table name and `:column-fn` if option, if provided, is applied to the assumed table name and `:column-fn` if
provided to the assumed foreign key column name." provided to the assumed foreign key column name."
@ -1139,7 +1157,7 @@
(try (try
(let [[table fk cardinality] (let [[table fk cardinality]
(expand-schema k (or (get-in opts [:schema k]) (expand-schema k (or (get-in opts [:schema k])
(default-schema k)))] (default-schema opts k)))]
(if (and fk connectable) (if (and fk connectable)
(let [table-fn (:table-fn opts identity) (let [table-fn (:table-fn opts identity)
column-fn (:column-fn opts identity) column-fn (:column-fn opts identity)

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
(ns next.jdbc.specs (ns next.jdbc.specs
"Specs for the core API of next.jdbc. "Specs for the core API of next.jdbc.
@ -110,7 +110,7 @@
:opts (s/? ::opts-map))) :opts (s/? ::opts-map)))
(s/fdef jdbc/prepare (s/fdef jdbc/prepare
:args (s/cat :connection ::connection :args (s/cat :connection ::proto-connectable
:sql-params ::sql-params :sql-params ::sql-params
:opts (s/? ::opts-map))) :opts (s/? ::opts-map)))
@ -154,7 +154,26 @@
:args (s/cat :binding (s/and vector? :args (s/cat :binding (s/and vector?
(s/cat :sym simple-symbol? (s/cat :sym simple-symbol?
:transactable ::transactable :transactable ::transactable
:opts (s/? ::opts-map))) :opts (s/? any?)))
:body (s/* any?)))
(s/fdef jdbc/with-transaction+options
:args (s/cat :binding (s/and vector?
(s/cat :sym simple-symbol?
:transactable ::transactable
:opts (s/? any?)))
:body (s/* any?)))
(s/fdef jdbc/on-connection
:args (s/cat :binding (s/and vector?
(s/cat :sym simple-symbol?
:connectable ::connectable))
:body (s/* any?)))
(s/fdef jdbc/on-connection+options
:args (s/cat :binding (s/and vector?
(s/cat :sym simple-symbol?
:connectable ::connectable))
:body (s/* any?))) :body (s/* any?)))
(s/fdef connection/->pool (s/fdef connection/->pool
@ -186,10 +205,10 @@
:with-rows-and-columns :with-rows-and-columns
(s/and (s/cat :connectable ::connectable (s/and (s/cat :connectable ::connectable
:table keyword? :table keyword?
:cols (s/coll-of keyword? :cols (s/coll-of keyword? :kind sequential?)
:rows (s/coll-of (s/coll-of any?
:kind sequential? :kind sequential?
:min-count 1) :min-count 1)
:rows (s/coll-of (s/coll-of any? :kind sequential?)
:kind sequential?) :kind sequential?)
:opts (s/? ::opts-map)) :opts (s/? ::opts-map))
#(apply = (count (:cols %)) #(apply = (count (:cols %))
@ -197,9 +216,7 @@
:with-hash-maps :with-hash-maps
(s/cat :connectable ::connectable (s/cat :connectable ::connectable
:table keyword? :table keyword?
:hash-maps (s/coll-of map? :hash-maps (s/coll-of map? :kind sequential?)
:kind sequential?
:min-count 1)
:opts (s/? ::opts-map)))) :opts (s/? ::opts-map))))
(s/fdef sql/query (s/fdef sql/query
@ -215,6 +232,15 @@
:all #{:all}) :all #{:all})
:opts (s/? ::opts-map))) :opts (s/? ::opts-map)))
(s/fdef sql/aggregate-by-keys
:args (s/cat :connectable ::connectable
:table keyword?
:aggregate string?
:key-map (s/or :example ::example-map
:where ::sql-params
:all #{:all})
:opts (s/? ::opts-map)))
(s/fdef sql/get-by-id (s/fdef sql/get-by-id
:args (s/alt :with-id (s/cat :connectable ::connectable :args (s/alt :with-id (s/cat :connectable ::connectable
:table keyword? :table keyword?

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2022 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
(ns next.jdbc.sql (ns next.jdbc.sql
"Some utility functions that make common operations easier by "Some utility functions that make common operations easier by
@ -21,10 +21,11 @@
In addition, `find-by-keys` supports `:order-by` to add an `ORDER BY` In addition, `find-by-keys` supports `:order-by` to add an `ORDER BY`
clause to the generated SQL." clause to the generated SQL."
(:require [next.jdbc :refer [execute! execute-one! execute-batch!]] (:require [clojure.string :as str]
[next.jdbc :refer [execute! execute-batch! execute-one!]]
[next.jdbc.sql.builder [next.jdbc.sql.builder
:refer [for-delete for-insert for-insert-multi :refer [for-delete for-insert for-insert-multi for-query
for-query for-update]])) for-update]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -52,8 +53,8 @@
generated keys. generated keys.
Given a connectable object, a table name, a sequence of hash maps of data, Given a connectable object, a table name, a sequence of hash maps of data,
inserts the data as multiple rows in the database and attempts to return which all have the same set of keys, inserts the data as multiple rows in
a vector of maps of generated keys. the database and attempts to return a vector of maps of generated keys.
If called with `:batch` true will call `execute-batch!` - see its documentation If called with `:batch` true will call `execute-batch!` - see its documentation
for situations in which the generated keys may or may not be returned as well as for situations in which the generated keys may or may not be returned as well as
@ -78,7 +79,9 @@
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
"insert-multi! hash maps must all have the same keys"))) "insert-multi! hash maps must all have the same keys")))
(insert-multi! connectable table cols (map ->row hash-maps-or-cols) opts-or-rows)) (insert-multi! connectable table cols (map ->row hash-maps-or-cols) opts-or-rows))
(insert-multi! connectable table hash-maps-or-cols opts-or-rows {}))) (if (map? opts-or-rows)
(insert-multi! connectable table hash-maps-or-cols [] opts-or-rows)
(insert-multi! connectable table hash-maps-or-cols opts-or-rows {}))))
([connectable table cols rows opts] ([connectable table cols rows opts]
(if (seq rows) (if (seq rows)
(let [opts (merge (:options connectable) opts) (let [opts (merge (:options connectable) opts)
@ -136,6 +139,42 @@
(let [opts (merge (:options connectable) opts)] (let [opts (merge (:options connectable) opts)]
(execute! connectable (for-query table key-map opts) opts)))) (execute! connectable (for-query table key-map opts) opts))))
(defn aggregate-by-keys
"A wrapper over `find-by-keys` that additionally takes an aggregate SQL
expression (a string), and returns just a single result: the value of that
of that aggregate for the matching rows.
Accepts all the same options as `find-by-keys` except `:columns` since that
is used internally by this wrapper to pass the aggregate expression in."
([connectable table aggregate key-map]
(aggregate-by-keys connectable table aggregate key-map {}))
([connectable table aggregate key-map opts]
(let [opts (merge (:options connectable) opts)
_
(when-not (string? aggregate)
(throw (IllegalArgumentException.
"aggregate-by-keys requires a string aggregate expression")))
_
(when (:columns opts)
(throw (IllegalArgumentException.
"aggregate-by-keys does not support the :columns option")))
;; this should be unique enough as an alias to never clash with
;; a real column name in anyone's tables -- in addition it is
;; stable for a given aggregate expression so it should allow
;; for query caching in the JDBC driver:
;; (we use abs to avoid negative hash codes which would produce
;; a hyphen in the alias name which is not valid in SQL identifiers)
total-name (str "next_jdbc_aggregate_"
(Math/abs (.hashCode ^String aggregate)))
total-column (keyword total-name)
;; because some databases return uppercase column names:
total-col-u (keyword (str/upper-case total-name))]
(-> (find-by-keys connectable table key-map
(assoc opts :columns [[aggregate total-column]]))
(first)
(as-> row (or (get row total-column) (get row total-col-u)))))))
(defn get-by-id (defn get-by-id
"Syntactic sugar over `execute-one!` to make certain common queries easier. "Syntactic sugar over `execute-one!` to make certain common queries easier.

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2022 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
(ns next.jdbc.sql.builder (ns next.jdbc.sql.builder
"Some utility functions for building SQL strings. "Some utility functions for building SQL strings.
@ -70,6 +70,9 @@
[key-map opts] [key-map opts]
(as-cols (keys key-map) opts)) (as-cols (keys key-map) opts))
(defn- validate [expr ^String msg]
(when-not expr (throw (IllegalArgumentException. msg))))
(defn by-keys (defn by-keys
"Given a hash map of column names and values and a clause type "Given a hash map of column names and values and a clause type
(`:set`, `:where`), return a vector of a SQL clause and its parameters. (`:set`, `:where`), return a vector of a SQL clause and its parameters.
@ -84,7 +87,7 @@
[(conj conds (str e " = ?")) (conj params v)]))) [(conj conds (str e " = ?")) (conj params v)])))
[[] []] [[] []]
key-map)] key-map)]
(assert (seq where) "key-map may not be empty") (validate (seq where) "key-map may not be empty")
(into [(str (str/upper-case (safe-name clause)) " " (into [(str (str/upper-case (safe-name clause)) " "
(str/join (if (= :where clause) " AND " ", ") where))] (str/join (if (= :where clause) " AND " ", ") where))]
params))) params)))
@ -122,7 +125,7 @@
(let [entity-fn (:table-fn opts identity) (let [entity-fn (:table-fn opts identity)
params (as-keys key-map opts) params (as-keys key-map opts)
places (as-? key-map opts)] places (as-? key-map opts)]
(assert (seq key-map) "key-map may not be empty") (validate (seq key-map) "key-map may not be empty")
(into [(str "INSERT INTO " (entity-fn (safe-name table)) (into [(str "INSERT INTO " (entity-fn (safe-name table))
" (" params ")" " (" params ")"
" VALUES (" places ")" " VALUES (" places ")"
@ -144,11 +147,11 @@
If `:suffix` is provided in `opts`, that string is appended to the If `:suffix` is provided in `opts`, that string is appended to the
`INSERT ...` statement." `INSERT ...` statement."
[table cols rows opts] [table cols rows opts]
(assert (apply = (count cols) (map count rows)) (validate (apply = (count cols) (map count rows))
"column counts are not consistent across cols and rows") "column counts are not consistent across cols and rows")
;; to avoid generating bad SQL ;; to avoid generating bad SQL
(assert (seq cols) "cols may not be empty") (validate (seq cols) "cols may not be empty")
(assert (seq rows) "rows may not be empty") (validate (seq rows) "rows may not be empty")
(let [table-fn (:table-fn opts identity) (let [table-fn (:table-fn opts identity)
batch? (:batch opts) batch? (:batch opts)
params (as-cols cols opts) params (as-cols cols opts)
@ -195,7 +198,7 @@
[order-by opts] [order-by opts]
(when-not (vector? order-by) (when-not (vector? order-by)
(throw (IllegalArgumentException. ":order-by must be a vector"))) (throw (IllegalArgumentException. ":order-by must be a vector")))
(assert (seq order-by) ":order-by may not be empty") (validate (seq order-by) ":order-by may not be empty")
(str "ORDER BY " (str "ORDER BY "
(str/join ", " (map #(for-order-col % opts) order-by)))) (str/join ", " (map #(for-order-col % opts) order-by))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2021-2023 Sean Corfield, all rights reserved ;; copyright (c) 2021-2025 Sean Corfield, all rights reserved
(ns ^:no-doc next.jdbc.sql-logging (ns ^:no-doc next.jdbc.sql-logging
"Implementation of sql-logging logic." "Implementation of sql-logging logic."
@ -8,6 +8,10 @@
(defrecord SQLLogging [connectable sql-logger result-logger options]) (defrecord SQLLogging [connectable sql-logger result-logger options])
(extend-protocol p/Wrapped
SQLLogging
(unwrap [this] (p/unwrap (:connectable this))))
(extend-protocol p/Sourceable (extend-protocol p/Sourceable
SQLLogging SQLLogging
(get-datasource [this] (get-datasource [this]

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved ;; copyright (c) 2018-2024 Sean Corfield, all rights reserved
(ns next.jdbc.transaction (ns next.jdbc.transaction
"Implementation of SQL transaction logic. "Implementation of SQL transaction logic.
@ -38,7 +38,7 @@
:allow) :allow)
(defonce ^:private ^:dynamic ^{:doc "Used to detect nested transactions."} (defonce ^:private ^:dynamic ^{:doc "Used to detect nested transactions."}
*active-tx* false) *active-tx* #{})
(def ^:private isolation-levels (def ^:private isolation-levels
"Transaction isolation levels." "Transaction isolation levels."
@ -112,17 +112,26 @@
(.setReadOnly con old-readonly) (.setReadOnly con old-readonly)
(catch Exception _)))))))) (catch Exception _))))))))
(defn- raw-connection ^Connection [^Connection con]
(try ; because some drivers do not implement this :(
(if (.isWrapperFor con Connection)
(.unwrap con Connection)
con)
(catch Throwable _ ; to catch AbstractMethodError :(
con)))
(extend-protocol p/Transactable (extend-protocol p/Transactable
java.sql.Connection java.sql.Connection
(-transact [this body-fn opts] (-transact [this body-fn opts]
(let [raw (raw-connection this)]
(cond (cond
(and (not *active-tx*) (= :ignore *nested-tx*)) (and (not (contains? *active-tx* raw)) (= :ignore *nested-tx*))
;; #245 do not lock when in c.j.j compatibility mode: ;; #245 do not lock when in c.j.j compatibility mode:
(binding [*active-tx* true] (binding [*active-tx* (conj *active-tx* raw)]
(transact* this body-fn opts)) (transact* this body-fn opts))
(or (not *active-tx*) (= :allow *nested-tx*)) (or (not (contains? *active-tx* raw)) (= :allow *nested-tx*))
(locking this (locking this
(binding [*active-tx* true] (binding [*active-tx* (conj *active-tx* raw)]
(transact* this body-fn opts))) (transact* this body-fn opts)))
(= :ignore *nested-tx*) (= :ignore *nested-tx*)
(body-fn this) (body-fn this)
@ -132,23 +141,15 @@
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "*nested-tx* (" (str "*nested-tx* ("
*nested-tx* *nested-tx*
") was not :allow, :ignore, or :prohibit"))))) ") was not :allow, :ignore, or :prohibit"))))))
javax.sql.DataSource javax.sql.DataSource
(-transact [this body-fn opts] (-transact [this body-fn opts]
(cond (or (not *active-tx*) (= :allow *nested-tx*))
(binding [*active-tx* true]
(with-open [con (p/get-connection this opts)] (with-open [con (p/get-connection this opts)]
(transact* con body-fn opts))) ;; this connection is assumed unique so we do not need the active-tx check:
(= :ignore *nested-tx*) (let [raw (raw-connection con)]
(with-open [con (p/get-connection this opts)] ;; we don't lock either, per #293:
(body-fn con)) (binding [*active-tx* (conj *active-tx* raw)]
(= :prohibit *nested-tx*) (transact* con body-fn opts)))))
(throw (IllegalStateException. "Nested transactions are prohibited"))
:else
(throw (IllegalArgumentException.
(str "*nested-tx* ("
*nested-tx*
") was not :allow, :ignore, or :prohibit")))))
Object Object
(-transact [this body-fn opts] (-transact [this body-fn opts]
(p/-transact (p/get-datasource this) body-fn opts))) (p/-transact (p/get-datasource this) body-fn opts)))

View file

@ -1,11 +1,11 @@
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved ;; copyright (c) 2018-2024 Sean Corfield, all rights reserved
(ns next.jdbc.types (ns next.jdbc.types
"Provides convenience functions for wrapping values you pass into SQL "Provides convenience functions for wrapping values you pass into SQL
operations that have per-instance implementations of `SettableParameter` operations that have per-instance implementations of `SettableParameter`
so that `.setObject()` is called with the appropriate `java.sql.Types` value." so that `.setObject()` is called with the appropriate `java.sql.Types` value."
(:require [clojure.string :as str] (:require [clojure.string :as str]
[next.jdbc.prepare :as prep]) [next.jdbc.prepare])
(:import (java.lang.reflect Field Modifier) (:import (java.lang.reflect Field Modifier)
(java.sql PreparedStatement))) (java.sql PreparedStatement)))

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>next.jdbc</name>
<description>The next generation of clojure.java.jdbc: a new low-level Clojure wrapper for JDBC-based access to databases.</description>
<url>https://github.com/seancorfield/next-jdbc</url>
<licenses>
<license>
<name>Eclipse Public License</name>
<url>http://www.eclipse.org/legal/epl-v10.html</url>
</license>
</licenses>
<developers>
<developer>
<name>Sean Corfield</name>
</developer>
</developers>
<scm>
<url>https://github.com/seancorfield/next-jdbc</url>
<connection>scm:git:git://github.com/seancorfield/next-jdbc.git</connection>
<developerConnection>scm:git:ssh://git@github.com/seancorfield/next-jdbc.git</developerConnection>
</scm>
</project>

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.connection-string-test (ns next.jdbc.connection-string-test
"Tests for the main hash map spec to JDBC URL logic and the get-datasource "Tests for the main hash map spec to JDBC URL logic and the get-datasource
@ -7,15 +7,17 @@
At some point, the datasource/connection tests should probably be extended At some point, the datasource/connection tests should probably be extended
to accept EDN specs from an external source (environment variables?)." to accept EDN specs from an external source (environment variables?)."
(:require [clojure.string :as str] (:require [clojure.string :as str]
[clojure.test :refer [deftest is testing use-fixtures]] [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc.connection :as c] [next.jdbc.connection :as c]
[next.jdbc.protocols :as p] [next.jdbc.protocols :as p]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures :refer [with-test-db db]])) [next.jdbc.test-fixtures :refer [db with-test-db]])
(:import [java.util Properties]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (set-ns-context! [(around [f] (with-test-db f))])
(specs/instrument) (specs/instrument)
@ -39,3 +41,16 @@
(when (and user password) (when (and user password)
(with-open [con (p/get-connection ds {})] (with-open [con (p/get-connection ds {})]
(is (instance? java.sql.Connection con))))))) (is (instance? java.sql.Connection con)))))))
(deftest property-tests
(is (string? (.getProperty ^Properties (#'c/as-properties {:foo [42]}) "foo")))
(is (string? (.get ^Properties (#'c/as-properties {:foo [42]}) "foo")))
(is (vector? (.get ^Properties (#'c/as-properties
{:foo [42]
:next.jdbc/as-is-properties [:foo]})
"foo")))
;; because .getProperty drops non-string values!
(is (nil? (.getProperty ^Properties (#'c/as-properties
{:foo [42]
:next.jdbc/as-is-properties [:foo]})
"foo"))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.connection-test (ns next.jdbc.connection-test
"Tests for the main hash map spec to JDBC URL logic and the get-datasource "Tests for the main hash map spec to JDBC URL logic and the get-datasource
@ -7,7 +7,7 @@
At some point, the datasource/connection tests should probably be extended At some point, the datasource/connection tests should probably be extended
to accept EDN specs from an external source (environment variables?)." to accept EDN specs from an external source (environment variables?)."
(:require [clojure.string :as str] (:require [clojure.string :as str]
[clojure.test :refer [deftest is testing]] [lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc.connection :as c] [next.jdbc.connection :as c]
[next.jdbc.protocols :as p]) [next.jdbc.protocols :as p])
(:import (com.zaxxer.hikari HikariDataSource) (:import (com.zaxxer.hikari HikariDataSource)

View file

@ -1,21 +1,22 @@
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved ;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
(ns next.jdbc.datafy-test (ns next.jdbc.datafy-test
"Tests for the datafy extensions over JDBC types." "Tests for the datafy extensions over JDBC types."
(:require [clojure.datafy :as d] (:require [clojure.datafy :as d]
[clojure.set :as set] [clojure.set :as set]
[clojure.test :refer [deftest is testing use-fixtures]] [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.datafy] [next.jdbc.datafy]
[next.jdbc.result-set :as rs] [next.jdbc.result-set :as rs]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db db ds :refer [db derby? ds jtds? mysql? postgres? sqlite? with-test-db
derby? jtds? mysql? postgres? sqlite?]])) xtdb?]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (set-ns-context! [(around [f] (with-test-db f))])
(specs/instrument) (specs/instrument)
@ -83,6 +84,26 @@
:rowIdLifetime/exception)) :rowIdLifetime/exception))
(postgres?) (-> (disj :rowIdLifetime) (postgres?) (-> (disj :rowIdLifetime)
(conj :rowIdLifetime/exception)) (conj :rowIdLifetime/exception))
(xtdb?) (-> (disj :clientInfoProperties
:defaultTransactionIsolation
:maxCatalogNameLength
:maxColumnNameLength
:maxCursorNameLength
:maxProcedureNameLength
:maxSchemaNameLength
:maxTableNameLength
:maxUserNameLength
:rowIdLifetime)
(conj :clientInfoProperties/exception
:defaultTransactionIsolation/exception
:maxCatalogNameLength/exception
:maxColumnNameLength/exception
:maxCursorNameLength/exception
:maxProcedureNameLength/exception
:maxSchemaNameLength/exception
:maxTableNameLength/exception
:maxUserNameLength/exception
:rowIdLifetime/exception))
(sqlite?) (-> (disj :clientInfoProperties :rowIdLifetime) (sqlite?) (-> (disj :clientInfoProperties :rowIdLifetime)
(conj :clientInfoProperties/exception (conj :clientInfoProperties/exception
:rowIdLifetime/exception))) :rowIdLifetime/exception)))
@ -97,7 +118,8 @@
(let [data (d/datafy (.getMetaData con))] (let [data (d/datafy (.getMetaData con))]
(doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo} (doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo}
(jtds?) (disj :clientInfoProperties) (jtds?) (disj :clientInfoProperties)
(sqlite?) (disj :clientInfoProperties))] (sqlite?) (disj :clientInfoProperties)
(xtdb?) (disj :clientInfoProperties))]
(let [rs (d/nav data k nil)] (let [rs (d/nav data k nil)]
(is (vector? rs)) (is (vector? rs))
(is (every? map? rs)))))))) (is (every? map? rs))))))))
@ -122,4 +144,5 @@
(.execute ps) (.execute ps)
(.getResultSet ps) (.getResultSet ps)
(.close ps) (.close ps)
(.close con)) (.close con)
)

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.date-time-test (ns next.jdbc.date-time-test
"Date/time parameter auto-conversion tests. "Date/time parameter auto-conversion tests.
@ -6,21 +6,22 @@
These tests contain no assertions. Without requiring `next.jdbc.date-time` These tests contain no assertions. Without requiring `next.jdbc.date-time`
several of the `insert` operations would throw exceptions for some databases several of the `insert` operations would throw exceptions for some databases
so the test here just checks those operations 'succeed'." so the test here just checks those operations 'succeed'."
(:require [clojure.test :refer [deftest is testing use-fixtures]] (:require [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest]]
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.date-time] ; to extend SettableParameter to date/time [next.jdbc.date-time] ; to extend SettableParameter to date/time
[next.jdbc.test-fixtures :refer [with-test-db db ds [next.jdbc.test-fixtures :refer [with-test-db ds
mssql?]] mssql? xtdb?]]
[next.jdbc.specs :as specs]) [next.jdbc.specs :as specs]))
(:import (java.sql ResultSet)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (set-ns-context! [(around [f] (with-test-db f))])
(specs/instrument) (specs/instrument)
(deftest issue-73 (deftest issue-73
(when-not (xtdb?)
(try (try
(jdbc/execute-one! (ds) ["drop table fruit_time"]) (jdbc/execute-one! (ds) ["drop table fruit_time"])
(catch Throwable _)) (catch Throwable _))
@ -46,4 +47,4 @@
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)]) (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)])
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 2 (java.time.Instant/now)]) (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 2 (java.time.Instant/now)])
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 3 (java.time.LocalDate/now)]) (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 3 (java.time.LocalDate/now)])
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)])) (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)])))

View file

@ -1,9 +1,8 @@
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved ;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
(ns next.jdbc.default-options-test (ns next.jdbc.default-options-test
"Stub test namespace for default options. Nothing can really be tested "Stub test namespace for default options. Nothing can really be tested
at this level tho'..." at this level tho'..."
(:require [clojure.test :refer [deftest is testing]] (:require [next.jdbc.default-options]))
[next.jdbc.default-options :refer :all]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)

View file

@ -0,0 +1,55 @@
;; copyright (c) 2024-2025 Sean Corfield, all rights reserved
(ns next.jdbc.defer-test
"The idea behind the next.jdbc.defer namespace is to provide a
way to defer the execution of a series of SQL statements until
a later time, but still provide a way for inserted keys to be
used in later SQL statements.
The principle is to provide a core subset of the next.jdbc
and next.jdbc.sql API that produces a data structure that
describes a series of SQL operations to be performed, that
are held in a dynamic var, and that can be executed at a
later time, in a transaction."
(:require [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc :as jdbc]
[next.jdbc.defer :as sut]
[next.jdbc.test-fixtures
:refer [ds with-test-db xtdb?]]))
(set! *warn-on-reflection* true)
(set-ns-context! [(around [f] (with-test-db f))])
(deftest basic-test
(when-not (xtdb?)
(testing "data structures"
(is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"]
:key-fn :GENERATED_KEY
:key :id
:opts {:key-fn :GENERATED_KEY :key :id}}]
@(sut/defer-ops
#(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id})))))
(testing "execution"
(let [effects (sut/with-deferred (ds)
(sut/insert! :fruit {:name "Mango"} {:key :test}))]
(is (= {:test 1} @effects))
(is (= 1 (count (jdbc/execute! (ds)
["select * from fruit where name = ?"
"Mango"])))))
(let [effects (sut/with-deferred (ds)
(sut/insert! :fruit {:name "Dragonfruit"} {:key :test})
(sut/update! :fruit {:cost 123} {:name "Dragonfruit"})
(sut/delete! :fruit {:name "Dragonfruit"}))]
(is (= {:test 1} @effects))
(is (= 0 (count (jdbc/execute! (ds)
["select * from fruit where name = ?"
"Dragonfruit"])))))
(let [effects (sut/with-deferred (ds)
(sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))]
(is (= :failed (try @effects
(catch Exception _ :failed))))
(is (= 0 (count (jdbc/execute! (ds)
["select * from fruit where name = ?"
"Grapefruit"]))))))))

View file

@ -1,23 +1,25 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.optional-test (ns next.jdbc.optional-test
"Test namespace for the optional builder functions." "Test namespace for the optional builder functions."
(:require [clojure.string :as str] (:require [clojure.string :as str]
[clojure.test :refer [deftest is testing use-fixtures]] [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc.optional :as opt] [next.jdbc.optional :as opt]
[next.jdbc.protocols :as p] [next.jdbc.protocols :as p]
[next.jdbc.test-fixtures :refer [with-test-db ds column [next.jdbc.test-fixtures :refer [col-kw column default-options ds index
default-options]]) with-test-db]])
(:import (java.sql ResultSet ResultSetMetaData))) (:import
(java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (set-ns-context! [(around [f] (with-test-db f))])
(deftest test-map-row-builder (deftest test-map-row-builder
(testing "default row builder" (testing "default row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 1] [(str "select * from fruit where " (index) " = ?") 1]
(assoc (default-options) (assoc (default-options)
:builder-fn opt/as-maps))] :builder-fn opt/as-maps))]
(is (map? row)) (is (map? row))
@ -26,7 +28,7 @@
(is (= "Apple" ((column :FRUIT/NAME) row))))) (is (= "Apple" ((column :FRUIT/NAME) row)))))
(testing "unqualified row builder" (testing "unqualified row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 2] [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn opt/as-unqualified-maps})] {:builder-fn opt/as-unqualified-maps})]
(is (map? row)) (is (map? row))
(is (not (contains? row (column :COST)))) (is (not (contains? row (column :COST))))
@ -34,23 +36,23 @@
(is (= "Banana" ((column :NAME) row))))) (is (= "Banana" ((column :NAME) row)))))
(testing "lower-case row builder" (testing "lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn opt/as-lower-maps))] :builder-fn opt/as-lower-maps))]
(is (map? row)) (is (map? row))
(is (not (contains? row :fruit/appearance))) (is (not (contains? row (col-kw :fruit/appearance))))
(is (= 3 (:fruit/id row))) (is (= 3 ((col-kw :fruit/id) row)))
(is (= "Peach" (:fruit/name row))))) (is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified lower-case row builder" (testing "unqualified lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 4] [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn opt/as-unqualified-lower-maps})] {:builder-fn opt/as-unqualified-lower-maps})]
(is (map? row)) (is (map? row))
(is (= 4 (:id row))) (is (= 4 ((col-kw :id) row)))
(is (= "Orange" (:name row))))) (is (= "Orange" ((col-kw :name) row)))))
(testing "custom row builder" (testing "custom row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn opt/as-modified-maps :builder-fn opt/as-modified-maps
:label-fn str/lower-case :label-fn str/lower-case
@ -61,13 +63,13 @@
(is (= "Peach" ((column :FRUIT/name) row)))))) (is (= "Peach" ((column :FRUIT/name) row))))))
(defn- default-column-reader (defn- default-column-reader
[^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i] [^ResultSet rs ^ResultSetMetaData _ ^Integer i]
(.getObject rs i)) (.getObject rs i))
(deftest test-map-row-adapter (deftest test-map-row-adapter
(testing "default row builder" (testing "default row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 1] [(str "select * from fruit where " (index) " = ?") 1]
(assoc (default-options) (assoc (default-options)
:builder-fn (opt/as-maps-adapter :builder-fn (opt/as-maps-adapter
opt/as-maps opt/as-maps
@ -78,7 +80,7 @@
(is (= "Apple" ((column :FRUIT/NAME) row))))) (is (= "Apple" ((column :FRUIT/NAME) row)))))
(testing "unqualified row builder" (testing "unqualified row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 2] [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn (opt/as-maps-adapter {:builder-fn (opt/as-maps-adapter
opt/as-unqualified-maps opt/as-unqualified-maps
default-column-reader)})] default-column-reader)})]
@ -88,27 +90,27 @@
(is (= "Banana" ((column :NAME) row))))) (is (= "Banana" ((column :NAME) row)))))
(testing "lower-case row builder" (testing "lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn (opt/as-maps-adapter :builder-fn (opt/as-maps-adapter
opt/as-lower-maps opt/as-lower-maps
default-column-reader)))] default-column-reader)))]
(is (map? row)) (is (map? row))
(is (not (contains? row :fruit/appearance))) (is (not (contains? row (col-kw :fruit/appearance))))
(is (= 3 (:fruit/id row))) (is (= 3 ((col-kw :fruit/id) row)))
(is (= "Peach" (:fruit/name row))))) (is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified lower-case row builder" (testing "unqualified lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 4] [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn (opt/as-maps-adapter {:builder-fn (opt/as-maps-adapter
opt/as-unqualified-lower-maps opt/as-unqualified-lower-maps
default-column-reader)})] default-column-reader)})]
(is (map? row)) (is (map? row))
(is (= 4 (:id row))) (is (= 4 ((col-kw :id) row)))
(is (= "Orange" (:name row))))) (is (= "Orange" ((col-kw :name) row)))))
(testing "custom row builder" (testing "custom row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn (opt/as-maps-adapter :builder-fn (opt/as-maps-adapter
opt/as-modified-maps opt/as-modified-maps

View file

@ -1,72 +1,75 @@
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved ;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
(ns next.jdbc.plan-test (ns next.jdbc.plan-test
"Tests for the plan helpers." "Tests for the plan helpers."
(:require [clojure.test :refer [deftest is use-fixtures]] (:require [lazytest.core :refer [around]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is]]
[next.jdbc.plan :as plan] [next.jdbc.plan :as plan]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db ds]] :refer [with-test-db ds col-kw index]]
[clojure.string :as str])) [clojure.string :as str]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
;; around each test because of the folding tests using 1,000 rows
(use-fixtures :each with-test-db)
(specs/instrument) (specs/instrument)
(deftest select-one!-tests (deftest select-one!-tests
(is (= {:id 1} {:context [(around [f] (with-test-db f))]}
(plan/select-one! (ds) [:id] ["select * from fruit order by id"]))) (is (= {(col-kw :id) 1}
(plan/select-one! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))])))
(is (= 1 (is (= 1
(plan/select-one! (ds) :id ["select * from fruit order by id"]))) (plan/select-one! (ds) (col-kw :id) [(str "select * from fruit order by " (index))])))
(is (= "Banana" (is (= "Banana"
(plan/select-one! (ds) :name ["select * from fruit where id = ?" 2]))) (plan/select-one! (ds) :name [(str "select * from fruit where " (index) " = ?") 2])))
(is (= [1 "Apple"] (is (= [1 "Apple"]
(plan/select-one! (ds) (juxt :id :name) (plan/select-one! (ds) (juxt (col-kw :id) :name)
["select * from fruit order by id"]))) [(str "select * from fruit order by " (index))])))
(is (= {:id 1 :name "Apple"} (is (= {(col-kw :id) 1 :name "Apple"}
(plan/select-one! (ds) #(select-keys % [:id :name]) (plan/select-one! (ds) #(select-keys % [(col-kw :id) :name])
["select * from fruit order by id"])))) [(str "select * from fruit order by " (index))]))))
(deftest select-vector-tests (deftest select-vector-tests
(is (= [{:id 1} {:id 2} {:id 3} {:id 4}] {:context [(around [f] (with-test-db f))]}
(plan/select! (ds) [:id] ["select * from fruit order by id"]))) (is (= [{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 4}]
(plan/select! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))])))
(is (= [1 2 3 4] (is (= [1 2 3 4]
(plan/select! (ds) :id ["select * from fruit order by id"]))) (plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))])))
(is (= ["Banana"] (is (= ["Banana"]
(plan/select! (ds) :name ["select * from fruit where id = ?" 2]))) (plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 2])))
(is (= [[2 "Banana"]] (is (= [[2 "Banana"]]
(plan/select! (ds) (juxt :id :name) (plan/select! (ds) (juxt (col-kw :id) :name)
["select * from fruit where id = ?" 2]))) [(str "select * from fruit where " (index) " = ?") 2])))
(is (= [{:id 2 :name "Banana"}] (is (= [{(col-kw :id) 2 :name "Banana"}]
(plan/select! (ds) [:id :name] (plan/select! (ds) [(col-kw :id) :name]
["select * from fruit where id = ?" 2])))) [(str "select * from fruit where " (index) " = ?") 2]))))
(deftest select-set-tests (deftest select-set-tests
(is (= #{{:id 1} {:id 2} {:id 3} {:id 4}} {:context [(around [f] (with-test-db f))]}
(plan/select! (ds) [:id] ["select * from fruit order by id"] (is (= #{{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 4}}
(plan/select! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))]
{:into #{}}))) {:into #{}})))
(is (= #{1 2 3 4} (is (= #{1 2 3 4}
(plan/select! (ds) :id ["select * from fruit order by id"] (plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))]
{:into #{}})))) {:into #{}}))))
(deftest select-map-tests (deftest select-map-tests
{:context [(around [f] (with-test-db f))]}
(is (= {1 "Apple", 2 "Banana", 3 "Peach", 4 "Orange"} (is (= {1 "Apple", 2 "Banana", 3 "Peach", 4 "Orange"}
(plan/select! (ds) (juxt :id :name) ["select * from fruit order by id"] (plan/select! (ds) (juxt (col-kw :id) :name) [(str "select * from fruit order by " (index))]
{:into {}})))) {:into {}}))))
(deftest select-issue-227 (deftest select-issue-227
{:context [(around [f] (with-test-db f))]}
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) :name ["select * from fruit where id = ?" 1] (plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) :foo/name ["select * from fruit where id = ?" 1] (plan/select! (ds) :foo/name [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) #(get % "name") ["select * from fruit where id = ?" 1] (plan/select! (ds) #(get % "name") [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= [["Apple"]] (is (= [["Apple"]]
(plan/select! (ds) (juxt :name) ["select * from fruit where id = ?" 1] (plan/select! (ds) (juxt :name) [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")})))) {:column-fn #(str/replace % "-" "_")}))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.prepare-test (ns next.jdbc.prepare-test
"Stub test namespace for PreparedStatement creation etc. "Stub test namespace for PreparedStatement creation etc.
@ -8,20 +8,22 @@
The tests for the deprecated version of `execute-batch!` are here The tests for the deprecated version of `execute-batch!` are here
as a guard against regressions." as a guard against regressions."
(:require [clojure.test :refer [deftest is testing use-fixtures]] (:require [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db ds jtds? mssql? sqlite?]] :refer [with-test-db ds jtds? mssql? sqlite? xtdb?]]
[next.jdbc.prepare :as prep] [next.jdbc.prepare :as prep]
[next.jdbc.specs :as specs])) [next.jdbc.specs :as specs]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (set-ns-context! [(around [f] (with-test-db f))])
(specs/instrument) (specs/instrument)
(deftest execute-batch-tests (deftest execute-batch-tests
(when-not (xtdb?)
(testing "simple batch insert" (testing "simple batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13] (is (= [1 1 1 1 1 1 1 1 1 13]
(jdbc/with-transaction [t (ds) {:rollback-only true}] (jdbc/with-transaction [t (ds) {:rollback-only true}]
@ -96,7 +98,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(conj result (count (jdbc/execute! t ["select * from fruit"])))))))) (conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
(testing "return generated keys" (testing "return generated keys"
(when-not (mssql?) (when-not (or (mssql?) (sqlite?))
(let [results (let [results
(jdbc/with-transaction [t (ds) {:rollback-only true}] (jdbc/with-transaction [t (ds) {:rollback-only true}]
(with-open [ps (jdbc/prepare t [" (with-open [ps (jdbc/prepare t ["
@ -120,4 +122,4 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
;; Derby and SQLite only return one generated key per batch so there ;; Derby and SQLite only return one generated key per batch so there
;; are only three keys, plus the overall count here: ;; are only three keys, plus the overall count here:
(is (< 3 (count results)))) (is (< 3 (count results))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))

View file

@ -1,9 +1,8 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.protocols-test (ns next.jdbc.protocols-test
"Stub test namespace for low-level protocols. Nothing can really be tested "Stub test namespace for low-level protocols. Nothing can really be tested
at this level tho'..." at this level tho'..."
(:require [clojure.test :refer [deftest is testing]] (:require [next.jdbc.protocols]))
[next.jdbc.protocols :refer :all]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)

View file

@ -1,34 +1,30 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.quoted-test (ns next.jdbc.quoted-test
"Basic tests for quoting strategies. These are also tested indirectly "Basic tests for quoting strategies. These are also tested indirectly
via the next.jdbc.sql tests." via the next.jdbc.sql tests."
(:require [clojure.test :refer [deftest are testing]] (:require [lazytest.core :refer [defdescribe describe it expect]]
[next.jdbc.quoted :refer [ansi mysql sql-server oracle postgres [next.jdbc.quoted :refer [ansi mysql sql-server oracle postgres
schema]])) schema]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(deftest basic-quoting (def ^:private quote-fns [ansi mysql sql-server oracle postgres])
(are [quote-fn quoted] (= (quote-fn "x") quoted)
ansi "\"x\""
mysql "`x`"
sql-server "[x]"
oracle "\"x\""
postgres "\"x\""))
(deftest schema-quoting (defdescribe quoted-functionality
(testing "verify non-schema behavior" (describe "base quoting"
(are [quote-fn quoted] (= (quote-fn "x.y") quoted) (it "should correctly quote simple names"
ansi "\"x.y\"" (doseq [[f e] (map vector quote-fns
mysql "`x.y`" ["\"x\"" "`x`" "[x]" "\"x\"" "\"x\""])]
sql-server "[x.y]" (expect (= (f "x") e)))))
oracle "\"x.y\"" (describe "dotted name quoting"
postgres "\"x.y\"")) (describe "basic quoting"
(testing "verify schema behavior" (it "should quote dotted names 'as-is'"
(are [quote-fn quoted] (= ((schema quote-fn) "x.y") quoted) (doseq [[f e] (map vector quote-fns
ansi "\"x\".\"y\"" ["\"x.y\"" "`x.y`" "[x.y]" "\"x.y\"" "\"x.y\""])]
mysql "`x`.`y`" (expect (= (f "x.y") e)))))
sql-server "[x].[y]" (describe "schema quoting"
oracle "\"x\".\"y\"" (it "should split and quote dotted names with schema"
postgres "\"x\".\"y\""))) (doseq [[f e] (map vector quote-fns
["\"x\".\"y\"" "`x`.`y`" "[x].[y]" "\"x\".\"y\"" "\"x\".\"y\""])]
(expect (= ((schema f) "x.y") e)))))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.result-set-test (ns next.jdbc.result-set-test
"Test namespace for the result set functions. "Test namespace for the result set functions.
@ -8,18 +8,19 @@
(:require [clojure.core.protocols :as core-p] (:require [clojure.core.protocols :as core-p]
[clojure.datafy :as d] [clojure.datafy :as d]
[clojure.string :as str] [clojure.string :as str]
[clojure.test :refer [deftest is testing use-fixtures]] [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc.protocols :as p] [next.jdbc.protocols :as p]
[next.jdbc.result-set :as rs] [next.jdbc.result-set :as rs]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures :refer [with-test-db ds column [next.jdbc.test-fixtures :refer [with-test-db ds column index col-kw
default-options default-options
derby? mssql? mysql? postgres?]]) derby? mssql? mysql? postgres? xtdb?]])
(:import (java.sql ResultSet ResultSetMetaData))) (:import (java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (set-ns-context! [(around [f] (with-test-db f))])
(specs/instrument) (specs/instrument)
@ -27,7 +28,9 @@
(testing "default schema" (testing "default schema"
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:TABLE/FRUIT_ID 1} connectable test-row (rs/datafiable-row {:TABLE/FRUIT_ID 1} connectable
(default-options)) (cond-> (default-options)
(xtdb?)
(assoc :schema-opts {:pk "_id"})))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :TABLE/FRUIT_ID)] v (get data :TABLE/FRUIT_ID)]
;; check datafication is sane ;; check datafication is sane
@ -40,7 +43,10 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 2} connectable test-row (rs/datafiable-row {:foo/bar 2} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar :fruit/id})) :schema {:foo/bar
(if (xtdb?)
:fruit/_id
:fruit/id)}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -53,7 +59,10 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 3} connectable test-row (rs/datafiable-row {:foo/bar 3} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar [:fruit/id]})) :schema {:foo/bar
[(if (xtdb?)
:fruit/_id
:fruit/id)]}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -67,7 +76,7 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 2} connectable test-row (rs/datafiable-row {:foo/bar 2} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar [:fruit :id]})) :schema {:foo/bar [:fruit (col-kw :id)]}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -79,7 +88,7 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 3} connectable test-row (rs/datafiable-row {:foo/bar 3} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar [:fruit :id :many]})) :schema {:foo/bar [:fruit (col-kw :id) :many]}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -93,7 +102,7 @@
(deftest test-map-row-builder (deftest test-map-row-builder
(testing "default row builder" (testing "default row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 1] [(str "select * from fruit where " (index) " = ?") 1]
(default-options))] (default-options))]
(is (map? row)) (is (map? row))
(is (contains? row (column :FRUIT/GRADE))) (is (contains? row (column :FRUIT/GRADE)))
@ -101,7 +110,7 @@
(is (= 1 ((column :FRUIT/ID) row))) (is (= 1 ((column :FRUIT/ID) row)))
(is (= "Apple" ((column :FRUIT/NAME) row)))) (is (= "Apple" ((column :FRUIT/NAME) row))))
(let [rs (p/-execute-all (ds) (let [rs (p/-execute-all (ds)
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
(default-options))] (default-options))]
(is (every? map? rs)) (is (every? map? rs))
(is (= 1 ((column :FRUIT/ID) (first rs)))) (is (= 1 ((column :FRUIT/ID) (first rs))))
@ -110,7 +119,7 @@
(is (= "Orange" ((column :FRUIT/NAME) (last rs)))))) (is (= "Orange" ((column :FRUIT/NAME) (last rs))))))
(testing "unqualified row builder" (testing "unqualified row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 2] [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn rs/as-unqualified-maps})] {:builder-fn rs/as-unqualified-maps})]
(is (map? row)) (is (map? row))
(is (contains? row (column :COST))) (is (contains? row (column :COST)))
@ -119,34 +128,35 @@
(is (= "Banana" ((column :NAME) row))))) (is (= "Banana" ((column :NAME) row)))))
(testing "lower-case row builder" (testing "lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-lower-maps))] :builder-fn rs/as-lower-maps))]
(is (map? row)) (is (map? row))
(is (contains? row :fruit/appearance)) (is (contains? row (col-kw :fruit/appearance)))
(is (nil? (:fruit/appearance row))) (is (nil? ((col-kw :fruit/appearance) row)))
(is (= 3 (:fruit/id row))) (is (= 3 ((col-kw :fruit/id) row)))
(is (= "Peach" (:fruit/name row))))) (is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified lower-case row builder" (testing "unqualified lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 4] [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn rs/as-unqualified-lower-maps})] {:builder-fn rs/as-unqualified-lower-maps})]
(is (map? row)) (is (map? row))
(is (= 4 (:id row))) (is (= 4 ((col-kw :id) row)))
(is (= "Orange" (:name row))))) (is (= "Orange" ((col-kw :name) row)))))
(testing "kebab-case row builder" (testing "kebab-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select id,name,appearance as looks_like from fruit where id = ?" 3] [(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-kebab-maps))] :builder-fn rs/as-kebab-maps))]
(is (map? row)) (is (map? row))
(is (contains? row :fruit/looks-like)) (is (contains? row (col-kw :fruit/looks-like)))
(is (nil? (:fruit/looks-like row))) (is (nil? ((col-kw :fruit/looks-like) row)))
(is (= 3 (:fruit/id row))) ;; kebab-case strips leading _ from _id (XTDB):
(is (= "Peach" (:fruit/name row))))) (is (= 3 ((if (xtdb?) :id :fruit/id) row)))
(is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified kebab-case row builder" (testing "unqualified kebab-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select id,name,appearance as looks_like from fruit where id = ?" 4] [(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 4]
{:builder-fn rs/as-unqualified-kebab-maps})] {:builder-fn rs/as-unqualified-kebab-maps})]
(is (map? row)) (is (map? row))
(is (contains? row :looks-like)) (is (contains? row :looks-like))
@ -155,7 +165,7 @@
(is (= "Orange" (:name row))))) (is (= "Orange" (:name row)))))
(testing "custom row builder 1" (testing "custom row builder 1"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select fruit.*, id + 100 as newid from fruit where id = ?" 3] [(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-modified-maps :builder-fn rs/as-modified-maps
:label-fn str/lower-case :label-fn str/lower-case
@ -168,7 +178,7 @@
(is (= "Peach" ((column :FRUIT/name) row))))) (is (= "Peach" ((column :FRUIT/name) row)))))
(testing "custom row builder 2" (testing "custom row builder 2"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select fruit.*, id + 100 as newid from fruit where id = ?" 3] [(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-modified-maps :builder-fn rs/as-modified-maps
:label-fn str/lower-case :label-fn str/lower-case
@ -176,12 +186,12 @@
(is (map? row)) (is (map? row))
(is (contains? row :vegetable/appearance)) (is (contains? row :vegetable/appearance))
(is (nil? (:vegetable/appearance row))) (is (nil? (:vegetable/appearance row)))
(is (= 3 (:vegetable/id row))) (is (= 3 ((if (xtdb?) :vegetable/_id :vegetable/id) row)))
(is (= 103 (:vegetable/newid row))) ; constant qualifier here (is (= 103 (:vegetable/newid row))) ; constant qualifier here
(is (= "Peach" (:vegetable/name row))))) (is (= "Peach" (:vegetable/name row)))))
(testing "adapted row builder" (testing "adapted row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (assoc
(default-options) (default-options)
:builder-fn (rs/as-maps-adapter :builder-fn (rs/as-maps-adapter
@ -207,7 +217,7 @@
(fn [^ResultSet rs _ ^Integer i] (fn [^ResultSet rs _ ^Integer i]
(.getObject rs i))) (.getObject rs i)))
row (p/-execute-one (ds) row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (assoc
(default-options) (default-options)
:builder-fn (rs/as-maps-adapter :builder-fn (rs/as-maps-adapter
@ -236,7 +246,7 @@
(testing "row-numbers on bare abstraction" (testing "row-numbers on bare abstraction"
(is (= [1 2 3] (is (= [1 2 3]
(into [] (map rs/row-number) (into [] (map rs/row-number)
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
;; we do not need a real builder here... ;; we do not need a real builder here...
(cond-> {:builder-fn (constantly nil)} (cond-> {:builder-fn (constantly nil)}
(derby?) (derby?)
@ -247,7 +257,7 @@
(is (= [1 2 3] (is (= [1 2 3]
(into [] (comp (map #(rs/datafiable-row % (ds) {})) (into [] (comp (map #(rs/datafiable-row % (ds) {}))
(map rs/row-number)) (map rs/row-number))
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
;; ...but datafiable-row requires a real builder ;; ...but datafiable-row requires a real builder
(cond-> {:builder-fn rs/as-arrays} (cond-> {:builder-fn rs/as-arrays}
(derby?) (derby?)
@ -257,7 +267,7 @@
(deftest test-column-names (deftest test-column-names
(testing "column-names on bare abstraction" (testing "column-names on bare abstraction"
(is (= #{"id" "appearance" "grade" "cost" "name"} (is (= #{(index) "appearance" "grade" "cost" "name"}
(reduce (fn [_ row] (reduce (fn [_ row]
(-> row (-> row
(->> (rs/column-names) (->> (rs/column-names)
@ -265,11 +275,11 @@
(set) (set)
(reduced)))) (reduced))))
nil nil
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
;; column-names require a real builder ;; column-names require a real builder
{:builder-fn rs/as-arrays}))))) {:builder-fn rs/as-arrays})))))
(testing "column-names on realized row" (testing "column-names on realized row"
(is (= #{"id" "appearance" "grade" "cost" "name"} (is (= #{(index) "appearance" "grade" "cost" "name"}
(reduce (fn [_ row] (reduce (fn [_ row]
(-> row (-> row
(rs/datafiable-row (ds) {}) (rs/datafiable-row (ds) {})
@ -278,7 +288,7 @@
(set) (set)
(reduced)))) (reduced))))
nil nil
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
{:builder-fn rs/as-arrays})))))) {:builder-fn rs/as-arrays}))))))
(deftest test-over-partition-all (deftest test-over-partition-all
@ -299,31 +309,31 @@
(testing "no row builder is used" (testing "no row builder is used"
(is (= [true] (is (= [true]
(into [] (map map?) ; it looks like a real map now (into [] (map map?) ; it looks like a real map now
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= ["Apple"] (is (= ["Apple"]
(into [] (map :name) ; keyword selection works (into [] (map :name) ; keyword selection works
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [[2 [:name "Banana"]]] (is (= [[2 [:name "Banana"]]]
(into [] (map (juxt #(get % "id") ; get by string key works (into [] (map (juxt #(get % (index)) ; get by string key works
#(find % :name))) ; get MapEntry works #(find % :name))) ; get MapEntry works
(p/-execute (ds) ["select * from fruit where id = ?" 2] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [{:id 3 :name "Peach"}] (is (= [{(col-kw :id) 3 :name "Peach"}]
(into [] (map #(select-keys % [:id :name])) ; select-keys works (into [] (map #(select-keys % [(col-kw :id) :name])) ; select-keys works
(p/-execute (ds) ["select * from fruit where id = ?" 3] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 3]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [[:orange 4]] (is (= [[:orange 4]]
(into [] (map #(vector (if (contains? % :name) ; contains works (into [] (map #(vector (if (contains? % :name) ; contains works
(keyword (str/lower-case (:name %))) (keyword (str/lower-case (:name %)))
:unnamed) :unnamed)
(get % :id 0))) ; get with not-found works (get % (col-kw :id) 0))) ; get with not-found works
(p/-execute (ds) ["select * from fruit where id = ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [{}] (is (= [{}]
(into [] (map empty) ; return empty map without building (into [] (map empty) ; return empty map without building
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)}))))) {:builder-fn (constantly nil)})))))
(testing "count does not build a map" (testing "count does not build a map"
(let [count-builder (fn [_1 _2] (let [count-builder (fn [_1 _2]
@ -331,7 +341,7 @@
(column-count [_] 13)))] (column-count [_] 13)))]
(is (= [13] (is (= [13]
(into [] (map count) ; count relies on columns, not row fields (into [] (map count) ; count relies on columns, not row fields
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn count-builder})))))) {:builder-fn count-builder}))))))
(testing "assoc, dissoc, cons, seq, and = build maps" (testing "assoc, dissoc, cons, seq, and = build maps"
(is (map? (reduce (fn [_ row] (reduced (assoc row :x 1))) (is (map? (reduce (fn [_ row] (reduced (assoc row :x 1)))
@ -417,7 +427,7 @@
(defn fruit-builder [^ResultSet rs ^ResultSetMetaData rsmeta] (defn fruit-builder [^ResultSet rs ^ResultSetMetaData rsmeta]
(reify (reify
rs/RowBuilder rs/RowBuilder
(->row [_] (->Fruit (.getObject rs "id") (->row [_] (->Fruit (.getObject rs ^String (index))
(.getObject rs "name") (.getObject rs "name")
(.getObject rs "appearance") (.getObject rs "appearance")
(.getObject rs "cost") (.getObject rs "cost")
@ -434,7 +444,7 @@
(valAt [this k] (get this k nil)) (valAt [this k] (get this k nil))
(valAt [this k not-found] (valAt [this k not-found]
(case k (case k
:cols [:id :name :appearance :cost :grade] :cols [(col-kw :id) :name :appearance :cost :grade]
:rsmeta rsmeta :rsmeta rsmeta
not-found)))) not-found))))
@ -467,7 +477,7 @@
metadata)))) metadata))))
(deftest clob-reading (deftest clob-reading
(when-not (or (mssql?) (mysql?) (postgres?)) ; no clob in these (when-not (or (mssql?) (mysql?) (postgres?) (xtdb?)) ; no clob in these
(with-open [con (p/get-connection (ds) {})] (with-open [con (p/get-connection (ds) {})]
(try (try
(p/-execute-one con ["DROP TABLE CLOBBER"] {}) (p/-execute-one con ["DROP TABLE CLOBBER"] {})
@ -497,10 +507,10 @@ CREATE TABLE CLOBBER (
(testing "get n on bare abstraction over arrays" (testing "get n on bare abstraction over arrays"
(is (= [1 2 3] (is (= [1 2 3]
(into [] (map #(get % 0)) (into [] (map #(get % 0))
(p/-execute (ds) ["select id from fruit where id < ?" 4] (p/-execute (ds) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4]
{:builder-fn rs/as-arrays}))))) {:builder-fn rs/as-arrays})))))
(testing "nth on bare abstraction over arrays" (testing "nth on bare abstraction over arrays"
(is (= [1 2 3] (is (= [1 2 3]
(into [] (map #(nth % 0)) (into [] (map #(nth % 0))
(p/-execute (ds) ["select id from fruit where id < ?" 4] (p/-execute (ds) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4]
{:builder-fn rs/as-arrays})))))) {:builder-fn rs/as-arrays}))))))

View file

@ -1,11 +1,10 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.specs-test (ns next.jdbc.specs-test
"Stub test namespace for the specs. "Stub test namespace for the specs.
The specs are used (and 'tested') as part of the tests for the The specs are used (and 'tested') as part of the tests for the
next.jdbc and next.jdbc.sql namespaces." next.jdbc and next.jdbc.sql namespaces."
(:require [clojure.test :refer [deftest is testing]] (:require [next.jdbc.specs]))
[next.jdbc.specs :refer :all]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)

View file

@ -1,8 +1,8 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.sql.builder-test (ns next.jdbc.sql.builder-test
"Tests for the SQL string building functions in next.jdbc.sql.builder." "Tests for the SQL string building functions in next.jdbc.sql.builder."
(:require [clojure.test :refer [deftest is testing]] (:require [lazytest.experimental.interfaces.clojure-test :refer [deftest is testing thrown?]]
[next.jdbc.quoted :refer [mysql sql-server]] [next.jdbc.quoted :refer [mysql sql-server]]
[next.jdbc.sql.builder :as builder])) [next.jdbc.sql.builder :as builder]))
@ -11,9 +11,13 @@
(deftest test-by-keys (deftest test-by-keys
(testing ":where clause" (testing ":where clause"
(is (= (builder/by-keys {:a nil :b 42 :c "s"} :where {}) (is (= (builder/by-keys {:a nil :b 42 :c "s"} :where {})
["WHERE a IS NULL AND b = ? AND c = ?" 42 "s"]))
(is (= (builder/by-keys {:q/a nil :q/b 42 :q/c "s"} :where {})
["WHERE a IS NULL AND b = ? AND c = ?" 42 "s"]))) ["WHERE a IS NULL AND b = ? AND c = ?" 42 "s"])))
(testing ":set clause" (testing ":set clause"
(is (= (builder/by-keys {:a nil :b 42 :c "s"} :set {}) (is (= (builder/by-keys {:a nil :b 42 :c "s"} :set {})
["SET a = ?, b = ?, c = ?" nil 42 "s"]))
(is (= (builder/by-keys {:q/a nil :q/b 42 :q/c "s"} :set {})
["SET a = ?, b = ?, c = ?" nil 42 "s"])))) ["SET a = ?, b = ?, c = ?" nil 42 "s"]))))
(deftest test-as-cols (deftest test-as-cols
@ -22,14 +26,24 @@
(is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {}) (is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {})
"a AS aa, b, count(*) AS c")) "a AS aa, b, count(*) AS c"))
(is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {:column-fn mysql}) (is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {:column-fn mysql})
"`a` AS `aa`, `b`, count(*) AS `c`"))
(is (= (builder/as-cols [:q/a :q/b :q/c] {})
"a, b, c"))
(is (= (builder/as-cols [[:q/a :q/aa] :q/b ["count(*)" :q/c]] {})
"a AS aa, b, count(*) AS c"))
(is (= (builder/as-cols [[:q/a :q/aa] :q/b ["count(*)" :q/c]] {:column-fn mysql})
"`a` AS `aa`, `b`, count(*) AS `c`"))) "`a` AS `aa`, `b`, count(*) AS `c`")))
(deftest test-as-keys (deftest test-as-keys
(is (= (builder/as-keys {:a nil :b 42 :c "s"} {}) (is (= (builder/as-keys {:a nil :b 42 :c "s"} {})
"a, b, c"))
(is (= (builder/as-keys {:q/a nil :q/b 42 :q/c "s"} {})
"a, b, c"))) "a, b, c")))
(deftest test-as-? (deftest test-as-?
(is (= (builder/as-? {:a nil :b 42 :c "s"} {}) (is (= (builder/as-? {:a nil :b 42 :c "s"} {})
"?, ?, ?"))
(is (= (builder/as-? {:q/a nil :q/b 42 :q/c "s"} {})
"?, ?, ?"))) "?, ?, ?")))
(deftest test-for-query (deftest test-for-query
@ -45,6 +59,18 @@
{:id nil} {:id nil}
{:table-fn sql-server :column-fn mysql {:table-fn sql-server :column-fn mysql
:suffix "FOR UPDATE"}) :suffix "FOR UPDATE"})
["SELECT * FROM [user] WHERE `id` IS NULL FOR UPDATE"]))
(is (= (builder/for-query
:t/user
{:q/id 9}
{:table-fn sql-server :column-fn mysql :order-by [:x/a [:x/b :desc]]})
["SELECT * FROM [user] WHERE `id` = ? ORDER BY `a`, `b` DESC" 9]))
(is (= (builder/for-query :t/user {:q/id nil} {:table-fn sql-server :column-fn mysql})
["SELECT * FROM [user] WHERE `id` IS NULL"]))
(is (= (builder/for-query :t/user
{:q/id nil}
{:table-fn sql-server :column-fn mysql
:suffix "FOR UPDATE"})
["SELECT * FROM [user] WHERE `id` IS NULL FOR UPDATE"]))) ["SELECT * FROM [user] WHERE `id` IS NULL FOR UPDATE"])))
(testing "by where clause" (testing "by where clause"
(is (= (builder/for-query (is (= (builder/for-query
@ -112,17 +138,27 @@
:user :user
{:opt nil :id 9} {:opt nil :id 9}
{:table-fn sql-server :column-fn mysql}) {:table-fn sql-server :column-fn mysql})
["DELETE FROM [user] WHERE `opt` IS NULL AND `id` = ?" 9]))
(is (= (builder/for-delete
:t/user
{:q/opt nil :q/id 9}
{:table-fn sql-server :column-fn mysql})
["DELETE FROM [user] WHERE `opt` IS NULL AND `id` = ?" 9]))) ["DELETE FROM [user] WHERE `opt` IS NULL AND `id` = ?" 9])))
(testing "by where clause" (testing "by where clause"
(is (= (builder/for-delete (is (= (builder/for-delete
:user :user
["id = ? and opt is null" 9] ["id = ? and opt is null" 9]
{:table-fn sql-server :column-fn mysql}) {:table-fn sql-server :column-fn mysql})
["DELETE FROM [user] WHERE id = ? and opt is null" 9]))
(is (= (builder/for-delete
:t/user
["id = ? and opt is null" 9]
{:table-fn sql-server :column-fn mysql})
["DELETE FROM [user] WHERE id = ? and opt is null" 9])))) ["DELETE FROM [user] WHERE id = ? and opt is null" 9]))))
(deftest test-for-update (deftest test-for-update
(testing "empty example (would be a SQL error)" (testing "empty example (would be a SQL error)"
(is (thrown? AssertionError ; changed in #44 (is (thrown? IllegalArgumentException
(builder/for-update :user (builder/for-update :user
{:status 42} {:status 42}
{} {}
@ -132,6 +168,11 @@
{:status 42} {:status 42}
{:id 9} {:id 9}
{:table-fn sql-server :column-fn mysql}) {:table-fn sql-server :column-fn mysql})
["UPDATE [user] SET `status` = ? WHERE `id` = ?" 42 9]))
(is (= (builder/for-update :t/user
{:q/status 42}
{:q/id 9}
{:table-fn sql-server :column-fn mysql})
["UPDATE [user] SET `status` = ? WHERE `id` = ?" 42 9]))) ["UPDATE [user] SET `status` = ? WHERE `id` = ?" 42 9])))
(testing "by where clause, with nil set value" (testing "by where clause, with nil set value"
(is (= (builder/for-update :user (is (= (builder/for-update :user
@ -145,6 +186,10 @@
(is (= (builder/for-insert :user (is (= (builder/for-insert :user
{:id 9 :status 42 :opt nil} {:id 9 :status 42 :opt nil}
{:table-fn sql-server :column-fn mysql}) {:table-fn sql-server :column-fn mysql})
["INSERT INTO [user] (`id`, `status`, `opt`) VALUES (?, ?, ?)" 9 42 nil]))
(is (= (builder/for-insert :t/user
{:q/id 9 :q/status 42 :q/opt nil}
{:table-fn sql-server :column-fn mysql})
["INSERT INTO [user] (`id`, `status`, `opt`) VALUES (?, ?, ?)" 9 42 nil]))) ["INSERT INTO [user] (`id`, `status`, `opt`) VALUES (?, ?, ?)" 9 42 nil])))
(testing "multi-row insert (normal mode)" (testing "multi-row insert (normal mode)"
(is (= (builder/for-insert-multi :user (is (= (builder/for-insert-multi :user
@ -153,6 +198,13 @@
[35 "world"] [35 "world"]
[64 "dollars"]] [64 "dollars"]]
{:table-fn sql-server :column-fn mysql}) {:table-fn sql-server :column-fn mysql})
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"]))
(is (= (builder/for-insert-multi :t/user
[:q/id :q/status]
[[42 "hello"]
[35 "world"]
[64 "dollars"]]
{:table-fn sql-server :column-fn mysql})
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"]))) ["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"])))
(testing "multi-row insert (batch mode)" (testing "multi-row insert (batch mode)"
(is (= (builder/for-insert-multi :user (is (= (builder/for-insert-multi :user
@ -161,4 +213,11 @@
[35 "world"] [35 "world"]
[64 "dollars"]] [64 "dollars"]]
{:table-fn sql-server :column-fn mysql :batch true}) {:table-fn sql-server :column-fn mysql :batch true})
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?)" [42 "hello"] [35 "world"] [64 "dollars"]]))
(is (= (builder/for-insert-multi :t/user
[:q/id :q/status]
[[42 "hello"]
[35 "world"]
[64 "dollars"]]
{:table-fn sql-server :column-fn mysql :batch true})
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?)" [42 "hello"] [35 "world"] [64 "dollars"]])))) ["INSERT INTO [user] (`id`, `status`) VALUES (?, ?)" [42 "hello"] [35 "world"] [64 "dollars"]]))))

View file

@ -1,25 +1,26 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.sql-test (ns next.jdbc.sql-test
"Tests for the syntactic sugar SQL functions." "Tests for the syntactic sugar SQL functions."
(:require [clojure.test :refer [deftest is testing use-fixtures]] (:require [lazytest.core :refer [around set-ns-context!]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing thrown?]]
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.sql :as sql] [next.jdbc.sql :as sql]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db ds column default-options :refer [col-kw column default-options derby? ds index jtds?
derby? jtds? maria? mssql? mysql? postgres? sqlite?]] maria? mssql? mysql? postgres? sqlite? with-test-db xtdb?]]
[next.jdbc.types :refer [as-other as-real as-varchar]])) [next.jdbc.types :refer [as-other as-real as-varchar]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (set-ns-context! [(around [f] (with-test-db f))])
(specs/instrument) (specs/instrument)
(deftest test-query (deftest test-query
(let [ds-opts (jdbc/with-options (ds) (default-options)) (let [ds-opts (jdbc/with-options (ds) (default-options))
rs (sql/query ds-opts ["select * from fruit order by id"])] rs (sql/query ds-opts [(str "select * from fruit order by " (index))])]
(is (= 4 (count rs))) (is (= 4 (count rs)))
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -34,10 +35,10 @@
(if (or (mysql?) (sqlite?)) (if (or (mysql?) (sqlite?))
{:limit 2 :offset 1} {:limit 2 :offset 1}
{:offset 1 :fetch 2}) {:offset 1 :fetch 2})
:columns [:ID :columns [(col-kw :ID)
["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END" ["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END"
:QUALITY]] :QUALITY]]
:order-by [:id]))] :order-by [(col-kw :id)]))]
(is (= 2 (count rs))) (is (= 2 (count rs)))
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -58,10 +59,27 @@
(is (every? meta rs)) (is (every? meta rs))
(is (= 2 ((column :FRUIT/ID) (first rs))))))) (is (= 2 ((column :FRUIT/ID) (first rs)))))))
(deftest test-aggregate-by-keys
(let [ds-opts (jdbc/with-options (ds) (default-options))]
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" {:appearance "neon-green"})]
(is (number? count-v))
(is (= 0 count-v)))
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" {:appearance "yellow"})]
(is (= 1 count-v)))
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all)]
(is (= 4 count-v)))
(let [max-id (sql/aggregate-by-keys ds-opts :fruit (str "max(" (index) ")") :all)]
(is (= 4 max-id)))
(when-not (xtdb?) ; XTDB does not support min/max on strings?
(let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)]
(is (= "Apple" min-name))))
(is (thrown? IllegalArgumentException
(sql/aggregate-by-keys ds-opts :fruit "count(*)" :all {:columns []})))))
(deftest test-get-by-id (deftest test-get-by-id
(let [ds-opts (jdbc/with-options (ds) (default-options))] (let [ds-opts (jdbc/with-options (ds) (default-options))]
(is (nil? (sql/get-by-id ds-opts :fruit -1))) (is (nil? (sql/get-by-id ds-opts :fruit -1 (col-kw :id) {})))
(let [row (sql/get-by-id ds-opts :fruit 3)] (let [row (sql/get-by-id ds-opts :fruit 3 (col-kw :id) {})]
(is (map? row)) (is (map? row))
(is (= "Peach" ((column :FRUIT/NAME) row)))) (is (= "Peach" ((column :FRUIT/NAME) row))))
(let [row (sql/get-by-id ds-opts :fruit "juicy" :appearance {})] (let [row (sql/get-by-id ds-opts :fruit "juicy" :appearance {})]
@ -72,23 +90,28 @@
(is (map? row)) (is (map? row))
(is (= 2 ((column :FRUIT/ID) row)))))) (is (= 2 ((column :FRUIT/ID) row))))))
(defn- update-count [n]
(if (xtdb?)
{:next.jdbc/update-count 0}
{:next.jdbc/update-count n}))
(deftest test-update! (deftest test-update!
(let [ds-opts (jdbc/with-options (ds) (default-options))] (let [ds-opts (jdbc/with-options (ds) (default-options))]
(try (try
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/update! ds-opts :fruit {:appearance "brown"} {:id 2}))) (sql/update! ds-opts :fruit {:appearance "brown"} {(col-kw :id) 2})))
(is (= "brown" ((column :FRUIT/APPEARANCE) (is (= "brown" ((column :FRUIT/APPEARANCE)
(sql/get-by-id ds-opts :fruit 2)))) (sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
(finally (finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2}))) (sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))
(try (try
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/update! ds-opts :fruit {:appearance "green"} (sql/update! ds-opts :fruit {:appearance "green"}
["name = ?" "Banana"]))) ["name = ?" "Banana"])))
(is (= "green" ((column :FRUIT/APPEARANCE) (is (= "green" ((column :FRUIT/APPEARANCE)
(sql/get-by-id ds-opts :fruit 2)))) (sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
(finally (finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2}))))) (sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))))
(deftest test-insert-delete (deftest test-insert-delete
(let [new-key (cond (derby?) :1 (let [new-key (cond (derby?) :1
@ -97,79 +120,99 @@
(mssql?) :GENERATED_KEYS (mssql?) :GENERATED_KEYS
(mysql?) :GENERATED_KEY (mysql?) :GENERATED_KEY
(postgres?) :fruit/id (postgres?) :fruit/id
(sqlite?) (keyword "last_insert_rowid()") ;; XTDB does not return the generated key so we fix it
;; to be the one we insert here, and then fake it in all
;; the other tests.
(xtdb?) (constantly 5)
:else :FRUIT/ID)] :else :FRUIT/ID)]
(testing "single insert/delete" (testing "single insert/delete"
(is (== 5 (new-key (sql/insert! (ds) :fruit (is (== 5 (new-key (sql/insert! (ds) :fruit
{:name (as-varchar "Kiwi") (cond-> {:name (as-varchar "Kiwi")
:appearance "green & fuzzy" :appearance "green & fuzzy"
:cost 100 :grade (as-real 99.9)})))) :cost 100 :grade (as-real 99.9)}
(xtdb?)
(assoc :_id 5))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
(is (= 5 (count (sql/query (ds) ["select * from fruit"])))) (is (= 5 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 5}))) (sql/delete! (ds) :fruit {(col-kw :id) 5})))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete" (testing "multiple insert/delete"
(is (= (cond (derby?) (is (= (cond (derby?)
[nil] ; WTF Apache Derby? [nil] ; WTF Apache Derby?
(mssql?) (mssql?)
[8M] [8M]
(sqlite?)
[8]
(maria?) (maria?)
[6] [6]
(xtdb?)
[]
:else :else
[6 7 8]) [6 7 8])
(mapv new-key (mapv new-key
(sql/insert-multi! (ds) :fruit (sql/insert-multi! (ds) :fruit
[:name :appearance :cost :grade] (cond->> [:name :appearance :cost :grade]
[["Kiwi" "green & fuzzy" 100 99.9] (xtdb?) (cons :_id))
(cond->> [["Kiwi" "green & fuzzy" 100 99.9]
["Grape" "black" 10 50] ["Grape" "black" 10 50]
["Lemon" "yellow" 20 9.9]])))) ["Lemon" "yellow" 20 9.9]]
(xtdb?)
(map cons [6 7 8]))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"])))) (is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 6}))) (sql/delete! (ds) :fruit {(col-kw :id) 6})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2} (is (= (update-count 2)
(sql/delete! (ds) :fruit ["id > ?" 4]))) (sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete with sequential cols/rows" ; per #43 (testing "multiple insert/delete with sequential cols/rows" ; per #43
(is (= (cond (derby?) (is (= (cond (derby?)
[nil] ; WTF Apache Derby? [nil] ; WTF Apache Derby?
(mssql?) (mssql?)
[11M] [11M]
(sqlite?)
[11]
(maria?) (maria?)
[9] [9]
(xtdb?)
[]
:else :else
[9 10 11]) [9 10 11])
(mapv new-key (mapv new-key
(sql/insert-multi! (ds) :fruit (sql/insert-multi! (ds) :fruit
'(:name :appearance :cost :grade) (cond->> '(:name :appearance :cost :grade)
'(("Kiwi" "green & fuzzy" 100 99.9) (xtdb?) (cons :_id))
(cond->> '(("Kiwi" "green & fuzzy" 100 99.9)
("Grape" "black" 10 50) ("Grape" "black" 10 50)
("Lemon" "yellow" 20 9.9)))))) ("Lemon" "yellow" 20 9.9))
(xtdb?)
(map cons [9 10 11]))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"])))) (is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 9}))) (sql/delete! (ds) :fruit {(col-kw :id) 9})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2} (is (= (update-count 2)
(sql/delete! (ds) :fruit ["id > ?" 4]))) (sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete with maps" (testing "multiple insert/delete with maps"
(is (= (cond (derby?) (is (= (cond (derby?)
[nil] ; WTF Apache Derby? [nil] ; WTF Apache Derby?
(mssql?) (mssql?)
[14M] [14M]
(sqlite?)
[14]
(maria?) (maria?)
[12] [12]
(xtdb?)
[]
:else :else
[12 13 14]) [12 13 14])
(mapv new-key (mapv new-key
(sql/insert-multi! (ds) :fruit (sql/insert-multi! (ds) :fruit
[{:name "Kiwi" (cond->> [{:name "Kiwi"
:appearance "green & fuzzy" :appearance "green & fuzzy"
:cost 100 :cost 100
:grade 99.9} :grade 99.9}
@ -180,18 +223,40 @@
{:name "Lemon" {:name "Lemon"
:appearance "yellow" :appearance "yellow"
:cost 20 :cost 20
:grade 9.9}])))) :grade 9.9}]
(xtdb?)
(map #(assoc %2 :_id %1) [12 13 14]))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"])))) (is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 12}))) (sql/delete! (ds) :fruit {(col-kw :id) 12})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2} (is (= (update-count 2)
(sql/delete! (ds) :fruit ["id > ?" 10]))) (sql/delete! (ds) :fruit [(str (index) " > ?") 10])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "empty insert-multi!" ; per #44 (testing "empty insert-multi!" ; per #44 and #264
(is (= [] (sql/insert-multi! (ds) :fruit (is (= [] (sql/insert-multi! (ds) :fruit
[:name :appearance :cost :grade] [:name :appearance :cost :grade]
[])))))) []
{:suffix
(when (sqlite?)
"RETURNING *")})))
;; per #264 the following should all be legal too:
(is (= [] (sql/insert-multi! (ds) :fruit
[]
{:suffix
(when (sqlite?)
"RETURNING *")})))
(is (= [] (sql/insert-multi! (ds) :fruit
[]
[]
{:suffix
(when (sqlite?)
"RETURNING *")})))
(is (= [] (sql/insert-multi! (ds) :fruit [])))
(is (= [] (sql/insert-multi! (ds) :fruit [] []))))))
(deftest no-empty-example-maps (deftest no-empty-example-maps
(is (thrown? clojure.lang.ExceptionInfo (is (thrown? clojure.lang.ExceptionInfo
@ -217,7 +282,7 @@
(deftest array-in (deftest array-in
(when (postgres?) (when (postgres?)
(let [data (sql/find-by-keys (ds) :fruit ["id = any(?)" (int-array [1 2 3 4])])] (let [data (sql/find-by-keys (ds) :fruit [(str (index) " = any(?)") (int-array [1 2 3 4])])]
(is (= 4 (count data)))))) (is (= 4 (count data))))))
(deftest enum-pg (deftest enum-pg

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
(ns next.jdbc.test-fixtures (ns next.jdbc.test-fixtures
"Multi-database testing fixtures." "Multi-database testing fixtures."
@ -64,16 +64,27 @@
(def ^:private test-jtds (def ^:private test-jtds
(when (System/getenv "NEXT_JDBC_TEST_MSSQL") test-jtds-map)) (when (System/getenv "NEXT_JDBC_TEST_MSSQL") test-jtds-map))
(def ^:private test-xtdb-map {:dbtype "xtdb" :dbname "xtdb"})
(def ^:private test-xtdb
(when (and (System/getenv "NEXT_JDBC_TEST_XTDB")
;; only if we're on jdk21+
(str/starts-with? (System/getProperty "java.version") "2"))
test-xtdb-map))
(def ^:private test-db-specs (def ^:private test-db-specs
(cond-> [test-derby test-h2-mem test-h2 test-hsql test-sqlite] (cond-> [test-derby test-h2-mem test-h2 test-hsql test-sqlite]
test-postgres (conj test-postgres) test-postgres (conj test-postgres)
test-mysql (conj test-mysql) test-mysql (conj test-mysql)
test-mssql (conj test-mssql test-jtds))) test-mssql (conj test-mssql test-jtds)
test-xtdb (conj test-xtdb)))
(def ^:private test-db-spec (atom nil)) (def ^:private test-db-spec (atom nil))
(defn derby? [] (= "derby" (:dbtype @test-db-spec))) (defn derby? [] (= "derby" (:dbtype @test-db-spec)))
(defn h2? [] (str/starts-with? (:dbtype @test-db-spec) "h2"))
(defn hsqldb? [] (= "hsqldb" (:dbtype @test-db-spec))) (defn hsqldb? [] (= "hsqldb" (:dbtype @test-db-spec)))
(defn jtds? [] (= "jtds" (:dbtype @test-db-spec))) (defn jtds? [] (= "jtds" (:dbtype @test-db-spec)))
@ -86,19 +97,34 @@
(defn postgres? [] (= "embedded-postgres" (:dbtype @test-db-spec))) (defn postgres? [] (= "embedded-postgres" (:dbtype @test-db-spec)))
(defn xtdb? [] (= "xtdb" (:dbtype @test-db-spec)))
(defn sqlite? [] (= "sqlite" (:dbtype @test-db-spec))) (defn sqlite? [] (= "sqlite" (:dbtype @test-db-spec)))
(defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite"} (:dbtype @test-db-spec)))) (defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite" "xtdb"}
(:dbtype @test-db-spec))))
(defn column [k] (defn column [k]
(let [n (namespace k)] (let [n (namespace k)]
(keyword (when n (cond (postgres?) (str/lower-case n) (keyword (when n (cond (postgres?) (str/lower-case n)
(mssql?) (str/lower-case n) (mssql?) (str/lower-case n)
(mysql?) (str/lower-case n) (mysql?) (str/lower-case n)
(xtdb?) nil
:else n)) :else n))
(cond (postgres?) (str/lower-case (name k)) (cond (postgres?) (str/lower-case (name k))
(xtdb?) (let [c (str/lower-case (name k))]
(if (= "id" c) "_id" c))
:else (name k))))) :else (name k)))))
(defn index []
(if (xtdb?) "_id" "id"))
(defn col-kw [k]
(if (xtdb?)
(let [n (name k)]
(if (= "id" (str/lower-case n)) :_id (keyword n)))
k))
(defn default-options [] (defn default-options []
(if (mssql?) ; so that we get table names back from queries (if (mssql?) ; so that we get table names back from queries
{:result-type :scroll-insensitive :concurrency :read-only} {:result-type :scroll-insensitive :concurrency :read-only}
@ -156,6 +182,31 @@
:else :else
"AUTO_INCREMENT PRIMARY KEY")] "AUTO_INCREMENT PRIMARY KEY")]
(with-open [con (jdbc/get-connection (ds))] (with-open [con (jdbc/get-connection (ds))]
(if (xtdb?) ; no DDL for creation
(do
(try
(do-commands con ["ERASE FROM fruit WHERE true"])
(catch Throwable _))
(try
(do-commands con ["ERASE FROM btest WHERE true"])
(catch Throwable _))
(sql/insert-multi! con :fruit
[:_id :name :appearance :cost]
[[1 "Apple" "red" 59]]
{:return-keys false})
(sql/insert-multi! con :fruit
[:_id :name :appearance :grade]
[[2 "Banana" "yellow" 92.2]]
{:return-keys false})
(sql/insert-multi! con :fruit
[:_id :name :cost :grade]
[[3 "Peach" 139 90.0]]
{:return-keys false})
(sql/insert-multi! con :fruit
[:_id :name :appearance :cost :grade]
[[4 "Orange" "juicy" 89 88.6]]
{:return-keys false}))
(do
(when (stored-proc?) (when (stored-proc?)
(try (try
(jdbc/execute-one! con ["DROP PROCEDURE FRUITP"]) (jdbc/execute-one! con ["DROP PROCEDURE FRUITP"])
@ -231,14 +282,14 @@ CREATE PROCEDURE FRUITP" (cond (hsqldb?) "() READS SQL DATA DYNAMIC RESULT SETS
["Banana" "yellow" nil 92.2] ["Banana" "yellow" nil 92.2]
["Peach" nil 139 90.0] ["Peach" nil 139 90.0]
["Orange" "juicy" 89 88.6]] ["Orange" "juicy" 89 88.6]]
{:return-keys false}) {:return-keys false})))
(t))))) (t)))))
(create-clojure-test) (create-clojure-test)
(comment (comment
;; this is a convenience to bring next.jdbc's test dependencies ;; this is a convenience to bring next.jdbc's test dependencies
;; into any REPL running Clojure 1.12.0 Alpha 2's new add-libs API ;; into any REPL running Clojure 1.12.0's new add-libs API
;; which allows me to develop and test next.jdbc inside my work's ;; which allows me to develop and test next.jdbc inside my work's
;; "everything" REPL environment ;; "everything" REPL environment
(require '[clojure.repl.deps :refer [add-libs]] (require '[clojure.repl.deps :refer [add-libs]]

View file

@ -1,17 +1,10 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc.transaction-test (ns next.jdbc.transaction-test
"Stub test namespace for transaction handling." "Stub test namespace for transaction handling."
(:require [clojure.test :refer [deftest is testing use-fixtures]] (:require [next.jdbc.specs :as specs]
[next.jdbc :as jdbc] [next.jdbc.transaction]))
[next.jdbc.specs :as specs]
[next.jdbc.test-fixtures :refer [with-test-db db ds column
default-options
derby? mssql? mysql? postgres?]]
[next.jdbc.transaction :as tx]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db)
(specs/instrument) (specs/instrument)

View file

@ -1,14 +1,19 @@
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved ;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
(ns next.jdbc.types-test (ns next.jdbc.types-test
"Some tests for the type-assist functions." "Some tests for the type-assist functions."
(:require [clojure.test :refer [deftest is]] (:require [lazytest.core :refer [defdescribe describe it expect]]
[next.jdbc.types :refer [as-varchar]])) [next.jdbc.types :refer [as-varchar]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(deftest as-varchar-test (defdescribe as-varchar-tests
(let [v (as-varchar "Hello")] (let [v (as-varchar "Hello")]
(is (= "Hello" (v))) (describe "produces a function"
(is (contains? (meta v) 'next.jdbc.prepare/set-parameter)) (it "yields the original value when invoked"
(is (fn? (get (meta v) 'next.jdbc.prepare/set-parameter))))) (expect (fn? v))
(expect (= "Hello" (v)))))
(describe "carries metadata"
(it "has a `set-parameter` function"
(expect (contains? (meta v) 'next.jdbc.prepare/set-parameter))
(expect (fn? (get (meta v) 'next.jdbc.prepare/set-parameter)))))))

View file

@ -1,42 +1,46 @@
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved ;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
(ns next.jdbc-test (ns next.jdbc-test
"Basic tests for the primary API of `next.jdbc`." "Basic tests for the primary API of `next.jdbc`."
(:require [clojure.core.reducers :as r] (:require
[clojure.core.reducers :as r]
[clojure.string :as str] [clojure.string :as str]
[clojure.test :refer [deftest is testing use-fixtures]] [lazytest.core :refer [around defdescribe it ok?]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing
thrown?]]
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.connection :as c] [next.jdbc.connection :as c]
[next.jdbc.test-fixtures
:refer [with-test-db db ds column
default-options stored-proc?
derby? hsqldb? jtds? mssql? mysql? postgres? sqlite?]]
[next.jdbc.prepare :as prep] [next.jdbc.prepare :as prep]
[next.jdbc.result-set :as rs] [next.jdbc.result-set :as rs]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures
:refer [col-kw column db default-options derby? ds h2? hsqldb?
index jtds? mssql? mysql? postgres? sqlite? stored-proc?
with-test-db xtdb?]]
[next.jdbc.types :as types]) [next.jdbc.types :as types])
(:import (com.zaxxer.hikari HikariDataSource) (:import
(com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource) (com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)
(com.zaxxer.hikari HikariDataSource)
(java.sql ResultSet ResultSetMetaData))) (java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
;; around each test because of the folding tests using 1,000 rows
(use-fixtures :each with-test-db)
(specs/instrument) (specs/instrument)
(deftest spec-tests (defdescribe spec-tests
"sanity checks on instrumented function calls"
{:context [(around [f] (with-test-db f))]}
(let [db-spec {:dbtype "h2:mem" :dbname "clojure_test"}] (let [db-spec {:dbtype "h2:mem" :dbname "clojure_test"}]
;; some sanity checks on instrumented function calls: (it "succeeds with a basic db-spec"
(jdbc/get-datasource db-spec) (ok? #(jdbc/get-datasource db-spec))
(jdbc/get-connection db-spec) (ok? #(jdbc/get-connection db-spec)))
;; and again with options:
(let [db-spec' (jdbc/with-options db-spec {})] (let [db-spec' (jdbc/with-options db-spec {})]
(jdbc/get-datasource db-spec') (it "succeeds with an option-wrapped db-spec"
(jdbc/get-connection db-spec')))) (ok? #(jdbc/get-datasource db-spec'))
(ok? #(jdbc/get-connection db-spec'))))))
(deftest basic-tests (deftest basic-tests
{:context [(around [f] (with-test-db f))]}
;; use ds-opts instead of (ds) anywhere you want default options applied: ;; use ds-opts instead of (ds) anywhere you want default options applied:
(let [ds-opts (jdbc/with-options (ds) (default-options))] (let [ds-opts (jdbc/with-options (ds) (default-options))]
(testing "plan" (testing "plan"
@ -60,15 +64,27 @@
(jdbc/execute-one! (jdbc/execute-one!
ds-opts ds-opts
["select * from fruit where appearance = ?" "red"])))) ["select * from fruit where appearance = ?" "red"]))))
(is (= "red" (:fruit/looks-like (is (= "red" ((col-kw :fruit/looks-like)
(jdbc/execute-one! (jdbc/execute-one!
ds-opts ds-opts
["select appearance as looks_like from fruit where id = ?" 1] [(str "select appearance as looks_like from fruit where " (index) " = ?") 1]
jdbc/snake-kebab-opts)))) jdbc/snake-kebab-opts))))
(let [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
(is (= "red" ((col-kw :fruit/looks-like)
(jdbc/execute-one!
ds'
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1])))))
(jdbc/with-transaction+options [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
(is (= (merge (default-options) jdbc/snake-kebab-opts)
(:options ds')))
(is (= "red" ((col-kw :fruit/looks-like)
(jdbc/execute-one!
ds'
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1])))))
(is (= "red" (:looks-like (is (= "red" (:looks-like
(jdbc/execute-one! (jdbc/execute-one!
ds-opts ds-opts
["select appearance as looks_like from fruit where id = ?" 1] [(str "select appearance as looks_like from fruit where " (index) " = ?") 1]
jdbc/unqualified-snake-kebab-opts))))) jdbc/unqualified-snake-kebab-opts)))))
(testing "execute!" (testing "execute!"
(let [rs (jdbc/execute! (let [rs (jdbc/execute!
@ -83,7 +99,7 @@
(is (= 1 ((column :FRUIT/ID) (first rs))))) (is (= 1 ((column :FRUIT/ID) (first rs)))))
(let [rs (jdbc/execute! (let [rs (jdbc/execute!
ds-opts ds-opts
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
{:builder-fn rs/as-maps})] {:builder-fn rs/as-maps})]
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -92,22 +108,23 @@
(is (= 4 ((column :FRUIT/ID) (last rs))))) (is (= 4 ((column :FRUIT/ID) (last rs)))))
(let [rs (jdbc/execute! (let [rs (jdbc/execute!
ds-opts ds-opts
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
{:builder-fn rs/as-arrays})] {:builder-fn rs/as-arrays})]
(is (every? vector? rs)) (is (every? vector? rs))
(is (= 5 (count rs))) (is (= 5 (count rs)))
(is (every? #(= 5 (count %)) rs)) (is (every? #(= 5 (count %)) rs))
;; columns come first ;; columns come first
(is (every? qualified-keyword? (first rs))) (is (every? (if (xtdb?) keyword? qualified-keyword?) (first rs)))
;; :FRUIT/ID should be first column ;; :FRUIT/ID should be first column
(is (= (column :FRUIT/ID) (ffirst rs))) (is (= (column :FRUIT/ID) (ffirst rs)))
;; and all its corresponding values should be ints ;; and all its corresponding values should be ints
(is (every? int? (map first (rest rs)))) (is (every? int? (map first (rest rs))))
(is (every? string? (map second (rest rs)))))) (let [n (max (.indexOf ^java.util.List (first rs) :name) 1)]
(is (every? string? (map #(nth % n) (rest rs)))))))
(testing "execute! with adapter" (testing "execute! with adapter"
(let [rs (jdbc/execute! ; test again, with adapter and lower columns (let [rs (jdbc/execute! ; test again, with adapter and lower columns
ds-opts ds-opts
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
{:builder-fn (rs/as-arrays-adapter {:builder-fn (rs/as-arrays-adapter
rs/as-lower-arrays rs/as-lower-arrays
(fn [^ResultSet rs _ ^Integer i] (fn [^ResultSet rs _ ^Integer i]
@ -116,16 +133,17 @@
(is (= 5 (count rs))) (is (= 5 (count rs)))
(is (every? #(= 5 (count %)) rs)) (is (every? #(= 5 (count %)) rs))
;; columns come first ;; columns come first
(is (every? qualified-keyword? (first rs))) (is (every? (if (xtdb?) keyword? qualified-keyword?) (first rs)))
;; :fruit/id should be first column ;; :fruit/id should be first column
(is (= :fruit/id (ffirst rs))) (is (= (col-kw :fruit/id) (ffirst rs)))
;; and all its corresponding values should be ints ;; and all its corresponding values should be ints
(is (every? int? (map first (rest rs)))) (is (every? int? (map first (rest rs))))
(is (every? string? (map second (rest rs)))))) (let [n (max (.indexOf ^java.util.List (first rs) :name) 1)]
(is (every? string? (map #(nth % n) (rest rs)))))))
(testing "execute! with unqualified" (testing "execute! with unqualified"
(let [rs (jdbc/execute! (let [rs (jdbc/execute!
(ds) (ds)
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
{:builder-fn rs/as-unqualified-maps})] {:builder-fn rs/as-unqualified-maps})]
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -134,7 +152,7 @@
(is (= 4 ((column :ID) (last rs))))) (is (= 4 ((column :ID) (last rs)))))
(let [rs (jdbc/execute! (let [rs (jdbc/execute!
ds-opts ds-opts
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
{:builder-fn rs/as-unqualified-arrays})] {:builder-fn rs/as-unqualified-arrays})]
(is (every? vector? rs)) (is (every? vector? rs))
(is (= 5 (count rs))) (is (= 5 (count rs)))
@ -145,11 +163,12 @@
(is (= (column :ID) (ffirst rs))) (is (= (column :ID) (ffirst rs)))
;; and all its corresponding values should be ints ;; and all its corresponding values should be ints
(is (every? int? (map first (rest rs)))) (is (every? int? (map first (rest rs))))
(is (every? string? (map second (rest rs)))))) (let [n (max (.indexOf ^java.util.List (first rs) :name) 1)]
(is (every? string? (map #(nth % n) (rest rs)))))))
(testing "execute! with :max-rows / :maxRows" (testing "execute! with :max-rows / :maxRows"
(let [rs (jdbc/execute! (let [rs (jdbc/execute!
ds-opts ds-opts
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
{:max-rows 2})] {:max-rows 2})]
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -158,7 +177,7 @@
(is (= 2 ((column :FRUIT/ID) (last rs))))) (is (= 2 ((column :FRUIT/ID) (last rs)))))
(let [rs (jdbc/execute! (let [rs (jdbc/execute!
ds-opts ds-opts
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
{:statement {:maxRows 2}})] {:statement {:maxRows 2}})]
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -170,7 +189,7 @@
(let [rs (with-open [con (jdbc/get-connection (ds)) (let [rs (with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare ps (jdbc/prepare
con con
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
(default-options))] (default-options))]
(jdbc/execute! ps))] (jdbc/execute! ps))]
(is (every? map? rs)) (is (every? map? rs))
@ -182,7 +201,7 @@
(let [rs (with-open [con (jdbc/get-connection (ds)) (let [rs (with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare ps (jdbc/prepare
con con
["select * from fruit where id = ?"] [(str "select * from fruit where " (index) " = ?")]
(default-options))] (default-options))]
(jdbc/execute! (prep/set-parameters ps [4]) nil {}))] (jdbc/execute! (prep/set-parameters ps [4]) nil {}))]
(is (every? map? rs)) (is (every? map? rs))
@ -193,7 +212,7 @@
;; default options do not flow over get-connection ;; default options do not flow over get-connection
(let [rs (with-open [con (jdbc/get-connection (ds))] (let [rs (with-open [con (jdbc/get-connection (ds))]
(jdbc/execute! (prep/statement con (default-options)) (jdbc/execute! (prep/statement con (default-options))
["select * from fruit order by id"]))] [(str "select * from fruit order by " (index))]))]
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
(is (= 4 (count rs))) (is (= 4 (count rs)))
@ -202,11 +221,12 @@
;; default options do not flow over get-connection ;; default options do not flow over get-connection
(let [rs (with-open [con (jdbc/get-connection (ds))] (let [rs (with-open [con (jdbc/get-connection (ds))]
(jdbc/execute! (prep/statement con (default-options)) (jdbc/execute! (prep/statement con (default-options))
["select * from fruit where id = 4"]))] [(str "select * from fruit where " (index) " = 4")]))]
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
(is (= 1 (count rs))) (is (= 1 (count rs)))
(is (= 4 ((column :FRUIT/ID) (first rs)))))) (is (= 4 ((column :FRUIT/ID) (first rs))))))
(when-not (xtdb?)
(testing "transact" (testing "transact"
(is (= [{:next.jdbc/update-count 1}] (is (= [{:next.jdbc/update-count 1}]
(jdbc/transact (ds) (jdbc/transact (ds)
@ -221,6 +241,7 @@ VALUES ('Pear', 'green', 49, 47)
(is (= [{:next.jdbc/update-count 1}] (is (= [{:next.jdbc/update-count 1}]
(jdbc/with-transaction [t (ds) {:rollback-only true}] (jdbc/with-transaction [t (ds) {:rollback-only true}]
(is (jdbc/active-tx?) "should be in a transaction") (is (jdbc/active-tx?) "should be in a transaction")
(is (jdbc/active-tx? t) "connection should be in a transaction")
(jdbc/execute! t [" (jdbc/execute! t ["
INSERT INTO fruit (name, appearance, cost, grade) INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47) VALUES ('Pear', 'green', 49, 47)
@ -232,6 +253,7 @@ VALUES ('Pear', 'green', 49, 47)
(is (= [{:next.jdbc/update-count 1}] (is (= [{:next.jdbc/update-count 1}]
(jdbc/with-transaction [t con {:rollback-only true}] (jdbc/with-transaction [t con {:rollback-only true}]
(is (jdbc/active-tx?) "should be in a transaction") (is (jdbc/active-tx?) "should be in a transaction")
(is (jdbc/active-tx? t) "connection should be in a transaction")
(jdbc/execute! t [" (jdbc/execute! t ["
INSERT INTO fruit (name, appearance, cost, grade) INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47) VALUES ('Pear', 'green', 49, 47)
@ -246,6 +268,7 @@ INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47) VALUES ('Pear', 'green', 49, 47)
"]) "])
(is (jdbc/active-tx?) "should be in a transaction") (is (jdbc/active-tx?) "should be in a transaction")
(is (jdbc/active-tx? t) "connection should be in a transaction")
(throw (ex-info "abort" {}))))) (throw (ex-info "abort" {})))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
(is (not (jdbc/active-tx?)) "should not be in a transaction") (is (not (jdbc/active-tx?)) "should not be in a transaction")
@ -258,6 +281,7 @@ INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47) VALUES ('Pear', 'green', 49, 47)
"]) "])
(is (jdbc/active-tx?) "should be in a transaction") (is (jdbc/active-tx?) "should be in a transaction")
(is (jdbc/active-tx? t) "connection should be in a transaction")
(throw (ex-info "abort" {}))))) (throw (ex-info "abort" {})))))
(is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) (is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
(is (= ac (.getAutoCommit con)))))) (is (= ac (.getAutoCommit con))))))
@ -271,6 +295,7 @@ VALUES ('Pear', 'green', 49, 47)
(.rollback t) (.rollback t)
;; still in a next.jdbc TX even tho' we rolled back! ;; still in a next.jdbc TX even tho' we rolled back!
(is (jdbc/active-tx?) "should be in a transaction") (is (jdbc/active-tx?) "should be in a transaction")
(is (jdbc/active-tx? t) "connection should be in a transaction")
result)))) result))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
(is (not (jdbc/active-tx?)) "should not be in a transaction") (is (not (jdbc/active-tx?)) "should not be in a transaction")
@ -297,6 +322,7 @@ VALUES ('Pear', 'green', 49, 47)
(.rollback t save-point) (.rollback t save-point)
;; still in a next.jdbc TX even tho' we rolled back to a save point! ;; still in a next.jdbc TX even tho' we rolled back to a save point!
(is (jdbc/active-tx?) "should be in a transaction") (is (jdbc/active-tx?) "should be in a transaction")
(is (jdbc/active-tx? t) "connection should be in a transaction")
result)))) result))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
(is (not (jdbc/active-tx?)) "should not be in a transaction") (is (not (jdbc/active-tx?)) "should not be in a transaction")
@ -336,11 +362,12 @@ VALUES ('Pear', 'green', 49, 47)
(.rollback t save-point) (.rollback t save-point)
result)))) result))))
(is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) (is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
(is (= ac (.getAutoCommit con))))))) (is (= ac (.getAutoCommit con))))))))
(deftest issue-146 (deftest issue-146
{:context [(around [f] (with-test-db f))]}
;; since we use an embedded PostgreSQL data source, we skip this: ;; since we use an embedded PostgreSQL data source, we skip this:
(when-not (or (postgres?) (when-not (or (postgres?) (xtdb?)
;; and now we skip MS SQL because we can't use the db-spec ;; and now we skip MS SQL because we can't use the db-spec
;; we'd need to build the jdbcUrl with encryption turned off: ;; we'd need to build the jdbcUrl with encryption turned off:
(and (mssql?) (not (jtds?)))) (and (mssql?) (not (jtds?))))
@ -455,6 +482,7 @@ VALUES ('Pear', 'green', 49, 47)
#_ #_
(deftest duplicate-insert-test (deftest duplicate-insert-test
{:context [(around [f] (with-test-db f))]}
;; this is primarily a look at exception types/information for #226 ;; this is primarily a look at exception types/information for #226
(try (try
(jdbc/execute! (ds) [" (jdbc/execute! (ds) ["
@ -477,14 +505,42 @@ VALUES ('Pear', 'green', 49, 47)
"\n\t" (ex-message t))))) "\n\t" (ex-message t)))))
(deftest bool-tests (deftest bool-tests
{:context [(around [f] (with-test-db f))]} ;; Ensure the test database is used
(testing (str "bool-tests for " (:dbtype (db)))
(let [lit-t (cond (hsqldb?) "(1=1)" (mssql?) "1" :else "TRUE")
lit-f (cond (hsqldb?) "(1=0)" (mssql?) "0" :else "FALSE")]
(when-not (or (hsqldb?) (derby?))
(testing "literal TRUE select"
(is (= {(column :V) (if (or (sqlite?) (mysql?) (mssql?)) 1 true)}
(jdbc/execute-one! (ds) [(str "SELECT " lit-t " AS V")]))))
(testing "literal FALSE select"
(is (= {(column :V) (if (or (sqlite?) (mysql?) (mssql?)) 0 false)}
(jdbc/execute-one! (ds) [(str "SELECT " lit-f " AS V")])))))
(testing "literal TRUE insert"
(jdbc/execute-one!
(ds)
[(str "insert into btest (" (if (xtdb?) "_id" "name") ",is_it,twiddle)"
" values ('lit_t'," lit-t ","
(if (or (derby?) (sqlite?) (h2?) (mssql?) (xtdb?)) "1" "b'1'")
")")]))
(testing "literal FALSE insert"
(jdbc/execute-one!
(ds)
[(str "insert into btest (" (if (xtdb?) "_id" "name") ",is_it,twiddle)"
" values ('lit_f'," lit-f ","
(if (or (derby?) (sqlite?) (h2?) (mssql?) (xtdb?)) "0" "b'0'")
")")]))
(testing "BIT/BOOLEAN value insertion"
(doseq [[n b] [["zero" 0] ["one" 1] ["false" false] ["true" true]] (doseq [[n b] [["zero" 0] ["one" 1] ["false" false] ["true" true]]
:let [v-bit (if (number? b) b (if b 1 0)) :let [v-bit (if (number? b) b (if b 1 0))
v-bool (if (number? b) (pos? b) b)]] v-bool (if (number? b) (pos? b) b)]]
(jdbc/execute-one! (jdbc/execute-one!
(ds) (ds)
["insert into btest (name,is_it,twiddle) values (?,?,?)" [(str "insert into btest ("
(if (xtdb?) "_id" "name")
",is_it,twiddle) values (?,?,?)")
n n
(if (postgres?) (if (or (postgres?) (xtdb?))
(types/as-boolean b) (types/as-boolean b)
b) ; 0, 1, false, true are all acceptable b) ; 0, 1, false, true are all acceptable
(cond (hsqldb?) (cond (hsqldb?)
@ -492,18 +548,20 @@ VALUES ('Pear', 'green', 49, 47)
(postgres?) (postgres?)
(types/as-other v-bit) ; really postgres?? (types/as-other v-bit) ; really postgres??
:else :else
v-bit)])) v-bit)])))
(testing "BOOLEAN results from SELECT"
(let [data (jdbc/execute! (ds) ["select * from btest"] (let [data (jdbc/execute! (ds) ["select * from btest"]
(default-options))] (default-options))]
(if (sqlite?) (if (sqlite?)
(is (every? number? (map (column :BTEST/IS_IT) data))) (is (every? number? (map (column :BTEST/IS_IT) data)))
(is (every? boolean? (map (column :BTEST/IS_IT) data)))) (is (every? boolean? (map (column :BTEST/IS_IT) data))))
(if (or (sqlite?) (derby?)) (if (or (sqlite?) (derby?) (xtdb?))
(is (every? number? (map (column :BTEST/TWIDDLE) data))) (is (every? number? (map (column :BTEST/TWIDDLE) data)))
(is (every? boolean? (map (column :BTEST/TWIDDLE) data))))) (is (every? boolean? (map (column :BTEST/TWIDDLE) data))))))
(testing "BOOLEAN read column by index"
(let [data (jdbc/execute! (ds) ["select * from btest"] (let [data (jdbc/execute! (ds) ["select * from btest"]
(cond-> (default-options) (cond-> (default-options)
(sqlite?) (or (sqlite?) (xtdb?))
(assoc :builder-fn (assoc :builder-fn
(rs/builder-adapter (rs/builder-adapter
rs/as-maps rs/as-maps
@ -511,8 +569,11 @@ VALUES ('Pear', 'green', 49, 47)
(let [rsm ^ResultSetMetaData (:rsmeta builder)] (let [rsm ^ResultSetMetaData (:rsmeta builder)]
(rs/read-column-by-index (rs/read-column-by-index
;; we only use bit and bool for ;; we only use bit and bool for
;; sqlite (not boolean) ;; sqlite (not boolean), and
(if (#{"BIT" "BOOL"} (.getColumnTypeName rsm i)) ;; int8 and bool for xtdb:
(if (#{"BIT" "BOOL"
"int8" "bool"}
(.getColumnTypeName rsm i))
(.getBoolean rs i) (.getBoolean rs i)
(.getObject rs i)) (.getObject rs i))
rsm rsm
@ -520,19 +581,22 @@ VALUES ('Pear', 'green', 49, 47)
(is (every? boolean? (map (column :BTEST/IS_IT) data))) (is (every? boolean? (map (column :BTEST/IS_IT) data)))
(if (derby?) (if (derby?)
(is (every? number? (map (column :BTEST/TWIDDLE) data))) (is (every? number? (map (column :BTEST/TWIDDLE) data)))
(is (every? boolean? (map (column :BTEST/TWIDDLE) data))))) (is (every? boolean? (map (column :BTEST/TWIDDLE) data))))))
(testing "BOOLEAN via coercion"
(let [data (reduce (fn [acc row] (let [data (reduce (fn [acc row]
(conj acc (cond-> (select-keys row [:is_it :twiddle]) (conj acc (cond-> (select-keys row [:is_it :twiddle])
(sqlite?) (sqlite?)
(update :is_it pos?) (update :is_it pos?)
(or (sqlite?) (derby?)) (or (sqlite?) (derby?) (xtdb?))
(update :twiddle pos?)))) (update :twiddle pos?))))
[] []
(jdbc/plan (ds) ["select * from btest"]))] (jdbc/plan (ds) ["select * from btest"]))]
(is (every? boolean? (map :is_it data))) (is (every? boolean? (map :is_it data)))
(is (every? boolean? (map :twiddle data))))) (is (every? boolean? (map :twiddle data))))))))
(deftest execute-batch-tests (deftest execute-batch-tests
{:context [(around [f] (with-test-db f))]}
(when-not (xtdb?)
(testing "simple batch insert" (testing "simple batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13] (is (= [1 1 1 1 1 1 1 1 1 13]
(jdbc/with-transaction [t (ds) {:rollback-only true}] (jdbc/with-transaction [t (ds) {:rollback-only true}]
@ -607,7 +671,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(conj result (count (jdbc/execute! t ["select * from fruit"])))))))) (conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
(testing "return generated keys" (testing "return generated keys"
(when-not (mssql?) (when-not (or (mssql?) (sqlite?))
(let [results (let [results
(jdbc/with-transaction [t (ds) {:rollback-only true}] (jdbc/with-transaction [t (ds) {:rollback-only true}]
(with-open [ps (jdbc/prepare t [" (with-open [ps (jdbc/prepare t ["
@ -631,9 +695,11 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
;; Derby and SQLite only return one generated key per batch so there ;; Derby and SQLite only return one generated key per batch so there
;; are only three keys, plus the overall count here: ;; are only three keys, plus the overall count here:
(is (< 3 (count results)))) (is (< 3 (count results))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))
(deftest execute-batch-connectable-tests (deftest execute-batch-connectable-tests
{:context [(around [f] (with-test-db f))]}
(when-not (xtdb?)
(testing "simple batch insert" (testing "simple batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13] (is (= [1 1 1 1 1 1 1 1 1 13]
(try (try
@ -651,7 +717,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{})] {})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally (finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "batch with-options" (testing "batch with-options"
(is (= [1 1 1 1 1 1 1 1 1 13] (is (= [1 1 1 1 1 1 1 1 1 13]
@ -670,12 +736,19 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{})] {})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally (finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "batch with-logging" (testing "batch with-logging"
(let [tracker (atom {:opts 0, :log1 0 :log2 0})
;; opts and log2 never get invoked with batch/prepare -- because
;; no fn opts are invoked on that path, and no post-logging is done:
track-fn (fn ([k] (fn [& _] (swap! tracker update k inc)))
([k f] (fn [& args] (swap! tracker update k inc) (apply f args))))]
(is (= {:opts 0, :log1 0 :log2 0} @tracker))
(is (= [1 1 1 1 1 1 1 1 1 13] (is (= [1 1 1 1 1 1 1 1 1 13]
(try (try
(let [result (jdbc/execute-batch! (jdbc/with-logging (ds) println println) ;; wrapping a non-connection will lose the wrapper:
(let [result (jdbc/execute-batch! (jdbc/with-options (ds) {:builder-fn (track-fn :opts rs/as-maps)})
"INSERT INTO fruit (name, appearance) VALUES (?,?)" "INSERT INTO fruit (name, appearance) VALUES (?,?)"
[["fruit1" "one"] [["fruit1" "one"]
["fruit2" "two"] ["fruit2" "two"]
@ -689,8 +762,109 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{})] {})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally (finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) (is (= {:opts 0, :log1 0 :log2 0} @tracker))
(is (= [1 1 1 1 1 1 1 1 1 13]
(try
;; wrapping a non-connection will lose the wrapper:
(let [result (jdbc/execute-batch! (jdbc/with-logging (ds) {:builder-fn (track-fn :opts rs/as-maps)})
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
[["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= {:opts 0, :log1 0 :log2 0} @tracker))
(is (= [1 1 1 1 1 1 1 1 1 13]
(try
(with-open [con (jdbc/get-connection (ds))]
(let [result (jdbc/execute-batch! (jdbc/with-options con {:builder-fn (track-fn :opts rs/as-maps)})
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
[["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
(finally
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= {:opts 0, :log1 0 :log2 0} @tracker))
(is (= [1 1 1 1 1 1 1 1 1 13]
(try
(with-open [con (jdbc/get-connection (ds))]
(let [result (jdbc/execute-batch! (jdbc/with-logging con (track-fn :log1) (track-fn :log2))
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
[["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
(finally
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= {:opts 0, :log1 1 :log2 0} @tracker))
(is (= [1 1 1 1 1 1 1 1 1 13]
(try
(with-open [con (jdbc/get-connection (ds))]
(let [result (jdbc/execute-batch! (jdbc/with-options
(jdbc/with-logging con (track-fn :log1) (track-fn :log2))
{:builder-fn (track-fn :opts rs/as-maps)})
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
[["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
(finally
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= {:opts 0, :log1 2 :log2 0} @tracker))
(is (= [1 1 1 1 1 1 1 1 1 13]
(try
(with-open [con (jdbc/get-connection (ds))]
(let [result (jdbc/execute-batch! (jdbc/with-logging
(jdbc/with-options con
{:builder-fn (track-fn :opts rs/as-maps)})
(track-fn :log1) (track-fn :log2))
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
[["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
(finally
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= {:opts 0, :log1 3 :log2 0} @tracker))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
(testing "small batch insert" (testing "small batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13] (is (= [1 1 1 1 1 1 1 1 1 13]
(try (try
@ -708,7 +882,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{:batch-size 3})] {:batch-size 3})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally (finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "big batch insert" (testing "big batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13] (is (= [1 1 1 1 1 1 1 1 1 13]
@ -727,7 +901,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{:batch-size 8})] {:batch-size 8})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally (finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "large batch insert" (testing "large batch insert"
(when-not (or (jtds?) (sqlite?)) (when-not (or (jtds?) (sqlite?))
@ -748,10 +922,10 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
:large true})] :large true})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally (finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
(testing "return generated keys" (testing "return generated keys"
(when-not (mssql?) (when-not (or (mssql?) (sqlite?))
(let [results (let [results
(try (try
(let [result (jdbc/execute-batch! (ds) (let [result (jdbc/execute-batch! (ds)
@ -772,26 +946,31 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
:return-generated-keys true})] :return-generated-keys true})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally (finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))] (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))]
(is (= 13 (last results))) (is (= 13 (last results)))
(is (every? map? (butlast results))) (is (every? map? (butlast results)))
;; Derby and SQLite only return one generated key per batch so there ;; Derby and SQLite only return one generated key per batch so there
;; are only three keys, plus the overall count here: ;; are only three keys, plus the overall count here:
(is (< 3 (count results)))) (is (< 3 (count results))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))
(deftest folding-test (deftest folding-test
{:context [(around [f] (with-test-db f))]}
(jdbc/execute-one! (ds) ["delete from fruit"]) (jdbc/execute-one! (ds) ["delete from fruit"])
(if (xtdb?)
(with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare con ["insert into fruit(_id,name) values (?,?)"])]
(jdbc/execute-batch! ps (mapv #(vector % (str "Fruit-" %)) (range 1 1001))))
(with-open [con (jdbc/get-connection (ds)) (with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare con ["insert into fruit(name) values (?)"])] ps (jdbc/prepare con ["insert into fruit(name) values (?)"])]
(jdbc/execute-batch! ps (mapv #(vector (str "Fruit-" %)) (range 1 1001)))) (jdbc/execute-batch! ps (mapv #(vector (str "Fruit-" %)) (range 1 1001)))))
(testing "foldable result set" (testing "foldable result set"
(testing "from a Connection" (testing "from a Connection"
(let [result (let [result
(with-open [con (jdbc/get-connection (ds))] (with-open [con (jdbc/get-connection (ds))]
(r/foldcat (r/foldcat
(r/map (column :FRUIT/NAME) (r/map (column :FRUIT/NAME)
(jdbc/plan con ["select * from fruit order by id"] (jdbc/plan con [(str "select * from fruit order by " (index))]
(default-options)))))] (default-options)))))]
(is (= 1000 (count result))) (is (= 1000 (count result)))
(is (= "Fruit-1" (first result))) (is (= "Fruit-1" (first result)))
@ -803,7 +982,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(try (try
(r/fold n r/cat r/append! (r/fold n r/cat r/append!
(r/map (column :FRUIT/NAME) (r/map (column :FRUIT/NAME)
(jdbc/plan (ds) ["select * from fruit order by id"] (jdbc/plan (ds) [(str "select * from fruit order by " (index))]
(default-options)))) (default-options))))
(catch java.util.concurrent.RejectedExecutionException _ (catch java.util.concurrent.RejectedExecutionException _
[]))] []))]
@ -814,7 +993,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(let [result (let [result
(with-open [con (jdbc/get-connection (ds)) (with-open [con (jdbc/get-connection (ds))
stmt (jdbc/prepare con stmt (jdbc/prepare con
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
(default-options))] (default-options))]
(r/foldcat (r/foldcat
(r/map (column :FRUIT/NAME) (r/map (column :FRUIT/NAME)
@ -828,15 +1007,16 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
stmt (prep/statement con (default-options))] stmt (prep/statement con (default-options))]
(r/foldcat (r/foldcat
(r/map (column :FRUIT/NAME) (r/map (column :FRUIT/NAME)
(jdbc/plan stmt ["select * from fruit order by id"] (jdbc/plan stmt [(str "select * from fruit order by " (index))]
(default-options)))))] (default-options)))))]
(is (= 1000 (count result))) (is (= 1000 (count result)))
(is (= "Fruit-1" (first result))) (is (= "Fruit-1" (first result)))
(is (= "Fruit-1000" (last result))))))) (is (= "Fruit-1000" (last result)))))))
(deftest connection-tests (deftest connection-tests
{:context [(around [f] (with-test-db f))]}
(testing "datasource via jdbcUrl" (testing "datasource via jdbcUrl"
(when-not (postgres?) (when-not (or (postgres?) (xtdb?))
(let [[url etc] (#'c/spec->url+etc (db)) (let [[url etc] (#'c/spec->url+etc (db))
ds (jdbc/get-datasource (assoc etc :jdbcUrl url))] ds (jdbc/get-datasource (assoc etc :jdbcUrl url))]
(cond (derby?) (is (= {:create true} etc)) (cond (derby?) (is (= {:create true} etc))
@ -861,6 +1041,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(is (instance? java.sql.Connection con))))))) (is (instance? java.sql.Connection con)))))))
(deftest multi-rs (deftest multi-rs
{:context [(around [f] (with-test-db f))]}
(when (mssql?) (when (mssql?)
(testing "script with multiple result sets" (testing "script with multiple result sets"
(let [multi-rs (let [multi-rs
@ -892,9 +1073,9 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{:multi-rs true}) {:multi-rs true})
zero-updates [{:next.jdbc/update-count 0}]] zero-updates [{:next.jdbc/update-count 0}]]
(cond (postgres?) ; does not support multiple result sets yet (cond (postgres?) ; does not support multiple result sets yet
(do ;; 4.7.3 (and earlier?) returned the fake zero-updates
(is (= 1 (count multi-rs))) ;; 4.7.4 returns -1 for update count and an empty result set
(is (= zero-updates (first multi-rs)))) (is (= 0 (count multi-rs)))
(hsqldb?) (hsqldb?)
(do (do
(is (= 3 (count multi-rs))) (is (= 3 (count multi-rs)))
@ -909,6 +1090,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(println 'call-proc (:dbtype (db)) (ex-message t) (some-> t (ex-cause) (ex-message)))))))) (println 'call-proc (:dbtype (db)) (ex-message t) (some-> t (ex-cause) (ex-message))))))))
(deftest plan-misuse (deftest plan-misuse
{:context [(around [f] (with-test-db f))]}
(let [s (pr-str (jdbc/plan (ds) ["select * from fruit"]))] (let [s (pr-str (jdbc/plan (ds) ["select * from fruit"]))]
(is (re-find #"missing reduction" s))) (is (re-find #"missing reduction" s)))
(let [s (pr-str (into [] (jdbc/plan (ds) ["select * from fruit"])))] (let [s (pr-str (into [] (jdbc/plan (ds) ["select * from fruit"])))]
@ -919,24 +1101,64 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(let [s (pr-str (into [] (take 3) (jdbc/plan (ds) ["select * from fruit"] (let [s (pr-str (into [] (take 3) (jdbc/plan (ds) ["select * from fruit"]
(default-options))))] (default-options))))]
(is (or (re-find #"missing `map` or `reduce`" s) (is (or (re-find #"missing `map` or `reduce`" s)
(re-find #"(?i)^\[#:fruit\{.*:id.*\}\]$" s)))) (re-find #"(?i)^\[(#:fruit)?\{.*:_?id.*\}\]$" s))))
(is (every? #(re-find #"(?i)^#:fruit\{.*:id.*\}$" %) (is (every? #(re-find #"(?i)^(#:fruit)?\{.*:_?id.*\}$" %)
(into [] (map str) (jdbc/plan (ds) ["select * from fruit"] (into [] (map str) (jdbc/plan (ds) ["select * from fruit"]
(default-options))))) (default-options)))))
(is (every? #(re-find #"(?i)^#:fruit\{.*:id.*\}$" %) (is (every? #(re-find #"(?i)^(#:fruit)?\{.*:_?id.*\}$" %)
(into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"] (into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"]
(default-options))))) (default-options)))))
(is (thrown? IllegalArgumentException (is (thrown? IllegalArgumentException
(doall (take 3 (jdbc/plan (ds) ["select * from fruit"])))))) (doall (take 3 (jdbc/plan (ds) ["select * from fruit"]))))))
(deftest issue-204 (deftest issue-204
{:context [(around [f] (with-test-db f))]}
(testing "against a Connection" (testing "against a Connection"
(is (seq (with-open [con (jdbc/get-connection (ds))] (is (seq (with-open [con (jdbc/get-connection (ds))]
(jdbc/on-connection [x con] (jdbc/execute! x ["select * from fruit"])))))) (jdbc/on-connection
[x con]
(jdbc/execute! x ["select * from fruit"]))))))
(testing "against a wrapped Connection" (testing "against a wrapped Connection"
(is (seq (with-open [con (jdbc/get-connection (ds))] (is (seq (with-open [con (jdbc/get-connection (ds))]
(jdbc/on-connection [x (jdbc/with-options con {})] (jdbc/execute! x ["select * from fruit"])))))) (jdbc/on-connection
[x (jdbc/with-options con {})]
(jdbc/execute! x ["select * from fruit"]))))))
(testing "against a wrapped Datasource" (testing "against a wrapped Datasource"
(is (seq (jdbc/on-connection [x (jdbc/with-options (ds) {})] (jdbc/execute! x ["select * from fruit"]))))) (is (seq (jdbc/on-connection
[x (jdbc/with-options (ds) {})]
(jdbc/execute! x ["select * from fruit"])))))
(testing "against a Datasource" (testing "against a Datasource"
(is (seq (jdbc/on-connection [x (ds)] (jdbc/execute! x ["select * from fruit"])))))) (is (seq (jdbc/on-connection
[x (ds)]
(jdbc/execute! x ["select * from fruit"]))))))
(deftest issue-256
{:context [(around [f] (with-test-db f))]}
(testing "against a Connection"
(is (seq (with-open [con (jdbc/get-connection (ds))]
(jdbc/on-connection+options
[x con] ; raw connection stays raw
(is (instance? java.sql.Connection x))
(jdbc/execute! x ["select * from fruit"]))))))
(testing "against a wrapped Connection"
(is (seq (with-open [con (jdbc/get-connection (ds))]
(jdbc/on-connection+options
[x (jdbc/with-options con {:test-option 42})]
;; ensure we get the same wrapped connection
(is (instance? java.sql.Connection (:connectable x)))
(is (= {:test-option 42} (:options x)))
(jdbc/execute! x ["select * from fruit"]))))))
(testing "against a wrapped Datasource"
(is (seq (jdbc/on-connection+options
[x (jdbc/with-options (ds) {:test-option 42})]
;; ensure we get a wrapped connection
(is (instance? java.sql.Connection (:connectable x)))
(is (= {:test-option 42} (:options x)))
(jdbc/execute! x ["select * from fruit"])))))
(testing "against a Datasource"
(is (seq (jdbc/on-connection+options
[x (ds)] ; unwrapped datasource has no options
;; ensure we get a wrapped connection (empty options)
(is (instance? java.sql.Connection (:connectable x)))
(is (= {} (:options x)))
(jdbc/execute! x ["select * from fruit"]))))))