Compare commits
676 commits
v2.0.0-alp
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5dbc274be | ||
|
|
df753e8635 | ||
|
|
b4b2ca7d79 | ||
|
|
78f7d5282f | ||
|
|
024d17b11e | ||
|
|
7611871935 | ||
|
|
1b042687f4 | ||
|
|
a981ed9171 | ||
|
|
74cf16c134 | ||
|
|
2c8fc30b1d | ||
|
|
3906aa53c0 | ||
|
|
92e4e16b45 | ||
|
|
30b5fabe58 | ||
|
|
d74046c658 | ||
|
|
44494e61c0 | ||
|
|
d70e89ae3b | ||
|
|
67ea477a5c | ||
|
|
89f39be55c | ||
|
|
0d1fd0e901 | ||
|
|
675c94b294 | ||
|
|
03d96f5747 | ||
|
|
f7dbfba57c | ||
|
|
f0eb68f151 | ||
|
|
4d1f5f83b7 | ||
|
|
c2990597f1 | ||
|
|
695351e33c | ||
|
|
0cd81b5d9b | ||
|
|
c295db44c0 | ||
|
|
44ca426b78 | ||
|
|
e3fcb3e278 | ||
|
|
7e1fe8f558 | ||
|
|
13eb8fe859 | ||
|
|
4bc1d16f24 | ||
|
|
206f980093 | ||
|
|
3beaa6b2bf | ||
|
|
30a04975f5 | ||
|
|
4f5b0ed256 | ||
|
|
1681764830 | ||
|
|
6c0c66e371 | ||
|
|
2c793ce441 | ||
|
|
f05c7051e2 | ||
|
|
d086631e54 | ||
|
|
3ecac63bea | ||
|
|
8e0d6984bd | ||
|
|
94fae3437f | ||
|
|
316f36751f | ||
|
|
8ae93d91f6 | ||
|
|
5fa85400f0 | ||
|
|
045634fd3c | ||
|
|
81167cb77e | ||
|
|
a3b79215c4 | ||
|
|
0cbe76329e | ||
|
|
c93eef06f6 | ||
|
|
60f5662d81 | ||
|
|
6531413325 | ||
|
|
fce39548d0 | ||
|
|
0f26e7d060 | ||
|
|
c98df6dd97 | ||
|
|
7a24fd0367 | ||
|
|
30d177165d | ||
|
|
e4762a1a70 | ||
|
|
e0356bc9c5 | ||
|
|
aa1f2bc0f6 | ||
|
|
0272c7b9ed | ||
|
|
10ec823151 | ||
|
|
fdfc6bc997 | ||
|
|
3f1677bff2 | ||
|
|
782bc4b78a | ||
|
|
f2763d5af5 | ||
|
|
573d6c75ca | ||
|
|
42d5f4baf1 | ||
|
|
8320571c4d | ||
|
|
559e71205d | ||
|
|
1bac4352e3 | ||
|
|
b64ab9b0b0 | ||
|
|
049fe5b68b | ||
|
|
b716d077c4 | ||
|
|
09fa8afefe | ||
|
|
129239a742 | ||
|
|
ee53c54255 | ||
|
|
f4d212ae18 | ||
|
|
e2f7991ad8 | ||
|
|
c0c455358f | ||
|
|
488ddd4bcb | ||
|
|
b2c1ae0068 | ||
|
|
21ce3a2242 | ||
|
|
6fa606ffd5 | ||
|
|
c1c7cba96a | ||
|
|
4992a3cb76 | ||
|
|
bbac863a2a | ||
|
|
44399b1984 | ||
|
|
4288ceae56 | ||
|
|
38080aff92 | ||
|
|
9999a90e62 | ||
|
|
7892ec6006 | ||
|
|
362818530a | ||
|
|
643cea4930 | ||
|
|
b271a898f5 | ||
|
|
35545facce | ||
|
|
170602e85f | ||
|
|
c6e6b54b8f | ||
|
|
9dba3860e2 | ||
|
|
a187ba98f1 | ||
|
|
203e923f99 | ||
|
|
ba78dc2d27 | ||
|
|
b55eb23edd | ||
|
|
e2dc330cf9 | ||
|
|
de75ace988 | ||
|
|
3d48ecac37 | ||
|
|
a27f72eab9 | ||
|
|
7c0e25f253 | ||
|
|
3ca197b45c | ||
|
|
48edb03b32 | ||
|
|
40d9aee6e6 | ||
|
|
2fb4df6bdd | ||
|
|
4c75db9a95 | ||
|
|
18a511b1c9 | ||
|
|
15f73f9442 | ||
|
|
bfd7eb2141 | ||
|
|
803584dc7c | ||
|
|
ac136dab08 | ||
|
|
f31533d8d6 | ||
|
|
8c93e287ff | ||
|
|
846123c57a | ||
|
|
694233e2f0 | ||
|
|
34b58e41c4 | ||
|
|
a6a1272d17 | ||
|
|
60c1549168 | ||
|
|
c3f10c507d | ||
|
|
230cc467a1 | ||
|
|
084c1ec5e5 | ||
|
|
150fcda6d3 | ||
|
|
dd9547bbc1 | ||
|
|
acb5112f03 | ||
|
|
53a6ea0f8a | ||
|
|
4efa9a38ef | ||
|
|
de6fe93b75 | ||
|
|
37fe8b21bc | ||
|
|
7fceefbe5c | ||
|
|
434d3877d7 | ||
|
|
bf34a23e68 | ||
|
|
ae62d2b474 | ||
|
|
bb72885027 | ||
|
|
d24ee428f3 | ||
|
|
cbb3b3e90b | ||
|
|
b531747918 | ||
|
|
669fee5bc8 | ||
|
|
e7ef940e24 | ||
|
|
4e124091b4 | ||
|
|
a3ef215485 | ||
|
|
b07ac78d68 | ||
|
|
544992be88 | ||
|
|
6c2d8de53f | ||
|
|
c7e7d47c4e | ||
|
|
2c6bf85f7f | ||
|
|
bab4ce4bd5 | ||
|
|
2f55f423b9 | ||
|
|
1b7ade9317 | ||
|
|
747383c847 | ||
|
|
8764759323 | ||
|
|
5b04aa28c5 | ||
|
|
3b72fefe23 | ||
|
|
508158112d | ||
|
|
d1e9617eae | ||
|
|
6c88c0ba3c | ||
|
|
a5bdf4d13c | ||
|
|
eb680a204e | ||
|
|
e27298e444 | ||
|
|
f69ee7e8de | ||
|
|
04e7e5b3ab | ||
|
|
5c58e46417 | ||
|
|
582c331117 | ||
|
|
bffcc67fa4 | ||
|
|
7a7a01eeaa | ||
|
|
0f0d24b510 | ||
|
|
1f37b46151 | ||
|
|
225c0db092 | ||
|
|
9c29cb29ff | ||
|
|
51e64e1891 | ||
|
|
2f159ac912 | ||
|
|
c9867097e8 | ||
|
|
fc983927ce | ||
|
|
2f4792253a | ||
|
|
2149a80852 | ||
|
|
9b611bb7ff | ||
|
|
52ed86284a | ||
|
|
d64177bde5 | ||
|
|
35f4c674e9 | ||
|
|
1291b328d0 | ||
|
|
ac947b1543 | ||
|
|
9b9ec47bcf | ||
|
|
2e34a9f4ea | ||
|
|
d0193d3c10 | ||
|
|
39e7c45b4f | ||
|
|
1c499ac8a7 | ||
|
|
45d1230102 | ||
|
|
e70e3713fc | ||
|
|
9c40ff4879 | ||
|
|
b55eeef7a3 | ||
|
|
2e8157047d | ||
|
|
1e44f82eb5 | ||
|
|
ba2e8ad583 | ||
|
|
b55b71ff0a | ||
|
|
9da2ccc812 | ||
|
|
f46dbc5ca7 | ||
|
|
002285a5af | ||
|
|
bcbaae5ef5 | ||
|
|
4a7d46dd1f | ||
|
|
7d05220cfa | ||
|
|
18fcddfc34 | ||
|
|
1fe526a734 | ||
|
|
737baa9d0e | ||
|
|
b3fe7c1436 | ||
|
|
e36ad64aa6 | ||
|
|
d45e1dff0f | ||
|
|
10b2a17718 | ||
|
|
caca08fd5b | ||
|
|
7d56daacca | ||
|
|
440b86633a | ||
|
|
5e9bdba777 | ||
|
|
2c6b89751d | ||
|
|
872cb1d006 | ||
|
|
0115424167 | ||
|
|
f9811f0c59 | ||
|
|
8334978a43 | ||
|
|
bf51f725e2 | ||
|
|
7fc411bdd7 | ||
|
|
ebd8f7ff47 | ||
|
|
a9d33a8873 | ||
|
|
11e1a93c59 | ||
|
|
a6a69a16f7 | ||
|
|
756ed95b43 | ||
|
|
ac09fc1abd | ||
|
|
75830df509 | ||
|
|
654a1cb67a | ||
|
|
87f3e731b1 | ||
|
|
16a9708790 | ||
|
|
ad1b0f9880 | ||
|
|
44ffd340f5 | ||
|
|
aa4ebf5f47 | ||
|
|
bf1517a60e | ||
|
|
1d6ae7b376 | ||
|
|
810e95fe11 | ||
|
|
aa5d8e094a | ||
|
|
686cbf7272 | ||
|
|
2f99103ed1 | ||
|
|
17dbbce0d2 | ||
|
|
664e5e2644 | ||
|
|
fd98efc95c | ||
|
|
2e955bfe57 | ||
|
|
4c711d790a | ||
|
|
d187c66987 | ||
|
|
c7ec650cfc | ||
|
|
2c463ec517 | ||
|
|
ba336f2884 | ||
|
|
c2088bff6c | ||
|
|
5164c24342 | ||
|
|
e679e93362 | ||
|
|
e7972ac1b4 | ||
|
|
290537c581 | ||
|
|
3ec884f881 | ||
|
|
19e73a3ebd | ||
|
|
23c9597870 | ||
|
|
2d5e89d545 | ||
|
|
445f66ae1e | ||
|
|
e7737ee7af | ||
|
|
93c8ad75a6 | ||
|
|
6ce63cad2c | ||
|
|
f14e95ec9d | ||
|
|
84a41cba7d | ||
|
|
e83fa1d76a | ||
|
|
3d88db6f21 | ||
|
|
7fcb9d97d3 | ||
|
|
75bf60667d | ||
|
|
077fc3f23a | ||
|
|
6748d86dae | ||
|
|
bc66ec5aee | ||
|
|
c8d4f58f0d | ||
|
|
3910df215a | ||
|
|
e66e7dc6d5 | ||
|
|
1710e07231 | ||
|
|
2efe05def2 | ||
|
|
dffedb115e | ||
|
|
6203a88615 | ||
|
|
54c2f0960a | ||
|
|
bb6750a982 | ||
|
|
1310591d05 | ||
|
|
8cbb7f3834 | ||
|
|
858d157863 | ||
|
|
025e445d5d | ||
|
|
522849381d | ||
|
|
f5744d4f21 | ||
|
|
7e67caaf0c | ||
|
|
18ed03985d | ||
|
|
ece9be3a4b | ||
|
|
e8753efa3e | ||
|
|
3f31e5a61f | ||
|
|
f7cf5718cc | ||
|
|
bfc8ad6821 | ||
|
|
7e35cb31bf | ||
|
|
1d7237884d | ||
|
|
3c65999ef1 | ||
|
|
f0ccaae192 | ||
|
|
3e23ad5f2b | ||
|
|
4b5e4e862b | ||
|
|
dbbe40dc94 | ||
|
|
c905853366 | ||
|
|
da150f03e3 | ||
|
|
5b4b709010 | ||
|
|
6d618dbd35 | ||
|
|
e6f9ffdc32 | ||
|
|
0936095040 | ||
|
|
a610f256dd | ||
|
|
63c7a45578 | ||
|
|
44b6de4139 | ||
|
|
0c7642a357 | ||
|
|
fc3397650e | ||
|
|
b1b7868c8b | ||
|
|
3fba12fbcc | ||
|
|
dae73c6195 | ||
|
|
de0adf56ef | ||
|
|
0a6f645d91 | ||
|
|
762252b660 | ||
|
|
6324eca4fc | ||
|
|
f8532dfd33 | ||
|
|
5fb150bd53 | ||
|
|
982ebd18e1 | ||
|
|
e783362c6a | ||
|
|
ca78c7eed4 | ||
|
|
213c152fdb | ||
|
|
6ab8c6452b | ||
|
|
22f259e897 | ||
|
|
4a7c3631a6 | ||
|
|
6bdc262e7c | ||
|
|
d17d44ffcf | ||
|
|
6b015400ed | ||
|
|
83ad5cff74 | ||
|
|
ca953e3c42 | ||
|
|
56823b8888 | ||
|
|
295be57bfc | ||
|
|
bf96f034c3 | ||
|
|
d0ae02a6ef | ||
|
|
c62f5da3f8 | ||
|
|
4ea630ed90 | ||
|
|
359d9de668 | ||
|
|
0faf611855 | ||
|
|
6e5e1b100d | ||
|
|
b5e55cf95a | ||
|
|
e4f0a5eff3 | ||
|
|
a87fa0c9ab | ||
|
|
562b20634a | ||
|
|
15cf3ae588 | ||
|
|
db1b37eaa3 | ||
|
|
fd64353f41 | ||
|
|
95e50a930e | ||
|
|
e8ea9283cc | ||
|
|
dda3aa017e | ||
|
|
6c107b7cf0 | ||
|
|
9a03dde4d1 | ||
|
|
73d36ab2b5 | ||
|
|
30e554f31c | ||
|
|
e553f4f169 | ||
|
|
74b05965c1 | ||
|
|
9af194f8a2 | ||
|
|
061288f1c0 | ||
|
|
5d7a3faea5 | ||
|
|
fb601c90d9 | ||
|
|
02d20bd78c | ||
|
|
a2571ef312 | ||
|
|
723b134e90 | ||
|
|
63df2f3dc9 | ||
|
|
ae27fb75e9 | ||
|
|
23be700b7e | ||
|
|
2f1d3ae870 | ||
|
|
d0e0badf2c | ||
|
|
9569b19a34 | ||
|
|
c913ffe155 | ||
|
|
3073d28525 | ||
|
|
737699c11a | ||
|
|
e204f3b45e | ||
|
|
a879a2d8a1 | ||
|
|
7a23ab649e | ||
|
|
e0157263ab | ||
|
|
204f6fa72a | ||
|
|
d31600d2c0 | ||
|
|
c10a97e1ec | ||
|
|
2cba8bc3d7 | ||
|
|
99e955f420 | ||
|
|
e6654f7a22 | ||
|
|
061edc42f7 | ||
|
|
f0ada59fff | ||
|
|
5be96ba6e3 | ||
|
|
43096f72e6 | ||
|
|
79c005b765 | ||
|
|
5c6ce8a379 | ||
|
|
5f54fab989 | ||
|
|
8e72cb8f2c | ||
|
|
16ce2e8b2d | ||
|
|
3def127276 | ||
|
|
ab0a3eebae | ||
|
|
91e75a7edf | ||
|
|
fb1decbd1e | ||
|
|
d065a04e18 | ||
|
|
2e9b395b31 | ||
|
|
5fe73c75bc | ||
|
|
afa5c6af99 | ||
|
|
8c8b05e67f | ||
|
|
70e8afc273 | ||
|
|
d179004180 | ||
|
|
796c734cba | ||
|
|
5e847cde0e | ||
|
|
270b9439c8 | ||
|
|
03275b1035 | ||
|
|
fdc70c67a9 | ||
|
|
e45ea8586e | ||
|
|
124fac6f28 | ||
|
|
13a8aa11b2 | ||
|
|
7f8b7a79b1 | ||
|
|
9e72587bf3 | ||
|
|
083833cd8b | ||
|
|
a2e02c8a03 | ||
|
|
314f497417 | ||
|
|
2e7da61f56 | ||
|
|
ab7c235329 | ||
|
|
c4efcc0cad | ||
|
|
17319cdd26 | ||
|
|
af69f12630 | ||
|
|
ccaf29292a | ||
|
|
fe17b51e9a | ||
|
|
ca0feb57e5 | ||
|
|
4823ec8694 | ||
|
|
f711b934fa | ||
|
|
beedc35a22 | ||
|
|
803ff41dc0 | ||
|
|
4bf76920ef | ||
|
|
c7c634d694 | ||
|
|
b7b1ebafb9 | ||
|
|
04324d5002 | ||
|
|
1f2773bd16 | ||
|
|
1d22086fce | ||
|
|
8a2f447676 | ||
|
|
139de6f56c | ||
|
|
30209b6cc7 | ||
|
|
f260185825 | ||
|
|
e3de2a621d | ||
|
|
18adeb1048 | ||
|
|
9ea822a1b2 | ||
|
|
e331ba0a0e | ||
|
|
33ddaa74b6 | ||
|
|
06102e9334 | ||
|
|
0a25bc2adf | ||
|
|
826407e9db | ||
|
|
91e054c58b | ||
|
|
2670abc75f | ||
|
|
99ce051a9d | ||
|
|
e8e6c7f932 | ||
|
|
a653f9b157 | ||
|
|
499b9de0ae | ||
|
|
8976379fa6 | ||
|
|
0867ce54dc | ||
|
|
6e4e1f6928 | ||
|
|
8979e938f3 | ||
|
|
b30aa0f3f9 | ||
|
|
fa0bb6830c | ||
|
|
aa8089524b | ||
|
|
df7ea21e7b | ||
|
|
e562329669 | ||
|
|
8118f5448e | ||
|
|
d607f01dd5 | ||
|
|
e0cafbd434 | ||
|
|
dc37852024 | ||
|
|
718b7f036f | ||
|
|
87039e7159 | ||
|
|
66fc3a68ee | ||
|
|
915857754d | ||
|
|
d660c059d4 | ||
|
|
be1df97b2b | ||
|
|
627bc25357 | ||
|
|
dce2269be7 | ||
|
|
36e0ede47e | ||
|
|
e9abb81892 | ||
|
|
ceabd49508 | ||
|
|
9052626805 | ||
|
|
3cacec9c32 | ||
|
|
6aee04e25c | ||
|
|
3d0a9ba79c | ||
|
|
80bbf85c3a | ||
|
|
78cc769b98 | ||
|
|
472701d728 | ||
|
|
ed844ed057 | ||
|
|
feb0c9a6f8 | ||
|
|
92e0a04a84 | ||
|
|
dae09ff601 | ||
|
|
e44a30a7fd | ||
|
|
c7c6102661 | ||
|
|
ec758dd818 | ||
|
|
871908d5c9 | ||
|
|
e19870aba7 | ||
|
|
bcdd81e931 | ||
|
|
241f0c2519 | ||
|
|
3798dca428 | ||
|
|
06e9917daa | ||
|
|
68a63b7078 | ||
|
|
1c042c375b | ||
|
|
17881ef2b4 | ||
|
|
ae29147a22 | ||
|
|
9b018aa4ee | ||
|
|
55b68c4918 | ||
|
|
458cc9fa52 | ||
|
|
cccd3e96fd | ||
|
|
4eb9eca147 | ||
|
|
c81d27a224 | ||
|
|
35c5aee584 | ||
|
|
584cd1c711 | ||
|
|
5246fe4549 | ||
|
|
943b9547c9 | ||
|
|
eac5e933c3 | ||
|
|
ee65201cfc | ||
|
|
68a1ccfa3c | ||
|
|
68eafc0c2e | ||
|
|
5f68b8ca77 | ||
|
|
4ba9d2f6ca | ||
|
|
5191abaa6c | ||
|
|
dabb501e4b | ||
|
|
9ece8972b5 | ||
|
|
249ab639ec | ||
|
|
aef6e4cba7 | ||
|
|
e51637008d | ||
|
|
97c42f5c78 | ||
|
|
964f9837b0 | ||
|
|
10e6e755e9 | ||
|
|
3288ee7ec5 | ||
|
|
4480c9c1c7 | ||
|
|
904af73505 | ||
|
|
3680a1beae | ||
|
|
13044d7394 | ||
|
|
01c3a555ba | ||
|
|
50bbfef07f | ||
|
|
52e2a57fca | ||
|
|
1836c7bcf1 | ||
|
|
679a0309db | ||
|
|
7e84b58463 | ||
|
|
994f166d40 | ||
|
|
a393cd89e2 | ||
|
|
8387dad01a | ||
|
|
fa5c45e985 | ||
|
|
98e94ada67 | ||
|
|
616543c35d | ||
|
|
1b2e742b38 | ||
|
|
1aec4c3e35 | ||
|
|
14a3dea831 | ||
|
|
8d159391c0 | ||
|
|
395ba6a5bd | ||
|
|
8ec08ca686 | ||
|
|
13b640d486 | ||
|
|
1c2f1f4a99 | ||
|
|
b32911d808 | ||
|
|
4324ed4250 | ||
|
|
d89cdb52d5 | ||
|
|
cf136c2fdc | ||
|
|
00b20f6bbf | ||
|
|
a2ee638b3b | ||
|
|
3b00d5c3e6 | ||
|
|
0ab991d856 | ||
|
|
bece0f1fd1 | ||
|
|
21c59ff96d | ||
|
|
228eb9fb67 | ||
|
|
a51cfe5a2e | ||
|
|
f508196ba3 | ||
|
|
21e59ed408 | ||
|
|
6b284bfc77 | ||
|
|
798219400a | ||
|
|
0d5f416de8 | ||
|
|
f1ed7f0c90 | ||
|
|
77191e26f6 | ||
|
|
fb60113858 | ||
|
|
ad55f81bc9 | ||
|
|
52308d484f | ||
|
|
5e98454f28 | ||
|
|
29a060d422 | ||
|
|
9f5b552ad0 | ||
|
|
31a613b84c | ||
|
|
81fda8592d | ||
|
|
fe4be6cca6 | ||
|
|
9e0b31bbd9 | ||
|
|
819c6e7c19 | ||
|
|
e38bb73295 | ||
|
|
572984a45d | ||
|
|
12bfa32962 | ||
|
|
9c38554154 | ||
|
|
07a4d9127d | ||
|
|
0f31fa331b | ||
|
|
6ca391c84a | ||
|
|
6ebc017969 | ||
|
|
d73560b7e3 | ||
|
|
7761867852 | ||
|
|
11fcfd5257 | ||
|
|
46b3c1773b | ||
|
|
97e8007068 | ||
|
|
20cba15da2 | ||
|
|
f606dc6044 | ||
|
|
e227e1b9ab | ||
|
|
dd52ebe7e8 | ||
|
|
272b088918 | ||
|
|
684b33a03a | ||
|
|
e6a5bdb001 | ||
|
|
d734767877 | ||
|
|
bb9d196e3e | ||
|
|
50fd829752 | ||
|
|
bbc0ac8500 | ||
|
|
84147b242d | ||
|
|
862a2496c6 | ||
|
|
f393a61010 | ||
|
|
8a1e2cca71 | ||
|
|
f231151243 | ||
|
|
7a83bc13fe | ||
|
|
35c6fc58a5 | ||
|
|
88282ee258 | ||
|
|
cf7e36a131 | ||
|
|
1548433c02 | ||
|
|
0411364ee5 | ||
|
|
c2e8bb9193 | ||
|
|
2da32c70f1 | ||
|
|
8f7c990eed | ||
|
|
2fe083f8e6 | ||
|
|
106b19bcf6 | ||
|
|
af382708e5 | ||
|
|
b321df25d9 | ||
|
|
cfb001e2f5 | ||
|
|
dbecb152a1 | ||
|
|
06f25ed2e3 | ||
|
|
b010f91586 | ||
|
|
24b87ebd69 | ||
|
|
230aa7088a | ||
|
|
337d2c7f5d | ||
|
|
fde22f6bc0 | ||
|
|
13d62b4ac1 | ||
|
|
73c2062d6e | ||
|
|
9672ed3704 | ||
|
|
fd84864279 | ||
|
|
d76b2d82b1 | ||
|
|
7c21a403c2 | ||
|
|
9f6393a2fd | ||
|
|
16d04a1dfd | ||
|
|
cff1e5b43c | ||
|
|
82ee465820 | ||
|
|
6b070df52c | ||
|
|
479008c294 | ||
|
|
dc6a3662f0 | ||
|
|
d35d9141bc | ||
|
|
ddebda9481 | ||
|
|
445fb08e2f | ||
|
|
efcee05e0a | ||
|
|
e70985e93b | ||
|
|
97c9236842 | ||
|
|
d789c00f54 | ||
|
|
41522c89a1 | ||
|
|
77577517c8 | ||
|
|
1bbdfeef75 | ||
|
|
b600348808 | ||
|
|
06d90c174e | ||
|
|
5e2036a922 | ||
|
|
78ca2a0530 | ||
|
|
8ffca0deae | ||
|
|
ec6814eaba | ||
|
|
9f2fe5e9d4 | ||
|
|
770beec886 | ||
|
|
2793619e50 | ||
|
|
81afe1206d | ||
|
|
7254ba0dc4 | ||
|
|
e8196e266d | ||
|
|
7c5ec5c953 | ||
|
|
94765ca7db | ||
|
|
4436932ed9 | ||
|
|
226dc0b280 | ||
|
|
f4545362f9 |
60 changed files with 9522 additions and 3179 deletions
|
|
@ -1,28 +0,0 @@
|
||||||
version: 2
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
working_directory: ~/honeysql
|
|
||||||
docker:
|
|
||||||
- image: circleci/clojure:openjdk-11-tools-deps-1.10.2.774
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
key: honeysql-{{ checksum "deps.edn" }}
|
|
||||||
- run:
|
|
||||||
name: Get rid of erroneous git config
|
|
||||||
command: rm -rf ~/.gitconfig
|
|
||||||
- run:
|
|
||||||
name: Install Node
|
|
||||||
command: sudo apt-get update && sudo apt-get install -y nodejs
|
|
||||||
- run:
|
|
||||||
name: Download Dependencies
|
|
||||||
command: clojure -P -M:test:runner:cljs-runner:eastwood:readme && clojure -P -M:1.9
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- ~/.m2
|
|
||||||
- ~/.gitlibs
|
|
||||||
- ~/node_modules
|
|
||||||
key: honeysql-{{ checksum "deps.edn" }}
|
|
||||||
- run:
|
|
||||||
name: Run all the tests
|
|
||||||
command: sh run-tests.sh all
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
{:lint-as
|
{}
|
||||||
{honeysql.helpers/defhelper clojure.core/defn}}
|
|
||||||
|
|
|
||||||
1
.clj-kondo/imports/babashka/fs/config.edn
Normal file
1
.clj-kondo/imports/babashka/fs/config.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{:lint-as {babashka.fs/with-temp-dir clojure.core/let}}
|
||||||
3
.clj-kondo/imports/http-kit/http-kit/config.edn
Normal file
3
.clj-kondo/imports/http-kit/http-kit/config.edn
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
{:hooks
|
||||||
|
{:analyze-call {org.httpkit.server/with-channel httpkit.with-channel/with-channel}}}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
(ns httpkit.with-channel
|
||||||
|
(:require [clj-kondo.hooks-api :as api]))
|
||||||
|
|
||||||
|
(defn with-channel [{node :node}]
|
||||||
|
(let [[request channel & body] (rest (:children node))]
|
||||||
|
(when-not (and request channel) (throw (ex-info "No request or channel provided" {})))
|
||||||
|
(when-not (api/token-node? channel) (throw (ex-info "Missing channel argument" {})))
|
||||||
|
(let [new-node
|
||||||
|
(api/list-node
|
||||||
|
(list*
|
||||||
|
(api/token-node 'let)
|
||||||
|
(api/vector-node [channel (api/vector-node [])])
|
||||||
|
request
|
||||||
|
body))]
|
||||||
|
|
||||||
|
{:node new-node})))
|
||||||
5
.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn
Normal file
5
.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn
Normal 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/->>}}
|
||||||
1
.clj-kondo/imports/taoensso/encore/config.edn
Normal file
1
.clj-kondo/imports/taoensso/encore/config.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{:hooks {:analyze-call {taoensso.encore/defalias taoensso.encore/defalias}}}
|
||||||
16
.clj-kondo/imports/taoensso/encore/taoensso/encore.clj
Normal file
16
.clj-kondo/imports/taoensso/encore/taoensso/encore.clj
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
(ns taoensso.encore
|
||||||
|
(:require
|
||||||
|
[clj-kondo.hooks-api :as hooks]))
|
||||||
|
|
||||||
|
(defn defalias [{:keys [node]}]
|
||||||
|
(let [[sym-raw src-raw] (rest (:children node))
|
||||||
|
src (if src-raw src-raw sym-raw)
|
||||||
|
sym (if src-raw
|
||||||
|
sym-raw
|
||||||
|
(symbol (name (hooks/sexpr src))))]
|
||||||
|
{:node (with-meta
|
||||||
|
(hooks/list-node
|
||||||
|
[(hooks/token-node 'def)
|
||||||
|
(hooks/token-node (hooks/sexpr sym))
|
||||||
|
(hooks/token-node (hooks/sexpr src))])
|
||||||
|
(meta src))}))
|
||||||
38
.github/workflows/test-and-release.yml
vendored
Normal file
38
.github/workflows/test-and-release.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
name: Release Version
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '11'
|
||||||
|
- name: Setup Clojure
|
||||||
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
|
with:
|
||||||
|
cli: '1.12.0.1530'
|
||||||
|
- name: Cache All The Things
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.m2/repository
|
||||||
|
~/.gitlibs
|
||||||
|
~/.clojure
|
||||||
|
~/.cpcache
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: clojure -T:build ci :snapshot false
|
||||||
|
- name: Deploy Release
|
||||||
|
run: clojure -T:build deploy :snapshot false
|
||||||
|
env:
|
||||||
|
CLOJARS_PASSWORD: ${{secrets.DEPLOY_TOKEN}}
|
||||||
|
CLOJARS_USERNAME: ${{secrets.DEPLOY_USERNAME}}
|
||||||
65
.github/workflows/test-and-snapshot.yml
vendored
Normal file
65
.github/workflows/test-and-snapshot.yml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
name: Develop & Snapshot
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "develop"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-snapshot:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '11'
|
||||||
|
- name: Setup Clojure
|
||||||
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
|
with:
|
||||||
|
cli: '1.12.0.1530'
|
||||||
|
- name: Cache All The Things
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.m2/repository
|
||||||
|
~/.gitlibs
|
||||||
|
~/.clojure
|
||||||
|
~/.cpcache
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: clojure -T:build ci :snapshot true
|
||||||
|
- name: Deploy Snapshot
|
||||||
|
run: clojure -T:build deploy :snapshot true
|
||||||
|
env:
|
||||||
|
CLOJARS_PASSWORD: ${{secrets.DEPLOY_TOKEN}}
|
||||||
|
CLOJARS_USERNAME: ${{secrets.DEPLOY_USERNAME}}
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
java: [ '8', '17', '21' ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
- name: Clojure CLI
|
||||||
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
|
with:
|
||||||
|
cli: '1.12.0.1530'
|
||||||
|
- name: Cache All The Things
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.m2/repository
|
||||||
|
~/.gitlibs
|
||||||
|
~/.clojure
|
||||||
|
~/.cpcache
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: clojure -T:build ci
|
||||||
|
- name: Check cljdoc.edn
|
||||||
|
run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn
|
||||||
33
.github/workflows/test-bb.yml
vendored
Normal file
33
.github/workflows/test-bb.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: Babashka tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: 21
|
||||||
|
- name: Clojure CLI
|
||||||
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
|
with:
|
||||||
|
cli: '1.12.0.1530'
|
||||||
|
bb: latest
|
||||||
|
- name: Cache All The Things
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.m2/repository
|
||||||
|
~/.gitlibs
|
||||||
|
~/.clojure
|
||||||
|
~/.cpcache
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn', '**/bb.edn') }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: bb test
|
||||||
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
|
|
@ -1,24 +1,33 @@
|
||||||
name: Clojure CI
|
name: Pull Request
|
||||||
|
|
||||||
on: [push]
|
on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
java: [ '8', '11', '14' ]
|
java: [ '8', '11', '17', '21' ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Java
|
- uses: actions/setup-java@v4
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
java-version: ${{ matrix.java }}
|
java-version: ${{ matrix.java }}
|
||||||
- name: Setup Clojure
|
- name: Clojure CLI
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
tools-deps: '1.10.2.774'
|
cli: '1.12.0.1530'
|
||||||
|
- name: Cache All The Things
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.m2/repository
|
||||||
|
~/.gitlibs
|
||||||
|
~/.clojure
|
||||||
|
~/.cpcache
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: sh run-tests.sh all
|
run: clojure -T:build ci
|
||||||
- name: Check cljdoc.edn
|
- name: Check cljdoc.edn
|
||||||
run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn
|
run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn
|
||||||
|
|
|
||||||
42
.gitignore
vendored
42
.gitignore
vendored
|
|
@ -1,21 +1,29 @@
|
||||||
*~
|
|
||||||
.sw*
|
|
||||||
*.swp
|
|
||||||
/target
|
|
||||||
/lib
|
|
||||||
/classes
|
|
||||||
/checkouts
|
|
||||||
/dist
|
|
||||||
*.jar
|
|
||||||
*.class
|
*.class
|
||||||
.cpcache
|
*.jar
|
||||||
.clj-kondo/.cache
|
*.swp
|
||||||
.eastwood
|
*~
|
||||||
.lsp
|
.calva/output-window/
|
||||||
.nrepl-port
|
.calva/repl.calva-repl
|
||||||
/.rebel_readline_history
|
|
||||||
/.socket-repl-port
|
|
||||||
.classpath
|
.classpath
|
||||||
|
.clj-kondo/.cache
|
||||||
|
.cpcache
|
||||||
|
.eastwood
|
||||||
|
.factorypath
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.java-version
|
||||||
|
.lein-*
|
||||||
|
.lsp/.cache
|
||||||
|
.lsp/sqlite.db
|
||||||
|
.nrepl-history
|
||||||
|
.nrepl-port
|
||||||
|
.portal
|
||||||
.project
|
.project
|
||||||
bin
|
.rebel_readline_history
|
||||||
|
.settings
|
||||||
|
.socket-repl-port
|
||||||
|
.sw*
|
||||||
|
/checkouts
|
||||||
|
/classes
|
||||||
/cljs-test-runner-out
|
/cljs-test-runner-out
|
||||||
|
/target
|
||||||
|
|
|
||||||
346
CHANGELOG.md
346
CHANGELOG.md
|
|
@ -1,16 +1,358 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
|
* 2.7.next in progress
|
||||||
|
* Address [#440](https://github.com/seancorfield/honeysql/issues/440) by supporting multiple tables in `:truncate`.
|
||||||
|
* Support `USING HASH` as well as `USING GIN`.
|
||||||
|
* Fix [#571](https://github.com/seancorfield/honeysql/issues/571) by allowing `:order-by` to take an empty sequence of columns (and be omitted).
|
||||||
|
* Update dev/build deps.
|
||||||
|
|
||||||
|
* 2.7.1295 -- 2025-03-12
|
||||||
|
* Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax, and `:at` as special syntax for general `[`..`]` path syntax.
|
||||||
|
* Drop support for Clojure 1.9 [#561](https://github.com/seancorfield/honeysql/issues/561).
|
||||||
|
|
||||||
|
* 2.6.1281 -- 2025-03-06
|
||||||
|
* Address [#568](https://github.com/seancorfield/honeysql/issues/568) by adding `honey.sql/semicolon` to merge multiple SQL+params vectors into one (with semicolons separating the SQL statements).
|
||||||
|
* Address [#567](https://github.com/seancorfield/honeysql/issues/567) by adding support for `ASSERT` clause.
|
||||||
|
* Address [#566](https://github.com/seancorfield/honeysql/issues/566) by adding `IS [NOT] DISTINCT FROM` operators.
|
||||||
|
* Add examples of `:alias` with `:group-by` (syntax is slightly different to existing examples for `:order-by`).
|
||||||
|
|
||||||
|
* 2.6.1270 -- 2025-01-17
|
||||||
|
* Fix autoboxing introduced in 2.6.1267 via PR [#564](https://github.com/seancorfield/honeysql/pull/564) [@alexander-yakushev](https://github.com/alexander-yakushev).
|
||||||
|
|
||||||
|
* 2.6.1267 -- 2025-01-16
|
||||||
|
* Support expressions in `WITH` clauses via PR [#563](https://github.com/seancorfield/honeysql/pull/563) [@krevedkokun](https://github.com/krevedkokun).
|
||||||
|
* More performance optimizations via PRs [#560](https://github.com/seancorfield/honeysql/pull/560) and [#562](https://github.com/seancorfield/honeysql/pull/562) [@alexander-yakushev](https://github.com/alexander-yakushev).
|
||||||
|
* Fix two broken links to the [HoneySQL web app](https://john.shaffe.rs/honeysql/) via PR [#559](https://github.com/seancorfield/honeysql/pull/559) [@whatacold](https://github.com/whatacold).
|
||||||
|
* Make SQL Server dialect auto-lift Boolean values to parameters since SQL Server has no `TRUE` / `FALSE` literals.
|
||||||
|
* Fix bug in `DEFAULT` values clause (that omitted some values).
|
||||||
|
|
||||||
|
* 2.6.1243 -- 2024-12-13
|
||||||
|
* Address [#558](https://github.com/seancorfield/honeysql/issues/558) by adding `:patch-into` (and `patch-into` helper) for XTDB (but in core).
|
||||||
|
* Address [#556](https://github.com/seancorfield/honeysql/issues/556) by adding an XTDB section to the documentation with examples.
|
||||||
|
* Address [#555](https://github.com/seancorfield/honeysql/issues/555) by supporting `SETTING` clause for XTDB.
|
||||||
|
* Replace `assert` calls with proper validation, throwing `ex-info` on failure (like other existing validation in HoneySQL).
|
||||||
|
* Experimental `:xtdb` dialect removed (since XTDB no longer supports qualified column names).
|
||||||
|
* Update dev/test deps.
|
||||||
|
|
||||||
|
* 2.6.1230 -- 2024-11-23
|
||||||
|
* Fix [#553](https://github.com/seancorfield/honeysql/issues/553) by adding `:not-between` as special syntax via PR [#554](https://github.com/seancorfield/honeysql/pull/554) [@plooney81](https://github.com/plooney81)
|
||||||
|
* Fix [#552](https://github.com/seancorfield/honeysql/issues/552) by changing the assert-on-load behavior into an explicit test in the test suite.
|
||||||
|
* Fix [#551](https://github.com/seancorfield/honeysql/issues/551) by supporting multiple `WINDOW` clauses.
|
||||||
|
* Fix [#549](https://github.com/seancorfield/honeysql/issues/549) by using `:bb` conditionals to support Babashka (and still support Clojure 1.9.0), and add testing against Babashka so it is fully-supported as a target via PR [#550](https://github.com/seancorfield/honeysql/pull/550) [@borkdude](https://github.com/borkdude)
|
||||||
|
* Address [#532](https://github.com/seancorfield/honeysql/issues/532) by adding support for XTDB SQL extensions `ERASE`, `EXCLUDE`, `OBJECT`, `RECORD`, `RECORDS`, and `RENAME`, along with inline hash maps (as records) and `:get-in` for object navigation, and starting to write tests for XTDB compatibility.
|
||||||
|
|
||||||
|
* 2.6.1203 -- 2024-10-22
|
||||||
|
* Fix [#548](https://github.com/seancorfield/honeysql/issues/548) which was a regression introduced in [#526](https://github.com/seancorfield/honeysql/issues/526) (in 2.6.1161).
|
||||||
|
* Address [#542](https://github.com/seancorfield/honeysql/issues/542) by adding support for `WITH` query tail options for PostgreSQL.
|
||||||
|
* Replace all optional argument destructuring with multiple arities to improve performance.
|
||||||
|
|
||||||
|
* 2.6.1196 -- 2024-10-06
|
||||||
|
* Address [#547](https://github.com/seancorfield/honeysql/issues/547) by adding examples of conditional SQL building with the helpers to the README and the `honey.sql.helpers` ns docstring.
|
||||||
|
* Performance optimizations via PRs [#545](https://github.com/seancorfield/honeysql/pull/545) and [#546](https://github.com/seancorfield/honeysql/pull/546) [@alexander-yakushev](https://github.com/alexander-yakushev).
|
||||||
|
* Address [#544](https://github.com/seancorfield/honeysql/issues/544) by adding support for MySQL's `VALUES ROW(..)` syntax.
|
||||||
|
* Fix [#543](https://github.com/seancorfield/honeysql/issues/543) by supporting both symbols and keywords in named parameters.
|
||||||
|
* Address [#541](https://github.com/seancorfield/honeysql/issues/541) by specifying the expected result of a formatter function passed to `register-clause!` and adding the example from the README to **Extending HoneySQL**.
|
||||||
|
* Getting Started updated based on feedback from Los Angeles Clojure meetup walkthrough [#539](https://github.com/seancorfield/honeysql/issues/539).
|
||||||
|
* Fix [#538](https://github.com/seancorfield/honeysql/issues/538) by removing `mod` from list of infix operators.
|
||||||
|
* Fixed a few symbol/keyword resolution bugs in the formatter. Thanks to [@irigarae](https://github.com/irigarae).
|
||||||
|
* Update Clojure version to 1.12.0; update dev/test/ci deps.
|
||||||
|
|
||||||
|
* 2.6.1161 -- 2024-08-29
|
||||||
|
* Address [#537](https://github.com/seancorfield/honeysql/issues/537) by ignoring non-scalar values in metadata, and expanding support to numbers, and checking strings for suspicious characters.
|
||||||
|
* Address [#536](https://github.com/seancorfield/honeysql/issues/536) by noting what will not work with PostgreSQL (but works with other databases).
|
||||||
|
* Address [#533](https://github.com/seancorfield/honeysql/issues/533) by adding `honey.sql/*escape-?*` which can be bound to `false` to prevent `?` being escaped to `??` when used as an operator or function.
|
||||||
|
* Address [#526](https://github.com/seancorfield/honeysql/issues/526) by using `format-var` in DDL, instead of `format-entity`.
|
||||||
|
* Update JDK test matrix (adopt -> temurin, 19 -> 21).
|
||||||
|
* Update Clojure versions (to 1.11.4 & 1.12.0-rc2).
|
||||||
|
|
||||||
|
* 2.6.1147 -- 2024-06-12
|
||||||
|
* Address [#531](https://github.com/seancorfield/honeysql/issues/531) and [#527](https://github.com/seancorfield/honeysql/issues/527) by adding tests and more documentation for `:composite`; fix bug in `set-dialect!` where clause order is not restored.
|
||||||
|
* Address [#530](https://github.com/seancorfield/honeysql/issues/530) by adding support for `:using-gin` to `:create-index`.
|
||||||
|
* Address [#529](https://github.com/seancorfield/honeysql/issues/529) by fixing `:join` special syntax to support aliases and to handle expressions the same way `select` / `from` etc handle them (extra `[...]` nesting).
|
||||||
|
* Add example of mixed `DO UPDATE SET` with `EXCLUDED` and regular SQL expressions.
|
||||||
|
* Improve exception message when un-`lift`-ed JSON expressions are used in the DSL.
|
||||||
|
* Update Clojure versions (to 1.11.3 and 1.12.0-alpha12); update other dev/test dependencies.
|
||||||
|
|
||||||
|
* 2.6.1126 -- 2024-03-04
|
||||||
|
* Address [#524](https://github.com/seancorfield/honeysql/issues/524) by adding example of `{:nest ..}` in `:union` clause reference docs.
|
||||||
|
* Address [#523](https://github.com/seancorfield/honeysql/issues/523) by expanding examples in README **Functions** to show aliases.
|
||||||
|
* Address [#522](https://github.com/seancorfield/honeysql/issues/522) by supporting metadata on table specifications in `:from` and `:join` clauses to provide index hints (SQL Server).
|
||||||
|
* ~Address [#521](https://github.com/seancorfield/honeysql/issues/521) by adding initial experimental support for an XTDB dialect.~ _[This was removed in 2.6.1243 since XTDB no longer supports qualified column names]_
|
||||||
|
* Address [#520](https://github.com/seancorfield/honeysql/issues/520) by expanding how `:inline` works, to support a sequence of arguments.
|
||||||
|
* Fix [#518](https://github.com/seancorfield/honeysql/issues/518) by moving temporal clause before alias.
|
||||||
|
* Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding `formatv` macro (`.clj` only!) -- and removing the experimental `formatf` function (added for discussion in 2.4.1045).
|
||||||
|
* Implemented `CREATE INDEX` [#348](https://github.com/seancorfield/honeysql/issues/348) via PR [#517](https://github.com/seancorfield/honeysql/pull/517) [@dancek](https://github.com/dancek).
|
||||||
|
* Mention `:not-in` explicitly in the documentation.
|
||||||
|
* Code cleanup per `clj-kondo`.
|
||||||
|
|
||||||
|
* 2.5.1103 -- 2023-12-03
|
||||||
|
* Address [#515](https://github.com/seancorfield/honeysql/issues/515) by:
|
||||||
|
* Quoting entities that start with a digit but are otherwise alphanumeric. Note that entities that are all digits (optionally including underscores) will still not be quoted as in previous releases,
|
||||||
|
* Adding a new `:quoted-always` option allows users to specify a regex that matches entities that should always be quoted (stropped) regardless of the value of `:quoted` (such as reserved words that you have used as column or table names).
|
||||||
|
* Address [#513](https://github.com/seancorfield/honeysql/issues/513) by:
|
||||||
|
* Ignoring `:file`, `:line`, `:column`, `:end-line`, and `:end-column` metadata keys (previously only `:line` and `:column` were ignored),
|
||||||
|
* Adding an `:ignored-metadata` option to allow additional keys to be ignored.
|
||||||
|
|
||||||
|
* 2.5.1091 -- 2023-10-28
|
||||||
|
* Address [#512](https://github.com/seancorfield/honeysql/issues/512) by adding support for subqueries in the `:array` special syntax (for BigQuery and PostgreSQL). This also adds support for metadata on the `:select` value to produce `AS STRUCT` or `DISTINCT`.
|
||||||
|
* Address [#511](https://github.com/seancorfield/honeysql/issues/511) by adding support for BigQuery `CREATE OR REPLACE`.
|
||||||
|
* Address [#510](https://github.com/seancorfield/honeysql/issues/510) by adding initial support for an NRQL dialect.
|
||||||
|
* Fix [#509](https://github.com/seancorfield/honeysql/issues/509) by checking for `ident?` before checking keyword/symbol.
|
||||||
|
|
||||||
|
* 2.4.1078 -- 2023-10-07
|
||||||
|
* Address [#507](https://github.com/seancorfield/honeysql/issues/507) by clarifying formatting of `:cast` in **Special Syntax**.
|
||||||
|
* Fix [#505](https://github.com/seancorfield/honeysql/issues/505) by rewriting the helper merge function to handle both keywords and symbols properly.
|
||||||
|
* Address [#503](https://github.com/seancorfield/honeysql/issues/503) by adding `:at-time-zone` special syntax.
|
||||||
|
* Address [#504](https://github.com/seancorfield/honeysql/issues/504) for BigQuery support, by adding special syntax for ignore/respect nulls, as well as new `:distinct` and `:expr` clauses to allow expressions to be qualified with SQL clauses. The latter will probably be useful for other dialects too.
|
||||||
|
* 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`).
|
||||||
|
|
||||||
|
* 2.4.1066 -- 2023-08-27
|
||||||
|
* Add `:select` with function call and alias example to README (PR [#502](https://github.com/seancorfield/honeysql/pull/502) [@markbastian](https://github.com/markbastian)).
|
||||||
|
* Address [#501](https://github.com/seancorfield/honeysql/issues/501) by making `INSERT INTO` (and `REPLACE INTO`) use the `:columns` or `:values` clauses to produce column names (which are then omitted from those other clauses).
|
||||||
|
* Address [#497](https://github.com/seancorfield/honeysql/issues/497) by adding `:alias` special syntax.
|
||||||
|
* Address [#496](https://github.com/seancorfield/honeysql/issues/496) by adding `:overriding-value` option to `:insert` clause.
|
||||||
|
* Address [#407](https://github.com/seancorfield/honeysql/issues/407) by adding support for temporal queries (see `FROM` in [SQL Clause Reference](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-clause-reference#from)).
|
||||||
|
* Address [#389](https://github.com/seancorfield/honeysql/issues/389) by adding examples of `[:only :table]` producing `ONLY(table)`.
|
||||||
|
* Add `:create-or-replace-view` to support PostgreSQL's lack of `IF NOT EXISTS` for `CREATE VIEW`.
|
||||||
|
* Attempt to clarify the formatting behavior of the `:values` clause when used to produce column names.
|
||||||
|
* Update `tools.build` to 0.9.5 (and remove `:java-opts` setting from `build/run-task`)
|
||||||
|
|
||||||
|
* 2.4.1045 -- 2023-06-25
|
||||||
|
* Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding (experimental) `formatf` function -- _note: this was removed in 2.6.1126, in favor of the `formatv` macro._
|
||||||
|
* Fix [#494](https://github.com/seancorfield/honeysql/issues/494) by supporting expressions in `:on-conflict` instead of just entities.
|
||||||
|
* Address [#493](https://github.com/seancorfield/honeysql/issues/493) by clarifying use of `:values` in CTEs (using `:with`).
|
||||||
|
* Address [#489](https://github.com/seancorfield/honeysql/issues/489) by adding more examples around `:update`.
|
||||||
|
* Attempt to improve `honey.sql.helpers` namespace docstring (by adding a note from the relevant **Getting Started** section).
|
||||||
|
* Update dev/test dependencies.
|
||||||
|
|
||||||
|
* 2.4.1033 -- 2023-05-22
|
||||||
|
* Tentative [ClojureCLR](https://github.com/clojure/clojure-clr) support.
|
||||||
|
* Improve `on-conflict` helper docstring [#490](https://github.com/seancorfield/honeysql/pull/490) [@holyjak](https://github.com/holyjak).
|
||||||
|
|
||||||
|
* 2.4.1026 -- 2023-04-15
|
||||||
|
* Fix [#486](https://github.com/seancorfield/honeysql/issues/486) by supporting ANSI-style `INTERVAL` syntax.
|
||||||
|
* Fix [#485](https://github.com/seancorfield/honeysql/issues/485) by adding `:with-ordinality` "operator".
|
||||||
|
* Fix [#484](https://github.com/seancorfield/honeysql/issues/484) by adding `TABLE` to `TRUNCATE`.
|
||||||
|
* Fix [#483](https://github.com/seancorfield/honeysql/issues/483) by adding a function-like `:join` syntax to produce nested `JOIN` expressions.
|
||||||
|
* Update `tools.build`; split alias `:test`/`:runner` for friendlier jack-in UX while developing.
|
||||||
|
|
||||||
|
* 2.4.1011 -- 2023-03-23
|
||||||
|
* Address [#481](https://github.com/seancorfield/honeysql/issues/481) by adding more examples around `:do-update-set`.
|
||||||
|
* Address [#480](https://github.com/seancorfield/honeysql/issues/480) by clarifying the general relationship between clauses and helpers.
|
||||||
|
* Address [#448](https://github.com/seancorfield/honeysql/issues/448) by adding a new section with hints and tips for database-specific syntax and solutions.
|
||||||
|
|
||||||
|
* 2.4.1006 -- 2023-03-17
|
||||||
|
* Fix [#478](https://github.com/seancorfield/honeysql/issues/478) by handling `:do-update-set` correctly in the `upsert` helper and by handling parameters correctly in the `:do-update-set` formatter.
|
||||||
|
* Fix [#476](https://github.com/seancorfield/honeysql/issues/476) by adding support for multiple arguments to `:raw`, essentially restoring 1.x functionality (while still allowing for embedded vectors as expressions, introduced in 2.x).
|
||||||
|
|
||||||
|
* 2.4.1002 -- 2023-03-03
|
||||||
|
* Address [#474](https://github.com/seancorfield/honeysql/issues/474) by adding dot-selection special syntax.
|
||||||
|
* Improve docstrings for PostgreSQL operators via PR [#473](https://github.com/seancorfield/honeysql/pull/473) [@holyjak](https://github.com/holyjak).
|
||||||
|
* Address [#471](https://github.com/seancorfield/honeysql/issues/471) by supporting interspersed SQL keywords in function calls.
|
||||||
|
* Fix [#467](https://github.com/seancorfield/honeysql/issues/467) by allowing single keywords (symbols) as a short hand for a single-element sequence in more constructs via PR [#470](https://github.com/seancorfield/honeysql/pull/470) [@p-himik](https://github.com/p-himik).
|
||||||
|
* Address [#466](https://github.com/seancorfield/honeysql/issues/466) by treating `[:and]` as `TRUE` and `[:or]` as `FALSE`.
|
||||||
|
* Fix [#465](https://github.com/seancorfield/honeysql/issues/465) to allow multiple columns in `:order-by` special syntax via PR [#468](https://github.com/seancorfield/honeysql/pull/468) [@p-himik](https://github.com/p-himik).
|
||||||
|
* Fix [#464](https://github.com/seancorfield/honeysql/issues/464) by adding an optional type argument to `:array` via PR [#469](https://github.com/seancorfield/honeysql/pull/469) [@p-himik](https://github.com/p-himik).
|
||||||
|
* Address [#463](https://github.com/seancorfield/honeysql/issues/463) by explaining `:quoted nil` via PR [#475](https://github.com/seancorfield/honeysql/pull/475) [@nharsch](https://github.com/nharsch).
|
||||||
|
* Address [#462](https://github.com/seancorfield/honeysql/issues/462) by adding a note in the documentation for set operations, clarifying precedence issues.
|
||||||
|
|
||||||
|
* 2.4.980 -- 2023-02-15
|
||||||
|
* Fix [#461](https://github.com/seancorfield/honeysql/issues/461) -- a regression introduced in 2.4.979 -- by restricting unary operators to just `+`, `-`, and `~` (bitwise negation).
|
||||||
|
|
||||||
|
* 2.4.979 -- 2023-02-11
|
||||||
|
* Address [#459](https://github.com/seancorfield/honeysql/issues/459) by making all operators variadic (except `:=` and `:<>`).
|
||||||
|
* Address [#458](https://github.com/seancorfield/honeysql/issues/458) by adding `registered-*?` predicates.
|
||||||
|
|
||||||
|
* 2.4.972 -- 2023-02-02
|
||||||
|
* Address [#456](https://github.com/seancorfield/honeysql/issues/456) by allowing `format` to handle expressions (like 1.x could) as well as statements. This should aid with migration from 1.x to 2.x.
|
||||||
|
|
||||||
|
* 2.4.969 -- 2023-01-14
|
||||||
|
* Fix [#454](https://github.com/seancorfield/honeysql/issues/454) by allowing `-` to be variadic.
|
||||||
|
* Address [#452](https://github.com/seancorfield/honeysql/pull/452) by adding `:replace-into` to the core SQL supported, instead of just for the MySQL and SQLite dialects (so the latter is not needed yet).
|
||||||
|
* Address [#451](https://github.com/seancorfield/honeysql/issues/451) by adding a test for it, showing how `:nest` produces the desired result.
|
||||||
|
* Address [#447](https://github.com/seancorfield/honeysql/issues/447) by updating GitHub Actions and dependencies.
|
||||||
|
* Address [#445](https://github.com/seancorfield/honeysql/issues/445) and [#453](https://github.com/seancorfield/honeysql/issues/453) by adding key/constraint examples to `CREATE TABLE` docs.
|
||||||
|
|
||||||
|
* 2.4.962 -- 2022-12-17
|
||||||
|
* Fix `set-options!` (only `:checking` worked in 2.4.947).
|
||||||
|
* Fix `:cast` formatting when quoting is enabled, via PR [#443](https://github.com/seancorfield/honeysql/pull/443) [duddlf23](https://github.com/duddlf23). **This changes how type names containing `-` are formatted in a cast.** See [`cast` Special Syntax](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-special-syntax-#cast) for more details.
|
||||||
|
* Fix [#441](https://github.com/seancorfield/honeysql/issues/441) by adding `:replace-into` to in-flight clause order (as well as registering it for the `:mysql` dialect).
|
||||||
|
* Fix [#434](https://github.com/seancorfield/honeysql/issues/434) by special-casing `:'ARRAY`.
|
||||||
|
* Fix [#433](https://github.com/seancorfield/honeysql/issues/433) by supporting additional `WITH` syntax, via PR [#432](https://github.com/seancorfield/honeysql/issues/432), [@MawiraIke](https://github.com/MawiraIke). _[Technically, this was in 2.4.947, but I kept the issue open while I wordsmithed the documentation]_
|
||||||
|
* Address [#405](https://github.com/seancorfield/honeysql/issues/405) by adding `:numbered` option, which can also be set globally using `set-options!`.
|
||||||
|
|
||||||
|
* 2.4.947 -- 2022-11-05
|
||||||
|
* Fix [#439](https://github.com/seancorfield/honeysql/issues/439) by rewriting how DDL options are processed; also fixes [#386](https://github.com/seancorfield/honeysql/issues/386) and [#437](https://github.com/seancorfield/honeysql/issues/437); **Whilst this is intended to be purely a bug fix, it has the potential to be a breaking change -- hence the version jump to 2.4!**
|
||||||
|
* Fix [#438](https://github.com/seancorfield/honeysql/issues/438) by
|
||||||
|
supporting options on `TRUNCATE`.
|
||||||
|
* Address [#435](https://github.com/seancorfield/honeysql/issues/435) by showing `CREATE TEMP TABLE` etc.
|
||||||
|
* Fix [#431](https://github.com/seancorfield/honeysql/issues/431) -- `WHERE false` differed between the DSL and the `where` helper.
|
||||||
|
* Address [#430](https://github.com/seancorfield/honeysql/issues/430) by treating `:'` as introducing a name that should be treated literally and not formatted as a SQL entity (which respects quoting, dot-splitting, etc); this effectively expands the "escape hatch" introduced via [#352](https://github.com/seancorfield/honeysql/issues/352) in 2.2.868. _Note that the function context behavior formats as a SQL entity, rather than the usual SQL "keyword", whereas this new context is a literal transcription rather than as a SQL entity!_
|
||||||
|
* Address [#427](https://github.com/seancorfield/honeysql/issues/427) by adding `set-options!`.
|
||||||
|
* Address [#415](https://github.com/seancorfield/honeysql/issues/415) by supporting multiple column names in `ADD COLUMN`, `ALTER COLUMN`, `DROP COLUMN`, and `MODIFY COLUMN`.
|
||||||
|
|
||||||
|
* 2.3.928 -- 2022-09-04
|
||||||
|
* Address [#425](https://github.com/seancorfield/honeysql/issues/425) by clarifying that `INTERVAL` as special syntax may be MySQL-specific and PostgreSQL uses difference syntax (because `INTERVAL` is a data type there).
|
||||||
|
* Address [#423](https://github.com/seancorfield/honeysql/issues/423) by supporting `DEFAULT` values and `DEFAULT` rows in `VALUES`.
|
||||||
|
* Address [#422](https://github.com/seancorfield/honeysql/issues/422) by auto-quoting unusual entity names when `:quoted` (and `:dialect`) are not specified, making HoneySQL more secure by default.
|
||||||
|
* Fix [#421](https://github.com/seancorfield/honeysql/issues/421) by adding `:replace-into` for `:mysql` dialect.
|
||||||
|
* Address [#419](https://github.com/seancorfield/honeysql/issues/419) by adding `honey.sql.protocols` and `InlineValue` with a `sqlize` function.
|
||||||
|
* Address [#413](https://github.com/seancorfield/honeysql/issues/413) by flagging a lack of `WHERE` clause for `DELETE`, `DELETE FROM`, and `UPDATE` when `:checking :basic` (or `:checking :strict`).
|
||||||
|
* Fix [#392](https://github.com/seancorfield/honeysql/issues/392) by adding support for `WITH` / (`NOT`) `MATERIALIZED` -- via PR [#420](https://github.com/seancorfield/honeysql/issues/420) [@robhanlon22](https://github.com/robhanlon22).
|
||||||
|
|
||||||
|
* 2.3.911 -- 2022-07-29
|
||||||
|
* Address [#418](https://github.com/seancorfield/honeysql/issues/418) by documenting a potential "gotcha" with multi-column `IN` expressions (a change from HoneySQL 1.x).
|
||||||
|
* Fix [#416](https://github.com/seancorfield/honeysql/issues/416) via PR [#417](https://github.com/seancorfield/honeysql/issues/417) from [@corasaurus-hex](https://github.com/corasaurus-hex) -- using the internal default state for the integrity assertion.
|
||||||
|
* Address [#414](https://github.com/seancorfield/honeysql/issues/414) by providing an example of `ORDER BY` with a `CASE` expression.
|
||||||
|
* Address [#412](https://github.com/seancorfield/honeysql/issues/412) by documenting options in a separate page and reorganizing the ToC structure.
|
||||||
|
* Address [#409](https://github.com/seancorfield/honeysql/issues/409) by making docstring check for public helpers conditional.
|
||||||
|
* Fix [#406](https://github.com/seancorfield/honeysql/issues/406) by adding `:alter-column` (which produces `MODIFY COLUMN` when the MySQL dialect is selected) and deprecating `:modify-column`.
|
||||||
|
* Address [#401](https://github.com/seancorfield/honeysql/issues/401) by adding `register-dialect!` and `get-dialect`, and also making `add-clause-before`, `strop`, and `upper-case` public so that new dialects are easier to construct.
|
||||||
|
|
||||||
|
* 2.2.891 -- 2022-04-23
|
||||||
|
* Address [#404](https://github.com/seancorfield/honeysql/issues/404) by documenting PostgreSQL's `ARRAY` constructor syntax and how to produce it.
|
||||||
|
* Address parts of [#403](https://github.com/seancorfield/honeysql/issues/403) by improving the documentation for `:array` and also improving the exception that was thrown when it was misused.
|
||||||
|
* Fix [#402](https://github.com/seancorfield/honeysql/issues/402) by allowing for expressions in `:insert-into` table.
|
||||||
|
* Address [#400](https://github.com/seancorfield/honeysql/issues/400) by adding `:table` clause.
|
||||||
|
* Address [#399](https://github.com/seancorfield/honeysql/issues/399) by correcting multi-column `RETURNING` clauses in docs and tests.
|
||||||
|
* Fix [#398](https://github.com/seancorfield/honeysql/issues/398) by adding `honey.sql.pg-ops` namespace that registers PostgreSQL JSON and regex operators and provides symbolic names for "unwritable" operators (that contain `@`, `#`, or `~`).
|
||||||
|
* Address [#396](https://github.com/seancorfield/honeysql/issues/396) by throwing an exception if you try to cache a SQL statement that includes an `IN ()` expression, using a named parameter for the `IN` values.
|
||||||
|
* Fix [#394](https://github.com/seancorfield/honeysql/issues/394) by restoring HoneySQL 1.x's behavior when quoting.
|
||||||
|
* Fix [#387](https://github.com/seancorfield/honeysql/issues/387) again.
|
||||||
|
* Update CI to reflect Clojure 1.11 release (master -> 1.11; new master is 1.12).
|
||||||
|
* Update `build-clj` to v0.8.0.
|
||||||
|
|
||||||
|
* 2.2.868 -- 2022-02-21
|
||||||
|
* Address [#387](https://github.com/seancorfield/honeysql/issues/387) by making the function simpler.
|
||||||
|
* Fix [#385](https://github.com/seancorfield/honeysql/issues/385) by quoting inlined UUIDs.
|
||||||
|
* Address [#352](https://github.com/seancorfield/honeysql/issues/352) by treating `:'` as introducing a function name that should be formatted as a SQL entity (which respects quoting, dot-splitting, etc), rather than as a SQL "keyword".
|
||||||
|
|
||||||
|
* 2.2.861 -- 2022-01-30
|
||||||
|
* Address [#382](https://github.com/seancorfield/honeysql/issues/382) by adding `:case-expr` for BigQuery support.
|
||||||
|
* Address [#381](https://github.com/seancorfield/honeysql/issues/381) by adding `generic-helper-variadic` and `generic-helper-unary` to `honey.sql.helpers`.
|
||||||
|
* Fix [#380](https://github.com/seancorfield/honeysql/issues/380) by correcting test for function type in `register-clause!` and `register-fn!`.
|
||||||
|
|
||||||
|
* 2.2.858 -- 2022-01-20
|
||||||
|
* Address #377 by adding `honey.sql/map=` to convert a hash map into an equality condition (for a `WHERE` clause).
|
||||||
|
* Address #351 by adding a `:cache` option to `honey.sql/format` (for Clojure only, not ClojureScript).
|
||||||
|
* Address #281 by adding support for `SELECT * EXCEPT ..` and `SELECT * REPLACE ..` and `ARRAY<>` and `STRUCT<>` column types -- see [SQL Clause Reference - SELECT](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-clause-reference#select-select-distinct) and [SQL Clause Reference - DDL](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-clause-reference#ddl-clauses) respectively for more details.
|
||||||
|
* Update `build-clj` to v0.6.7.
|
||||||
|
|
||||||
|
* 2.2.840 -- 2021-12-23
|
||||||
|
* Fix #375 for `:nest` statement.
|
||||||
|
* Fix #374 by removing aliasing of `:is` / `:is-not` -- this changes the behavior of `[:is-not :col true/false]` to be _correct_ and _include `NULL` values_. Using `:is` / `:is-not` with values that are not Boolean and not `nil` will produce invalid SQL.
|
||||||
|
* Update test dependencies.
|
||||||
|
* Update `build-clj` to v0.6.5.
|
||||||
|
|
||||||
|
* 2.1.833 -- 2021-12-03
|
||||||
|
* Fix #372 by merging `:select-distinct-on` differently.
|
||||||
|
* Add empty column list check for `SELECT` and several other clauses, when `:checking :basic` (or `:strict`) is provided.
|
||||||
|
* Update `build-clj` to v0.6.0.
|
||||||
|
|
||||||
|
* 2.1.829 -- 2021-11-27
|
||||||
|
* Fix #371 by treating the operand of `NOT` as a nested expression (so it is parenthesized unless it is a simple value).
|
||||||
|
* Fix #370 by **always** parenthesizing the operand of `:nest`.
|
||||||
|
* Address #369 by adding a big clarifying docstring to the `honey.sql.helpers` namespace pointing out that all helper functions are variadic, they are all `[& args]`, some have `:arglists` metadata to provide a more specific usage hint but those _all omit the optional first argument (the DSL hash map)_.
|
||||||
|
* Fix #354 by supporting `DROP COLUMN IF EXISTS` / `ADD COLUMN IF NOT EXISTS`.
|
||||||
|
* Update `build-clj` to v0.5.5.
|
||||||
|
|
||||||
|
* 2.1.818 -- 2021-10-04
|
||||||
|
* Fix #367 by supporting parameters in subexpressions around `IS NULL` / `IS NOT NULL` tests.
|
||||||
|
* Address #366 by introducing `:values-default-columns` option to control whether missing columns are treated as `NULL` or `DEFAULT` in `:values` clauses with sequences of hash maps.
|
||||||
|
* Fix #365 -- a regression from 1.x -- where subclauses for `UNION`, `EXCEPT`, etc were incorrectly parenthesized.
|
||||||
|
* Update `build-clj` to v0.5.0.
|
||||||
|
|
||||||
|
* 2.0.813 -- 2021-09-25
|
||||||
|
* Address #364 by recommending how to handle PostgreSQL operators that contain `@`.
|
||||||
|
* Fix #363 and #362 by aligning more closely the semantics of `:inline` syntax with the `:inline true` option. A side effect of this is that `[:inline [:param :foo]]` will now (correctly) inline the value of the parameter `:foo` whereas it previously produced `PARAMS SOURCE`. In addition, inlining has been extended to vector values, so `[:inline ["a" "b" "c"]]` will now produce `('a', 'b', 'c')` and `[:inline [:lift ["a" "b" "c"]]]` will now produce `['a', 'b', 'c']` which is what people seemed to expect (the behavior was previously unspecified).
|
||||||
|
* Fix #353 by correcting handling of strings used as SQL entities (such as table names); this was a regression introduced by a recent enhancement to `:create-table`.
|
||||||
|
* Fix #349 by adding an optional `:quoted` argument to `set-dialect!`.
|
||||||
|
* Address #347 by adding example of adding a primary key to an existing table via `:add-index`.
|
||||||
|
* Support `AS` aliasing in `DELETE FROM`.
|
||||||
|
* Switch from `readme` to `test-doc-blocks` so all documentation is tested!
|
||||||
|
* Clean up build/update deps.
|
||||||
|
|
||||||
|
* 2.0.783 -- 2021-08-15 (a.k.a "2.0 Gold")
|
||||||
|
* Fixes #344 by no longer dropping the qualifier on columns in a `SET` clause _for the `:mysql` dialect only_; the behavior is unchanged for all other dialects.
|
||||||
|
* Fixes #340 by making the "hyphen to space" logic more general so _operators_ containing `-` should retain the hyphen without special cases.
|
||||||
|
* Documentation improvements: `:fetch`, `:lift`, `:limit`, `:offset`, `:param`, `:select`; also around JSON/PostgreSQL.
|
||||||
|
* Link to the [HoneySQL web app](https://john.shaffe.rs/honeysql/) in both the README and **Getting Started**.
|
||||||
|
* Switch to `tools.build` for running tests and JAR building etc.
|
||||||
|
|
||||||
|
* 2.0.0-rc5 (for testing; 2021-07-17)
|
||||||
|
* Fix #338 by producing `OFFSET n ROWS` (or `ROW` if `n` is 1) if `:fetch` is present or `:sqlserver` dialect is specified; and by producing `FETCH NEXT n ROWS ONLY` (or `ROW` is `n` is 1; or `FIRST` instead of `NEXT` if `:offset` is not present).
|
||||||
|
* Fix #337 by switching to `clojure.test` even for ClojureScript.
|
||||||
|
* Address #332 by improving `:cross-join` documentation.
|
||||||
|
* Address #330 by improving exception when a non-entity is encountered where an entity is expected.
|
||||||
|
* Fix `fetch` helper (it previously returned an `:offset` clause).
|
||||||
|
* Fix bug in unrolling nested argument to `with-columns` helper.
|
||||||
|
|
||||||
|
* 2.0.0-rc3 (for testing; 2021-06-16)
|
||||||
|
* Fix #328 by adding `:distinct` as special syntax, affecting an expression.
|
||||||
|
* Address #327 by changing "unknown clause" error to including mention of "nil values" (which are also illegal).
|
||||||
|
* Fix #327 by making single-argument helpers consistent with multi-argument helpers.
|
||||||
|
* Support PostgreSQL's `&&` array operator.
|
||||||
|
* Clarify how to `SELECT` a function expression (in **Getting Started**).
|
||||||
|
* Update `test-runner`.
|
||||||
|
|
||||||
|
* 2.0.0-rc2 (for testing; 2021-05-10)
|
||||||
|
* Fix #326 by allowing `ON`/`USING` to be optional and not dropping parameters on the floor.
|
||||||
|
* Fix #325 by making the `%` function call syntax respect `:quoted true` and/or `:dialect` options, and also allowing for qualified column names. (PR from @lognush)
|
||||||
|
* Add `:quoted-snake true` option to force conversion from kebab-case to snake_case when `:quoted true` or a `:dialect` is specified to `format`.
|
||||||
|
* Update `test-runner`.
|
||||||
|
|
||||||
|
* 2.0.0-rc1 (for testing; 2021-05-06)
|
||||||
|
* Fix #324 so that `insert-into` supports merging into another statement in all cases.
|
||||||
|
* Fix #323 by supporting more than one SQL entity in `:on-conflict`.
|
||||||
|
* Fix #321 by adding `:checking` mode. Currently only detects potential problems with `IN` clauses.
|
||||||
|
|
||||||
|
* 2.0.0-beta2 (for testing; 2021-04-13)
|
||||||
|
* The documentation continues to be expanded and clarified in response to feedback!
|
||||||
|
* Fix #322 by rewriting/simplifying `WHERE`/`HAVING` merge logic. **Important bug fix!**
|
||||||
|
* Fix #310 by adding support for `FILTER`, `WITHIN GROUP`, and `ORDER BY` (as an expression), from [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) 0.4.112. These are [Special Syntax](doc/special-syntax.md) and there are also helpers for `filter` and `within-group` -- so **be careful about referring in all of `honey.sql.helpers`** since it will now shadow `clojure.core/filter` (it already shadows `for`, `group-by`, `into`, `partition-by`, `set`, and `update`).
|
||||||
|
* Fix #308 by supporting join clauses in `join-by` (and correcting the helper docstring).
|
||||||
|
|
||||||
|
* 2.0.0-beta1 (for testing; 2021-04-09)
|
||||||
|
* **The merging behavior of `where`/`having` is broken in Beta 1!**
|
||||||
|
* Since Alpha 3, more documentation has been written and existing documentation clarified (addressing #300, #309, #313, #314).
|
||||||
|
* Fix #319 by ensuring `register-clause!` is idempotent.
|
||||||
|
* Fix #317 by dropping qualifiers in `:set` clauses (just like we do with `:insert` columns). Note that you can still use explicit _dotted_ names if you want table qualification.
|
||||||
|
* Fix #316 by disallowing entity names containing `;` (to avoid SQL injection risks).
|
||||||
|
* Fix #312 by adding `:raw` as a clause. There is no helper function equivalent (because it would be ambiguous whether you meant a function form -- `[:raw ..]` -- or a clause form -- `{:raw ..}`; and for the same reason, there is no `nest` helper function since that also works as a clause and as a function/special syntax).
|
||||||
|
|
||||||
|
* 2.0.0-alpha3 (for early testing; 2021-03-13)
|
||||||
|
* Change coordinates to `com.github.seancorfield/honeysql` (although new versions will continue to be deployed to `seancorfield/honeysql` for a while -- see the [Clojars Verified Group Names policy](https://github.com/clojars/clojars-web/wiki/Verified-Group-Names)).
|
||||||
|
* Support much richer range of syntax on `CREATE`/`DROP` statements in general, including columns, `TABLESPACE`, `CASCADE`, `WITH [NO] DATA`, etc.
|
||||||
|
* Fix #306 by supporting `CREATE TABLE .. AS ..`.
|
||||||
|
* Fix #305 by supporting more complex join clauses.
|
||||||
|
* Fix #303 by supporting MySQL's `ON DUPLICATE KEY UPDATE`.
|
||||||
|
* Fix #301 by adding support for `CREATE`/`DROP`/`REFRESH` on `MATERIALIZED VIEW`.
|
||||||
|
* Add tests to confirm #299 does not affect 2.x.
|
||||||
|
* Fix #297 by adding both `SELECT .. INTO ..` and `SELECT .. BULK COLLECT INTO ..`.
|
||||||
|
* Fix #295 by adding docstrings to all helper functions (and adding an assert to ensure it stays that way as more are added in future).
|
||||||
|
* Confirm the whole of the [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) is implemented out-of-the-box (#293, up to 0.3.104 -- see also #310 which brought parity up to 0.4.112).
|
||||||
|
* Fix #292 by adding support for `SELECT TOP` and `OFFSET`/`FETCH`.
|
||||||
|
* Fix #284 by adding support for `LATERAL` (as special syntax, with a helper).
|
||||||
|
* Reconcile `where` behavior with recent 1.x changes (porting #283 to 2.x).
|
||||||
|
* Fix #280 by adding `:escape` as special syntax for regular expression patterns.
|
||||||
|
* Fix #277 by adding `:join-by`/`join-by` so that you can have multiple `JOIN`'s in a specific order.
|
||||||
|
|
||||||
* 2.0.0-alpha2 (for early testing)
|
* 2.0.0-alpha2 (for early testing)
|
||||||
* Since Alpha 1, a lot more documentation has been written and docstrings have been added to most functions in `honey.sql.helpers`.
|
* Since Alpha 1, a lot more documentation has been written and docstrings have been added to most functions in `honey.sql.helpers`.
|
||||||
* Numerous small improvements have been made to clauses and helpers around insert/upsert.
|
* Numerous small improvements have been made to clauses and helpers around insert/upsert.
|
||||||
|
|
||||||
* 2.0.0-alpha1 (for early testing)
|
* 2.0.0-alpha1 (for early testing)
|
||||||
* This is a complete rewrite/simplification of HoneySQL that provides just two namespaces:
|
* This is a complete rewrite/simplification of HoneySQL that provides just two namespaces:
|
||||||
* `honey.sql` -- this is the primary API via the `format` function as well as the various extension points.
|
* `honey.sql` -- this is the primary API via the `format` function as well as the various extension points.
|
||||||
* `honey.sql.helpers` -- provides a helper function for every piece of the DSL that is supported out-of-the-box.
|
* `honey.sql.helpers` -- provides a helper function for every piece of the DSL that is supported out-of-the-box.
|
||||||
* The coordinates for HoneySQL 2.0 are `seancorfield/honeysql` so it can be added to a project that already uses HoneySQL 1.0 without any conflicts, making it easier to migrate piecemeal from 1.0 to 2.0.
|
* The coordinates for HoneySQL 2.x are `com.github.seancorfield/honeysql` so it can be added to a project that already uses HoneySQL 1.x without any conflicts, making it easier to migrate piecemeal from 1.x to 2.x.
|
||||||
|
|
||||||
# HoneySQL pre-2.x Changes
|
# HoneySQL pre-2.x Changes
|
||||||
|
|
||||||
|
* 1.0.461 -- 2021-02-22
|
||||||
|
* **Fix #299 potential SQL injection vulnerability.**
|
||||||
|
* Fix/Improve `merge-where` (and `merge-having`) behavior. #282 via #283 (@camsaul)
|
||||||
|
|
||||||
* 1.0.444 -- 2020-05-29
|
* 1.0.444 -- 2020-05-29
|
||||||
* Fix #259 so column names are always unqualified in inserts. (@jrdoane)
|
* Fix #259 so column names are always unqualified in inserts. (@jrdoane)
|
||||||
* Fix #257 by adding support for `cross-join` / `merge-cross-join` / `:cross-join`. (@dcj)
|
* Fix #257 by adding support for `cross-join` / `merge-cross-join` / `:cross-join`. (@dcj)
|
||||||
|
|
@ -106,7 +448,7 @@ Not all of these releases were tagged on GitHub and none of them have release no
|
||||||
|
|
||||||
* 0.8.0
|
* 0.8.0
|
||||||
* Get arglists right for generated helpers (@camsaul, @michaelblume)
|
* Get arglists right for generated helpers (@camsaul, @michaelblume)
|
||||||
* Allow HoneySQL to be used from Clojurescript (@rnewman, @michaelblume)
|
* Allow HoneySQL to be used from ClojureScript (@rnewman, @michaelblume)
|
||||||
* BREAKING CHANGE: HoneySQL now requires Clojure 1.7.0 or above.
|
* BREAKING CHANGE: HoneySQL now requires Clojure 1.7.0 or above.
|
||||||
|
|
||||||
* 0.7.0
|
* 0.7.0
|
||||||
|
|
|
||||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at sean@corfield.org. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
532
README.md
532
README.md
|
|
@ -1,37 +1,78 @@
|
||||||
# Honey SQL [](https://circleci.com/gh/seancorfield/honeysql/tree/v2)
|
# Honey SQL [](https://github.com/seancorfield/honeysql/actions/workflows/test-and-release.yml) [](https://github.com/seancorfield/honeysql/actions/workflows/test-and-snapshot.yml) [](https://github.com/seancorfield/honeysql/actions/workflows/test.yml)
|
||||||
|
|
||||||
SQL as Clojure data structures. Build queries programmatically -- even at runtime -- without having to bash strings together.
|
SQL as Clojure data structures. Build queries programmatically -- even at runtime -- without having to bash strings together.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
The latest stable version (1.0.444) on Clojars and on cljdoc (note: `honeysql/honeysql`):
|
[](https://clojars.org/com.github.seancorfield/honeysql)
|
||||||
|
[](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT)
|
||||||
[](https://clojars.org/honeysql/honeysql) [](https://cljdoc.org/d/honeysql/honeysql/CURRENT)
|
[](https://clojurians.slack.com/app_redirect?channel=honeysql)
|
||||||
|
[](http://clojurians.net)
|
||||||
|
[](https://clojurians.zulipchat.com/#narrow/channel/152091-honeysql)
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
Prerelease builds of the upcoming 2.x version of HoneySQL will soon be available:
|
> 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 HoneySQL is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/honeysql) or via a git dependency on the latest SHA.
|
||||||
|
|
||||||
[](https://clojars.org/seancorfield/honeysql) [](https://cljdoc.org/d/seancorfield/honeysql/2.0.0-alpha2)
|
HoneySQL 2.7.y requires Clojure 1.10.3 or later.
|
||||||
|
Earlier versions of HoneySQL support Clojure 1.9.0.
|
||||||
|
It also supports recent versions of ClojureScript and Babashka.
|
||||||
|
|
||||||
This is the README for the upcoming 2.x version of HoneySQL which provides a streamlined codebase and a simpler method for extending the DSL. It also supports SQL dialects out-of-the-box and will be extended to support vendor-specific language features over time (unlike the 1.x version).
|
Compared to the [legacy 1.x version](#1.x), HoneySQL 2.x provides a streamlined codebase and a simpler method for extending the DSL. It also supports SQL dialects out-of-the-box and will be extended to support vendor-specific language features over time (unlike 1.x).
|
||||||
|
|
||||||
See this [summary of differences between v1 and v2](doc/differences-from-1-x.md) if you want to help test v2!
|
> Note: you can use 1.x and 2.x side-by-side as they use different group IDs and different namespaces. This allows for a piecemeal migration. See this [summary of differences between 1.x and 2.x](doc/differences-from-1-x.md) if you are migrating from 1.x!
|
||||||
|
|
||||||
|
## Try HoneySQL Online!
|
||||||
|
|
||||||
|
[John Shaffer](https://github.com/john-shaffer) has created this awesome
|
||||||
|
[HoneySQL web app](https://john.shaffe.rs/honeysql/), written in ClojureScript,
|
||||||
|
so you can experiment with HoneySQL in a browser, including setting different
|
||||||
|
options so you can generate pretty SQL with inline values (via `:inline true`)
|
||||||
|
for copying and pasting directly into your SQL tool of choice!
|
||||||
|
|
||||||
## Note on code samples
|
## Note on code samples
|
||||||
|
|
||||||
All sample code in this README is automatically run as a unit test using
|
Sample code in this documentation is verified via
|
||||||
[seancorfield/readme](https://github.com/seancorfield/readme).
|
[lread/test-doc-blocks](https://github.com/lread/test-doc-blocks).
|
||||||
|
|
||||||
Some of these samples show pretty-printed SQL: HoneySQL 2.x supports `:pretty true` which inserts newlines between clauses in the generated SQL strings.
|
Some of these samples show pretty-printed SQL: HoneySQL 2.x supports `:pretty true` which inserts newlines between clauses in the generated SQL strings.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
This section includes a number of usage examples but does not dive deep into the
|
||||||
|
way the data structure acts as a DSL that can specify SQL statements (as hash maps)
|
||||||
|
and SQL expressions and function calls (as vectors). It is recommended that you read the
|
||||||
|
[**Getting Started**](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started)
|
||||||
|
section of the documentation before trying to use HoneySQL to build your own queries!
|
||||||
|
|
||||||
|
From Clojure:
|
||||||
|
<!-- {:test-doc-blocks/reader-cond :clj} -->
|
||||||
```clojure
|
```clojure
|
||||||
(refer-clojure :exclude '[for group-by set update])
|
(refer-clojure :exclude '[assert distinct filter for group-by into partition-by set update])
|
||||||
(require '[honey.sql :as sql]
|
(require '[honey.sql :as sql]
|
||||||
;; caution: this overwrites for, group-by, set, and update
|
;; CAUTION: this overwrites several clojure.core fns:
|
||||||
'[honey.sql.helpers :refer :all :as h])
|
;;
|
||||||
|
;; distinct, filter, for, group-by, into, partition-by, set, and update
|
||||||
|
;;
|
||||||
|
;; you should generally only refer in the specific
|
||||||
|
;; helpers that you want to use!
|
||||||
|
'[honey.sql.helpers :refer :all :as h]
|
||||||
|
;; so we can still get at clojure.core functions:
|
||||||
|
'[clojure.core :as c])
|
||||||
|
```
|
||||||
|
|
||||||
|
From ClojureScript, we don't have `:refer :all`. If we want to use `:refer`, we have no choice but to be specific:
|
||||||
|
<!-- {:test-doc-blocks/reader-cond :cljs} -->
|
||||||
|
```Clojure
|
||||||
|
(refer-clojure :exclude '[filter for group-by into partition-by set update])
|
||||||
|
(require '[honey.sql :as sql]
|
||||||
|
'[honey.sql.helpers :refer [select select-distinct from
|
||||||
|
join left-join right-join
|
||||||
|
where for group-by having union
|
||||||
|
order-by limit offset values columns
|
||||||
|
update insert-into set composite
|
||||||
|
delete delete-from truncate] :as h]
|
||||||
|
'[clojure.core :as c])
|
||||||
```
|
```
|
||||||
|
|
||||||
Everything is built on top of maps representing SQL queries:
|
Everything is built on top of maps representing SQL queries:
|
||||||
|
|
@ -39,7 +80,7 @@ Everything is built on top of maps representing SQL queries:
|
||||||
```clojure
|
```clojure
|
||||||
(def sqlmap {:select [:a :b :c]
|
(def sqlmap {:select [:a :b :c]
|
||||||
:from [:foo]
|
:from [:foo]
|
||||||
:where [:= :f.a "baz"]})
|
:where [:= :foo.a "baz"]})
|
||||||
```
|
```
|
||||||
|
|
||||||
Column names can be provided as keywords or symbols (but not strings -- HoneySQL treats strings as values that should be lifted out of the SQL as parameters).
|
Column names can be provided as keywords or symbols (but not strings -- HoneySQL treats strings as values that should be lifted out of the SQL as parameters).
|
||||||
|
|
@ -50,29 +91,39 @@ Column names can be provided as keywords or symbols (but not strings -- HoneySQL
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format sqlmap)
|
(sql/format sqlmap)
|
||||||
=> ["SELECT a, b, c FROM foo WHERE f.a = ?" "baz"]
|
=> ["SELECT a, b, c FROM foo WHERE foo.a = ?" "baz"]
|
||||||
|
;; sqlmap as symbols instead of keywords:
|
||||||
|
(-> '{select (a, b, c) from (foo) where (= foo.a "baz")}
|
||||||
|
(sql/format))
|
||||||
|
=> ["SELECT a, b, c FROM foo WHERE foo.a = ?" "baz"]
|
||||||
```
|
```
|
||||||
|
|
||||||
HoneySQL is a relatively "pure" library, it does not manage your sql connection
|
HoneySQL is a relatively "pure" library, it does not manage your JDBC connection
|
||||||
or run queries for you, it simply generates SQL strings. You can then pass them
|
or run queries for you, it simply generates SQL strings. You can then pass them
|
||||||
to a JDBC library, such as [`next.jdbc`](https://github.com/seancorfield/next-jdbc):
|
to a JDBC library, such as [`next.jdbc`](https://github.com/seancorfield/next-jdbc):
|
||||||
|
|
||||||
```clj
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
(jdbc/execute! conn (sql/format sqlmap))
|
(jdbc/execute! conn (sql/format sqlmap))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: you'll need to add your preferred JDBC library as a dependency in your project -- HoneySQL deliberately does not make that choice for you.
|
||||||
|
|
||||||
If you want to format the query as a string with no parameters (e.g. to use the SQL statement in a SQL console), pass `:inline true` as an option to `sql/format`:
|
If you want to format the query as a string with no parameters (e.g. to use the SQL statement in a SQL console), pass `:inline true` as an option to `sql/format`:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format sqlmap {:inline true})
|
(sql/format sqlmap {:inline true})
|
||||||
=> ["SELECT a, b, c FROM foo WHERE f.a = 'baz'"]
|
=> ["SELECT a, b, c FROM foo WHERE foo.a = 'baz'"]
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: you'll need to add your preferred JDBC library as a dependency in your project -- HoneySQL deliberately does not make that choice for you.
|
As seen above, the default parameterization uses positional parameters (`?`) with the order of values in the generated vector matching the order of those placeholders in the SQL. As of 2.4.962, you can specified `:numbered true` as an option to produce numbered parameters (`$1`, `$2`, etc):
|
||||||
|
|
||||||
Namespace-qualified keywords are generally treated as table-qualified columns: `:foo/bar` becomes `foo.bar`, except in contexts where that would be illegal (such as the list of columns in an `INSERT` statement). This approach is likely to be more compatible with code that uses libraries like [`next.jdbc`](https://github.com/seancorfield/next-jdbc) and [`seql`](https://github.com/exoscale/seql), as well as being more convenient in a world of namespace-qualified keywords, following the example of `clojure.spec` etc.
|
```clojure
|
||||||
|
(sql/format sqlmap {:numbered true})
|
||||||
|
=> ["SELECT a, b, c FROM foo WHERE foo.a = $1" "baz"]
|
||||||
|
```
|
||||||
|
|
||||||
_[In HoneySQL 1.x, this was the behavior when `:namespace-as-table? true` was specified]_
|
Namespace-qualified keywords (and symbols) are generally treated as table-qualified columns: `:foo/bar` becomes `foo.bar`, except in contexts where that would be illegal (such as the list of columns in an `INSERT` statement). This approach is likely to be more compatible with code that uses libraries like [`next.jdbc`](https://github.com/seancorfield/next-jdbc) and [`seql`](https://github.com/exoscale/seql), as well as being more convenient in a world of namespace-qualified keywords, following the example of `clojure.spec` etc.
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(def q-sqlmap {:select [:foo/a :foo/b :foo/c]
|
(def q-sqlmap {:select [:foo/a :foo/b :foo/c]
|
||||||
|
|
@ -80,18 +131,54 @@ _[In HoneySQL 1.x, this was the behavior when `:namespace-as-table? true` was sp
|
||||||
:where [:= :foo/a "baz"]})
|
:where [:= :foo/a "baz"]})
|
||||||
(sql/format q-sqlmap)
|
(sql/format q-sqlmap)
|
||||||
=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"]
|
=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"]
|
||||||
|
;; this also works with symbols instead of keywords:
|
||||||
|
(-> '{select (foo/a, foo/b, foo/c)
|
||||||
|
from (foo)
|
||||||
|
where (= foo/a "baz")}
|
||||||
|
(sql/format))
|
||||||
|
=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
As of 2.6.1126, there is a helper macro you can use with quoted symbolic
|
||||||
|
queries (that are purely literal, not programmatically constructed) to
|
||||||
|
provide "escape hatches" for certain symbols that you want to be treated
|
||||||
|
as locally bound symbols (and, hence, their values):
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
;; quoted symbolic query with local substitution:
|
||||||
|
(let [search-value "baz"]
|
||||||
|
(sql/formatv [search-value]
|
||||||
|
'{select (foo/a, foo/b, foo/c)
|
||||||
|
from (foo)
|
||||||
|
where (= foo/a search-value)}))
|
||||||
|
=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: this is a Clojure-only feature and is not available in ClojureScript, and it is intended for literal, inline symbolic queries only, not for programmatically constructed queries (where you would be able to substitute the values directly, as you build the query).
|
||||||
|
|
||||||
|
Documentation for the entire data DSL can be found in the
|
||||||
|
[Clause Reference](doc/clause-reference.md), the
|
||||||
|
[Operator Reference](doc/operator-reference.md), and the
|
||||||
|
[Special Syntax reference](doc/special-syntax.md).
|
||||||
|
|
||||||
### Vanilla SQL clause helpers
|
### Vanilla SQL clause helpers
|
||||||
|
|
||||||
There are also functions for each clause type in the `honey.sql.helpers` namespace:
|
For every single SQL clause supported by HoneySQL (as keywords or symbols
|
||||||
|
in the data structure that is the DSL), there is also a corresponding
|
||||||
|
function in the `honey.sql.helpers` namespace:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(-> (select :a :b :c)
|
(-> (select :a :b :c)
|
||||||
(from :foo)
|
(from :foo)
|
||||||
(where [:= :f.a "baz"]))
|
(where [:= :foo.a "baz"]))
|
||||||
|
=> {:select [:a :b :c] :from [:foo] :where [:= :foo.a "baz"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In general, `(helper :foo expr)` will produce `{:helper [:foo expr]}`
|
||||||
|
(with a few exceptions -- see the docstring of the helper function
|
||||||
|
for details).
|
||||||
|
|
||||||
Order doesn't matter (for independent clauses):
|
Order doesn't matter (for independent clauses):
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
|
|
@ -104,7 +191,7 @@ When using the vanilla helper functions, repeated clauses will be merged into ex
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(-> sqlmap (select :d))
|
(-> sqlmap (select :d))
|
||||||
=> {:from [:foo], :where [:= :f.a "baz"], :select [:a :b :c :d]}
|
=> {:from [:foo], :where [:= :foo.a "baz"], :select [:a :b :c :d]}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to replace a clause, you can `dissoc` the existing clause first, since this is all data:
|
If you want to replace a clause, you can `dissoc` the existing clause first, since this is all data:
|
||||||
|
|
@ -115,9 +202,11 @@ If you want to replace a clause, you can `dissoc` the existing clause first, sin
|
||||||
(select :*)
|
(select :*)
|
||||||
(where [:> :b 10])
|
(where [:> :b 10])
|
||||||
sql/format)
|
sql/format)
|
||||||
=> ["SELECT * FROM foo WHERE (f.a = ?) AND (b > ?)" "baz" 10]
|
=> ["SELECT * FROM foo WHERE (foo.a = ?) AND (b > ?)" "baz" 10]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: the helpers always produce keywords so you can rely on `dissoc` with the desired keyword to remove. If you are building the data DSL "manually" and using symbols instead of keywords, you'll need to `dissoc` the symbol form instead.
|
||||||
|
|
||||||
`where` will combine multiple clauses together using SQL's `AND`:
|
`where` will combine multiple clauses together using SQL's `AND`:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
|
|
@ -128,6 +217,24 @@ If you want to replace a clause, you can `dissoc` the existing clause first, sin
|
||||||
=> ["SELECT * FROM foo WHERE (a = ?) AND (b < ?)" 1 100]
|
=> ["SELECT * FROM foo WHERE (a = ?) AND (b < ?)" 1 100]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The power of this approach comes from the abiliity to programmatically and
|
||||||
|
conditionally build up queries:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
(defn fetch-user [& {:keys [id name]}]
|
||||||
|
(-> (select :*)
|
||||||
|
(from :users)
|
||||||
|
(cond->
|
||||||
|
id (where [:= :id id])
|
||||||
|
name (where [:= :name name]))
|
||||||
|
sql/format))
|
||||||
|
```
|
||||||
|
|
||||||
|
You can call `fetch-user` with either `:id` or `:name` _or both_ and get back
|
||||||
|
a query with the appropriate `WHERE` clause, since the helpers will merge the
|
||||||
|
conditions into the query DSL.
|
||||||
|
|
||||||
Column and table names may be aliased by using a vector pair of the original
|
Column and table names may be aliased by using a vector pair of the original
|
||||||
name and the desired alias:
|
name and the desired alias:
|
||||||
|
|
||||||
|
|
@ -139,10 +246,33 @@ name and the desired alias:
|
||||||
=> ["SELECT a, b AS bar, c, d AS x FROM foo AS quux WHERE (quux.a = ?) AND (bar < ?)" 1 100]
|
=> ["SELECT a, b AS bar, c, d AS x FROM foo AS quux WHERE (quux.a = ?) AND (bar < ?)" 1 100]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or conditionally:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
(-> (select :a [:b :bar])
|
||||||
|
(cond->
|
||||||
|
need-c (select :c)
|
||||||
|
x-val (select [:d :x]))
|
||||||
|
(from [:foo :quux])
|
||||||
|
(where [:= :quux.a 1] [:< :bar 100])
|
||||||
|
(cond->
|
||||||
|
x-val (where [:> :x x-val]))
|
||||||
|
sql/format)
|
||||||
|
```
|
||||||
|
|
||||||
In particular, note that `(select [:a :b])` means `SELECT a AS b` rather than
|
In particular, note that `(select [:a :b])` means `SELECT a AS b` rather than
|
||||||
`SELECT a, b` -- helpers like `select` are generally variadic and do not take
|
`SELECT a, b` -- helpers like `select` are generally variadic and do not take
|
||||||
a collection of column names.
|
a collection of column names.
|
||||||
|
|
||||||
|
The examples in this README use a mixture of data structures and the helper
|
||||||
|
functions interchangably. For any example using the helpers, you could evaluate
|
||||||
|
it (without the call to `sql/format`) to see what the equivalent data structure
|
||||||
|
would be.
|
||||||
|
|
||||||
|
Documentation for all the helpers can be found in the
|
||||||
|
[`honey.sql.helpers` API reference](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/api/honey.sql.helpers).
|
||||||
|
|
||||||
### Inserts
|
### Inserts
|
||||||
|
|
||||||
Inserts are supported in two patterns.
|
Inserts are supported in two patterns.
|
||||||
|
|
@ -158,8 +288,19 @@ then provide a collection of rows, each a collection of column values:
|
||||||
["Jane" "Daniels" 56]])
|
["Jane" "Daniels" 56]])
|
||||||
(sql/format {:pretty true}))
|
(sql/format {:pretty true}))
|
||||||
=> ["
|
=> ["
|
||||||
INSERT INTO properties
|
INSERT INTO properties (name, surname, age)
|
||||||
(name, surname, age)
|
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
||||||
|
"
|
||||||
|
"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:insert-into [:properties]
|
||||||
|
:columns [:name :surname :age]
|
||||||
|
:values [["Jon" "Smith" 34]
|
||||||
|
["Andrew" "Cooper" 12]
|
||||||
|
["Jane" "Daniels" 56]]}
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
=> ["
|
||||||
|
INSERT INTO properties (name, surname, age)
|
||||||
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
||||||
"
|
"
|
||||||
"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
|
"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
|
||||||
|
|
@ -176,8 +317,21 @@ Alternately, you can simply specify the values as maps:
|
||||||
{:name "Jane" :surname "Daniels" :age 56}])
|
{:name "Jane" :surname "Daniels" :age 56}])
|
||||||
(sql/format {:pretty true}))
|
(sql/format {:pretty true}))
|
||||||
=> ["
|
=> ["
|
||||||
INSERT INTO properties
|
INSERT INTO properties (name, surname, age)
|
||||||
(name, surname, age) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
||||||
|
"
|
||||||
|
"John" "Smith" 34
|
||||||
|
"Andrew" "Cooper" 12
|
||||||
|
"Jane" "Daniels" 56]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:insert-into [:properties]
|
||||||
|
:values [{:name "John", :surname "Smith", :age 34}
|
||||||
|
{:name "Andrew", :surname "Cooper", :age 12}
|
||||||
|
{:name "Jane", :surname "Daniels", :age 56}]}
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
=> ["
|
||||||
|
INSERT INTO properties (name, surname, age)
|
||||||
|
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
||||||
"
|
"
|
||||||
"John" "Smith" 34
|
"John" "Smith" 34
|
||||||
"Andrew" "Cooper" 12
|
"Andrew" "Cooper" 12
|
||||||
|
|
@ -185,7 +339,37 @@ INSERT INTO properties
|
||||||
```
|
```
|
||||||
|
|
||||||
The set of columns used in the insert will be the union of all column names from all
|
The set of columns used in the insert will be the union of all column names from all
|
||||||
the hash maps: columns that are missing from any rows will have `NULL` as their value.
|
the hash maps: columns that are missing from any rows will have `NULL` as their value
|
||||||
|
unless you specify those columns in the `:values-default-columns` option, which takes
|
||||||
|
a set of column names that should get the value `DEFAULT` instead of `NULL`:
|
||||||
|
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(-> (insert-into :properties)
|
||||||
|
(values [{:name "John" :surname "Smith" :age 34}
|
||||||
|
{:name "Andrew" :age 12}
|
||||||
|
{:name "Jane" :surname "Daniels"}])
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
=> ["
|
||||||
|
INSERT INTO properties (name, surname, age)
|
||||||
|
VALUES (?, ?, ?), (?, NULL, ?), (?, ?, NULL)
|
||||||
|
"
|
||||||
|
"John" "Smith" 34
|
||||||
|
"Andrew" 12
|
||||||
|
"Jane" "Daniels"]
|
||||||
|
(-> (insert-into :properties)
|
||||||
|
(values [{:name "John" :surname "Smith" :age 34}
|
||||||
|
{:name "Andrew" :age 12}
|
||||||
|
{:name "Jane" :surname "Daniels"}])
|
||||||
|
(sql/format {:pretty true :values-default-columns #{:age}}))
|
||||||
|
=> ["
|
||||||
|
INSERT INTO properties (name, surname, age)
|
||||||
|
VALUES (?, ?, ?), (?, NULL, ?), (?, ?, DEFAULT)
|
||||||
|
"
|
||||||
|
"John" "Smith" 34
|
||||||
|
"Andrew" 12
|
||||||
|
"Jane" "Daniels"]
|
||||||
|
```
|
||||||
|
|
||||||
### Nested subqueries
|
### Nested subqueries
|
||||||
|
|
||||||
|
|
@ -202,8 +386,23 @@ The column values do not have to be literals, they can be nested queries:
|
||||||
(sql/format {:pretty true})))
|
(sql/format {:pretty true})))
|
||||||
|
|
||||||
=> ["
|
=> ["
|
||||||
INSERT INTO user_profile_to_role
|
INSERT INTO user_profile_to_role (user_profile_id, role_id)
|
||||||
(user_profile_id, role_id) VALUES (?, (SELECT id FROM role WHERE name = ?))
|
VALUES (?, (SELECT id FROM role WHERE name = ?))
|
||||||
|
"
|
||||||
|
12345
|
||||||
|
"user"]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(let [user-id 12345
|
||||||
|
role-name "user"]
|
||||||
|
(-> {:insert-into [:user_profile_to_role]
|
||||||
|
:values [{:user_profile_id 12345,
|
||||||
|
:role_id {:select [:id],
|
||||||
|
:from [:role],
|
||||||
|
:where [:= :name "user"]}}]}
|
||||||
|
(sql/format {:pretty true})))
|
||||||
|
=> ["
|
||||||
|
INSERT INTO user_profile_to_role (user_profile_id, role_id)
|
||||||
|
VALUES (?, (SELECT id FROM role WHERE name = ?))
|
||||||
"
|
"
|
||||||
12345
|
12345
|
||||||
"user"]
|
"user"]
|
||||||
|
|
@ -213,10 +412,25 @@ INSERT INTO user_profile_to_role
|
||||||
(-> (select :*)
|
(-> (select :*)
|
||||||
(from :foo)
|
(from :foo)
|
||||||
(where [:in :foo.a (-> (select :a) (from :bar))])
|
(where [:in :foo.a (-> (select :a) (from :bar))])
|
||||||
sql/format)
|
(sql/format))
|
||||||
|
=> ["SELECT * FROM foo WHERE foo.a IN (SELECT a FROM bar)"]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:select [:*],
|
||||||
|
:from [:foo],
|
||||||
|
:where [:in :foo.a {:select [:a], :from [:bar]}]}
|
||||||
|
(sql/format))
|
||||||
=> ["SELECT * FROM foo WHERE foo.a IN (SELECT a FROM bar)"]
|
=> ["SELECT * FROM foo WHERE foo.a IN (SELECT a FROM bar)"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Because values can be nested queries -- and also because values can be function calls --
|
||||||
|
whenever you are working with values that are, themselves, structured data, you will
|
||||||
|
need to tell HoneySQL not to interpret that structured data as part of the DSL. This
|
||||||
|
especially affects using JSON values with HoneySQL (e.g., targeting PostgreSQL). There
|
||||||
|
are two possible approaches:
|
||||||
|
|
||||||
|
1. Use named parameters instead of having the values directly in the DSL structure (see `:param` under **Miscellaneous** below), or
|
||||||
|
2. Use `[:lift ..]` wrapped around any structured values which tells HoneySQL not to interpret the vector or hash map value as a DSL.
|
||||||
|
|
||||||
### Composite types
|
### Composite types
|
||||||
|
|
||||||
Composite types are supported:
|
Composite types are supported:
|
||||||
|
|
@ -229,8 +443,30 @@ Composite types are supported:
|
||||||
["large" (composite 10 "feet")]])
|
["large" (composite 10 "feet")]])
|
||||||
(sql/format {:pretty true}))
|
(sql/format {:pretty true}))
|
||||||
=> ["
|
=> ["
|
||||||
INSERT INTO comp_table
|
INSERT INTO comp_table (name, comp_column)
|
||||||
(name, comp_column)
|
VALUES (?, (?, ?)), (?, (?, ?))
|
||||||
|
"
|
||||||
|
"small" 1 "inch" "large" 10 "feet"]
|
||||||
|
;; with numbered parameters:
|
||||||
|
(-> (insert-into :comp_table)
|
||||||
|
(columns :name :comp_column)
|
||||||
|
(values
|
||||||
|
[["small" (composite 1 "inch")]
|
||||||
|
["large" (composite 10 "feet")]])
|
||||||
|
(sql/format {:pretty true :numbered true}))
|
||||||
|
=> ["
|
||||||
|
INSERT INTO comp_table (name, comp_column)
|
||||||
|
VALUES ($1, ($2, $3)), ($4, ($5, $6))
|
||||||
|
"
|
||||||
|
"small" 1 "inch" "large" 10 "feet"]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:insert-into [:comp_table],
|
||||||
|
:columns [:name :comp_column],
|
||||||
|
:values [["small" [:composite 1 "inch"]]
|
||||||
|
["large" [:composite 10 "feet"]]]}
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
=> ["
|
||||||
|
INSERT INTO comp_table (name, comp_column)
|
||||||
VALUES (?, (?, ?)), (?, (?, ?))
|
VALUES (?, (?, ?)), (?, (?, ?))
|
||||||
"
|
"
|
||||||
"small" 1 "inch" "large" 10 "feet"]
|
"small" 1 "inch" "large" 10 "feet"]
|
||||||
|
|
@ -241,7 +477,7 @@ VALUES (?, (?, ?)), (?, (?, ?))
|
||||||
Updates are possible too:
|
Updates are possible too:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(-> (h/update :films)
|
(-> (update :films)
|
||||||
(set {:kind "dramatic"
|
(set {:kind "dramatic"
|
||||||
:watched [:+ :watched 1]})
|
:watched [:+ :watched 1]})
|
||||||
(where [:= :kind "drama"])
|
(where [:= :kind "drama"])
|
||||||
|
|
@ -254,6 +490,19 @@ WHERE kind = ?
|
||||||
"dramatic"
|
"dramatic"
|
||||||
1
|
1
|
||||||
"drama"]
|
"drama"]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:update :films,
|
||||||
|
:set {:kind "dramatic", :watched [:+ :watched 1]},
|
||||||
|
:where [:= :kind "drama"]}
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
=> ["
|
||||||
|
UPDATE films
|
||||||
|
SET kind = ?, watched = watched + ?
|
||||||
|
WHERE kind = ?
|
||||||
|
"
|
||||||
|
"dramatic"
|
||||||
|
1
|
||||||
|
"drama"]
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are trying to build a compound update statement (with `from` or `join`),
|
If you are trying to build a compound update statement (with `from` or `join`),
|
||||||
|
|
@ -272,6 +521,11 @@ Deletes look as you would expect:
|
||||||
(where [:<> :kind "musical"])
|
(where [:<> :kind "musical"])
|
||||||
(sql/format))
|
(sql/format))
|
||||||
=> ["DELETE FROM films WHERE kind <> ?" "musical"]
|
=> ["DELETE FROM films WHERE kind <> ?" "musical"]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:delete-from [:films],
|
||||||
|
:where [:<> :kind "musical"]}
|
||||||
|
(sql/format))
|
||||||
|
=> ["DELETE FROM films WHERE kind <> ?" "musical"]
|
||||||
```
|
```
|
||||||
|
|
||||||
If your database supports it, you can also delete from multiple tables:
|
If your database supports it, you can also delete from multiple tables:
|
||||||
|
|
@ -289,6 +543,19 @@ INNER JOIN directors ON films.director_id = directors.id
|
||||||
WHERE kind <> ?
|
WHERE kind <> ?
|
||||||
"
|
"
|
||||||
"musical"]
|
"musical"]
|
||||||
|
;; or pure data DSL:
|
||||||
|
(-> {:delete [:films :directors],
|
||||||
|
:from [:films],
|
||||||
|
:join [:directors [:= :films.director_id :directors.id]],
|
||||||
|
:where [:<> :kind "musical"]}
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
=> ["
|
||||||
|
DELETE films, directors
|
||||||
|
FROM films
|
||||||
|
INNER JOIN directors ON films.director_id = directors.id
|
||||||
|
WHERE kind <> ?
|
||||||
|
"
|
||||||
|
"musical"]
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to delete everything from a table, you can use `truncate`:
|
If you want to delete everything from a table, you can use `truncate`:
|
||||||
|
|
@ -296,7 +563,11 @@ If you want to delete everything from a table, you can use `truncate`:
|
||||||
```clojure
|
```clojure
|
||||||
(-> (truncate :films)
|
(-> (truncate :films)
|
||||||
(sql/format))
|
(sql/format))
|
||||||
=> ["TRUNCATE films"]
|
=> ["TRUNCATE TABLE films"]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:truncate :films}
|
||||||
|
(sql/format))
|
||||||
|
=> ["TRUNCATE TABLE films"]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set operations
|
### Set operations
|
||||||
|
|
@ -306,7 +577,7 @@ Queries may be combined with a `:union`, `:union-all`, `:intersect` or `:except`
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format {:union [(-> (select :*) (from :foo))
|
(sql/format {:union [(-> (select :*) (from :foo))
|
||||||
(-> (select :*) (from :bar))]})
|
(-> (select :*) (from :bar))]})
|
||||||
=> ["(SELECT * FROM foo) UNION (SELECT * FROM bar)"]
|
=> ["SELECT * FROM foo UNION SELECT * FROM bar"]
|
||||||
```
|
```
|
||||||
|
|
||||||
There are also helpers for each of those:
|
There are also helpers for each of those:
|
||||||
|
|
@ -314,19 +585,38 @@ There are also helpers for each of those:
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format (union (-> (select :*) (from :foo))
|
(sql/format (union (-> (select :*) (from :foo))
|
||||||
(-> (select :*) (from :bar))))
|
(-> (select :*) (from :bar))))
|
||||||
=> ["(SELECT * FROM foo) UNION (SELECT * FROM bar)"]
|
=> ["SELECT * FROM foo UNION SELECT * FROM bar"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: different databases have different precedence rules for these set operations when used in combination -- you may need to use `:nest` to add `(` .. `)` in order to combine these operations in a single SQL statement, if the natural order produced by HoneySQL does not work "as expected" for your database.
|
||||||
|
|
||||||
### Functions
|
### Functions
|
||||||
|
|
||||||
Keywords that begin with `%` are interpreted as SQL function calls:
|
Function calls (and expressions with operators) can be specified as
|
||||||
|
vectors where the first element is either a keyword or a symbol:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(-> (select :*) (from :foo)
|
||||||
|
(where [:> :date_created [:date_add [:now] [:interval 24 :hours]]])
|
||||||
|
(sql/format))
|
||||||
|
=> ["SELECT * FROM foo WHERE date_created > DATE_ADD(NOW(), INTERVAL ? HOURS)" 24]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: The above example may be specific to MySQL but the general principle of vectors for function calls applies to all dialects.
|
||||||
|
|
||||||
|
A shorthand syntax also exists for simple function calls:
|
||||||
|
keywords that begin with `%` are interpreted as SQL function calls:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(-> (select :%count.*) (from :foo) sql/format)
|
(-> (select :%count.*) (from :foo) sql/format)
|
||||||
=> ["SELECT COUNT(*) FROM foo"]
|
=> ["SELECT COUNT(*) FROM foo"]
|
||||||
```
|
```
|
||||||
```clojure
|
```clojure
|
||||||
|
;; with an alias:
|
||||||
|
(-> (select [:%count.* :total]) (from :foo) sql/format)
|
||||||
|
=> ["SELECT COUNT(*) AS total FROM foo"]
|
||||||
|
```
|
||||||
|
```clojure
|
||||||
(-> (select :%max.id) (from :foo) sql/format)
|
(-> (select :%max.id) (from :foo) sql/format)
|
||||||
=> ["SELECT MAX(id) FROM foo"]
|
=> ["SELECT MAX(id) FROM foo"]
|
||||||
```
|
```
|
||||||
|
|
@ -340,10 +630,75 @@ regular function calls in a select:
|
||||||
=> ["SELECT COUNT(*) FROM foo"]
|
=> ["SELECT COUNT(*) FROM foo"]
|
||||||
```
|
```
|
||||||
```clojure
|
```clojure
|
||||||
|
(-> (select [[:count :*] :total]) (from :foo) sql/format)
|
||||||
|
=> ["SELECT COUNT(*) AS total FROM foo"]
|
||||||
|
```
|
||||||
|
```clojure
|
||||||
|
(-> (select [:%count.*]) (from :foo) sql/format)
|
||||||
|
=> ["SELECT COUNT(*) FROM foo"]
|
||||||
|
;; or even:
|
||||||
|
(-> (select :%count.*) (from :foo) sql/format)
|
||||||
|
=> ["SELECT COUNT(*) FROM foo"]
|
||||||
|
```
|
||||||
|
```clojure
|
||||||
(-> (select [[:max :id]]) (from :foo) sql/format)
|
(-> (select [[:max :id]]) (from :foo) sql/format)
|
||||||
=> ["SELECT MAX(id) FROM foo"]
|
=> ["SELECT MAX(id) FROM foo"]
|
||||||
|
(-> (select [[:max :id] :highest]) (from :foo) sql/format)
|
||||||
|
=> ["SELECT MAX(id) AS highest FROM foo"]
|
||||||
|
;; the pure data DSL requires an extra level of brackets:
|
||||||
|
(-> {:select [[[:max :id]]], :from [:foo]} sql/format)
|
||||||
|
=> ["SELECT MAX(id) FROM foo"]
|
||||||
|
(-> {:select [[[:max :id] :highest]], :from [:foo]} sql/format)
|
||||||
|
=> ["SELECT MAX(id) AS highest FROM foo"]
|
||||||
|
;; the shorthand makes this simpler:
|
||||||
|
(-> {:select [[:%max.id]], :from [:foo]} sql/format)
|
||||||
|
=> ["SELECT MAX(id) FROM foo"]
|
||||||
|
(-> {:select [[:%max.id :highest]], :from [:foo]} sql/format)
|
||||||
|
=> ["SELECT MAX(id) AS highest FROM foo"]
|
||||||
|
;; or even (no alias):
|
||||||
|
(-> {:select [:%max.id], :from [:foo]} sql/format)
|
||||||
|
=> ["SELECT MAX(id) FROM foo"]
|
||||||
|
;; or even (no alias, no other columns):
|
||||||
|
(-> {:select :%max.id, :from :foo} sql/format)
|
||||||
|
=> ["SELECT MAX(id) FROM foo"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Custom columns using functions are built with the same vector format.
|
||||||
|
Be sure to properly nest the vectors so that the first element in the selection
|
||||||
|
is the custom function and the second is the column alias.
|
||||||
|
```clojure
|
||||||
|
(sql/format
|
||||||
|
{:select [:job_name ;; A bare field selection
|
||||||
|
[[:avg [:/ [:- :end_time :start_time] 1000.0]] ;; A custom function
|
||||||
|
:avg_exec_time_seconds ;; The column alias
|
||||||
|
]]
|
||||||
|
:from [:job_data]
|
||||||
|
:group-by :job_name})
|
||||||
|
=> ["SELECT job_name, AVG((end_time - start_time) / ?) AS avg_exec_time_seconds FROM job_data GROUP BY job_name" 1000.0]
|
||||||
|
```
|
||||||
|
|
||||||
|
If a keyword begins with `'`, the function name is formatted as a SQL
|
||||||
|
entity rather than being converted to uppercase and having hyphens `-`
|
||||||
|
converted to spaces). That means that hyphens `-` will become underscores `_`
|
||||||
|
unless you have quoting enabled:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(-> (select :*) (from :foo)
|
||||||
|
(where [:'my-schema.SomeFunction :bar 0])
|
||||||
|
(sql/format))
|
||||||
|
=> ["SELECT * FROM foo WHERE my_schema.SomeFunction(bar, ?)" 0]
|
||||||
|
(-> (select :*) (from :foo)
|
||||||
|
(where [:'my-schema.SomeFunction :bar 0])
|
||||||
|
(sql/format :quoted true))
|
||||||
|
=> ["SELECT * FROM \"foo\" WHERE \"my-schema\".\"SomeFunction\"(\"bar\", ?)" 0]
|
||||||
|
(-> (select :*) (from :foo)
|
||||||
|
(where [:'my-schema.SomeFunction :bar 0])
|
||||||
|
(sql/format :dialect :mysql))
|
||||||
|
=> ["SELECT * FROM `foo` WHERE `my-schema`.`SomeFunction`(`bar`, ?)" 0]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: in non-function contexts, if a keyword begins with `'`, it is transcribed into the SQL exactly as-is, with no case or character conversion at all.
|
||||||
|
|
||||||
### Bindable parameters
|
### Bindable parameters
|
||||||
|
|
||||||
Keywords that begin with `?` are interpreted as bindable parameters:
|
Keywords that begin with `?` are interpreted as bindable parameters:
|
||||||
|
|
@ -354,6 +709,16 @@ Keywords that begin with `?` are interpreted as bindable parameters:
|
||||||
(where [:= :a :?baz])
|
(where [:= :a :?baz])
|
||||||
(sql/format {:params {:baz "BAZ"}}))
|
(sql/format {:params {:baz "BAZ"}}))
|
||||||
=> ["SELECT id FROM foo WHERE a = ?" "BAZ"]
|
=> ["SELECT id FROM foo WHERE a = ?" "BAZ"]
|
||||||
|
;; or with numbered parameters:
|
||||||
|
(-> (select :id)
|
||||||
|
(from :foo)
|
||||||
|
(where [:= :a :?baz])
|
||||||
|
(sql/format {:params {:baz "BAZ"} :numbered true}))
|
||||||
|
=> ["SELECT id FROM foo WHERE a = $1" "BAZ"]
|
||||||
|
;; or as pure data DSL:
|
||||||
|
(-> {:select [:id], :from [:foo], :where [:= :a :?baz]}
|
||||||
|
(sql/format {:params {:baz "BAZ"}}))
|
||||||
|
=> ["SELECT id FROM foo WHERE a = ?" "BAZ"]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
|
|
@ -375,7 +740,7 @@ The `:lift` syntax will prevent interpretation of Clojure data structures as
|
||||||
part of the DSL and instead turn such values into parameters (useful when you
|
part of the DSL and instead turn such values into parameters (useful when you
|
||||||
want to pass a vector or a hash map directly as a positional parameter value,
|
want to pass a vector or a hash map directly as a positional parameter value,
|
||||||
for example when you have extended `next.jdbc`'s `SettableParameter` protocol
|
for example when you have extended `next.jdbc`'s `SettableParameter` protocol
|
||||||
to a data structure).
|
to a data structure -- as is common when working with PostgreSQL's JSON/JSONB types).
|
||||||
|
|
||||||
Finally, the `:nest` syntax will cause an extra set of parentheses to be
|
Finally, the `:nest` syntax will cause an extra set of parentheses to be
|
||||||
wrapped around its argument, after formatting that argument as a SQL expression.
|
wrapped around its argument, after formatting that argument as a SQL expression.
|
||||||
|
|
@ -390,9 +755,9 @@ These can be combined to allow more fine-grained control over SQL generation:
|
||||||
```
|
```
|
||||||
```clojure
|
```clojure
|
||||||
call-qualify-map
|
call-qualify-map
|
||||||
=> '{:where [:and [:= :a [:param :baz]] [:= :b [:inline 42]]]
|
=> {:where [:and [:= :a [:param :baz]] [:= :b [:inline 42]]]
|
||||||
:from (:foo)
|
:from (:foo)
|
||||||
:select [[[:foo :bar]] [[:raw "@var := foo.bar"]]]}
|
:select [[[:foo :bar]] [[:raw "@var := foo.bar"]]]}
|
||||||
```
|
```
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format call-qualify-map {:params {:baz "BAZ"}})
|
(sql/format call-qualify-map {:params {:baz "BAZ"}})
|
||||||
|
|
@ -443,20 +808,20 @@ have a lot of function calls needed in code:
|
||||||
[:cast 4325 :integer]]}])
|
[:cast 4325 :integer]]}])
|
||||||
(sql/format {:pretty true}))
|
(sql/format {:pretty true}))
|
||||||
=> ["
|
=> ["
|
||||||
INSERT INTO sample
|
INSERT INTO sample (location)
|
||||||
(location) VALUES (ST_SETSRID(ST_MAKEPOINT(?, ?), CAST(? AS integer)))
|
VALUES (ST_SETSRID(ST_MAKEPOINT(?, ?), CAST(? AS INTEGER)))
|
||||||
"
|
"
|
||||||
0.291 32.621 4325]
|
0.291 32.621 4325]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Identifiers
|
#### Entity Names
|
||||||
|
|
||||||
To quote identifiers, pass the `:quoted true` option to `format` and they will
|
To quote SQL entity names, pass the `:quoted true` option to `format` and they will
|
||||||
be quoted according to the selected dialect. If you override the dialect in a
|
be quoted according to the selected dialect. If you override the dialect in a
|
||||||
`format` call, by passing the `:dialect` option, identifiers will be automatically
|
`format` call, by passing the `:dialect` option, SQL entity names will be automatically
|
||||||
quoted. You can override the dialect and turn off quoting by passing `:quoted false`.
|
quoted. You can override the dialect and turn off quoting by passing `:quoted false`.
|
||||||
Valid `:dialect` options are `:ansi` (the default, use this for PostgreSQL),
|
Valid `:dialect` options are `:ansi` (the default, use this for PostgreSQL),
|
||||||
`:mysql`, `:oracle`, or `:sqlserver`:
|
`:mysql`, `:oracle`, or `:sqlserver`. As of 2.5.1091, `:nrql` is also supported:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(-> (select :foo.a)
|
(-> (select :foo.a)
|
||||||
|
|
@ -465,6 +830,15 @@ Valid `:dialect` options are `:ansi` (the default, use this for PostgreSQL),
|
||||||
(sql/format {:dialect :mysql}))
|
(sql/format {:dialect :mysql}))
|
||||||
=> ["SELECT `foo`.`a` FROM `foo` WHERE `foo`.`a` = ?" "baz"]
|
=> ["SELECT `foo`.`a` FROM `foo` WHERE `foo`.`a` = ?" "baz"]
|
||||||
```
|
```
|
||||||
|
```clojure
|
||||||
|
(-> (select :foo.a)
|
||||||
|
(from :foo)
|
||||||
|
(where [:= :foo.a "baz"])
|
||||||
|
(sql/format {:dialect :nrql}))
|
||||||
|
=> ["SELECT `foo.a` FROM foo WHERE `foo.a` = 'baz'"]
|
||||||
|
```
|
||||||
|
|
||||||
|
See [New Relic NRQL Support](nrsql.md) for more details of the NRQL dialect.
|
||||||
|
|
||||||
#### Locking
|
#### Locking
|
||||||
|
|
||||||
|
|
@ -521,7 +895,8 @@ Here's a big, complicated query. Note that HoneySQL makes no attempt to verify t
|
||||||
(-> (select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
(-> (select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
||||||
[[:now]] [[:raw "@x := 10"]])
|
[[:now]] [[:raw "@x := 10"]])
|
||||||
(from [:foo :f] [:baz :b])
|
(from [:foo :f] [:baz :b])
|
||||||
(join :draq [:= :f.b :draq.x])
|
(join :draq [:= :f.b :draq.x]
|
||||||
|
:eldr [:= :f.e :eldr.t])
|
||||||
(left-join [:clod :c] [:= :f.a :c.d])
|
(left-join [:clod :c] [:= :f.a :c.d])
|
||||||
(right-join :bock [:= :bock.z :c.e])
|
(right-join :bock [:= :bock.z :c.e])
|
||||||
(where [:or
|
(where [:or
|
||||||
|
|
@ -540,7 +915,8 @@ big-complicated-map
|
||||||
=> {:select-distinct [:f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
=> {:select-distinct [:f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
||||||
[[:now]] [[:raw "@x := 10"]]]
|
[[:now]] [[:raw "@x := 10"]]]
|
||||||
:from [[:foo :f] [:baz :b]]
|
:from [[:foo :f] [:baz :b]]
|
||||||
:join [:draq [:= :f.b :draq.x]]
|
:join [:draq [:= :f.b :draq.x]
|
||||||
|
:eldr [:= :f.e :eldr.t]]
|
||||||
:left-join [[:clod :c] [:= :f.a :c.d]]
|
:left-join [[:clod :c] [:= :f.a :c.d]]
|
||||||
:right-join [:bock [:= :bock.z :c.e]]
|
:right-join [:bock [:= :bock.z :c.e]]
|
||||||
:where [:or
|
:where [:or
|
||||||
|
|
@ -561,7 +937,7 @@ big-complicated-map
|
||||||
=> ["
|
=> ["
|
||||||
SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10
|
SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10
|
||||||
FROM foo AS f, baz AS b
|
FROM foo AS f, baz AS b
|
||||||
INNER JOIN draq ON f.b = draq.x
|
INNER JOIN draq ON f.b = draq.x INNER JOIN eldr ON f.e = eldr.t
|
||||||
LEFT JOIN clod AS c ON f.a = c.d
|
LEFT JOIN clod AS c ON f.a = c.d
|
||||||
RIGHT JOIN bock ON bock.z = c.e
|
RIGHT JOIN bock ON bock.z = c.e
|
||||||
WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ?
|
WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ?
|
||||||
|
|
@ -572,10 +948,30 @@ LIMIT ?
|
||||||
OFFSET ?
|
OFFSET ?
|
||||||
"
|
"
|
||||||
"bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
"bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
||||||
|
;; with numbered parameters:
|
||||||
|
(sql/format big-complicated-map
|
||||||
|
{:params {:param1 "gabba" :param2 2}
|
||||||
|
:pretty true :numbered true})
|
||||||
|
=> ["
|
||||||
|
SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10
|
||||||
|
FROM foo AS f, baz AS b
|
||||||
|
INNER JOIN draq ON f.b = draq.x INNER JOIN eldr ON f.e = eldr.t
|
||||||
|
LEFT JOIN clod AS c ON f.a = c.d
|
||||||
|
RIGHT JOIN bock ON bock.z = c.e
|
||||||
|
WHERE ((f.a = $1) AND (b.baz <> $2)) OR (($3 < $4) AND ($5 < $6)) OR (f.e IN ($7, $8, $9)) OR f.e BETWEEN $10 AND $11
|
||||||
|
GROUP BY f.a, c.e
|
||||||
|
HAVING $12 < f.e
|
||||||
|
ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST
|
||||||
|
LIMIT $13
|
||||||
|
OFFSET $14
|
||||||
|
"
|
||||||
|
"bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
||||||
```
|
```
|
||||||
```clojure
|
```clojure
|
||||||
;; Printable and readable
|
;; Printable and readable
|
||||||
(= big-complicated-map (read-string (pr-str big-complicated-map)))
|
(require '[clojure.edn :as edn])
|
||||||
|
|
||||||
|
(= big-complicated-map (edn/read-string (pr-str big-complicated-map)))
|
||||||
=> true
|
=> true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -587,11 +983,9 @@ If your database supports `<=>` as an operator, you can tell HoneySQL about it u
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/register-op! :<=>)
|
(sql/register-op! :<=>)
|
||||||
;; default is a binary operator:
|
;; all operators are assumed to be variadic:
|
||||||
(-> (select :a) (where [:<=> :a "foo"]) sql/format)
|
(-> (select :a) (where [:<=> :a "foo"]) sql/format)
|
||||||
=> ["SELECT a WHERE a <=> ?" "foo"]
|
=> ["SELECT a WHERE a <=> ?" "foo"]
|
||||||
;; you can declare that an operator is variadic:
|
|
||||||
(sql/register-op! :<=> :variadic true)
|
|
||||||
(-> (select :a) (where [:<=> "food" :a "fool"]) sql/format)
|
(-> (select :a) (where [:<=> "food" :a "fool"]) sql/format)
|
||||||
=> ["SELECT a WHERE ? <=> a <=> ?" "food" "fool"]
|
=> ["SELECT a WHERE ? <=> a <=> ?" "food" "fool"]
|
||||||
```
|
```
|
||||||
|
|
@ -614,14 +1008,19 @@ Or perhaps your database supports syntax like `a BETWIXT b AND c`, in which case
|
||||||
[sql-c & params-c] (sql/format-expr c)]
|
[sql-c & params-c] (sql/format-expr c)]
|
||||||
(-> [(str sql-a " " (sql/sql-kw op) " "
|
(-> [(str sql-a " " (sql/sql-kw op) " "
|
||||||
sql-b " AND " sql-c)]
|
sql-b " AND " sql-c)]
|
||||||
(into params-a)
|
(c/into params-a)
|
||||||
(into params-b)
|
(c/into params-b)
|
||||||
(into params-c)))))
|
(c/into params-c)))))
|
||||||
;; example usage:
|
;; example usage:
|
||||||
(-> (select :a) (where [:betwixt :a 1 10]) sql/format)
|
(-> (select :a) (where [:betwixt :a 1 10]) sql/format)
|
||||||
=> ["SELECT a WHERE a BETWIXT ? AND ?" 1 10]
|
=> ["SELECT a WHERE a BETWIXT ? AND ?" 1 10]
|
||||||
|
;; with numbered parameters:
|
||||||
|
(-> (select :a) (where [:betwixt :a 1 10]) (sql/format {:numbered true}))
|
||||||
|
=> ["SELECT a WHERE a BETWIXT $1 AND $2" 1 10]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: the generation of positional placeholders (`?`) or numbered placeholders (`$1`, `$2`, etc) is handled automatically by `format-expr` so you get this behavior "for free" in your extensions, as long as you use the public API for `honey.sql`. You should avoid writing extensions that generate placeholders directly if you want them to work with numbered parameters.
|
||||||
|
|
||||||
You can also register SQL clauses, specifying the keyword, the formatting function, and an existing clause that this new clause should be processed before:
|
You can also register SQL clauses, specifying the keyword, the formatting function, and an existing clause that this new clause should be processed before:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
|
|
@ -631,10 +1030,10 @@ You can also register SQL clauses, specifying the keyword, the formatting functi
|
||||||
(sql/register-clause! :foobar
|
(sql/register-clause! :foobar
|
||||||
(fn [clause x]
|
(fn [clause x]
|
||||||
(let [[sql & params]
|
(let [[sql & params]
|
||||||
(if (keyword? x)
|
(if (ident? x)
|
||||||
(sql/format-expr x)
|
(sql/format-expr x)
|
||||||
(sql/format-dsl x))]
|
(sql/format-dsl x))]
|
||||||
(into [(str (sql/sql-kw clause) " " sql)] params)))
|
(c/into [(str (sql/sql-kw clause) " " sql)] params)))
|
||||||
:from) ; SELECT ... FOOBAR ... FROM ...
|
:from) ; SELECT ... FOOBAR ... FROM ...
|
||||||
;; example usage:
|
;; example usage:
|
||||||
(sql/format {:select [:a :b] :foobar :baz})
|
(sql/format {:select [:a :b] :foobar :baz})
|
||||||
|
|
@ -645,8 +1044,15 @@ You can also register SQL clauses, specifying the keyword, the formatting functi
|
||||||
|
|
||||||
If you find yourself registering an operator, a function (syntax), or a new clause, consider submitting a [pull request to HoneySQL](https://github.com/seancorfield/honeysql/pulls) so others can use it, too. If it is dialect-specific, let me know in the pull request.
|
If you find yourself registering an operator, a function (syntax), or a new clause, consider submitting a [pull request to HoneySQL](https://github.com/seancorfield/honeysql/pulls) so others can use it, too. If it is dialect-specific, let me know in the pull request.
|
||||||
|
|
||||||
|
<a name="1.x"/>
|
||||||
|
## HoneySQL 1.x (legacy)
|
||||||
|
|
||||||
|
[](https://clojars.org/honeysql/honeysql) [](https://cljdoc.org/d/honeysql/honeysql/CURRENT)
|
||||||
|
|
||||||
|
HoneySQL 1.x will continue to get critical security fixes but otherwise should be considered "legacy" at this point.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2020-2021 Sean Corfield. HoneySQL 1.x was copyright (c) 2012-2020 Justin Kramer and Sean Corfield.
|
Copyright (c) 2020-2024 Sean Corfield. HoneySQL 1.x was copyright (c) 2012-2020 Justin Kramer and Sean Corfield.
|
||||||
|
|
||||||
Distributed under the Eclipse Public License, the same as Clojure.
|
Distributed under the Eclipse Public License, the same as Clojure.
|
||||||
|
|
|
||||||
8
bb.edn
Normal file
8
bb.edn
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{:paths ["src"]
|
||||||
|
:tasks
|
||||||
|
{test
|
||||||
|
{:extra-paths ["test"]
|
||||||
|
:extra-deps {io.github.cognitect-labs/test-runner
|
||||||
|
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
|
||||||
|
:task (exec 'cognitect.test-runner.api/test)
|
||||||
|
:exec-args {:patterns ["^(?!honey.cache).*-test$"]}}}}
|
||||||
125
build.clj
Normal file
125
build.clj
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
(ns build
|
||||||
|
"HoneySQL's build script.
|
||||||
|
|
||||||
|
clojure -T:build ci
|
||||||
|
|
||||||
|
clojure -T:build run-doc-tests :aliases '[:cljs]'
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
clojure -X:test
|
||||||
|
clojure -X:test:1.12
|
||||||
|
|
||||||
|
For more information, run:
|
||||||
|
|
||||||
|
clojure -A:deps -T:build help/doc"
|
||||||
|
(:refer-clojure :exclude [test])
|
||||||
|
(:require [clojure.string :as str]
|
||||||
|
[clojure.tools.build.api :as b]
|
||||||
|
[clojure.tools.deps :as t]
|
||||||
|
[deps-deploy.deps-deploy :as dd]))
|
||||||
|
|
||||||
|
(def lib 'com.github.seancorfield/honeysql)
|
||||||
|
(defn- the-version [patch] (format "2.7.%s" patch))
|
||||||
|
(def version (the-version (b/git-count-revs nil)))
|
||||||
|
(def snapshot (the-version "9999-SNAPSHOT"))
|
||||||
|
(def class-dir "target/classes")
|
||||||
|
|
||||||
|
(defn- run-task [aliases]
|
||||||
|
(println "\nRunning task for" (str/join "," (map name aliases)))
|
||||||
|
(let [basis (b/create-basis {:aliases aliases})
|
||||||
|
combined (t/combine-aliases basis aliases)
|
||||||
|
cmds (b/java-command
|
||||||
|
{:basis basis
|
||||||
|
:main 'clojure.main
|
||||||
|
:main-args (:main-opts combined)})
|
||||||
|
{:keys [exit]} (b/process cmds)]
|
||||||
|
(when-not (zero? exit) (throw (ex-info "Task failed" {})))))
|
||||||
|
|
||||||
|
(defn eastwood "Run Eastwood." [opts]
|
||||||
|
(run-task [:eastwood])
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(defn gen-doc-tests "Generate tests from doc code blocks." [opts]
|
||||||
|
(run-task [:gen-doc-tests])
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(defn run-doc-tests
|
||||||
|
"Generate and run doc tests.
|
||||||
|
|
||||||
|
Optionally specify :aliases vector:
|
||||||
|
[:1.10] -- test against Clojure 1.10.3 (the default)
|
||||||
|
[:1.11] -- test against Clojure 1.11.0
|
||||||
|
[:1.12] -- test against Clojure 1.12.0
|
||||||
|
[:cljs] -- test against ClojureScript"
|
||||||
|
[{:keys [aliases] :as opts}]
|
||||||
|
(gen-doc-tests opts)
|
||||||
|
(run-task (-> [:test :runner :test-doc]
|
||||||
|
(into aliases)
|
||||||
|
(into (if (some #{:cljs} aliases)
|
||||||
|
[:test-doc-cljs]
|
||||||
|
[:test-doc-clj]))))
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(defn test "Run basic tests." [opts]
|
||||||
|
(run-task [:test :runner :1.11])
|
||||||
|
(run-task [:test :runner :cljs])
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(defn- pom-template [version]
|
||||||
|
[[:description "SQL as Clojure data structures."]
|
||||||
|
[:url "https://github.com/seancorfield/honeysql"]
|
||||||
|
[:licenses
|
||||||
|
[:license
|
||||||
|
[:name "Eclipse Public License"]
|
||||||
|
[:url "http://www.eclipse.org/legal/epl-v10.html"]]]
|
||||||
|
[:developers
|
||||||
|
[:developer
|
||||||
|
[:name "Sean Corfield"]]
|
||||||
|
[:developer
|
||||||
|
[:name "Justin Kramer"]]]
|
||||||
|
[:scm
|
||||||
|
[:url "https://github.com/seancorfield/honeysql"]
|
||||||
|
[:connection "scm:git:https://github.com/seancorfield/honeysql.git"]
|
||||||
|
[:developerConnection "scm:git:ssh:git@github.com:seancorfield/honeysql.git"]
|
||||||
|
[:tag (str "v" version)]]])
|
||||||
|
|
||||||
|
(defn- jar-opts [opts]
|
||||||
|
(let [version (if (:snapshot opts) snapshot version)]
|
||||||
|
(println "\nVersion:" version)
|
||||||
|
(assoc opts
|
||||||
|
:lib lib :version version
|
||||||
|
:jar-file (format "target/%s-%s.jar" lib version)
|
||||||
|
:basis (b/create-basis {})
|
||||||
|
:class-dir class-dir
|
||||||
|
:target "target"
|
||||||
|
:src-dirs ["src"]
|
||||||
|
:pom-data (pom-template version))))
|
||||||
|
|
||||||
|
(defn ci
|
||||||
|
"Run the CI pipeline of tests (and build the JAR).
|
||||||
|
|
||||||
|
Default Clojure version is 1.10.3 (:1.10) so :elide
|
||||||
|
tests for #409 on that version."
|
||||||
|
[opts]
|
||||||
|
(let [aliases [:cljs :elide :1.11 :1.12]
|
||||||
|
opts (jar-opts opts)]
|
||||||
|
(b/delete {:path "target"})
|
||||||
|
(doseq [alias aliases]
|
||||||
|
(run-doc-tests {:aliases [alias]}))
|
||||||
|
(eastwood opts)
|
||||||
|
(doseq [alias aliases]
|
||||||
|
(run-task [:test :runner alias]))
|
||||||
|
(b/delete {:path "target"})
|
||||||
|
(println "\nWriting pom.xml...")
|
||||||
|
(b/write-pom opts)
|
||||||
|
(println "\nCopying source...")
|
||||||
|
(b/copy-dir {:src-dirs ["src"] :target-dir class-dir})
|
||||||
|
(println "\nBuilding" (:jar-file opts) "...")
|
||||||
|
(b/jar opts))
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(defn deploy "Deploy the JAR to Clojars." [opts]
|
||||||
|
(let [{:keys [jar-file] :as opts} (jar-opts opts)]
|
||||||
|
(dd/deploy {:installer :remote :artifact (b/resolve-path jar-file)
|
||||||
|
:pom-file (b/pom-path (select-keys opts [:lib :class-dir]))}))
|
||||||
|
opts)
|
||||||
36
build/honey/gen_doc_tests.clj
Normal file
36
build/honey/gen_doc_tests.clj
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
(ns honey.gen-doc-tests
|
||||||
|
(:require [babashka.fs :as fs]
|
||||||
|
[lread.test-doc-blocks :as tdb]))
|
||||||
|
|
||||||
|
(defn -main [& _args]
|
||||||
|
(let [target "target/test-doc-blocks"
|
||||||
|
success-marker (fs/file target "SUCCESS")
|
||||||
|
docs ["README.md"
|
||||||
|
"doc/clause-reference.md"
|
||||||
|
"doc/databases.md"
|
||||||
|
"doc/differences-from-1-x.md"
|
||||||
|
"doc/extending-honeysql.md"
|
||||||
|
"doc/general-reference.md"
|
||||||
|
"doc/getting-started.md"
|
||||||
|
"doc/nrql.md"
|
||||||
|
;;"doc/operator-reference.md"
|
||||||
|
"doc/options.md"
|
||||||
|
"doc/postgresql.md"
|
||||||
|
"doc/special-syntax.md"
|
||||||
|
"doc/xtdb.md"]
|
||||||
|
regen-reason (if (not (fs/exists? success-marker))
|
||||||
|
"a previous successful gen result not found"
|
||||||
|
(let [newer-thans (fs/modified-since target
|
||||||
|
(concat docs
|
||||||
|
["build.clj" "deps.edn"]
|
||||||
|
(fs/glob "build" "**/*.*")
|
||||||
|
(fs/glob "src" "**/*.*")))]
|
||||||
|
(when (seq newer-thans)
|
||||||
|
(str "found files newer than last gen: " (mapv str newer-thans)))))]
|
||||||
|
(if regen-reason
|
||||||
|
(do
|
||||||
|
(fs/delete-if-exists success-marker)
|
||||||
|
(println "gen-doc-tests: Regenerating:" regen-reason)
|
||||||
|
(tdb/gen-tests {:docs docs})
|
||||||
|
(spit success-marker "SUCCESS"))
|
||||||
|
(println "gen-doc-tests: Tests already successfully generated"))))
|
||||||
67
deps.edn
67
deps.edn
|
|
@ -1,30 +1,45 @@
|
||||||
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
|
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
|
||||||
:paths ["src"]
|
:paths ["src"]
|
||||||
:deps {org.clojure/clojure {:mvn/version "1.10.2"}}
|
:deps {org.clojure/clojure {:mvn/version "1.10.3"}}
|
||||||
:aliases
|
:aliases
|
||||||
{:1.9 {:override-deps {org.clojure/clojure {:mvn/version "1.9.0"}}}
|
{;; for help: clojure -A:deps -T:build help/doc
|
||||||
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.2"}}}
|
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.8"}
|
||||||
:master {:override-deps {org.clojure/clojure {:mvn/version "1.11.0-master-SNAPSHOT"}}}
|
slipset/deps-deploy {:mvn/version "0.2.2"}}
|
||||||
:test {:extra-paths ["test"]}
|
:ns-default build}
|
||||||
|
|
||||||
|
;; versions to test against:
|
||||||
|
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
|
||||||
|
:1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
|
||||||
|
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
|
||||||
|
|
||||||
|
:elide ; to test #409 (assertion on helper docstrings)
|
||||||
|
{:jvm-opts ["-Dclojure.compiler.elide-meta=[:doc]"]}
|
||||||
|
|
||||||
|
;; running tests/checks of various kinds:
|
||||||
|
:test
|
||||||
|
{:extra-paths ["test"]
|
||||||
|
:extra-deps {io.github.cognitect-labs/test-runner
|
||||||
|
{:git/tag "v0.5.1" :git/sha "dfb30dd"}
|
||||||
|
org.clojure/core.cache {:mvn/version "RELEASE"}}
|
||||||
|
:exec-fn cognitect.test-runner.api/test}
|
||||||
:runner
|
:runner
|
||||||
{:extra-deps {com.cognitect/test-runner
|
{:main-opts ["-m" "cognitect.test-runner"]}
|
||||||
{:git/url "https://github.com/cognitect-labs/test-runner"
|
|
||||||
:sha "b6b3193fcc42659d7e46ecd1884a228993441182"}}
|
;; various "runners" for tests/CI:
|
||||||
:main-opts ["-m" "cognitect.test-runner"
|
:cljs {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.1"}}
|
||||||
;"-d" "target/test-doc-blocks/test"
|
:main-opts ["-m" "cljs-test-runner.main"]}
|
||||||
"-d" "test"]}
|
|
||||||
:cljs-runner {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}}
|
:gen-doc-tests {:replace-paths ["build"]
|
||||||
:main-opts ["-m" "cljs-test-runner.main"]}
|
:extra-deps {babashka/fs {:mvn/version "0.5.24"}
|
||||||
:readme {:extra-deps {seancorfield/readme {:mvn/version "1.0.16"}}
|
com.github.lread/test-doc-blocks {:mvn/version "1.1.20"}}
|
||||||
:main-opts ["-m" "seancorfield.readme"]}
|
:main-opts ["-m" "honey.gen-doc-tests"]}
|
||||||
:eastwood {:extra-deps {jonase/eastwood {:mvn/version "0.3.13"}}
|
|
||||||
:main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}
|
:test-doc {:replace-paths ["src" "target/test-doc-blocks/test"]}
|
||||||
:jar {:replace-deps {seancorfield/depstar {:mvn/version "2.0.171"}}
|
:test-doc-clj {:main-opts ["-m" "cognitect.test-runner"
|
||||||
:exec-fn hf.depstar/jar
|
"-d" "target/test-doc-blocks/test"]}
|
||||||
:exec-args {:jar "honeysql.jar" :sync-pom true}}
|
:test-doc-cljs {:main-opts ["-m" "cljs-test-runner.main"
|
||||||
:install {:replace-deps {slipset/deps-deploy {:mvn/version "0.1.5"}}
|
"-c" "{:warnings,{:single-segment-namespace,false}}"
|
||||||
:exec-fn deps-deploy.deps-deploy/deploy
|
"-d" "target/test-doc-blocks/test"]}
|
||||||
:exec-args {:installer :local :artifact "honeysql.jar"}}
|
|
||||||
:deploy {:replace-deps {slipset/deps-deploy {:mvn/version "0.1.5"}}
|
:eastwood {:extra-deps {jonase/eastwood {:mvn/version "1.4.3"}}
|
||||||
:exec-fn deps-deploy.deps-deploy/deploy
|
:main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}}}
|
||||||
:exec-args {:installer :remote :artifact "honeysql.jar"}}}}
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,9 +2,14 @@
|
||||||
[["Readme" {:file "README.md"}]
|
[["Readme" {:file "README.md"}]
|
||||||
["Changes" {:file "CHANGELOG.md"}]
|
["Changes" {:file "CHANGELOG.md"}]
|
||||||
["Getting Started" {:file "doc/getting-started.md"}
|
["Getting Started" {:file "doc/getting-started.md"}
|
||||||
|
["General Reference" {:file "doc/general-reference.md"}]
|
||||||
["SQL Clause Reference" {:file "doc/clause-reference.md"}]
|
["SQL Clause Reference" {:file "doc/clause-reference.md"}]
|
||||||
["SQL Operator Reference" {:file "doc/operator-reference.md"}]
|
["SQL Operator Reference" {:file "doc/operator-reference.md"}]
|
||||||
["SQL 'Special Syntax'" {:file "doc/special-syntax.md"}]
|
["SQL 'Special Syntax'" {:file "doc/special-syntax.md"}]
|
||||||
["PostgreSQL Support" {:file "doc/postgresql.md"}]
|
["PostgreSQL Support" {:file "doc/postgresql.md"}]
|
||||||
["Extending HoneySQL" {:file "doc/extending-honeysql.md"}]]
|
["XTDB Support" {:file "doc/xtdb.md"}]
|
||||||
|
["New Relic NRQL Support" {:file "doc/nrql.md"}]
|
||||||
|
["Other Databases" {:file "doc/databases.md"}]]
|
||||||
|
["All the Options" {:file "doc/options.md"}]
|
||||||
|
["Extending HoneySQL" {:file "doc/extending-honeysql.md"}]
|
||||||
["Differences from 1.x" {:file "doc/differences-from-1-x.md"}]]}
|
["Differences from 1.x" {:file "doc/differences-from-1-x.md"}]]}
|
||||||
|
|
|
||||||
86
doc/databases.md
Normal file
86
doc/databases.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Other Databases
|
||||||
|
|
||||||
|
There are dedicated sections for [New Relic Query Language Support](nrql.md),
|
||||||
|
[PostgreSQL Support](postgres.md), and
|
||||||
|
[XTDB Support](xtdb.md).
|
||||||
|
This section provides hints and tips for generating SQL for other
|
||||||
|
databases.
|
||||||
|
|
||||||
|
As a reminder, HoneySQL supports the following dialects out of the box:
|
||||||
|
* `:ansi` -- which is the default and provides broad support for PostgreSQL as well
|
||||||
|
* `:mysql` -- which includes MariaDB and Percona
|
||||||
|
* `:nrql` -- as of 2.5.1091
|
||||||
|
* `:oracle`
|
||||||
|
* `:sqlserver` -- Microsoft SQL Server
|
||||||
|
|
||||||
|
For the most part, these dialects only change the "stropping" --
|
||||||
|
how SQL entities are quoted in the generated SQL -- but dialects
|
||||||
|
can change clause order and/or add dialect-specific clauses.
|
||||||
|
|
||||||
|
This section is a work-in-progress and more hints and tips will be
|
||||||
|
added over time for more databases.
|
||||||
|
|
||||||
|
## Precedence
|
||||||
|
|
||||||
|
The biggest difference between database dialects tends to be
|
||||||
|
precedence. MySQL actually has different precedence in the `SET`
|
||||||
|
clause but several databases disagree on the precedence of actual
|
||||||
|
"set" operations: `UNION`, `EXCEPT`, `INTERSECT`, etc.
|
||||||
|
|
||||||
|
HoneySQL tries to be fairly neutral in this area and follows ANSI SQL
|
||||||
|
precedence. This means that some databases may have problems with
|
||||||
|
complex SQL operations that combine multiple clauses with contentious
|
||||||
|
precedence. In general, you can solve this using the `:nest`
|
||||||
|
pseudo-clause in the DSL:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
{:nest DSL}
|
||||||
|
;; will produce DSL wrapped in ( .. )
|
||||||
|
```
|
||||||
|
|
||||||
|
This should allow you to cater to various databases' precedence
|
||||||
|
peculiarities.
|
||||||
|
|
||||||
|
## BigQuery (Google)
|
||||||
|
|
||||||
|
Function names can be case-sensitive: you can use the "as-is" notation
|
||||||
|
for SQL entities to avoid conversion to upper-case: `[:'domain :ref]`
|
||||||
|
produces `domain(ref)` rather than `DOMAIN(ref)`.
|
||||||
|
|
||||||
|
## ClickHouse
|
||||||
|
|
||||||
|
This is another case-sensitive database than requires the "as-is"
|
||||||
|
notation described for **BigQuery** above.
|
||||||
|
|
||||||
|
`WITH expr AS ident` is supported as a core part of the DSL,
|
||||||
|
as of 2.4.962.
|
||||||
|
|
||||||
|
## MySQL
|
||||||
|
|
||||||
|
When you select the `:mysql` dialect, the precedence of `:set` is
|
||||||
|
changed. All the other databases get this correct.
|
||||||
|
|
||||||
|
`REPLACE INTO`, while specific to MySQL and SQLite, is supported as
|
||||||
|
a core part of the DSL, as `:replace-into`, as of 2.4.969.
|
||||||
|
|
||||||
|
## SQLite
|
||||||
|
|
||||||
|
Precedence of "set" operations: SQLite differs from other databases
|
||||||
|
in handling compound SQL operations that use multiple `UNION`,
|
||||||
|
`EXCEPT`, `INTERSECT` clauses. Use `:nest` to disambiguate your
|
||||||
|
intentions.
|
||||||
|
See issue [#462](https://github.com/seancorfield/honeysql/issues/462)
|
||||||
|
for some background on this.
|
||||||
|
|
||||||
|
`INSERT OR IGNORE INTO`: this syntax is specific to SQLite for
|
||||||
|
performing upserts. However, SQLite supports the PostgreSQL-style
|
||||||
|
upsert with `ON CONFLICT` so you can use that syntax instead, for
|
||||||
|
`DO NOTHING` and `DO UPDATE SET`. In addition,
|
||||||
|
`INSERT OR REPLACE INTO` can be written using just `REPLACE INTO`
|
||||||
|
(see below).
|
||||||
|
Issue [#448](https://github.com/seancorfield/honeysql/issues/448)
|
||||||
|
has more background on this.
|
||||||
|
|
||||||
|
`REPLACE INTO`, while specific to MySQL and SQLite, is supported as
|
||||||
|
a core part of the DSL, as `:replace-into`, as of 2.4.969.
|
||||||
|
|
@ -4,27 +4,45 @@ The goal of HoneySQL 1.x and earlier was to provide a DSL for vendor-neutral SQL
|
||||||
|
|
||||||
The goal of HoneySQL 2.x is to provide an easily-extensible DSL for SQL, supporting vendor-specific differences and extensions, that is as consistent as possible. A secondary goal is to make maintenance much easier by streamlining the machinery and reducing the number of different ways to write and/or extend the DSL.
|
The goal of HoneySQL 2.x is to provide an easily-extensible DSL for SQL, supporting vendor-specific differences and extensions, that is as consistent as possible. A secondary goal is to make maintenance much easier by streamlining the machinery and reducing the number of different ways to write and/or extend the DSL.
|
||||||
|
|
||||||
The DSL itself -- the data structures that both versions convert to SQL and parameters via the `format` function -- is almost exactly the same between the two versions so that migration is relatively painless. The primary API -- the `format` function -- is preserved in 2.x, although the variadic options from 1.x have changed to an options hash map in 2.x as this is generally considered more idiomatic. See the **Option Changes** section below for the differences in the options supported.
|
The DSL itself -- the data structures that both versions convert to SQL and parameters via the `format` function -- is almost exactly the same between the two versions so that migration is relatively painless. The primary API -- the `format` function -- is preserved in 2.x, although the options have changed between 1.x and 2.x. See the **Option Changes** section below for the differences in the options supported.
|
||||||
|
`format` can accept its options as a single hash map or as named arguments (1.x only supported the latter).
|
||||||
|
If you are using Clojure 1.11, you can invoke `format` with a mixture of named arguments and a trailing hash
|
||||||
|
map of additional options, if you wish.
|
||||||
|
|
||||||
|
HoneySQL 1.x supported Clojure 1.7 and later. HoneySQL 2.7.y requires Clojure 1.10.3 or later. Earlier versions of HoneySQL 2.x support Clojure 1.9.0.
|
||||||
|
|
||||||
## Group, Artifact, and Namespaces
|
## Group, Artifact, and Namespaces
|
||||||
|
|
||||||
HoneySQL 2.x uses the group ID `seancorfield` with the original artifact ID of `honeysql`, in line with the recommendations in Inside Clojure's post about the changes in the Clojure CLI: [Deprecated unqualified lib names](https://insideclojure.org/2020/07/28/clj-exec/).
|
HoneySQL 2.x uses the group ID `com.github.seancorfield` with the original artifact ID of `honeysql`, in line with the recommendations in Inside Clojure's post about the changes in the Clojure CLI: [Deprecated unqualified lib names](https://insideclojure.org/2020/07/28/clj-exec/); also Clojars [Verified Group Names policy](https://github.com/clojars/clojars-web/wiki/Verified-Group-Names).
|
||||||
|
|
||||||
In addition, HoneySQL 2.x contains different namespaces so you can have both versions on your classpath without introducing any conflicts. The primary API is now in `honey.sql` and the helpers are in `honey.sql.helpers`. A Spec for the DSL data structure will be available in `honey.specs` at some point (work in progress).
|
In addition, HoneySQL 2.x contains different namespaces so you can have both versions on your classpath without introducing any conflicts. The primary API is now in `honey.sql` and the helpers are in `honey.sql.helpers`.
|
||||||
|
|
||||||
### HoneySQL 1.x
|
### HoneySQL 1.x
|
||||||
|
|
||||||
|
In `deps.edn`:
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
;; in deps.edn:
|
honeysql {:mvn/version "1.0.461"}
|
||||||
honeysql {:mvn/version "1.0.444"}
|
|
||||||
;; or, more correctly:
|
;; or, more correctly:
|
||||||
honeysql/honeysql {:mvn/version "1.0.444"}
|
honeysql/honeysql {:mvn/version "1.0.461"}
|
||||||
|
```
|
||||||
|
|
||||||
;; in use:
|
Required as:
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
(ns my.project
|
(ns my.project
|
||||||
(:require [honeysql.core :as sql]))
|
(:require [honeysql.core :as sql]))
|
||||||
|
```
|
||||||
|
|
||||||
...
|
Or if in the REPL:
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
(require '[honeysql.core :as sq])
|
||||||
|
```
|
||||||
|
|
||||||
|
In use:
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
(sql/format {:select [:*] :from [:table] :where [:= :id 1]})
|
(sql/format {:select [:*] :from [:table] :where [:= :id 1]})
|
||||||
;;=> ["SELECT * FROM table WHERE id = ?" 1]
|
;;=> ["SELECT * FROM table WHERE id = ?" 1]
|
||||||
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} :quoting :mysql)
|
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} :quoting :mysql)
|
||||||
|
|
@ -42,15 +60,26 @@ Supported Clojure versions: 1.7 and later.
|
||||||
|
|
||||||
### HoneySQL 2.x
|
### HoneySQL 2.x
|
||||||
|
|
||||||
|
In `deps.edn`:
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
;; in deps.edn:
|
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"}
|
||||||
seancorfield/honeysql {:mvn/version "2.x"}
|
```
|
||||||
|
|
||||||
;; in use:
|
Required as:
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
(ns my.project
|
(ns my.project
|
||||||
(:require [honey.sql :as sql]))
|
(:require [honey.sql :as sql]))
|
||||||
|
```
|
||||||
|
|
||||||
...
|
Or if in the REPL:
|
||||||
|
```clojure
|
||||||
|
(require '[honey.sql :as sql])
|
||||||
|
```
|
||||||
|
|
||||||
|
In use:
|
||||||
|
```clojure
|
||||||
(sql/format {:select [:*] :from [:table] :where [:= :id 1]})
|
(sql/format {:select [:*] :from [:table] :where [:= :id 1]})
|
||||||
;;=> ["SELECT * FROM table WHERE id = ?" 1]
|
;;=> ["SELECT * FROM table WHERE id = ?" 1]
|
||||||
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql})
|
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql})
|
||||||
|
|
@ -59,35 +88,34 @@ seancorfield/honeysql {:mvn/version "2.x"}
|
||||||
|
|
||||||
The new namespaces are:
|
The new namespaces are:
|
||||||
* `honey.sql` -- the primary API (just `format` now),
|
* `honey.sql` -- the primary API (just `format` now),
|
||||||
* `honey.sql.helpers` -- helper functions to build the DSL,
|
* `honey.sql.helpers` -- helper functions to build the DSL.
|
||||||
* `honey.specs` -- a description of the DSL using `clojure.spec.alpha`.
|
|
||||||
|
|
||||||
Supported Clojure versions: 1.9 and later.
|
Supported Clojure versions: 1.10.3 and later.
|
||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
The primary API is just `honey.sql/format`. The `array`, `call`, `inline`, `param`, and `raw` functions have all become standard syntax in the DSL as functions (and their tagged literal equivalents have also gone away because they are no longer needed).
|
The primary API is just `honey.sql/format`. The `array`, `call`, `inline`, `param`, and `raw` functions have all become standard syntax in the DSL as functions (and their tagged literal equivalents have also gone away because they are no longer needed). _[As of 2.0.0-rc3, `call` has been reinstated as an undocumented function in `honey.sql` purely to aid migration from 1.x]_
|
||||||
|
|
||||||
Other `honeysql.core` functions that no longer exist include: `build`, `qualify`, and `quote-identifier`. Many other public functions were essentially undocumented (neither mentioned in the README nor in the tests) and also no longer exist.
|
Other `honeysql.core` functions that no longer exist include: `build`, `qualify`, and `quote-identifier`. Many other public functions were essentially undocumented (neither mentioned in the README nor in the tests) and also no longer exist.
|
||||||
|
|
||||||
|
> As of 2.4.1002, the functionality of `qualify` can be achieved through the `:.` dot-selection special syntax.
|
||||||
|
|
||||||
You can now select a non-ANSI dialect of SQL using the new `honey.sql/set-dialect!` function (which sets a default dialect for all `format` operations) or by passing the new `:dialect` option to the `format` function. `:ansi` is the default dialect (which will mostly incorporate PostgreSQL usage over time). Other dialects supported are `:mysql` (which has a different quoting strategy and uses a different ranking for the `:set` clause), `:oracle` (which is essentially the `:ansi` dialect but will control other things over time), and `:sqlserver` (which is essentially the `:ansi` dialect but with a different quoting strategy). Other dialects and changes may be added over time.
|
You can now select a non-ANSI dialect of SQL using the new `honey.sql/set-dialect!` function (which sets a default dialect for all `format` operations) or by passing the new `:dialect` option to the `format` function. `:ansi` is the default dialect (which will mostly incorporate PostgreSQL usage over time). Other dialects supported are `:mysql` (which has a different quoting strategy and uses a different ranking for the `:set` clause), `:oracle` (which is essentially the `:ansi` dialect but will control other things over time), and `:sqlserver` (which is essentially the `:ansi` dialect but with a different quoting strategy). Other dialects and changes may be added over time.
|
||||||
|
|
||||||
> Note: `:limit` and `:offset` are currently in the default `:ansi` dialect even though they are MySQL-specific. This will change as the dialects are fleshed out. I plan to add `:top` for `:sqlserver` and `:offset` / `:fetch` for `:ansi`, at which point `:limit` / `:offset` will become MySQL-only.
|
> Note: in general, all clauses are available in all dialects in HoneySQL unless the syntax of the clauses conflict between dialects (currently, no such clauses exist). The `:mysql` dialect is the only one so far that changes the priority ordering of a few clauses.
|
||||||
|
|
||||||
## Option Changes
|
## Option Changes
|
||||||
|
|
||||||
As noted above, the variadic options for `format` have been replaced by a single hash map as the optional second argument to `format`.
|
The `:quoting <dialect>` option has been superseded by the new dialect machinery and a new `:quoted` option that turns quoting on or off. You either use `:dialect <dialect>` instead (which turns on quoting by default) or set a default dialect (via `set-dialect!`) and then use `:quoted true` in `format` calls where you want quoting.
|
||||||
|
|
||||||
The `:quoting <dialect>` option has superseded by the new dialect machinery and a new `:quoted` option that turns quoting on or off. You either use `:dialect <dialect>` instead or set a default dialect (via `set-dialect!`) and then use `{:quoted true}` in `format` calls where you want quoting.
|
SQL entity names are automatically quoted if you specify a `:dialect` option to `format`, unless you also specify `:quoted false`.
|
||||||
|
|
||||||
Identifiers are automatically quoted if you specify a `:dialect` option to `format`, unless you also specify `:quoted false`.
|
|
||||||
|
|
||||||
The following options are no longer supported:
|
The following options are no longer supported:
|
||||||
* `:allow-dashed-names?` -- if you provide dashed-names in v2, they will be left as-is if quoting is enabled, else they will be converted to snake_case (so you will either get `"dashed-names"` with quoting or `dashed_names` without).
|
* `:allow-dashed-names?` -- if you provide dashed-names in 2.x, they will be left as-is if quoting is enabled, else they will be converted to snake_case (so you will either get `"dashed-names"` with quoting or `dashed_names` without). If you want dashed-names to be converted to snake_case when `:quoted true`, you also need to specify `:quoted-snake true`.
|
||||||
* `:allow-namespaced-names?` -- this supported `foo/bar` column names in SQL which I'd like to discourage.
|
* `:allow-namespaced-names?` -- this supported `foo/bar` column names in SQL which I'd like to discourage.
|
||||||
* `:namespace-as-table?` -- this is the default in v2: `:foo/bar` will be treated as `foo.bar` which is more in keeping with `next.jdbc`.
|
* `:namespace-as-table?` -- this is the default in 2.x: `:foo/bar` will be treated as `foo.bar` which is more in keeping with `next.jdbc`.
|
||||||
* `:parameterizer` -- this would add a lot of complexity to the formatting engine and I do not know how widely it was used (especially in its arbitrarily extensible form).
|
* `:parameterizer` -- this would add a lot of complexity to the formatting engine and I do not know how widely it was used (especially in its arbitrarily extensible form). _[As of 2.4.962, the ability to generated SQL with numbered parameters, i.e., `$1` instead of positional parameters, `?`, has been added via the `:numbered true` option]_
|
||||||
* `:return-param-names` -- this was added to v1 back in 2013 without an associated issue or PR so I've no idea what use case this was intended to support.
|
* `:return-param-names` -- this was added to 1.x back in 2013 without an associated issue or PR so I've no idea what use case this was intended to support.
|
||||||
|
|
||||||
> Note: I expect some push back on those first three options and the associated behavior changes.
|
> Note: I expect some push back on those first three options and the associated behavior changes.
|
||||||
|
|
||||||
|
|
@ -103,20 +131,25 @@ The following new syntax has been added:
|
||||||
* `:cast` -- `[:cast expr :type]` => `CAST( expr AS type )`,
|
* `:cast` -- `[:cast expr :type]` => `CAST( expr AS type )`,
|
||||||
* `:composite` -- explicit syntax to produce a comma-separated list of expressions, wrapped in parentheses,
|
* `:composite` -- explicit syntax to produce a comma-separated list of expressions, wrapped in parentheses,
|
||||||
* `:default` -- for `DEFAULT` values (in inserts) and for declaring column defaults in table definitions,
|
* `:default` -- for `DEFAULT` values (in inserts) and for declaring column defaults in table definitions,
|
||||||
|
* `:escape` -- used to wrap a regular expression so that non-standard escape characters can be provided,
|
||||||
* `:inline` -- used as a function to replace the `sql/inline` / `#sql/inline` machinery,
|
* `:inline` -- used as a function to replace the `sql/inline` / `#sql/inline` machinery,
|
||||||
* `:interval` -- used as a function to support `INTERVAL <n> <units>`, e.g., `[:interval 30 :days]`.
|
* `:interval` -- used as a function to support `INTERVAL <n> <units>`, e.g., `[:interval 30 :days]` for databases that support it (e.g., MySQL) and, as of 2.4.1026, for `INTERVAL 'n units'`, e.g., `[:interval "24 hours"]` for ANSI/PostgreSQL.
|
||||||
|
* `:lateral` -- used to wrap a statement or expression, to provide a `LATERAL` join,
|
||||||
* `:lift` -- used as a function to prevent interpretation of a Clojure data structure as DSL syntax (e.g., when passing a vector or hash map as a parameter value) -- this should mostly be a replacement for `honeysql.format/value`,
|
* `:lift` -- used as a function to prevent interpretation of a Clojure data structure as DSL syntax (e.g., when passing a vector or hash map as a parameter value) -- this should mostly be a replacement for `honeysql.format/value`,
|
||||||
* `:nest` -- used as a function to add an extra level of nesting (parentheses) around an expression,
|
* `:nest` -- used as a function to add an extra level of nesting (parentheses) around an expression,
|
||||||
* `:not` -- this is now explicit syntax,
|
* `:not` -- this is now explicit syntax,
|
||||||
|
* `:over` -- the function-like part of a T-SQL window clause,
|
||||||
* `:param` -- used as a function to replace the `sql/param` / `#sql/param` machinery,
|
* `:param` -- used as a function to replace the `sql/param` / `#sql/param` machinery,
|
||||||
* `:raw` -- used as a function to replace the `sql/raw` / `#sql/raw` machinery. Vector subexpressions inside a `[:raw ..]` expression are formatted to SQL and parameters. Other subexpressions are just turned into strings and concatenated. This is different to the v1 behavior but should be more flexible, since you can now embed `:inline`, `:param`, and `:lift` inside a `:raw` expression.
|
* `:raw` -- used as a function to replace the `sql/raw` / `#sql/raw` machinery. Vector subexpressions inside a `[:raw ..]` expression are formatted to SQL and parameters. Other subexpressions are just turned into strings and concatenated. This is different to the 1.x behavior but should be more flexible, since you can now embed `:inline`, `:param`, and `:lift` inside a `:raw` expression.
|
||||||
|
|
||||||
> Note 1: in 1.x, inlining a string `"foo"` produced `foo` but in 2.x it produces `'foo'`, i.e., string literals become SQL strings without needing internal quotes (1.x required `"'foo'"`).
|
> Note 1: in 1.x, inlining a string `"foo"` produced `foo` but in 2.x it produces `'foo'`, i.e., string literals become SQL strings without needing internal quotes (1.x required `"'foo'"`).
|
||||||
|
|
||||||
Several additional pieces of syntax have also been added to support column
|
Several additional pieces of syntax have also been added to support column
|
||||||
definitions in `CREATE TABLE` clauses, now that v2 supports DDL statement
|
definitions in `CREATE TABLE` clauses, now that 2.x supports DDL statement
|
||||||
construction: `:constraint`, `:foreign-key`, `:index`, `:primary-key`,
|
construction:
|
||||||
`:references`, `:unique`, and -- as noted above -- `:default`.
|
|
||||||
|
* `:constraint`, `:default`, `:foreign-key`, `:index`, `:primary-key`, `:references`, `:unique`,
|
||||||
|
* `:entity` -- used to force an expression to be rendered as a SQL entity (instead of a SQL keyword).
|
||||||
|
|
||||||
### select and function calls
|
### select and function calls
|
||||||
|
|
||||||
|
|
@ -128,14 +161,15 @@ user=> (sql/format {:select [:a [:b :c] [[:d :e]] [[:f :g] :h]]})
|
||||||
["SELECT a, b AS c, D(e), F(g) AS h"]
|
["SELECT a, b AS c, D(e), F(g) AS h"]
|
||||||
```
|
```
|
||||||
|
|
||||||
On a related note, `sql/call` has been removed because it should never be needed now: `[:foo ...]` should always be treated as a function call, consistently, avoiding the special cases in v1 that necessitated the explicit `sql/call` syntax.
|
On a related note, `sql/call` has been removed because it should never be needed now: `[:foo ...]` should always be treated as a function call, consistently, avoiding the special cases in 1.x that necessitated the explicit `sql/call` syntax.
|
||||||
|
|
||||||
### select modifiers
|
### select modifiers
|
||||||
|
|
||||||
HoneySQL 1.x provided a `:modifiers` clause (and a `modifiers`) helper as a way to "modify"
|
HoneySQL 1.x provided a `:modifiers` clause (and a `modifiers`) helper as a way to "modify"
|
||||||
a `SELECT` to be `DISTINCT`. nilenso/honeysql-helpers extended that to support `:distinct-on`
|
a `SELECT` to be `DISTINCT`. The [nilenso/honeysql-helpers](https://github.com/nilenso/honeysql-postgres) library extended that to support `:distinct-on`
|
||||||
a group of columns. In HoneySQL 2.x, you use `:select-distinct` and `:select-distinct-on`
|
a group of columns. In HoneySQL 2.x, you use `:select-distinct` and `:select-distinct-on`
|
||||||
(and their associated helpers) for that instead.
|
(and their associated helpers) for that instead. MS SQL Server's `TOP` modifier is also
|
||||||
|
supported via `:select-top` and `:select-distinct-top`.
|
||||||
|
|
||||||
### set vs sset, set0, set1
|
### set vs sset, set0, set1
|
||||||
|
|
||||||
|
|
@ -150,28 +184,49 @@ it should have been a function, and in 2.x it is:
|
||||||
```clojure
|
```clojure
|
||||||
;; 1.x: EXISTS should never have been implemented as SQL syntax: it's an operator!
|
;; 1.x: EXISTS should never have been implemented as SQL syntax: it's an operator!
|
||||||
;; (sq/format {:exists {:select [:a] :from [:foo]}})
|
;; (sq/format {:exists {:select [:a] :from [:foo]}})
|
||||||
;;=> ["EXISTS (SELECT a FROM foo)"]
|
;; -> ["EXISTS (SELECT a FROM foo)"]
|
||||||
|
|
||||||
;; 2.x: select function call with an alias:
|
;; 2.x: select function call with an alias:
|
||||||
user=> (sql/format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]})
|
user=> (sql/format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]})
|
||||||
["SELECT EXISTS (SELECT a FROM foo) AS x"]
|
["SELECT EXISTS (SELECT a FROM foo) AS x"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `ORDER BY` with `NULLS FIRST` or `NULLS LAST`
|
||||||
|
|
||||||
|
In HoneySQL 1.x, if you wanted to generate SQL like
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ORDER BY ... DESC NULLS LAST
|
||||||
|
```
|
||||||
|
|
||||||
|
you needed to pass `:nulls-last` as a separate keyword, after `:asc` or `:desc`:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
{:order-by [[:my-column :desc :nulls-last]]}
|
||||||
|
```
|
||||||
|
|
||||||
|
In HoneySQL 2.x, the direction and the null ordering rule are now combined into a single keyword:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
{:order-by [[:my-column :desc-nulls-last]]}
|
||||||
|
```
|
||||||
|
|
||||||
## Extensibility
|
## Extensibility
|
||||||
|
|
||||||
The protocols and multimethods in 1.x have all gone away. The primary extension point is `honey.sql/register-clause!` which lets you specify the new clause (keyword), the formatter function for it, and the existing clause that it should be ranked before (`format` processes the DSL in clause order).
|
The protocols and multimethods in 1.x have all gone away. The primary extension point is `honey.sql/register-clause!` which lets you specify the new clause (keyword), the formatter function for it, and the existing clause that it should be ranked before (`format` processes the DSL in clause order).
|
||||||
|
|
||||||
You can also register new "functions" that can implement special syntax (such as `:array`, `:inline`, `:raw` etc above) via `honey.sql/register-fn!`. This accepts a "function" name as a keyword and a formatter which will generally be a function of two arguments: the function name (so formatters can be reused across different names) and a vector of the arguments the function should accept.
|
You can also register new "functions" that can implement special syntax (such as `:array`, `:inline`, `:raw` etc above) via `honey.sql/register-fn!`. This accepts a "function" name as a keyword and a formatter which will generally be a function of two arguments: the function name (so formatters can be reused across different names) and a vector of the arguments the function should accept.
|
||||||
|
|
||||||
And, finally, you can register new operators that will be recognized in expressions via `honey.sql/register-op!`. This accepts an operator name as a keyword and optional named parameters to indicate whether the operator is `:variadic` (the default is strictly binary) and whether it should ignore operands that evaluate to `nil` (via `:ignore-nil`). The latter can make it easier to construct complex expressions programmatically without having to worry about conditionally removing "optional" (`nil`) values.
|
And, finally, you can register new operators that will be recognized in expressions via `honey.sql/register-op!`. This accepts an operator name as a keyword and an optional named parameter to indicate whether it should ignore operands that evaluate to `nil` (via `:ignore-nil`). That can make it easier to construct complex expressions programmatically without having to worry about conditionally removing "optional" (`nil`) values.
|
||||||
|
|
||||||
> Note: because of the changes in the extension machinery between v1 and v2, it is not possible to use the https://github.com/nilenso/honeysql-postgres library with HoneySQL v2 but the goal is to incorporate all of the syntax from that library into the core of HoneySQL.
|
> Note: because of the changes in the extension machinery between 1.x and 2.x, it is not possible to use the [nilenso/honeysql-postgress](https://github.com/nilenso/honeysql-postgres) library with HoneySQL 2.x but the goal is to incorporate all of the syntax from that library into the core of HoneySQL.
|
||||||
|
|
||||||
## Helpers
|
## Helpers
|
||||||
|
|
||||||
The `honey.sql.helpers` namespace includes a helper function that corresponds to every supported piece of the data DSL understood by HoneySQL (v1 only had a limited set of helper functions). Unlike v1 helpers which sometimes had both a regular helper and a `merge-` helper, v2 helpers will all merge clauses by default (if that makes sense for the underlying DSL): use `:dissoc` if you want to force an overwrite.
|
The `honey.sql.helpers` namespace includes a helper function that corresponds to every supported piece of the data DSL understood by HoneySQL (1.x only had a limited set of helper functions). Unlike 1.x helpers which sometimes had both a regular helper and a `merge-` helper, 2.x helpers will all merge clauses by default (if that makes sense for the underlying DSL): use `:dissoc` if you want to force an overwrite.
|
||||||
|
|
||||||
The only helpers that have non-merging behavior are:
|
The only helpers that have non-merging behavior are:
|
||||||
* `intersect`, `union`, `union-all`, `except`, and `except-all` which always wrap around their arguments,
|
* The SQL set operations `intersect`, `union`, `union-all`, `except`, and `except-all` which always wrap around their arguments,
|
||||||
* `delete`, `set`, `limit`, `offset`, `for`, and `values` which overwrite, rather than merge,
|
* The SQL clauses `delete`, `fetch`, `for`, `limit`, `lock`, `offset`, `on-constraint`, `set`, `truncate`, `update`, and `values` which overwrite, rather than merge,
|
||||||
* `composite` which is a convenience for the `:composite` syntax mentioned above: `(composite :a :b)` is the same as `[:composite :a :b]` which produces `(a, b)`.
|
* The DDL helpers `drop-column`, `drop-index`, `rename-table`, and `with-data`,
|
||||||
|
* The function helper `composite` which is a convenience for the `:composite` syntax mentioned above: `(composite :a :b)` is the same as `[:composite :a :b]` which produces `(a, b)`.
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,31 @@ register formatters or behavior corresponding to clauses,
|
||||||
operators, and functions.
|
operators, and functions.
|
||||||
|
|
||||||
Built in clauses include: `:select`, `:from`, `:where` and
|
Built in clauses include: `:select`, `:from`, `:where` and
|
||||||
many more. Built in operators include: `:=`, `:+`, `:mod`.
|
many more. Built in operators include: `:=`, `:+`, `:%`.
|
||||||
Built in functions (special syntax) include: `:array`, `:case`,
|
Built in functions (special syntax) include: `:array`, `:case`,
|
||||||
`:cast`, `:inline`, `:raw` and many more.
|
`:cast`, `:inline`, `:raw` and many more.
|
||||||
|
|
||||||
|
See also the section on
|
||||||
|
[database-specific hints and tips](databases.md), which may
|
||||||
|
let you avoid extending HoneySQL.
|
||||||
|
|
||||||
|
## Extending what `:inline` can do
|
||||||
|
|
||||||
|
By default, the `:inline` option can convert a fairly
|
||||||
|
basic set of values/types to SQL strings:
|
||||||
|
* `nil`
|
||||||
|
* strings
|
||||||
|
* keywords and symbols
|
||||||
|
* vectors
|
||||||
|
* UUIDs (Clojure only)
|
||||||
|
|
||||||
|
Everything is naively converted by calling `str`.
|
||||||
|
|
||||||
|
You can extend `honey.sql.protocols/InlineValue` to
|
||||||
|
other types and defining how the `sqlize` function
|
||||||
|
should behave. It takes a single argument, the value
|
||||||
|
to be inlined (converted to a SQL string).
|
||||||
|
|
||||||
## Registering a New Clause Formatter
|
## Registering a New Clause Formatter
|
||||||
|
|
||||||
`honey.sql/register-clause!` accepts a keyword (or a symbol)
|
`honey.sql/register-clause!` accepts a keyword (or a symbol)
|
||||||
|
|
@ -29,30 +50,74 @@ The formatter function will be called with:
|
||||||
* The clause name (always as a keyword),
|
* The clause name (always as a keyword),
|
||||||
* The sequence of arguments provided.
|
* The sequence of arguments provided.
|
||||||
|
|
||||||
|
The formatter function should return a vector whose first element is the
|
||||||
|
generated SQL string and whose remaining elements (if any) are the parameters
|
||||||
|
lifted from the DSL (for which the generated SQL string should contain `?`
|
||||||
|
placeholders).
|
||||||
|
|
||||||
The third argument to `register-clause!` allows you to
|
The third argument to `register-clause!` allows you to
|
||||||
insert your new clause formatter so that clauses are
|
insert your new clause formatter so that clauses are
|
||||||
formatted in the correct order for your SQL dialect.
|
formatted in the correct order for your SQL dialect.
|
||||||
For example, `:select` comes before `:from` which comes
|
For example, `:select` comes before `:from` which comes
|
||||||
before `:where`. This is the most implementation-specific
|
before `:where`. You can call `clause-order` to see what the
|
||||||
part of extending HoneySQL because you'll need to look at
|
current ordering of clauses is.
|
||||||
the (private) Var `default-clause-order` in `honey.sql`
|
|
||||||
for guidance. _[I plan to add a section in the documentation
|
<!-- :test-doc-blocks/skip -->
|
||||||
somewhere that lists built-in clauses in order which this
|
```clojure
|
||||||
can link to...]_
|
;; the formatter will be passed your new clause and the value associated
|
||||||
|
;; with that clause in the DSL (which is often a sequence but does not
|
||||||
|
;; need to be -- it can be whatever syntax you desire in the DSL):
|
||||||
|
(sql/register-clause! :foobar
|
||||||
|
(fn [clause x]
|
||||||
|
(let [[sql & params]
|
||||||
|
(if (ident? x)
|
||||||
|
(sql/format-expr x)
|
||||||
|
(sql/format-dsl x))]
|
||||||
|
(c/into [(str (sql/sql-kw clause) " " sql)] params)))
|
||||||
|
:from) ; SELECT ... FOOBAR ... FROM ...
|
||||||
|
;; example usage:
|
||||||
|
(sql/format {:select [:a :b] :foobar :baz})
|
||||||
|
=> ["SELECT a, b FOOBAR baz"]
|
||||||
|
(sql/format {:select [:a :b] :foobar {:where [:= :id 1]}})
|
||||||
|
=> ["SELECT a, b FOOBAR WHERE id = ?" 1]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: if you call `register-clause!` more than once for the same clause, the last call "wins". This allows you to correct an incorrect clause order insertion by simply calling `register-clause!` again with a different third argument.
|
||||||
|
|
||||||
|
## Defining a Helper Function for a New Clause
|
||||||
|
|
||||||
|
Having registered a new clause, you might also want a helper function
|
||||||
|
for it, just as the built-in clauses have helpers in `honey.sql.helpers`.
|
||||||
|
Two functions exist in that namespace to make it easier for you to
|
||||||
|
define your own helpers:
|
||||||
|
|
||||||
|
* `generic-helper-variadic` -- most clauses accept an arbitrary number of items in a sequence and multiple calls in a DSL expression will merge so this is the helper you will use for most clauses,
|
||||||
|
* `generic-helper-unary` -- a handful of clauses only accept a single item and cannot be merged (they behave as "last one wins"), so this helper supports that semantic.
|
||||||
|
|
||||||
|
Each of these helper support functions should be called with the keyword that
|
||||||
|
identifies your new clause and the sequence of arguments passed to it. See
|
||||||
|
the docstrings for more detail.
|
||||||
|
|
||||||
|
You might have:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
(sql/register-clause! :my-clause my-formatter :where)
|
||||||
|
(defn my-clause [& args] (h/generic-helper-variadic :my-clause args))
|
||||||
|
```
|
||||||
|
|
||||||
## Registering a New Operator
|
## Registering a New Operator
|
||||||
|
|
||||||
`honey.sql/register-op!` accepts a keyword (or a symbol) that
|
`honey.sql/register-op!` accepts a keyword (or a symbol) that
|
||||||
should be treated as a new infix operator.
|
should be treated as a new infix operator.
|
||||||
|
|
||||||
By default, operators are treated as strictly binary --
|
All operators are treated as variadic and an exception will be
|
||||||
accepting just two arguments -- and an exception will be
|
thrown if they are provided no arguments:
|
||||||
thrown if they are provided less than two or more than
|
|
||||||
two arguments. You can optionally specify that an operator
|
|
||||||
can take any number of arguments with `:variadic true`:
|
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/register-op! :<=> :variadic true)
|
(require '[honey.sql :as sql])
|
||||||
|
|
||||||
|
(sql/register-op! :<=>)
|
||||||
;; and then use the new operator:
|
;; and then use the new operator:
|
||||||
(sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]})
|
(sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]})
|
||||||
;; will produce:
|
;; will produce:
|
||||||
|
|
@ -67,13 +132,25 @@ such `nil` expressions. You can specify `:ignore-nil true`
|
||||||
to achieve that:
|
to achieve that:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/register-op! :<=> :variadic true :ignore-nil true)
|
(sql/register-op! :<=> :ignore-nil true)
|
||||||
;; and then use the new operator:
|
;; and then use the new operator:
|
||||||
(sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]})
|
(sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]})
|
||||||
;; will produce:
|
;; will produce:
|
||||||
;;=> ["SELECT * FROM table WHERE x <=> ?" 42]
|
;;=> ["SELECT * FROM table WHERE x <=> ?" 42]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### PostgreSQL Operators
|
||||||
|
|
||||||
|
A number of PostgreSQL operators contain `@` which is not legal in a Clojure keyword or symbol (as literal syntax). The recommendation is to `def` your own name for these
|
||||||
|
operators, using `at` in place of `@`, with an explicit call to `keyword` (or `symbol`), and then use that `def`'d name when registering new operators and when writing
|
||||||
|
your DSL expressions:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(def <at (keyword "<@"))
|
||||||
|
(sql/register-op! <at)
|
||||||
|
;; and use it in expressions: [<at :submitted [:range :begin :end]]
|
||||||
|
```
|
||||||
|
|
||||||
## Registering a New Function (Special Syntax)
|
## Registering a New Function (Special Syntax)
|
||||||
|
|
||||||
`honey.sql/register-fn!` accepts a keyword (or a symbol)
|
`honey.sql/register-fn!` accepts a keyword (or a symbol)
|
||||||
|
|
@ -88,6 +165,7 @@ The formatter function will be called with:
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
(sql/register-fn! :foo (fn [f args] ..))
|
(sql/register-fn! :foo (fn [f args] ..))
|
||||||
|
|
||||||
|
|
@ -121,3 +199,39 @@ of it and would call `sql/format-expr` on each argument:
|
||||||
;; produces:
|
;; produces:
|
||||||
;;=> ["SELECT * FROM table WHERE FOO(a + ?)" 1]
|
;;=> ["SELECT * FROM table WHERE FOO(a + ?)" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Registering a new Dialect
|
||||||
|
|
||||||
|
_New in HoneySQL 2.3.x_
|
||||||
|
|
||||||
|
The built-in dialects that HoneySQL supports are:
|
||||||
|
* `:ansi` -- the default, that quotes SQL entity names with double-quotes, like `"this"`
|
||||||
|
* `:mysql` -- quotes SQL entity names with backticks, and changes the precedence of `SET` in `UPDATE`
|
||||||
|
* `:nrql` -- as of 2.5.1091, see [New Relic NRQL Support](nrsql.md) for more details of the NRQL dialect
|
||||||
|
* `:oracle` -- quotes SQL entity names like `:ansi`, and does not use `AS` in aliases
|
||||||
|
* `:sqlserver` -- quotes SQL entity names with brackets, like `[this]`
|
||||||
|
|
||||||
|
A dialect spec is a hash map containing at least `:quote` but also optionally `:clause-order-fn` and/or `:as`:
|
||||||
|
* `:quote` -- a unary function that takes a string and returns the quoted version of it
|
||||||
|
* `:clause-order-fn` -- a unary function that takes a sequence of clause names (keywords) and returns an updated sequence of clause names; this defines the precedence of clauses in the DSL parser
|
||||||
|
* `:as` -- a boolean that indicates whether `AS` should be present in aliases (the default, if `:as` is omitted) or not (by specifying `:as false`)
|
||||||
|
|
||||||
|
To make writing new dialects easier, the following helper functions in `honey.sql` are available:
|
||||||
|
* `add-clause-before` -- a function that accepts the sequence of clause names, the (new) clause to add, and the clause to add it before (`nil` means add at the end)
|
||||||
|
* `get-dialect` -- a function that accepts an existing dialect name (keyword) and returns its spec (hash map)
|
||||||
|
* `strop` -- a function that accepts an opening quote, a string, and a closing quote and returns the quoted string, doubling-up any closing quote characters inside the string to make it legal SQL
|
||||||
|
* `upper-case` -- a locale-insensitive version of `clojure.string/upper-case`
|
||||||
|
|
||||||
|
For example, to add a variant of the `:ansi` dialect that forces names to be upper-case as well as double-quoting them:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/register-dialect! ::ANSI (update (sql/get-dialect :ansi) :quote comp sql/upper-case))
|
||||||
|
;; or you could do this:
|
||||||
|
(sql/register-dialect! ::ANSI {:quote #(sql/strop \" (sql/upper-case %) \")})
|
||||||
|
|
||||||
|
(sql/format {:select :foo :from :bar} {:dialect :ansi})
|
||||||
|
;;=> ["SELECT \"foo\" FROM \"bar\""]
|
||||||
|
|
||||||
|
(sql/format {:select :foo :from :bar} {:dialect ::ANSI})
|
||||||
|
;;=> ["SELECT \"FOO\" FROM \"BAR\""]
|
||||||
|
```
|
||||||
|
|
|
||||||
169
doc/general-reference.md
Normal file
169
doc/general-reference.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# General Reference Documentation
|
||||||
|
|
||||||
|
This section provides more details about specific behavior in HoneySQL and
|
||||||
|
how to generate certain SQL constructs.
|
||||||
|
|
||||||
|
## SQL Entity Generation
|
||||||
|
|
||||||
|
HoneySQL treats keywords and symbols as SQL entities (in any context other
|
||||||
|
than function call position in a sequence). If quoting is in effect,
|
||||||
|
either because `:dialect` was specified as an option to `format` or
|
||||||
|
because `:quoted true` was specified, the literal name of an unqualified,
|
||||||
|
single-segment keyword or symbol is used as-is and quoted:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(require '[honey.sql :as sql])
|
||||||
|
|
||||||
|
(sql/format {:select :foo-bar} {:quoted true})
|
||||||
|
;;=> ["SELECT \"foo-bar\""]
|
||||||
|
(sql/format {:select :foo-bar} {:dialect :mysql})
|
||||||
|
;;=> ["SELECT `foo-bar`"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If quoting is not in effect, any dashes (`-`) in the name will be converted to underscores (`_`):
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select :foo-bar})
|
||||||
|
;;=> ["SELECT foo_bar"]
|
||||||
|
(sql/format {:select :foo-bar} {:dialect :mysql :quoted false})
|
||||||
|
;;=> ["SELECT foo_bar"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If a keyword or symbol contains a dot (`.`), it will be split apart
|
||||||
|
and treated as a table (or alias) name and a column name:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select :foo-bar.baz-quux} {:quoted true})
|
||||||
|
;;=> ["SELECT \"foo-bar\".\"baz-quux\""]
|
||||||
|
(sql/format {:select :foo-bar.baz-quux} {:dialect :mysql})
|
||||||
|
;;=> ["SELECT `foo-bar`.`baz-quux`"]
|
||||||
|
(sql/format {:select :foo-bar.baz-quux})
|
||||||
|
;;=> ["SELECT foo_bar.baz_quux"]
|
||||||
|
(sql/format {:select :foo-bar.baz-quux} {:dialect :mysql :quoted false})
|
||||||
|
;;=> ["SELECT foo_bar.baz_quux"]
|
||||||
|
```
|
||||||
|
|
||||||
|
A qualified keyword or symbol, will also be split apart, but dashes (`-`)
|
||||||
|
in the namespace portion _will_ be converted to underscores (`_`) even
|
||||||
|
when quoting is in effect:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select :foo-bar/baz-quux} {:quoted true})
|
||||||
|
;;=> ["SELECT \"foo_bar\".\"baz-quux\""] ; _ in table, - in column
|
||||||
|
(sql/format {:select :foo-bar/baz-quux} {:dialect :mysql})
|
||||||
|
;;=> ["SELECT `foo_bar`.`baz-quux`"] ; _ in table, - in column
|
||||||
|
(sql/format {:select :foo-bar/baz-quux})
|
||||||
|
;;=> ["SELECT foo_bar.baz_quux"] ; _ in table and _ in column
|
||||||
|
(sql/format {:select :foo-bar/baz-quux} {:dialect :mysql :quoted false})
|
||||||
|
;;=> ["SELECT foo_bar.baz_quux"] ; _ in table and _ in column
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, there are some contexts where only a SQL entity is accepted, rather than an
|
||||||
|
arbitrary SQL expression, so a string will also be treated as a SQL entity and in such cases
|
||||||
|
the entity name will always be quoted, dashes (`-`) will not be converted to
|
||||||
|
underscores (`_`), and a slash (`/`) is not treated as separating a
|
||||||
|
qualifier from the name, regardless of the `:dialect` or `:quoted` settings:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:update :table :set {"foo-bar" 1 "baz/quux" 2}})
|
||||||
|
;;=> ["UPDATE table SET \"foo-bar\" = ?, \"baz/quux\" = ?" 1 2]
|
||||||
|
(sql/format {:update :table :set {"foo-bar" 1 "baz/quux" 2}} {:quoted true})
|
||||||
|
;;=> ["UPDATE \"table\" SET \"foo-bar\" = ?, \"baz/quux\" = ?" 1 2]
|
||||||
|
(sql/format {:update :table :set {"foo-bar" 1 "baz/quux" 2}} {:dialect :mysql})
|
||||||
|
;;=> ["UPDATE `table` SET `foo-bar` = ?, `baz/quux` = ?" 1 2]
|
||||||
|
(sql/format {:update :table :set {"foo-bar" 1 "baz/quux" 2}} {:dialect :sqlserver :quoted false})
|
||||||
|
;;=> ["UPDATE table SET [foo-bar] = ?, [baz/quux] = ?" 1 2]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tuples and Composite Values
|
||||||
|
|
||||||
|
Some databases support "composite values" which are usually
|
||||||
|
represented as tuples in SQL, eg., `(col1,col2)` or `(13,42,'foo')`.
|
||||||
|
In HoneySQL 1.x, you could sometimes get away with just using a
|
||||||
|
vector of entities and/or values, but it was very much dependent
|
||||||
|
on the context. HoneySQL 2.x always treats vectors (and sequences)
|
||||||
|
as function calls (which may be "special syntax" or an actual
|
||||||
|
function call).
|
||||||
|
|
||||||
|
HoneySQL provides `:composite` as special syntax to construct
|
||||||
|
these tuples:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format-expr [:composite :col1 :col2])
|
||||||
|
;;=> ["(col1, col2)"]
|
||||||
|
(sql/format-expr [:composite 13 42 "foo"])
|
||||||
|
;;=> ["(?, ?, ?)" 13 42 "foo"]
|
||||||
|
;; or using symbols:
|
||||||
|
(sql/format-expr '(composite col1 col2))
|
||||||
|
;;=> ["(col1, col2)"]
|
||||||
|
(sql/format-expr '(composite 13 42 "foo"))
|
||||||
|
;;=> ["(?, ?, ?)" 13 42 "foo"]
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also a `composite` helper function.
|
||||||
|
|
||||||
|
## Working with JSON/JSONB (PostgreSQL)
|
||||||
|
|
||||||
|
It is increasingly common for PostgreSQL users to be working with JSON columns
|
||||||
|
in their databases these days. PostgreSQL has really good support for JSON types.
|
||||||
|
|
||||||
|
When using HoneySQL to generate SQL that manipulates JSON, you need to be careful
|
||||||
|
because it is common to use regular Clojure data structures to represent the JSON
|
||||||
|
and rely on protocol extensions for the JDBC libraries to handle automatic
|
||||||
|
conversion of Clojure data structures to JSON (e.g., see
|
||||||
|
[Tips & Tricks > Working with JSON and JSONB](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/tips-tricks#working-with-json-and-jsonb) in the `next.jdbc`
|
||||||
|
documentation).
|
||||||
|
|
||||||
|
HoneySQL also uses Clojure data structures, to represent function calls (vectors) and
|
||||||
|
SQL statements (hash maps), so if you are also using Clojure data structures for your
|
||||||
|
JSON, you need to tell HoneySQL not to interpret those values. There
|
||||||
|
are two possible approaches:
|
||||||
|
|
||||||
|
1. Use named parameters (e.g., `[:param :myval]`) instead of having the values directly in the DSL structure and then pass `{:params {:myval some-json}}` as part of the options in the call to `format`, or
|
||||||
|
2. Use `[:lift ..]` wrapped around any structured values which tells HoneySQL not to interpret the vector or hash map value as a DSL: `[:lift some-json]`.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
As of 2.2.858, `format` can cache the SQL and parameters produced from the data structure so that it does not need to be computed on every call. This functionality is available only in Clojure and depends on [`org.clojure/core.cache`](https://github.com/clojure/core.cache) being on your classpath. If you are repeatedly building the same complex SQL statements over and over again, this can be a good way to provide a performance boost but there are some caveats.
|
||||||
|
|
||||||
|
* You need `core.cache` as a dependency: `org.clojure/core.cache {:mvn/version "1.0.225"}` was the latest as of January 20th, 2022,
|
||||||
|
* You need to create one or more caches yourself, from the various factory functions in the [`clojure.core.cache.wrapped` namespace](http://clojure.github.io/core.cache/#clojure.core.cache.wrapped),
|
||||||
|
* You should use named parameters in your SQL DSL data structure, e.g., `:?foo` or `'?foo`, and pass the actual parameter values via the `:params` option to `format`.
|
||||||
|
|
||||||
|
You can then pass the (atom containing the) cache to `format` using the `:cache` option. The call to `format` then looks in that cache for a match for the data structure passed in, i.e., the entire data structure is used as a key into the cache, including any literal parameter values. If the cache contains a match, the corresponding vector of a SQL string and parameters is used, otherwise the data structure is parsed as usual and the SQL string (and parameters) generated from it (and stored in the cache for the next call). Finally, named parameters in the vector are replaced by their values from the `:params` option.
|
||||||
|
|
||||||
|
The code that _builds_ the DSL data structure will be run in all cases, so any conditional logic and helper function calls will still happen, since that is how the data structure is created and then passed to `format`. If you want to also avoid that overhead, you'd need to take steps to build the data structure separately and store it somewhere for reuse in the call to `format`.
|
||||||
|
|
||||||
|
Since the data structure is used as the key into the cache, literal parameter values will lead to different keys:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
;; these are two different cache entries:
|
||||||
|
(sql/format {:select :* :from :table :where [:= :id 1]} {:cache my-cache})
|
||||||
|
(sql/format {:select :* :from :table :where [:= :id 2]} {:cache my-cache})
|
||||||
|
;; these are the same cache entry:
|
||||||
|
(sql/format {:select :* :from :table :where [:= :id :?id]} {:cache my-cache :params {:id 1}})
|
||||||
|
(sql/format {:select :* :from :table :where [:= :id :?id]} {:cache my-cache :params {:id 2}})
|
||||||
|
```
|
||||||
|
|
||||||
|
Since HoneySQL accepts any of the `clojure.core.cache.wrapped` caches and runs every data structure through the provided `:cache`, it's up to you to ensure that your cache is appropriate for that usage: a "basic" cache will keep every entry until the cache is explicitly emptied; a TTL cache will keep each entry for a specific period of time; and so on.
|
||||||
|
|
||||||
|
> Note: because `IN ()` expressions are inlined, you cannot cache SQL that includes them. If you try to `format` a statement that includes an `IN ()` expression when you provide the `:cache` option, you will get an exception. See [#396](https://github.com/seancorfield/honeysql/issues/396) for details of why this doesn't work.
|
||||||
|
|
||||||
|
## Other Sections Will Be Added!
|
||||||
|
|
||||||
|
As questions arise about the use of HoneySQL 2.x, I will add new sections here.
|
||||||
|
|
||||||
|
## Other Reference Documentation
|
||||||
|
|
||||||
|
The full list of supported SQL clauses is documented in the
|
||||||
|
[Clause Reference](clause-reference.md). The full list
|
||||||
|
of operators supported (as prefix-form "functions") is
|
||||||
|
documented in the [Operator Reference](operator-reference.md)
|
||||||
|
section. The full list
|
||||||
|
of "special syntax" functions is documented in the
|
||||||
|
[Special Syntax](special-syntax.md) section. The best
|
||||||
|
documentation for the helper functions is in the
|
||||||
|
[honey.sql.helpers](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/api/honey.sql.helpers) namespace.
|
||||||
|
If you're migrating to HoneySQL 2.x, this [overview of differences
|
||||||
|
between 1.x and 2.x](differences-from-1-x.md) should help.
|
||||||
|
|
@ -8,20 +8,26 @@ data to a SQL statement (string) and any parameters it needs.
|
||||||
|
|
||||||
For the Clojure CLI, add the following dependency to your `deps.edn` file:
|
For the Clojure CLI, add the following dependency to your `deps.edn` file:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
seancorfield/honeysql {:mvn/version "2.0.0-alpha2"}
|
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"}
|
||||||
```
|
```
|
||||||
|
|
||||||
For Leiningen, add the following dependency to your `project.clj` file:
|
For Leiningen, add the following dependency to your `project.clj` file:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[seancorfield/honeysql "2.0.0-alpha2"]
|
[com.github.seancorfield/honeysql "2.7.1295"]
|
||||||
```
|
```
|
||||||
|
|
||||||
HoneySQL produces SQL statements but does not execute them.
|
HoneySQL produces SQL statements but does not execute them.
|
||||||
To execute SQL statements, you will also need a JDBC wrapper like
|
To execute SQL statements, you will also need a JDBC wrapper like
|
||||||
[`seancorfield/next.jdbc`](https://github.com/seancorfield/next-jdbc) and a JDBC driver for the database you use.
|
[`seancorfield/next.jdbc`](https://github.com/seancorfield/next-jdbc) and a JDBC driver for the database you use.
|
||||||
|
|
||||||
|
You can also experiment with HoneySQL directly in a browser -- no installation
|
||||||
|
required -- using [John Shaffer](https://github.com/john-shaffer)'s awesome
|
||||||
|
[HoneySQL web app](https://john.shaffe.rs/honeysql/), written in ClojureScript!
|
||||||
|
|
||||||
## Basic Concepts
|
## Basic Concepts
|
||||||
|
|
||||||
SQL statements are represented as hash maps, with keys that
|
SQL statements are represented as hash maps, with keys that
|
||||||
|
|
@ -37,11 +43,9 @@ SQL string as the first element followed by any parameter
|
||||||
values identified in the SQL expressions:
|
values identified in the SQL expressions:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(ns my.example
|
(require '[honey.sql :as sql])
|
||||||
(:require [honey.sql :as sql]))
|
|
||||||
|
|
||||||
(sql/format {:select [:*], :from [:table], :where [:= :id 1]})
|
(sql/format {:select [:*], :from [:table], :where [:= :id 1]})
|
||||||
;; produces:
|
|
||||||
;;=> ["SELECT * FROM table WHERE id = ?" 1]
|
;;=> ["SELECT * FROM table WHERE id = ?" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -53,15 +57,16 @@ is returned from `format`.
|
||||||
Most clauses expect a vector as their value, containing
|
Most clauses expect a vector as their value, containing
|
||||||
either a list of SQL entities or the representation of a SQL
|
either a list of SQL entities or the representation of a SQL
|
||||||
expression. Some clauses accept a single SQL entity. A few
|
expression. Some clauses accept a single SQL entity. A few
|
||||||
accept a most specialized form (such as `:set` accepting a
|
accept a more specialized form (such as `:set` within an `:update` clause
|
||||||
hash map of SQL entities and SQL expressions).
|
accepting a hash map of SQL entities and SQL expressions).
|
||||||
|
|
||||||
|
> Note: clauses can have a list as their value, but literal vectors and keywords are easier to type without quoting.
|
||||||
|
|
||||||
A SQL entity can be a simple keyword (or symbol) or a pair
|
A SQL entity can be a simple keyword (or symbol) or a pair
|
||||||
that represents a SQL entity and its alias (where aliases are allowed):
|
that represents a SQL entity and its alias (where aliases are allowed):
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format {:select [:t.id [:name :item]], :from [[:table :t]], :where [:= :id 1]})
|
(sql/format {:select [:t.id [:name :item]], :from [[:table :t]], :where [:= :id 1]})
|
||||||
;; produces:
|
|
||||||
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -76,21 +81,24 @@ avoid evaluation:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format '{select [t.id [name item]], from [[table t]], where [= id 1]})
|
(sql/format '{select [t.id [name item]], from [[table t]], where [= id 1]})
|
||||||
;; or you can use (..) instead of [..] when quoted:
|
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
|
|
||||||
|
;; or you can use (..) instead of [..] when quoted to produce the same result:
|
||||||
(sql/format '{select (t.id (name item)), from ((table t)), where (= id 1)})
|
(sql/format '{select (t.id (name item)), from ((table t)), where (= id 1)})
|
||||||
;; also produces:
|
|
||||||
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: these quoted forms may be appealing to users familiar with Datalog-family query languages, and they can be easier to type (and read) in some cases since you do not need to add `:` (shift-`;` on most keyboards) to the start of each SQL entity. The quoted forms do not work well in the [HoneySQL web app](https://john.shaffe.rs/honeysql/) so it's better to stick with vectors and keywords when using that.
|
||||||
|
|
||||||
If you wish, you can specify SQL entities as namespace-qualified
|
If you wish, you can specify SQL entities as namespace-qualified
|
||||||
keywords (or symbols) and the namespace portion will treated as
|
keywords (or symbols) and the namespace portion will treated as
|
||||||
the table name, i.e., `:foo/bar` instead of `:foo.bar`:
|
the table name, i.e., `:foo/bar` instead of `:foo.bar`:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
|
;; notice the following both produce the same result:
|
||||||
(sql/format {:select [:t/id [:name :item]], :from [[:table :t]], :where [:= :id 1]})
|
(sql/format {:select [:t/id [:name :item]], :from [[:table :t]], :where [:= :id 1]})
|
||||||
;; and
|
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
(sql/format '{select [t/id [name item]], from [[table t]], where [= id 1]})
|
(sql/format '{select [t/id [name item]], from [[table t]], where [= id 1]})
|
||||||
;; both produce:
|
|
||||||
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -98,7 +106,7 @@ the table name, i.e., `:foo/bar` instead of `:foo.bar`:
|
||||||
|
|
||||||
In addition to using hash maps to describe SQL clauses,
|
In addition to using hash maps to describe SQL clauses,
|
||||||
HoneySQL uses vectors to describe SQL expressions. Any
|
HoneySQL uses vectors to describe SQL expressions. Any
|
||||||
sequence that begins with a keyword (or symbol) is considered
|
vector that begins with a keyword (or symbol) is considered
|
||||||
to be a kind of function invocation. Certain "functions" are
|
to be a kind of function invocation. Certain "functions" are
|
||||||
considered to be "special syntax" and have custom rendering.
|
considered to be "special syntax" and have custom rendering.
|
||||||
Some "functions" are considered to be operators. In general,
|
Some "functions" are considered to be operators. In general,
|
||||||
|
|
@ -106,12 +114,40 @@ Some "functions" are considered to be operators. In general,
|
||||||
`42` and `"c"` lifted out into the overall vector result
|
`42` and `"c"` lifted out into the overall vector result
|
||||||
(with a SQL string followed by all its parameters).
|
(with a SQL string followed by all its parameters).
|
||||||
|
|
||||||
Operators can be strictly binary or variadic (most are strictly binary).
|
> Note: you can use the `:numbered true` option to `format` to produce SQL containing numbered placeholders, like `FOO(a, $1, $2)`, instead of positional placeholders (`?`).
|
||||||
|
|
||||||
|
As of 2.4.1002, function calls with "named" arguments are supported
|
||||||
|
which some databases support, e.g., MySQL and PostgreSQL both have
|
||||||
|
`SUBSTRING()`:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
[:substring :col 3 4] ;=> SUBSTRING(col, 3, 4)
|
||||||
|
;; can also be written:
|
||||||
|
[:substring :col :!from 3 :!for 4] ;=> SUBSTRING(col FROM 3 FOR 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
In a function call, any keywords (or symbols) that begin with `!` followed
|
||||||
|
by a letter are treated as inline SQL keywords to be used instead of `,`
|
||||||
|
between arguments -- or in front of arguments, such as for `TRIM()`:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
[:trim :!leading "x" :!from :col] ;=> TRIM(LEADING ? FROM col), with "x" parameter
|
||||||
|
[:trim :!both :!from :col] ;=> TRIM(BOTH FROM col), trims spaces
|
||||||
|
;; adjacent inline SQL keywords can be combined with a hyphen:
|
||||||
|
[:trim :!both-from :col] ;=> TRIM(BOTH FROM col)
|
||||||
|
;; (because - in a SQL keyword is replaced by a space)
|
||||||
|
```
|
||||||
|
|
||||||
|
Operators are all treated as variadic (except for `:=` and
|
||||||
|
`:<>` / `:!=` / `:not=` which are binary and require exactly two operands).
|
||||||
Special syntax can have zero or more arguments and each form is
|
Special syntax can have zero or more arguments and each form is
|
||||||
described in the [Special Syntax](special-syntax.md) section.
|
described in the [Special Syntax](special-syntax.md) section.
|
||||||
|
|
||||||
Some examples:
|
Some examples:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[:= :a 42] ;=> "a = ?" with a parameter of 42
|
[:= :a 42] ;=> "a = ?" with a parameter of 42
|
||||||
[:+ 42 :a :b] ;=> "? + a + b" with a parameter of 42
|
[:+ 42 :a :b] ;=> "? + a + b" with a parameter of 42
|
||||||
|
|
@ -119,34 +155,79 @@ Some examples:
|
||||||
[:now] ;=> "NOW()"
|
[:now] ;=> "NOW()"
|
||||||
[:count :*] ;=> "COUNT(*)"
|
[:count :*] ;=> "COUNT(*)"
|
||||||
[:or [:<> :name nil] [:= :status-id 0]] ;=> "(name IS NOT NULL) OR (status_id = ?)"
|
[:or [:<> :name nil] [:= :status-id 0]] ;=> "(name IS NOT NULL) OR (status_id = ?)"
|
||||||
;; with a parameter of 0 -- the nil value is inlined as NULL
|
;; the nil value is inlined as NULL but 0 is provided as a parameter
|
||||||
```
|
```
|
||||||
|
|
||||||
`:inline` is an example of "special syntax" and it renders its
|
`:inline` is an example of "special syntax" and it renders its
|
||||||
(single) argument as part of the SQL string generated by `format`.
|
arguments as part of the SQL string generated by `format`.
|
||||||
|
|
||||||
Another form of special syntax that is treated as function calls
|
Another form of special syntax that is treated as function calls
|
||||||
is keywords or symbols that begin with `%`. Such keywords (or symbols)
|
is keywords or symbols that begin with `%`. Such keywords (or quoted symbols)
|
||||||
are split at `.` and turned into function calls:
|
are split at `.` and turned into function calls:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
%now ;=> NOW()
|
:%now ;=> NOW()
|
||||||
%count.* ;=> COUNT(*)
|
:%count.* ;=> COUNT(*)
|
||||||
%max.foo ;=> MAX(foo)
|
:%max.foo ;=> MAX(foo)
|
||||||
%f.a.b ;=> F(a,b)
|
:%f.a.b ;=> F(a,b)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need to reference a table or alias for a column, you can use
|
||||||
|
qualified names in a function invocation:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
%max.foo/bar ;=> MAX(foo.bar)
|
||||||
|
```
|
||||||
|
|
||||||
|
The latter syntax can be convenient in a `SELECT` because `[:a :b]` is
|
||||||
|
otherwise taken as a column and its alias, so selecting a function call
|
||||||
|
expression requires an extra level of nesting:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [:a]})
|
||||||
|
;;=> ["SELECT a"]
|
||||||
|
(sql/format {:select [[:a :b]]})
|
||||||
|
;;=> ["SELECT a AS b"]
|
||||||
|
(sql/format {:select [[[:a :b]]]})
|
||||||
|
;;=> ["SELECT A(b)"]
|
||||||
|
;; or use the % notification:
|
||||||
|
(sql/format {:select [:%a.b]})
|
||||||
|
;;=> ["SELECT A(b)"]
|
||||||
|
(sql/format {:select [[[:a :b] :c]]})
|
||||||
|
;;=> ["SELECT A(b) AS c"]
|
||||||
|
(sql/format {:select [[:%a.b :c]]})
|
||||||
|
;;=> ["SELECT A(b) AS c"]
|
||||||
|
;; putting it all together:
|
||||||
|
(sql/format {:select [:x [:y :d] [[:z :e]] [[:z :f] :g]]})
|
||||||
|
;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"]
|
||||||
|
(sql/format {:select [:x [:y :d] [:%z.e] [:%z.f :g]]})
|
||||||
|
;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"]
|
||||||
|
(sql/format {:select [:x [:y :d] :%z.e [:%z.f :g]]})
|
||||||
|
;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## SQL Parameters
|
## SQL Parameters
|
||||||
|
|
||||||
As indicated in the preceding sections, values found in the DSL data structure
|
As indicated in the preceding sections, values found in the DSL data structure
|
||||||
that are not keywords or symbols are lifted out as positional parameters.
|
that are not keywords or symbols are lifted out as positional parameters.
|
||||||
They are replaced by `?` in the generated SQL string and added to the
|
By default, they are replaced by `?` in the generated SQL string and added to the
|
||||||
parameter list in order:
|
parameter list in order:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[:between :size 10 20] ;=> "size BETWEEN ? AND ?" with parameters 10 and 20
|
[:between :size 10 20] ;=> "size BETWEEN ? AND ?" with parameters 10 and 20
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you specify the `:numbered true` option to `format`, numbered placeholders (`$1`, `$2`, etc) will be used instead of positional placeholders (`?`).
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
;; with :numbered true option:
|
||||||
|
[:between :size 10 20] ;=> "size BETWEEN $1 AND $2" with parameters 10 and 20
|
||||||
|
```
|
||||||
|
|
||||||
HoneySQL also supports named parameters. There are two ways
|
HoneySQL also supports named parameters. There are two ways
|
||||||
of identifying a named parameter:
|
of identifying a named parameter:
|
||||||
* a keyword or symbol that begins with `?`
|
* a keyword or symbol that begins with `?`
|
||||||
|
|
@ -159,38 +240,55 @@ call as the `:params` key of the options hash map.
|
||||||
(sql/format {:select [:*] :from [:table]
|
(sql/format {:select [:*] :from [:table]
|
||||||
:where [:= :a :?x]}
|
:where [:= :a :?x]}
|
||||||
{:params {:x 42}})
|
{:params {:x 42}})
|
||||||
["SELECT * FROM table WHERE a = ?" 42]
|
;;=> ["SELECT * FROM table WHERE a = ?" 42]
|
||||||
(sql/format {:select [:*] :from [:table]
|
(sql/format {:select [:*] :from [:table]
|
||||||
:where [:= :a [:param :x]]}
|
:where [:= :a [:param :x]]}
|
||||||
{:params {:x 42}})
|
{:params {:x 42}})
|
||||||
["SELECT * FROM table WHERE a = ?" 42]
|
;;=> ["SELECT * FROM table WHERE a = ?" 42]
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with `:numbered true`:
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [:*] :from [:table]
|
||||||
|
:where [:= :a :?x]}
|
||||||
|
{:params {:x 42} :numbered true})
|
||||||
|
;;=> ["SELECT * FROM table WHERE a = $1" 42]
|
||||||
|
(sql/format {:select [:*] :from [:table]
|
||||||
|
:where [:= :a [:param :x]]}
|
||||||
|
{:params {:x 42} :numbered true})
|
||||||
|
;;=> ["SELECT * FROM table WHERE a = $1" 42]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Functional Helpers
|
## Functional Helpers
|
||||||
|
|
||||||
In addition to the hash map (and vectors) approach of building
|
In addition to the hash map (and vectors) approach of building
|
||||||
SQL queries with raw Clojure data structures, a namespace full
|
SQL queries with raw Clojure data structures, a
|
||||||
of helper functions is also available. These functions are
|
[namespace full of helper functions](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/api/honey.sql.helpers)
|
||||||
generally variadic and threadable:
|
is also available. These functions are generally variadic and threadable:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(ns my.example
|
(require '[honey.sql :as sql]
|
||||||
(:require [honey.sql :as sql]
|
'[honey.sql.helpers :refer [select from where]])
|
||||||
[honey.sql.helpers :refer [select from where]]))
|
|
||||||
|
|
||||||
(-> (select :t/id [:name :item])
|
(-> (select :t/id [:name :item])
|
||||||
(from [:table :t])
|
(from [:table :t])
|
||||||
(where [:= :id 1])
|
(where [:= :id 1])
|
||||||
(sql/format))
|
(sql/format))
|
||||||
;; produces:
|
|
||||||
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
There is a helper function for every single clause that HoneySQL
|
There is a helper function for every single clause that HoneySQL
|
||||||
supports out of the box. In addition, there are helpers for
|
supports out of the box. In addition, there are helpers for
|
||||||
`composite` and `over` that make it easier to construct those
|
`composite`, `lateral`, `over`, and `upsert` that make it easier to construct those
|
||||||
parts of the SQL DSL (examples of the former appear in the [README](README.md),
|
parts of the SQL DSL (examples of `composite` appear in the
|
||||||
examples of the latter appear in the [Clause Reference](docs/clause-reference.md))
|
[README](/README.md#composite-types)
|
||||||
|
and in the [General Reference](general-reference.md#tuples-and-composite-values);
|
||||||
|
examples of `over` appear in the
|
||||||
|
[Clause Reference](clause-reference.md#window-partition-by-and-over))
|
||||||
|
|
||||||
|
In general, `(helper :foo expr)` will produce `{:helper [:foo expr]}`
|
||||||
|
(with a few exceptions -- see the docstring of the helper function
|
||||||
|
for details).
|
||||||
|
|
||||||
In addition to being variadic -- which often lets you omit one
|
In addition to being variadic -- which often lets you omit one
|
||||||
level of `[`..`]` -- the helper functions merge clauses, which
|
level of `[`..`]` -- the helper functions merge clauses, which
|
||||||
|
|
@ -202,7 +300,6 @@ can make it easier to build queries programmatically:
|
||||||
(where [:= :id 1])
|
(where [:= :id 1])
|
||||||
(select [:name :item])
|
(select [:name :item])
|
||||||
(sql/format))
|
(sql/format))
|
||||||
;; produces:
|
|
||||||
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -216,7 +313,6 @@ you need to explicitly remove the prior value:
|
||||||
(dissoc :select)
|
(dissoc :select)
|
||||||
(select [:name :item])
|
(select [:name :item])
|
||||||
(sql/format))
|
(sql/format))
|
||||||
;; produces:
|
|
||||||
;;=> ["SELECT name AS item FROM table AS t WHERE id = ?" 1]
|
;;=> ["SELECT name AS item FROM table AS t WHERE id = ?" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -225,20 +321,24 @@ can rely on using keywords in `dissoc`.
|
||||||
|
|
||||||
The following helpers shadow functions in `clojure.core` so
|
The following helpers shadow functions in `clojure.core` so
|
||||||
you need to consider this when referring symbols in from the
|
you need to consider this when referring symbols in from the
|
||||||
`honey.sql.helpers` namespace: `for`, `group-by`, `partition-by`,
|
`honey.sql.helpers` namespace: `filter`, `for`, `group-by`, `into`, `partition-by`,
|
||||||
`set`, and `update`.
|
`set`, and `update`.
|
||||||
|
|
||||||
## DDL Statements
|
## DDL Statements
|
||||||
|
|
||||||
HoneySQL 1.x did not support any DDL statements. It was fairly
|
HoneySQL 1.x did not support any DDL statements. It was fairly
|
||||||
common for people to use the [nilenso/honeysql-postgres library](https://github.com/nilenso/honeysql-postgres)
|
common for people to use the
|
||||||
|
[nilenso/honeysql-postgres library](https://github.com/nilenso/honeysql-postgres)
|
||||||
to get DDL support, even if they didn't need the PostgreSQL-specific
|
to get DDL support, even if they didn't need the PostgreSQL-specific
|
||||||
extensions. That library does not work with HoneySQL 2.x but all
|
extensions. That library does not work with HoneySQL 2.x but all
|
||||||
of the functionality from it has been incorporated
|
of the functionality from it (up to 0.4.112) has been incorporated
|
||||||
into HoneySQL now and is described in the [PostgreSQL](postgresql.md)
|
into HoneySQL now and is described in the [PostgreSQL](postgresql.md)
|
||||||
section (because that covers all of the things that the nilenso
|
section (because that covers all of the things that the nilenso
|
||||||
library supported and much of it was PostgreSQL-specific!).
|
library supported and much of it was PostgreSQL-specific!).
|
||||||
|
|
||||||
|
See also the [DDL Clauses section](clause-reference.md#ddl-clauses) of
|
||||||
|
the Clause Reference for documentation about supported DDL.
|
||||||
|
|
||||||
## Dialects
|
## Dialects
|
||||||
|
|
||||||
By default, HoneySQL operates in ANSI SQL mode but it supports
|
By default, HoneySQL operates in ANSI SQL mode but it supports
|
||||||
|
|
@ -246,35 +346,45 @@ a lot of PostgreSQL extensions in that mode. PostgreSQL is mostly
|
||||||
a superset of ANSI SQL so it makes sense to support as much as
|
a superset of ANSI SQL so it makes sense to support as much as
|
||||||
possible of the union of ANSI SQL and PostgreSQL out of the box.
|
possible of the union of ANSI SQL and PostgreSQL out of the box.
|
||||||
|
|
||||||
The dialects supported by HoneySQL v2 are:
|
The dialects supported by HoneySQL 2.x are:
|
||||||
* `:ansi` -- the default, including most PostgreSQL extensions
|
* `:ansi` -- the default, including most PostgreSQL extensions
|
||||||
* `:sqlserver` -- Microsoft SQL Server
|
* `:sqlserver` -- Microsoft SQL Server
|
||||||
* `:mysql` -- MySQL (and Percona and MariaDB)
|
* `:mysql` -- MySQL (and Percona and MariaDB)
|
||||||
|
* `:nrql` -- as of 2.5.1091
|
||||||
* `:oracle` -- Oracle
|
* `:oracle` -- Oracle
|
||||||
|
|
||||||
The most visible difference between dialects is how SQL entities
|
The most visible difference between dialects is how SQL entities
|
||||||
should be quoted (if the `:quoted true` option is provided to `format`).
|
should be quoted (if the `:quoted true` option is provided to `format`).
|
||||||
Most databases use `"` for quoting (the `:ansi` and `:oracle` dialects).
|
Most databases use `"` for quoting (the `:ansi` and `:oracle` dialects).
|
||||||
The `:sqlserver` dialect uses `[`..`]` and the `:mysql` dialect uses
|
The `:sqlserver` dialect uses `[`..`]` and the `:mysql` dialect uses
|
||||||
```..```. In addition, the `:oracle` dialect disables `AS` in aliases.
|
`` ` ``..`` ` ``. In addition, the `:oracle` dialect disables `AS` in aliases.
|
||||||
|
|
||||||
|
> Note: by default, quoting is **off** which produces cleaner-looking SQL and assumes you control all the symbols/keywords used as table, column, and function names -- the "SQL entities". If you are building any SQL or DDL where the table, column, or function names could be provided by an external source, **you should specify `:quoted true` to ensure all SQL entities are safely quoted**. As of 2.3.928, if you do _not_ specify `:quoted` as an option, HoneySQL will automatically quote any SQL entities that seem unusual, i.e., that contain any characters that are not alphanumeric or underscore. Purely alphanumeric entities will not be quoted (no entities were quoted by default prior to 2.3.928). You can prevent that auto-quoting by explicitly passing `:quoted false` into the `format` call but, from a security point of view, you should think very carefully before you do that: quoting entity names helps protect you from injection attacks! As of 2.4.947, you can change the default setting of `:quoted` from `nil` to `true` (or `false`) via the `set-options!` function.
|
||||||
|
|
||||||
Currently, the only dialect that has substantive differences from
|
Currently, the only dialect that has substantive differences from
|
||||||
the others is `:mysql` which has a `:lock` clause (that is very
|
the others is `:mysql` for which the `:set` clause
|
||||||
similar to the ANSI `:for` clause) and for which the `:set` clause
|
|
||||||
has a different precedence than ANSI SQL.
|
has a different precedence than ANSI SQL.
|
||||||
|
|
||||||
|
See [New Relic NRQL Support](nrsql.md) for more details of the NRQL dialect.
|
||||||
|
|
||||||
You can change the dialect globally using the `set-dialect!` function,
|
You can change the dialect globally using the `set-dialect!` function,
|
||||||
passing in one of the keywords above. You need to call this function
|
passing in one of the keywords above. You need to call this function
|
||||||
before you call `format` for the first time.
|
before you call `format` for the first time. See below for examples.
|
||||||
|
|
||||||
You can change the dialect for a single `format` call by
|
You can change the dialect for a single `format` call by
|
||||||
specifying the `:dialect` option in that call.
|
specifying the `:dialect` option in that call.
|
||||||
|
|
||||||
SQL entities are not quoted by default but if you specify the
|
Alphanumeric SQL entities are not quoted by default but if you specify the
|
||||||
dialect in a `format` call, they will be quoted. If you don't
|
dialect in a `format` call, they will be quoted. If you don't
|
||||||
specify a dialect in the `format` call, you can specify
|
specify a dialect in the `format` call, you can specify
|
||||||
`:quoted true` to have SQL entities quoted.
|
`:quoted true` to have SQL entities quoted. You can also enable quoting
|
||||||
|
globally via the `set-dialect!` function. See below for an example
|
||||||
|
with `:quoted true`.
|
||||||
|
|
||||||
|
If you want to use a dialect _and_ use the default quoting strategy (automatically quote any SQL entities that seem unusual), specify a `:dialect` option and set `:quoted nil`:
|
||||||
|
|
||||||
|
<!-- Reminder to doc author:
|
||||||
|
Reset dialect to default so other blocks are not affected for test-doc-blocks -->
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format '{select (id) from (table)} {:quoted true})
|
(sql/format '{select (id) from (table)} {:quoted true})
|
||||||
;;=> ["SELECT \"id\" FROM \"table\""]
|
;;=> ["SELECT \"id\" FROM \"table\""]
|
||||||
|
|
@ -284,30 +394,36 @@ specify a dialect in the `format` call, you can specify
|
||||||
;;=> nil
|
;;=> nil
|
||||||
(sql/format '{select (id) from (table)} {:quoted true})
|
(sql/format '{select (id) from (table)} {:quoted true})
|
||||||
;;=> ["SELECT [id] FROM [table]"]
|
;;=> ["SELECT [id] FROM [table]"]
|
||||||
|
;; you can also choose to enable quoting globally
|
||||||
|
;; when you set a dialect:
|
||||||
|
(sql/set-dialect! :mysql :quoted true)
|
||||||
|
(sql/format '{select (id) from (table)})
|
||||||
|
;;=> ["SELECT `id` FROM `table`"]
|
||||||
|
;; and opt out for a specific call:
|
||||||
|
(sql/format '{select (id) from (table)} {:quoted false})
|
||||||
|
;;=> ["SELECT id FROM table"]
|
||||||
|
;; and reset back to the default of :ansi
|
||||||
|
(sql/set-dialect! :ansi)
|
||||||
|
;;=> nil
|
||||||
|
;; which also resets the quoting default (back to nil)
|
||||||
|
;; so only unusual entity names get quoted:
|
||||||
|
(sql/format '{select (id) from (table)} {:quoted true})
|
||||||
|
;;=> ["SELECT \"id\" FROM \"table\""]
|
||||||
|
;; use default quoting strategy with dialect specific quotes, only unusual entities quoted
|
||||||
|
(sql/format '{select (id, iffy##field ) from (table)} {:dialect :sqlserver :quoted nil})
|
||||||
|
;; => ["SELECT id, [iffy##field] FROM table"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Out of the box, as part of the extended ANSI SQL support,
|
Out of the box, as part of the extended ANSI SQL support,
|
||||||
HoneySQL supports quite a few [PostgreSQL extensions](postgresql.md).
|
HoneySQL supports quite a few [PostgreSQL extensions](postgresql.md)
|
||||||
|
and [XTDB extensions](xtdb.md).
|
||||||
|
|
||||||
> Note: the [nilenso/honeysql-postgres library](https://github.com/nilenso/honeysql-postgres) which provided PostgreSQL support for HoneySQL 1.x does not work with HoneySQL 2.x. However, HoneySQL 2.x includes all of the functionality from that library out of the box!
|
> Note: the [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) library which provided PostgreSQL support for HoneySQL 1.x does not work with HoneySQL 2.x. However, HoneySQL 2.x includes all of the functionality from that library (up to 0.4.112) out of the box!
|
||||||
|
|
||||||
## Format Options
|
See also the section on
|
||||||
|
[database-specific hints and tips](databases.md) which may
|
||||||
In addition to the `:quoted` and `:dialect` options described above,
|
provide ways to satisfy your database's needs without changing
|
||||||
`format` also accepts `:inline` and `:params`.
|
the dialect or extending HoneySQL.
|
||||||
|
|
||||||
The `:params` option was mentioned above and is used to specify
|
|
||||||
the values of named parameters in the DSL.
|
|
||||||
|
|
||||||
The `:inline` option suppresses the generation of parameters in
|
|
||||||
the SQL string and instead tries to inline all the values directly
|
|
||||||
into the SQL string. The behavior is as if each value in the DSL
|
|
||||||
was wrapped in `[:inline `..`]`:
|
|
||||||
|
|
||||||
* `nil` becomes the SQL value `NULL`,
|
|
||||||
* Clojure strings become inline SQL strings with single quotes (so `"foo"` becomes `'foo'`),
|
|
||||||
* keywords and symbols become SQL keywords (uppercase, with `-` replaced by a space),
|
|
||||||
* everything else is just turned into a string (by calling `str`) and added to the SQL string.
|
|
||||||
|
|
||||||
## Reference Documentation
|
## Reference Documentation
|
||||||
|
|
||||||
|
|
@ -319,6 +435,8 @@ section. The full list
|
||||||
of "special syntax" functions is documented in the
|
of "special syntax" functions is documented in the
|
||||||
[Special Syntax](special-syntax.md) section. The best
|
[Special Syntax](special-syntax.md) section. The best
|
||||||
documentation for the helper functions is in the
|
documentation for the helper functions is in the
|
||||||
[honey.sql.helpers](https://cljdoc.org/d/seancorfield/honeysql/2.0.0-alpha2/api/honey.sql.helpers) namespace.
|
[honey.sql.helpers](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/api/honey.sql.helpers) namespace.
|
||||||
If you're migrating to HoneySQL 2.0, this [overview of differences
|
More detail about certain core HoneySQL functionality can be found in the
|
||||||
between 1.0 and 2.0](differences-from-1-x.md) should help.
|
[Reference documentation](general-reference.md).
|
||||||
|
If you're migrating to HoneySQL 2.x, this [overview of differences
|
||||||
|
between 1.x and 2.x](differences-from-1-x.md) should help.
|
||||||
|
|
|
||||||
44
doc/nrql.md
Normal file
44
doc/nrql.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# New Relic NRQL Support
|
||||||
|
|
||||||
|
As of 2.5.1091, HoneySQL provides some support for New Relic's NRQL query language.
|
||||||
|
|
||||||
|
At present, the following additional SQL clauses (and their corresponding
|
||||||
|
helper functions) are supported:
|
||||||
|
|
||||||
|
* `:facet` - implemented just like `:select`
|
||||||
|
* `:since` - implemented like `:interval`
|
||||||
|
* `:until` - implemented like `:interval`
|
||||||
|
* `:compare-with` - implemented like `:interval`
|
||||||
|
* `:timeseries` - implemented like `:interval`
|
||||||
|
|
||||||
|
> Note: `:timeseries :auto` is the shortest way to specify a timeseries.
|
||||||
|
|
||||||
|
When you select the `:nrql` dialect, SQL formatting assumes `:inline true`
|
||||||
|
so that the generated SQL string can be used directly in NRQL queries.
|
||||||
|
|
||||||
|
In addition, stropping (quoting) is done using backticks, like MySQL,
|
||||||
|
but entities are not split at `/` or `.` characters, so that:
|
||||||
|
|
||||||
|
```
|
||||||
|
:foo/bar.baz ;;=> `foo/bar.baz`
|
||||||
|
```
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (require '[honey.sql :as sql])
|
||||||
|
nil
|
||||||
|
```
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [:mulog/timestamp :mulog/event-name]
|
||||||
|
:from :Log
|
||||||
|
:where [:= :mulog/data.account "foo-account-id"]
|
||||||
|
:since [2 :days :ago]
|
||||||
|
:limit 2000}
|
||||||
|
{:dialect :nrql :pretty true})
|
||||||
|
["
|
||||||
|
SELECT `mulog/timestamp`, `mulog/event-name`
|
||||||
|
FROM Log
|
||||||
|
WHERE `mulog/data.account` = 'foo-account-id'
|
||||||
|
LIMIT 2000
|
||||||
|
SINCE 2 DAYS AGO
|
||||||
|
"]
|
||||||
|
```
|
||||||
|
|
@ -32,27 +32,106 @@ can simply evaluate to `nil` instead).
|
||||||
;;=> ["...WHERE (id = ?) OR (type = ?)..." 42 "match"]
|
;;=> ["...WHERE (id = ?) OR (type = ?)..." 42 "match"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## = <> < > <= >=
|
## in, not-in
|
||||||
|
|
||||||
|
Predicates for checking an expression is or is not a member of a specified set of values.
|
||||||
|
|
||||||
|
The two most common forms are:
|
||||||
|
|
||||||
|
* `[:in :col [val1 val2 ...]]` or `[:not-in :col [val1 val2 ...]]` where the `valN` can be arbitrary expressions,
|
||||||
|
* `[:in :col {:select ...}]` or `[:not-in :col {:select ...}]` where the `SELECT` specifies a single column.
|
||||||
|
|
||||||
|
`:col` could be an arbitrary SQL expression (but is most
|
||||||
|
commonly just a column name).
|
||||||
|
|
||||||
|
The former produces an inline vector expression with the
|
||||||
|
values resolved as regular SQL expressions (i.e., with
|
||||||
|
literal values lifted out as parameters): `col IN [?, ?, ...]`
|
||||||
|
or `col NOT IN [?, ?, ...]`
|
||||||
|
|
||||||
|
The latter produces a sub-select, as expected: `col IN (SELECT ...)`
|
||||||
|
or `col NOT IN (SELECT ...)`
|
||||||
|
|
||||||
|
You can also specify the set of values via a named parameter:
|
||||||
|
|
||||||
|
* `[:in :col :?values]` or `[:not-in :col :?values]` where `:params {:values [1 2 ...]}` is provided to `format` in the options.
|
||||||
|
|
||||||
|
In this case, the named parameter is expanded directly when
|
||||||
|
`:in` (or `:not-in`) is formatted to obtain the sequence of values (which
|
||||||
|
must be _sequential_, not a Clojure set). That means you
|
||||||
|
cannot use this approach and also specify `:cache` -- see
|
||||||
|
[cache in All the Options](options.md#cache) for more details.
|
||||||
|
|
||||||
|
Another supported form is checking whether a tuple is in
|
||||||
|
a selected set of values that specifies a matching number
|
||||||
|
of columns, producing `(col1, col2) IN (SELECT ...)`, but
|
||||||
|
you need to specify the columns (or expressions) using the
|
||||||
|
`:composite` special syntax:
|
||||||
|
|
||||||
|
* `[:in [:composite :col1 :col2] ...]` or `[:not-in [:composite :col1 :col2] ...]`
|
||||||
|
|
||||||
|
This produces `(col1, col2) IN ...` or `(col1, col2) NOT IN ...`
|
||||||
|
|
||||||
|
> Note: This is a change from HoneySQL 1.x which accepted a sequence of column names but required more work for arbitrary expressions.
|
||||||
|
|
||||||
|
## = <>
|
||||||
|
|
||||||
Binary comparison operators. These expect exactly
|
Binary comparison operators. These expect exactly
|
||||||
two arguments.
|
two arguments.
|
||||||
|
|
||||||
The following aliases are also supported:
|
`not=` and `!=` are accepted as aliases for `<>`.
|
||||||
* `is` -- an alias for `=`
|
|
||||||
* `is-not`, `not=`, `!=` -- aliases for `<>`
|
|
||||||
|
|
||||||
## mod, xor, + - * / % | & ^
|
## < > <= >=
|
||||||
|
|
||||||
Mathematical and bitwise operators. `+` and `*` are
|
Comparison operators. These expect exactly
|
||||||
variadic; the rest are strictly binary operators.
|
two arguments.
|
||||||
|
|
||||||
|
## is, is-not
|
||||||
|
|
||||||
|
Predicates for `NULL` and Boolean values:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
{...
|
||||||
|
:where [:is :id nil]
|
||||||
|
...}
|
||||||
|
;;=> ["...WHERE col IS NULL..."]
|
||||||
|
{...
|
||||||
|
:where [:is-not :id nil]
|
||||||
|
...}
|
||||||
|
;;=> ["...WHERE col IS NOT NULL..."]
|
||||||
|
{...
|
||||||
|
:where [:is :col true]
|
||||||
|
...}
|
||||||
|
;;=> ["...WHERE col IS TRUE..."]
|
||||||
|
{...
|
||||||
|
;; unlike [:<> :col false], the following will include NULLs:
|
||||||
|
:where [:is-not :col false]
|
||||||
|
...}
|
||||||
|
;;=> ["...WHERE col IS NOT FALSE..."]
|
||||||
|
```
|
||||||
|
|
||||||
|
## xor, + - * / % | & ^
|
||||||
|
|
||||||
|
Mathematical and bitwise operators.
|
||||||
|
|
||||||
## like, not like, ilike, not ilike, regexp
|
## like, not like, ilike, not ilike, regexp
|
||||||
|
|
||||||
Pattern matching binary operators. `regex` is accepted
|
Pattern matching operators. `regex` is accepted
|
||||||
as an alias for `regexp`.
|
as an alias for `regexp`.
|
||||||
|
|
||||||
`similar-to` and `not-similar-to` are also supported.
|
`similar-to` and `not-similar-to` are also supported.
|
||||||
|
|
||||||
|
## with ordinality
|
||||||
|
|
||||||
|
The ANSI SQL `WITH ORDINALITY` expression is supported as an infix operator:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
{...
|
||||||
|
[:with-ordinality [:jsonb_array_elements :j] [:arr :item :index]]
|
||||||
|
...}
|
||||||
|
;;=> ["...JSONB_ARRAY_ELEMENTS(j) WITH ORDINALITY ARR(item, index)..."]
|
||||||
|
```
|
||||||
|
|
||||||
## ||
|
## ||
|
||||||
|
|
||||||
Variadic string concatenation operator.
|
String concatenation operator.
|
||||||
|
|
|
||||||
183
doc/options.md
Normal file
183
doc/options.md
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
# All the Options
|
||||||
|
|
||||||
|
`format` accepts options as either a single hash map argument or
|
||||||
|
as named arguments (alternating keys and values). If you are using
|
||||||
|
Clojure 1.11 (or later) you can mix'n'match, providing some options
|
||||||
|
as named arguments followed by other options in a hash map.
|
||||||
|
|
||||||
|
[**Getting Started**](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started)
|
||||||
|
talked about the `:dialect`, `:params`, and `:quoted` options,
|
||||||
|
but `format` accepts a number of other options that control
|
||||||
|
how the data structure DSL is converted to a SQL string
|
||||||
|
and the associated parameters.
|
||||||
|
|
||||||
|
## Format Options
|
||||||
|
|
||||||
|
All options may be omitted. The default behavior of each option is described in the following list, with expanded details of each option in the sections that follow.
|
||||||
|
|
||||||
|
* `:cache` -- an atom containing a [clojure.core.cache](https://github.com/clojure/core.cache) cache used to cache generated SQL; the default behavior is to generate SQL on each call to `format`,
|
||||||
|
* `:checking` -- `:none` (default), `:basic`, or `:strict` to control the amount of lint-like checking that HoneySQL performs,
|
||||||
|
* `:dialect` -- a keyword that identifies a dialect to be used for this specific call to `format`; the default is to use what was specified in `set-dialect!` or `:ansi` if no other dialect has been set,
|
||||||
|
* `:ignored-metadata` -- a sequence of metadata keys that should be ignored when formatting (in addition to `:file`, `:line`, `:column`, `:end-line` and `:end-column` which are always ignored); the default is `[]` -- no additional metadata is ignored (since 2.5.1103),
|
||||||
|
* `:inline` -- a Boolean indicating whether or not to inline parameter values, rather than use `?` placeholders and a sequence of parameter values; the default is `false` -- values are not inlined,
|
||||||
|
* `:numbered` -- a Boolean indicating whether to generate numbered placeholders in the generated SQL (`$1`, `$2`, etc) or positional placeholders (`?`); the default is `false` (positional placeholders); this option was added in 2.4.962,
|
||||||
|
* `:params` -- a hash map providing values for named parameters, identified by names (keywords or symbols) that start with `?` in the DSL; the default is that any such named parameters will have `nil` values,
|
||||||
|
* `:quoted` -- a Boolean indicating whether or not to quote (strop) SQL entity names (table and column names); the default is `nil` -- alphanumeric SQL entity names are not quoted but (as of 2.3.928) "unusual" SQL entity names are quoted; a `false` value turns off all quoting,
|
||||||
|
* `:quoted-always` -- an optional regex that matches SQL entity names that should always be quoted (stropped) regardless of the value of `:quoted`; the default is `nil` -- no SQL entity names are always quoted,
|
||||||
|
* `:quoted-snake` -- a Boolean indicating whether or not quoted and string SQL entity names should have `-` replaced by `_`; the default is `false` -- quoted and string SQL entity names are left exactly as-is,
|
||||||
|
* `:values-default-columns` -- a sequence of column names that should have `DEFAULT` values instead of `NULL` values if used in a `VALUES` clause with no associated matching value in the hash maps passed in; the default behavior is for such missing columns to be given `NULL` values.
|
||||||
|
|
||||||
|
As of 2.4.947, you can call `set-options!` with an options hash map to change the
|
||||||
|
global defaults of certain options:
|
||||||
|
|
||||||
|
* `:checking` -- can be `:basic` or `:strict`; specify `:none` to reset to the default,
|
||||||
|
* `:inline` -- can be `true` but consider the security issues this causes by not using parameterized SQL statements; specify `false` (or `nil`) to reset to the default,
|
||||||
|
* `:numbered` -- can be `true` or `false`; specify `false` to reset to the default,
|
||||||
|
* `:quoted` -- can be `true` or `false`; specify `nil` to reset to the default; calling `set-dialect!` or providing a `:dialect` option to `format` will override the global default,
|
||||||
|
* `:quoted-snake` -- can be `true`; specify `false` (or `nil`) to reset to the default.
|
||||||
|
|
||||||
|
Other options may only be specified directly in calls to `format` as they are considered
|
||||||
|
per-statement, rather than global.
|
||||||
|
|
||||||
|
See below for the interaction between `:dialect` and `:quoted`.
|
||||||
|
|
||||||
|
## `:cache`
|
||||||
|
|
||||||
|
Providing a `:cache` option -- an atom containing a `core.cache` style cache data structure -- causes `format` to try to cache the
|
||||||
|
generated SQL string, based on the value of the DSL data structure.
|
||||||
|
When you use `:cache`, you should generally use named parameters
|
||||||
|
(names that start with `?`) instead of regular values.
|
||||||
|
|
||||||
|
See the [**Caching** section of the **General Reference**](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/general-reference#caching)
|
||||||
|
for details.
|
||||||
|
|
||||||
|
> Note: you cannot use named parameters with `:in` when using `:cache` because `:in` "unrolls" the parameter and that will break the cache lookup rules.
|
||||||
|
|
||||||
|
Added in 2.2.858.
|
||||||
|
|
||||||
|
## `:checking`
|
||||||
|
|
||||||
|
The `:checking` option defaults to `:none`.
|
||||||
|
If `:checking :basic` is specified, certain obvious errors
|
||||||
|
are treated as an error and an exception is thrown.
|
||||||
|
If `:checking :strict` is specified, certain dubious constructs are also treated as an error and an exception is
|
||||||
|
thrown.
|
||||||
|
It is expected that this feature will be expanded over time
|
||||||
|
to help avoid generating illegal SQL.
|
||||||
|
|
||||||
|
Here are the checks for each level:
|
||||||
|
* `:basic` -- `DELETE` and `DELETE FROM` without a `WHERE` clause; `IN` with an empty collection; `SELECT` with an empty list of columns; `UPDATE` without a `WHERE` clause.
|
||||||
|
* `:strict` -- (all the `:basic` checks plus) `IN` with a collection containing `NULL` values (since this will not match rows).
|
||||||
|
|
||||||
|
## `:dialect`
|
||||||
|
|
||||||
|
If `:dialect` is provided, `:quoted` will default to `true` for this call. You can still specify `:quoted false` to turn that back off.
|
||||||
|
|
||||||
|
Valid dialects are:
|
||||||
|
|
||||||
|
* `:ansi`
|
||||||
|
* `:mysql`
|
||||||
|
* `:oracle`
|
||||||
|
* `:sqlserver`
|
||||||
|
|
||||||
|
New dialects can be created with the `register-dialect!` call.
|
||||||
|
|
||||||
|
By default, `:ansi` is the dialect used. `set-dialect!` can
|
||||||
|
set a different default dialect. The `:dialect` option only affects
|
||||||
|
the current call to `format`.
|
||||||
|
|
||||||
|
## `:inline`
|
||||||
|
|
||||||
|
The `:inline` option suppresses the generation of parameters in
|
||||||
|
the SQL string and instead tries to inline all the values directly
|
||||||
|
into the SQL string. The behavior is as if each value in the DSL
|
||||||
|
was wrapped in `[:inline `..`]`:
|
||||||
|
|
||||||
|
* `nil` becomes the SQL value `NULL`,
|
||||||
|
* Clojure strings become inline SQL strings with single quotes (so `"foo"` becomes `'foo'`),
|
||||||
|
* keywords and symbols become SQL keywords (uppercase, with `-` replaced by a space),
|
||||||
|
* everything else is just turned into a string (by calling `str`) and added to the SQL string.
|
||||||
|
|
||||||
|
> Note: you can provide additional inline formatting by extending the `InlineValue` protocol from `honey.sql.protocols` to new types.
|
||||||
|
|
||||||
|
## `:numbered`
|
||||||
|
|
||||||
|
By default, HoneySQL generates SQL using positional placeholders (`?`).
|
||||||
|
Specifying `:numbered true` tells HoneySQL to generate SQL using
|
||||||
|
numbered placeholders instead (`$1`, `$2`, etc). This can be set
|
||||||
|
globally using `set-options!`.
|
||||||
|
|
||||||
|
## `:params`
|
||||||
|
|
||||||
|
The `:params` option provides a mapping from named parameters
|
||||||
|
to values for this call to `format`. For example:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(require '[honey.sql :as sql])
|
||||||
|
|
||||||
|
(-> {:select :* :from :table :where [:= :id :?id]}
|
||||||
|
(sql/format {:params {:id 42}}))
|
||||||
|
;;=> ["SELECT * FROM table WHERE id = ?" 42]
|
||||||
|
(-> '{select * from table where (= id ?id)}
|
||||||
|
(sql/format {:params {:id 42}}))
|
||||||
|
;;=> ["SELECT * FROM table WHERE id = ?" 42]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `:quoted`
|
||||||
|
|
||||||
|
If `:quoted true`, or `:dialect` is provided (and `:quoted` is not
|
||||||
|
specified as `false`), SQL entity names that represent
|
||||||
|
tables and columns will be quoted (stropped) according to the
|
||||||
|
selected dialect.
|
||||||
|
|
||||||
|
If `:quoted false`, SQL entity names that represent tables and columns
|
||||||
|
will not be quoted. If those SQL entity names are reserved words in
|
||||||
|
SQL, the generated SQL will be invalid. You can use the `:quoted-always`
|
||||||
|
option to specify a regex, to identify SQL entity names that should
|
||||||
|
always be quoted (stropped) regardless of the value of `:quoted`, e.g.,
|
||||||
|
reserved words that you happen to use as table or column names.
|
||||||
|
|
||||||
|
The quoting (stropping) is dialect-dependent:
|
||||||
|
* `:ansi` -- uses double quotes
|
||||||
|
* `:mysql` -- uses backticks
|
||||||
|
* `:oracle` -- uses double quotes
|
||||||
|
* `:sqlserver` -- user square brackets
|
||||||
|
|
||||||
|
As of 2.3.928, if `:quoted` and `:dialect` are not provided, and no
|
||||||
|
default quoting strategy has been specified (via `set-dialect!`) then
|
||||||
|
alphanumeric SQL entity names will not be quoted but "unusual" SQL entity names will
|
||||||
|
|
||||||
|
## `:quoted-snake`
|
||||||
|
|
||||||
|
Where strings are used to identify table or column names, they are
|
||||||
|
treated as-is. If `:quoted true` (or a `:dialect` is specified),
|
||||||
|
those SQL entity names are quoted as-is.
|
||||||
|
|
||||||
|
Where keywords or symbols are used to identify table or column
|
||||||
|
names, and `:quoted true` is provided, those SQL entity names are
|
||||||
|
quoted as-is.
|
||||||
|
|
||||||
|
If `:quoted-snake true` is provided, those SQL entity names are quoted
|
||||||
|
but any `-` in them are replaced by `_` -- that replacement is the
|
||||||
|
default in unquoted SQL entity names.
|
||||||
|
|
||||||
|
This allows quoting to be used but still maintain the Clojure
|
||||||
|
(kebab case) to SQL (snake case) mappings.
|
||||||
|
|
||||||
|
## `:values-default-columns`
|
||||||
|
|
||||||
|
This option determines the behavior of the `:values` clause, when
|
||||||
|
column values are missing from one or more of the hash maps passed
|
||||||
|
in.
|
||||||
|
|
||||||
|
By default, missing column values are replaced with `NULL` in the
|
||||||
|
generated SQL. `:values-default-columns` can specify a set of
|
||||||
|
column names that should instead be given the value `DEFAULT` if
|
||||||
|
their column value is missing from one or more hash maps.
|
||||||
|
|
||||||
|
That in turn should cause their declared default value to be used
|
||||||
|
(from the column definition in the table) and is useful for
|
||||||
|
situations where `NULL` is not an appropriate default for a missing
|
||||||
|
column value.
|
||||||
|
|
||||||
|
Added in 2.1.818.
|
||||||
|
|
@ -3,12 +3,93 @@
|
||||||
This section covers the PostgreSQL-specific
|
This section covers the PostgreSQL-specific
|
||||||
features that HoneySQL supports out of the box
|
features that HoneySQL supports out of the box
|
||||||
for which you previously needed the
|
for which you previously needed the
|
||||||
[nilenso/honeysql-postgres library](https://github.com/nilenso/honeysql-postgres).
|
[nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres)
|
||||||
|
library.
|
||||||
|
|
||||||
Everything that the nilenso library provided is implemented
|
Everything that the nilenso library provided (in 0.4.112) is implemented
|
||||||
directly in HoneySQL 2.x although a few things have a
|
directly in HoneySQL 2.x although a few things have a
|
||||||
slightly different syntax.
|
slightly different syntax.
|
||||||
|
|
||||||
|
If you are using HoneySQL with the Node.js PostgreSQL driver, it
|
||||||
|
only accepts numbered placeholders, not positional placeholders,
|
||||||
|
so you will need to specify the `:numbered true` option that was
|
||||||
|
added in 2.4.962. You may find it convenient to set this option
|
||||||
|
globally, via `set-options!`.
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
The code examples herein assume:
|
||||||
|
```clojure
|
||||||
|
(refer-clojure :exclude '[update set])
|
||||||
|
(require '[honey.sql :as sql]
|
||||||
|
'[honey.sql.helpers :refer [select from where
|
||||||
|
update set
|
||||||
|
insert-into values
|
||||||
|
create-table with-columns create-view create-extension
|
||||||
|
add-column alter-table add-index
|
||||||
|
alter-column rename-column rename-table
|
||||||
|
drop-table drop-column drop-index drop-extension
|
||||||
|
upsert returning on-conflict on-constraint
|
||||||
|
do-update-set do-nothing]])
|
||||||
|
```
|
||||||
|
|
||||||
|
Clojure users can opt for the shorter `(require '[honey.sql :as sql] '[honey.sql.helpers :refer :all])` but this syntax is not available to ClojureScript users.
|
||||||
|
|
||||||
|
## Working with Arrays
|
||||||
|
|
||||||
|
HoneySQL supports `:array` as special syntax to produce `ARRAY[..]` expressions:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:array [1 2 3]] :a]]})
|
||||||
|
["SELECT ARRAY[?, ?, ?] AS a" 1 2 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
PostgreSQL also has an "array constructor" for creating arrays from subquery results.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT ARRAY(SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%');
|
||||||
|
```
|
||||||
|
|
||||||
|
As of 2.5.1091, HoneySQL supports this syntax directly:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:array {:select :oid :from :pg_proc :where [:like :proname [:inline "bytea%"]]}]]]})
|
||||||
|
["SELECT ARRAY(SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%')"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Prior to 2.5.1091, you had to use HoneySQL's "as-is" function syntax to circumvent
|
||||||
|
the special syntax:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:'ARRAY {:select :oid :from :pg_proc :where [:like :proname [:inline "bytea%"]]}]]]})
|
||||||
|
["SELECT ARRAY (SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%')"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operators with @, #, and ~
|
||||||
|
|
||||||
|
A number of PostgreSQL operators contain `@`, `#`, or `~` which are not legal in a Clojure keyword or symbol (as literal syntax). The namespace `honey.sql.pg-ops` provides convenient symbolic names for these JSON and regex operators, substituting `at` for `@`, `hash` for `#`, and `tilde` for `~`.
|
||||||
|
|
||||||
|
The regex operators also have more memorable aliases: `regex` for `~`, `iregex` for `~*`, `!regex` for `!~`, and `!iregex` for `!~*`.
|
||||||
|
|
||||||
|
Requiring the namespace automatically registers these operators for use in expressions:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (require '[honey.sql.pg-ops :refer [regex]])
|
||||||
|
nil
|
||||||
|
user=> (sql/format {:select [[[regex :straw [:inline "needle"]] :match]] :from :haystack})
|
||||||
|
["SELECT straw ~ 'needle' AS match FROM haystack"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON/JSONB
|
||||||
|
|
||||||
|
If you are using JSON with PostgreSQL, you will probably try to pass Clojure
|
||||||
|
data structures as values into your HoneySQL DSL -- but HoneySQL will see those
|
||||||
|
vectors as function calls and hash maps as SQL statements, so you need to tell
|
||||||
|
HoneySQL not to do that. There are two possible approaches:
|
||||||
|
|
||||||
|
1. Use named parameters (e.g., `[:param :myval]`) instead of having the values directly in the DSL structure and then pass `{:params {:myval some-json}}` as part of the options in the call to `format`, or
|
||||||
|
2. Use `[:lift ..]` wrapped around any structured values which tells HoneySQL not to interpret the vector or hash map value as a DSL: `[:lift some-json]`.
|
||||||
|
|
||||||
## Upsert
|
## Upsert
|
||||||
|
|
||||||
Upserting data is relatively easy in PostgreSQL
|
Upserting data is relatively easy in PostgreSQL
|
||||||
|
|
@ -25,11 +106,16 @@ user=> (-> (insert-into :distributors)
|
||||||
(upsert (-> (on-conflict :did)
|
(upsert (-> (on-conflict :did)
|
||||||
(do-update-set :dname)))
|
(do-update-set :dname)))
|
||||||
(returning :*)
|
(returning :*)
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?)
|
INSERT INTO distributors (did, dname)
|
||||||
ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *"
|
VALUES (?, ?), (?, ?)
|
||||||
5 "Gizmo Transglobal" 6 "Associated Computing, Inc"]
|
ON CONFLICT (did)
|
||||||
|
DO UPDATE SET dname = EXCLUDED.dname
|
||||||
|
RETURNING *
|
||||||
|
"
|
||||||
|
5 "Gizmo Transglobal"
|
||||||
|
6 "Associated Computing, Inc"]
|
||||||
```
|
```
|
||||||
|
|
||||||
However, the nested `upsert` helper is no longer needed
|
However, the nested `upsert` helper is no longer needed
|
||||||
|
|
@ -42,11 +128,16 @@ user=> (-> (insert-into :distributors)
|
||||||
(on-conflict :did)
|
(on-conflict :did)
|
||||||
(do-update-set :dname)
|
(do-update-set :dname)
|
||||||
(returning :*)
|
(returning :*)
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?)
|
INSERT INTO distributors (did, dname)
|
||||||
ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *"
|
VALUES (?, ?), (?, ?)
|
||||||
5 "Gizmo Transglobal" 6 "Associated Computing, Inc"]
|
ON CONFLICT (did)
|
||||||
|
DO UPDATE SET dname = EXCLUDED.dname
|
||||||
|
RETURNING *
|
||||||
|
"
|
||||||
|
5 "Gizmo Transglobal"
|
||||||
|
6 "Associated Computing, Inc"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Similarly, the `do-nothing` helper behaves just the same
|
Similarly, the `do-nothing` helper behaves just the same
|
||||||
|
|
@ -57,11 +148,14 @@ user=> (-> (insert-into :distributors)
|
||||||
(values [{:did 7 :dname "Redline GmbH"}])
|
(values [{:did 7 :dname "Redline GmbH"}])
|
||||||
(upsert (-> (on-conflict :did)
|
(upsert (-> (on-conflict :did)
|
||||||
do-nothing))
|
do-nothing))
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["INSERT INTO distributors (did, dname) VALUES (?, ?)
|
INSERT INTO distributors (did, dname)
|
||||||
ON CONFLICT (did) DO NOTHING"
|
VALUES (?, ?)
|
||||||
7 "Redline GmbH"]
|
ON CONFLICT (did)
|
||||||
|
DO NOTHING
|
||||||
|
"
|
||||||
|
7 "Redline GmbH"]
|
||||||
```
|
```
|
||||||
|
|
||||||
As above, the nested `upsert` helper is no longer needed:
|
As above, the nested `upsert` helper is no longer needed:
|
||||||
|
|
@ -71,15 +165,18 @@ user=> (-> (insert-into :distributors)
|
||||||
(values [{:did 7 :dname "Redline GmbH"}])
|
(values [{:did 7 :dname "Redline GmbH"}])
|
||||||
(on-conflict :did)
|
(on-conflict :did)
|
||||||
do-nothing
|
do-nothing
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["INSERT INTO distributors (did, dname) VALUES (?, ?)
|
INSERT INTO distributors (did, dname)
|
||||||
ON CONFLICT (did) DO NOTHING"
|
VALUES (?, ?)
|
||||||
7 "Redline GmbH"]
|
ON CONFLICT (did)
|
||||||
|
DO NOTHING
|
||||||
|
"
|
||||||
|
7 "Redline GmbH"]
|
||||||
```
|
```
|
||||||
|
|
||||||
`ON CONSTRAINT` is handled slightly differently to the nilenso library,
|
`ON CONSTRAINT` is handled slightly differently to the nilenso library,
|
||||||
with provided a single `on-conflict-constraint` helper (and clause):
|
which provided a single `on-conflict-constraint` helper (and clause):
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
user=> (-> (insert-into :distributors)
|
user=> (-> (insert-into :distributors)
|
||||||
|
|
@ -87,22 +184,29 @@ user=> (-> (insert-into :distributors)
|
||||||
;; can specify as a nested clause...
|
;; can specify as a nested clause...
|
||||||
(on-conflict (on-constraint :distributors_pkey))
|
(on-conflict (on-constraint :distributors_pkey))
|
||||||
do-nothing
|
do-nothing
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["INSERT INTO distributors (did, dname) VALUES (?, ?)
|
INSERT INTO distributors (did, dname)
|
||||||
ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING"
|
VALUES (?, ?)
|
||||||
9 "Antwerp Design"]
|
ON CONFLICT ON CONSTRAINT distributors_pkey
|
||||||
|
DO NOTHING
|
||||||
|
"
|
||||||
|
9 "Antwerp Design"]
|
||||||
user=> (-> (insert-into :distributors)
|
user=> (-> (insert-into :distributors)
|
||||||
(values [{:did 9 :dname "Antwerp Design"}])
|
(values [{:did 9 :dname "Antwerp Design"}])
|
||||||
;; ...or as two separate clauses
|
;; ...or as two separate clauses
|
||||||
on-conflict
|
on-conflict
|
||||||
(on-constraint :distributors_pkey)
|
(on-constraint :distributors_pkey)
|
||||||
do-nothing
|
do-nothing
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["INSERT INTO distributors (did, dname) VALUES (?, ?)
|
INSERT INTO distributors (did, dname)
|
||||||
ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING"
|
VALUES (?, ?)
|
||||||
9 "Antwerp Design"]
|
ON CONFLICT
|
||||||
|
ON CONSTRAINT distributors_pkey
|
||||||
|
DO NOTHING
|
||||||
|
"
|
||||||
|
9 "Antwerp Design"]
|
||||||
```
|
```
|
||||||
|
|
||||||
As above, the `upsert` helper has been omitted here.
|
As above, the `upsert` helper has been omitted here.
|
||||||
|
|
@ -115,12 +219,14 @@ user=> (-> (insert-into :user)
|
||||||
(values [{:phone "5555555" :name "John"}])
|
(values [{:phone "5555555" :name "John"}])
|
||||||
(on-conflict :phone (where [:<> :phone nil]))
|
(on-conflict :phone (where [:<> :phone nil]))
|
||||||
(do-update-set :phone :name (where [:= :user.active false]))
|
(do-update-set :phone :name (where [:= :user.active false]))
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["INSERT INTO user (phone, name) VALUES (?, ?)
|
INSERT INTO user (phone, name)
|
||||||
ON CONFLICT (phone) WHERE phone IS NOT NULL
|
VALUES (?, ?)
|
||||||
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name
|
ON CONFLICT (phone) WHERE phone IS NOT NULL
|
||||||
WHERE user.active = FALSE" "5555555" "John"]
|
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE
|
||||||
|
"
|
||||||
|
"5555555" "John"]
|
||||||
;; using the DSL directly:
|
;; using the DSL directly:
|
||||||
user=> (sql/format
|
user=> (sql/format
|
||||||
{:insert-into :user
|
{:insert-into :user
|
||||||
|
|
@ -128,16 +234,20 @@ user=> (sql/format
|
||||||
:on-conflict [:phone
|
:on-conflict [:phone
|
||||||
{:where [:<> :phone nil]}]
|
{:where [:<> :phone nil]}]
|
||||||
:do-update-set {:fields [:phone :name]
|
:do-update-set {:fields [:phone :name]
|
||||||
:where [:= :user.active false]}})
|
:where [:= :user.active false]}}
|
||||||
;; newlines inserted for readability:
|
{:pretty true})
|
||||||
["INSERT INTO user (phone, name) VALUES (?, ?)
|
["
|
||||||
ON CONFLICT (phone) WHERE phone IS NOT NULL
|
INSERT INTO user (phone, name)
|
||||||
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name
|
VALUES (?, ?)
|
||||||
WHERE user.active = FALSE" "5555555" "John"]
|
ON CONFLICT (phone) WHERE phone IS NOT NULL
|
||||||
|
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE
|
||||||
|
"
|
||||||
|
"5555555" "John"]
|
||||||
```
|
```
|
||||||
|
|
||||||
By comparison, this is the DSL structure that nilenso would have required:
|
By comparison, this is the DSL structure that nilenso would have required:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
;; NOT VALID FOR HONEYSQL!
|
;; NOT VALID FOR HONEYSQL!
|
||||||
{:insert-into :user
|
{:insert-into :user
|
||||||
|
|
@ -151,8 +261,103 @@ By comparison, this is the DSL structure that nilenso would have required:
|
||||||
:where [:= :user.active false]}}}
|
:where [:= :user.active false]}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
All of the examples for `:do-update-set` so far provide one or
|
||||||
|
more columns and generated `SET` clauses using `EXCLUDED` columns.
|
||||||
|
You can also perform regular `SET` operations, where the right-hand
|
||||||
|
side is a full SQL expression by specifying a hash map of column /
|
||||||
|
expression pairs, like you would for a regular `:set` clause:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (-> (insert-into :table)
|
||||||
|
(values [{:id "id" :counter 1}])
|
||||||
|
(on-conflict :id)
|
||||||
|
(do-update-set {:counter [:+ :table.counter 1]})
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
["
|
||||||
|
INSERT INTO table (id, counter)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT (id)
|
||||||
|
DO UPDATE SET counter = table.counter + ?
|
||||||
|
" "id" 1 1]
|
||||||
|
;; using the DSL directly:
|
||||||
|
user=> (-> {:insert-into :table
|
||||||
|
:values [{:id "id" :counter 1}]
|
||||||
|
:on-conflict :id
|
||||||
|
:do-update-set {:counter [:+ :table.counter 1]}}
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
["
|
||||||
|
INSERT INTO table (id, counter)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT (id)
|
||||||
|
DO UPDATE SET counter = table.counter + ?
|
||||||
|
" "id" 1 1]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use `:EXCLUDED.column` in a hash map to produce the
|
||||||
|
same effect as `:column` in a vector:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (-> (insert-into :table)
|
||||||
|
(values [{:id "id" :counter 1}])
|
||||||
|
(on-conflict :id)
|
||||||
|
(do-update-set {:name :EXCLUDED.name
|
||||||
|
:counter [:+ :table.counter 1]})
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
["
|
||||||
|
INSERT INTO table (id, counter)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT (id)
|
||||||
|
DO UPDATE SET name = EXCLUDED.name, counter = table.counter + ?
|
||||||
|
" "id" 1 1]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need to combine a `DO UPDATE SET` hash map expression
|
||||||
|
with a `WHERE` clause, you need to explicitly use the `:fields` /
|
||||||
|
`:where` format explained above. Here's how those two examples
|
||||||
|
look with a `WHERE` clause added:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (-> (insert-into :table)
|
||||||
|
(values [{:id "id" :counter 1}])
|
||||||
|
(on-conflict :id)
|
||||||
|
(do-update-set {:fields {:counter [:+ :table.counter 1]}
|
||||||
|
:where [:> :table.counter 1]})
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
["
|
||||||
|
INSERT INTO table (id, counter)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT (id)
|
||||||
|
DO UPDATE SET counter = table.counter + ? WHERE table.counter > ?
|
||||||
|
" "id" 1 1 1]
|
||||||
|
;; using the DSL directly:
|
||||||
|
user=> (-> {:insert-into :table
|
||||||
|
:values [{:id "id" :counter 1}]
|
||||||
|
:on-conflict :id
|
||||||
|
:do-update-set {:fields {:counter [:+ :table.counter 1]}
|
||||||
|
:where [:> :table.counter 1]}}
|
||||||
|
(sql/format {:pretty true}))
|
||||||
|
["
|
||||||
|
INSERT INTO table (id, counter)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT (id)
|
||||||
|
DO UPDATE SET counter = table.counter + ? WHERE table.counter > ?
|
||||||
|
" "id" 1 1 1]
|
||||||
|
```
|
||||||
|
|
||||||
## INSERT INTO AS
|
## INSERT INTO AS
|
||||||
|
|
||||||
|
HoneySQL supports aliases directly in `:insert-into` so no special
|
||||||
|
clause is needed for this any more:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format (-> (insert-into :table :alias)
|
||||||
|
(values [[1 2 3] [4 5 6]])))
|
||||||
|
["INSERT INTO table AS alias VALUES (?, ?, ?), (?, ?, ?)" 1 2 3 4 5 6]
|
||||||
|
user=> (sql/format {:insert-into [:table :alias],
|
||||||
|
:values [[1 2 3] [4 5 6]]})
|
||||||
|
["INSERT INTO table AS alias VALUES (?, ?, ?), (?, ?, ?)" 1 2 3 4 5 6]
|
||||||
|
```
|
||||||
|
|
||||||
## Returning
|
## Returning
|
||||||
|
|
||||||
The `RETURNING` clause is supported identically to the nilenso library:
|
The `RETURNING` clause is supported identically to the nilenso library:
|
||||||
|
|
@ -167,9 +372,9 @@ user=> (sql/format {:delete-from :distributors
|
||||||
user=> (-> (update :distributors)
|
user=> (-> (update :distributors)
|
||||||
(set {:dname "Foo Bar Designs"})
|
(set {:dname "Foo Bar Designs"})
|
||||||
(where [:= :did 2])
|
(where [:= :did 2])
|
||||||
(returning [:did :dname])
|
(returning :did :dname)
|
||||||
sql/format)
|
sql/format)
|
||||||
["UPDATE distributors SET dname = ? WHERE did = ? RETURNING did dname"
|
["UPDATE distributors SET dname = ? WHERE did = ? RETURNING did, dname"
|
||||||
"Foo Bar Designs" 2]
|
"Foo Bar Designs" 2]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -206,15 +411,15 @@ user=> (-> (create-table :cities)
|
||||||
;; default values for columns:
|
;; default values for columns:
|
||||||
user=> (-> (create-table :distributors)
|
user=> (-> (create-table :distributors)
|
||||||
(with-columns [[:did :integer [:primary-key]
|
(with-columns [[:did :integer [:primary-key]
|
||||||
;; "serial" is inlined as 'SERIAL':
|
;; "serial" is inlined as 'serial':
|
||||||
[:default [:nextval "serial"]]]
|
[:default [:nextval "serial"]]]
|
||||||
[:name [:varchar 40] [:not nil]]])
|
[:name [:varchar 40] [:not nil]]])
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
;; newlines inserted for readability:
|
||||||
["CREATE TABLE distributors (
|
["
|
||||||
did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'),
|
CREATE TABLE distributors
|
||||||
name VARCHAR(40) NOT NULL
|
(did INTEGER PRIMARY KEY DEFAULT NEXTVAL('serial'), name VARCHAR(40) NOT NULL)
|
||||||
)"]
|
"]
|
||||||
;; PostgreSQL CHECK constraint is supported:
|
;; PostgreSQL CHECK constraint is supported:
|
||||||
user=> (-> (create-table :products)
|
user=> (-> (create-table :products)
|
||||||
(with-columns [[:product_no :integer]
|
(with-columns [[:product_no :integer]
|
||||||
|
|
@ -222,20 +427,16 @@ user=> (-> (create-table :products)
|
||||||
[:price :numeric [:check [:> :price 0]]]
|
[:price :numeric [:check [:> :price 0]]]
|
||||||
[:discounted_price :numeric]
|
[:discounted_price :numeric]
|
||||||
[[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]])
|
[[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]])
|
||||||
sql/format)
|
(sql/format {:pretty true}))
|
||||||
;; newlines inserted for readability:
|
["
|
||||||
["CREATE TABLE products (
|
CREATE TABLE products
|
||||||
product_no INTEGER,
|
(product_no INTEGER, name TEXT, price NUMERIC CHECK(price > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))
|
||||||
name TEXT,
|
"]
|
||||||
price NUMERIC CHECK(PRICE > 0),
|
|
||||||
discounted_price NUMERIC,
|
|
||||||
CHECK((discounted_price > 0) AND (price > discounted_price))
|
|
||||||
)"]
|
|
||||||
;; conditional creation:
|
;; conditional creation:
|
||||||
user=> (-> (create-table :products :if-not-exists)
|
user=> (-> (create-table :products :if-not-exists)
|
||||||
...
|
(with-columns [[:name :text]])
|
||||||
sql/format)
|
sql/format)
|
||||||
["CREATE TABLE IF NOT EXISTS products (...)"]
|
["CREATE TABLE IF NOT EXISTS products (name TEXT)"]
|
||||||
;; drop table:
|
;; drop table:
|
||||||
user=> (sql/format (drop-table :cities))
|
user=> (sql/format (drop-table :cities))
|
||||||
["DROP TABLE cities"]
|
["DROP TABLE cities"]
|
||||||
|
|
@ -255,11 +456,6 @@ user=> (-> (alter-table :fruit)
|
||||||
(drop-column :skin)
|
(drop-column :skin)
|
||||||
sql/format)
|
sql/format)
|
||||||
["ALTER TABLE fruit DROP COLUMN skin"]
|
["ALTER TABLE fruit DROP COLUMN skin"]
|
||||||
;; alter table modify column:
|
|
||||||
user=> (-> (alter-table :fruit)
|
|
||||||
(modify-column :name [:varchar 64] [:not nil])
|
|
||||||
sql/format)
|
|
||||||
["ALTER TABLE fruit MODIFY COLUMN name VARCHAR(64) NOT NULL"]
|
|
||||||
;; alter table rename column:
|
;; alter table rename column:
|
||||||
user=> (-> (alter-table :fruit)
|
user=> (-> (alter-table :fruit)
|
||||||
(rename-column :cost :price)
|
(rename-column :cost :price)
|
||||||
|
|
@ -272,6 +468,29 @@ user=> (-> (alter-table :fruit)
|
||||||
["ALTER TABLE fruit RENAME TO vegetable"]
|
["ALTER TABLE fruit RENAME TO vegetable"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The following does not work for PostgreSQL, but does work for several other databases:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; alter table alter column:
|
||||||
|
user=> (-> (alter-table :fruit)
|
||||||
|
(alter-column :name [:varchar 64] [:not nil])
|
||||||
|
sql/format)
|
||||||
|
["ALTER TABLE fruit ALTER COLUMN name VARCHAR(64) NOT NULL"]
|
||||||
|
```
|
||||||
|
|
||||||
|
For PostgreSQL, you need separate statements:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (-> (alter-table :fruit)
|
||||||
|
(alter-column :name :type [:varchar 64])
|
||||||
|
sql/format)
|
||||||
|
["ALTER TABLE fruit ALTER COLUMN name TYPE VARCHAR(64)"]
|
||||||
|
user=> (-> (alter-table :fruit)
|
||||||
|
(alter-column :name :set [:not nil])
|
||||||
|
sql/format)
|
||||||
|
["ALTER TABLE fruit ALTER COLUMN name SET NOT NULL"]
|
||||||
|
```
|
||||||
|
|
||||||
The following PostgreSQL-specific DDL statements are supported
|
The following PostgreSQL-specific DDL statements are supported
|
||||||
(with the same syntax as the nilenso library but `sql/format`
|
(with the same syntax as the nilenso library but `sql/format`
|
||||||
takes slightly different options):
|
takes slightly different options):
|
||||||
|
|
@ -318,10 +537,20 @@ user=> (-> (alter-table :fruit)
|
||||||
user=> (sql/format (alter-table :fruit
|
user=> (sql/format (alter-table :fruit
|
||||||
(add-column :skin [:varchar 16] nil)
|
(add-column :skin [:varchar 16] nil)
|
||||||
(add-index :unique :fruit-name :name)))
|
(add-index :unique :fruit-name :name)))
|
||||||
;; newlines inserted for readability:
|
["ALTER TABLE fruit ADD COLUMN skin VARCHAR(16) NULL, ADD UNIQUE fruit_name(name)"]
|
||||||
["ALTER TABLE fruit
|
|
||||||
ADD COLUMN skin VARCHAR(16) NULL,
|
|
||||||
ADD UNIQUE fruit_name(name)"]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Filter / Within Group
|
||||||
|
|
||||||
|
`honeysql-postgres` added support for `FILTER` and `WITHIN GROUP`
|
||||||
|
in its 0.4.112 release. Those features have been integrated into
|
||||||
|
HoneySQL 2.x (as of 2.0.0-beta2), along with support for `ORDER BY`
|
||||||
|
in expressions. `:filter`, `:within-group`, and `:order-by` are
|
||||||
|
all available as "functions" in [Special Syntax](special-syntax.md),
|
||||||
|
and there are helpers for `filter` and `within-group`.
|
||||||
|
|
||||||
## Window / Partition Support
|
## Window / Partition Support
|
||||||
|
|
||||||
|
HoneySQL supports `:window`, `:partition-by`, and `:over`
|
||||||
|
directly now.
|
||||||
|
See the Clause Reference for examples of [WINDOW, PARTITION BY, and OVER](clause-reference.md#window-partition-by-and-over).
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,127 @@ This section lists the function-like expressions that
|
||||||
HoneySQL supports out of the box which are formatted
|
HoneySQL supports out of the box which are formatted
|
||||||
as special syntactic forms.
|
as special syntactic forms.
|
||||||
|
|
||||||
The first group are used for SQL expressions. The second (last group) are used primarily in column definitions (as part of `:with-columns` and `:add-column` / `:modify-column`).
|
The first group are used for SQL expressions. The second (last group) are used primarily in column definitions (as part of `:with-columns` and `:add-column` / `:alter-column`).
|
||||||
|
|
||||||
|
The examples in this section assume the following:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(require '[honey.sql :as sql])
|
||||||
|
```
|
||||||
|
|
||||||
|
## alias
|
||||||
|
|
||||||
|
Accepts a single argument which should be an alias name (from an `AS` clause
|
||||||
|
elsewhere in the overall SQL statement) and uses alias formatting rules rather
|
||||||
|
than table/column formatting rules (different handling of dots and hyphens).
|
||||||
|
This allows you to override HoneySQL's default assumption about entity names
|
||||||
|
and strings.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [[:column-name "some-alias"]]
|
||||||
|
:from :b
|
||||||
|
:order-by [[[:alias "some-alias"]]]})
|
||||||
|
;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
|
||||||
|
(sql/format {:select [[:column-name :'some-alias]]
|
||||||
|
:from :b
|
||||||
|
:order-by [[[:alias :'some-alias]]]})
|
||||||
|
;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
|
||||||
|
(sql/format {:select [[:column-name "some-alias"]]
|
||||||
|
:from :b
|
||||||
|
:group-by [[:alias "some-alias"]]})
|
||||||
|
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
|
||||||
|
(sql/format {:select [[:column-name "some-alias"]]
|
||||||
|
:from :b
|
||||||
|
:group-by [[:alias :'some-alias]]})
|
||||||
|
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
|
||||||
|
```
|
||||||
|
|
||||||
## array
|
## array
|
||||||
|
|
||||||
Accepts a single argument, which is expected to evaluate to
|
Accepts either an expression (that evaluates to a sequence) or a subquery
|
||||||
a sequence, and produces `ARRAY[?, ?, ..]` for the elements
|
(hash map). In the expression case, also accepts an optional second argument
|
||||||
of that sequence (as SQL parameters):
|
that specifies the type of the array.
|
||||||
|
|
||||||
|
Produces either an `ARRAY[..]` or an `ARRAY(subquery)` expression.
|
||||||
|
|
||||||
|
In the expression case, produces `ARRAY[?, ?, ..]` for the elements of that
|
||||||
|
sequence (as SQL parameters):
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format-expr [:array (range 5)])
|
(sql/format-expr [:array (range 5)])
|
||||||
;;=> ["ARRAY[?, ?, ?, ?, ?]" 0 1 2 3 4]
|
;;=> ["ARRAY[?, ?, ?, ?, ?]" 0 1 2 3 4]
|
||||||
|
(sql/format-expr [:array (range 3) :text])
|
||||||
|
;;=> ["ARRAY[?, ?, ?]::TEXT[]" 0 1 2]
|
||||||
|
(sql/format-expr [:array [] :integer])
|
||||||
|
;;=> ["ARRAY[]::INTEGER[]"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## between
|
> Note: you cannot provide a named parameter as the argument for `:array` because the generated SQL depends on the number of elements in the sequence, so the following throws an exception:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [[[:array :?tags] :arr]]} {:params {:tags [1 2 3]}})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can do the following instead:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(let [tags [1 2 3]]
|
||||||
|
(sql/format {:select [[[:array tags] :arr]]} {:inline true}))
|
||||||
|
;;=> ["SELECT ARRAY[1, 2, 3] AS arr"]
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, the argument to `:array` is treated as a literal sequence of Clojure values and is **not** interpreted as a HoneySQL expression, so you must use the `{:inline true}` formatting option as shown above rather than try to inline the values like this:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [[[:array [:inline [1 2 3]]] :arr]]})
|
||||||
|
;;=> ["SELECT ARRAY[inline, (?, ?, ?)] AS arr" 1 2 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
In the subquery case, produces `ARRAY(subquery)`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [[[:array {:select :* :from :table}] :arr]]})
|
||||||
|
;;=> ["SELECT ARRAY(SELECT * FROM table) AS arr"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## at
|
||||||
|
|
||||||
|
If addition to dot navigation (for JSON) -- see the `.` and `.:.` syntax below --
|
||||||
|
HoneySQL also supports bracket notation for JSON navigation.
|
||||||
|
|
||||||
|
The first argument to `:at` is treated as an expression that identifies
|
||||||
|
the column, and subsequent arguments are treated as field names or array
|
||||||
|
indices to navigate into that document.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:at :col :field1 :field2]]]})
|
||||||
|
["SELECT col.field1.field2"]
|
||||||
|
user=> (sql/format {:select [[[:at :table.col 0 :field]]]})
|
||||||
|
["SELECT table.col[0].field"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want an array index to be a parameter, use `:lift`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:at :col [:lift 0] :field]]]})
|
||||||
|
["SELECT col[?].field" 0]
|
||||||
|
```
|
||||||
|
|
||||||
|
## at time zone
|
||||||
|
|
||||||
|
Accepts two arguments: an expression (assumed to be a date/time of some sort)
|
||||||
|
and a time zone name or identifier (can be a string, a symbol, or a keyword):
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format-expr [:at-time-zone [:now] :UTC])
|
||||||
|
;;=> ["NOW() AT TIME ZONE 'UTC'"]
|
||||||
|
```
|
||||||
|
|
||||||
|
The time zone name or identifier will be inlined (as a string) and therefore
|
||||||
|
cannot be an expression.
|
||||||
|
|
||||||
|
## between and not-between
|
||||||
|
|
||||||
Accepts three arguments: an expression, a lower bound, and
|
Accepts three arguments: an expression, a lower bound, and
|
||||||
an upper bound:
|
an upper bound:
|
||||||
|
|
@ -25,6 +132,9 @@ an upper bound:
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format-expr [:between :id 1 100])
|
(sql/format-expr [:between :id 1 100])
|
||||||
;;=> ["id BETWEEN ? AND ?" 1 100]
|
;;=> ["id BETWEEN ? AND ?" 1 100]
|
||||||
|
|
||||||
|
(sql/format-expr [:not-between :id 1 100])
|
||||||
|
;;=> ["id NOT BETWEEN ? AND ?" 1 100]
|
||||||
```
|
```
|
||||||
|
|
||||||
## case
|
## case
|
||||||
|
|
@ -36,20 +146,58 @@ may be `:else` (or `'else`) to produce `ELSE`, otherwise
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format-expr [:case [:< :a 10] "small" [:> :a 100] "big" :else "medium"])
|
(sql/format-expr [:case [:< :a 10] "small" [:> :a 100] "big" :else "medium"])
|
||||||
;;=> ["CASE WHEN a < ? THEN ? WHEN a > ? THEN ? ELSE ? END"
|
;; => ["CASE WHEN a < ? THEN ? WHEN a > ? THEN ? ELSE ? END" 10 "small" 100 "big" "medium"]
|
||||||
;; 10 "small" 100 "big" "medium"]
|
```
|
||||||
|
|
||||||
|
Google BigQuery supports a variant of `CASE` that takes an expression and then the `WHEN`
|
||||||
|
clauses contain expressions to match against, rather than conditions. HoneySQL supports
|
||||||
|
this using `:case-expr`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format-expr [:case-expr :a 10 "small" 100 "big" :else "medium"])
|
||||||
|
;; => ["CASE a WHEN ? THEN ? WHEN ? THEN ? ELSE ? END" 10 "small" 100 "big" "medium"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## cast
|
## cast
|
||||||
|
|
||||||
A SQL CAST expression. Expects an expression and something
|
A SQL `CAST` expression. Expects an expression and something
|
||||||
that produces a SQL type:
|
that produces a SQL type:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format-expr [:cast :a :int])
|
(sql/format [:cast :a :int])
|
||||||
;;=> ["CAST(a AS int)"]
|
;;=> ["CAST(a AS INT)"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Quoting does not affect the type in a `CAST`, only the expression:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format [:cast :a :int] {:quoted true})
|
||||||
|
;;=> ["CAST(\"a\" AS INT)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
A hyphen (`-`) in the type name becomes a space:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format [:cast :a :double-precision])
|
||||||
|
;;=> ["CAST(a AS DOUBLE PRECISION)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want an underscore in the type name, you have two choices:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format [:cast :a :some_type])
|
||||||
|
;;=> ["CAST(a AS SOME_TYPE)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format [:cast :a :'some-type])
|
||||||
|
;;=> ["CAST(a AS some_type)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: In HoneySQL 2.4.947 and earlier, the type name was incorrectly affected by the quoting feature, and a hyphen in a type name was incorrectly changed to underscore. This was corrected in 2.4.962.
|
||||||
|
|
||||||
## composite
|
## composite
|
||||||
|
|
||||||
Accepts any number of expressions and produces a composite
|
Accepts any number of expressions and produces a composite
|
||||||
|
|
@ -60,10 +208,146 @@ expression (comma-separated, wrapped in parentheses):
|
||||||
;;=> ["(a, b, ?, x + ?)" "red" 1]
|
;;=> ["(a, b, ?, x + ?)" "red" 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This can be useful in a number of situations where you want a composite
|
||||||
|
value, as above, or a composite based on or declaring columns names:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [[[:composite :a :b] :c]] :from :table})
|
||||||
|
;;=> ["SELECT (a, b) AS c FROM table"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:update :table :set {:a :v.a}
|
||||||
|
:from [[{:values [[1 2 3]
|
||||||
|
[4 5 6]]}
|
||||||
|
[:v [:composite :a :b :c]]]]
|
||||||
|
:where [:and [:= :x :v.b] [:> :y :v.c]]})
|
||||||
|
;;=> ["UPDATE table SET a = v.a FROM (VALUES (?, ?, ?), (?, ?, ?)) AS v (a, b, c) WHERE (x = v.b) AND (y > v.c)" 1 2 3 4 5 6]
|
||||||
|
```
|
||||||
|
|
||||||
|
## distinct
|
||||||
|
|
||||||
|
Accepts a single expression and prefixes it with `DISTINCT `:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [ [[:count [:distinct :status]] :n] ] :from :table})
|
||||||
|
;;=> ["SELECT COUNT(DISTINCT status) AS n FROM table"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## dot . .:.
|
||||||
|
|
||||||
|
Accepts an expression and one or more fields (or columns). Plain dot produces
|
||||||
|
plain dotted selection:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [ [[:. :t :c]] [[:. :s :t :c]] ]})
|
||||||
|
;;=> ["SELECT t.c, s.t.c"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Dot colon dot produces Snowflake-style dotted selection:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [ [[:.:. :t :c]] [[:.:. :s :t :c]] ]})
|
||||||
|
;;=> ["SELECT t:c, s:t.c"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Can be used with `:nest` for field selection from composites:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [ [[:. [:nest :v] :*]] [[:. [:nest [:myfunc :x]] :y]] ]})
|
||||||
|
;;=> ["SELECT (v).*, (MYFUNC(x)).y"]
|
||||||
|
```
|
||||||
|
|
||||||
|
See also [`get-in`](xtdb.md#object-navigation-expressions)
|
||||||
|
and [`at`](#at) for additional path navigation functions.
|
||||||
|
|
||||||
|
## entity
|
||||||
|
|
||||||
|
Accepts a single keyword or symbol argument and produces a
|
||||||
|
SQL entity. This is intended for use in contexts that would
|
||||||
|
otherwise produce a sequence of SQL keywords, such as when
|
||||||
|
constructing DDL statements.
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
|
```clojure
|
||||||
|
[:tablespace :quux]
|
||||||
|
;;=> TABLESPACE QUUX
|
||||||
|
[:tablespace [:entity :quux]]
|
||||||
|
;;=> TABLESPACE quux
|
||||||
|
```
|
||||||
|
|
||||||
|
## escape
|
||||||
|
|
||||||
|
Intended to be used with regular expression patterns to
|
||||||
|
specify the escape characters (if any).
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select :* :from :foo
|
||||||
|
:where [:similar-to :foo [:escape "bar" [:inline "*"]]]})
|
||||||
|
;;=> ["SELECT * FROM foo WHERE foo SIMILAR TO ? ESCAPE '*'" "bar"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## filter, within-group
|
||||||
|
|
||||||
|
Used to produce PostgreSQL's `FILTER` and `WITHIN GROUP` expressions.
|
||||||
|
See also **order-by** below.
|
||||||
|
|
||||||
|
These both accept a SQL expression followed by a SQL clause.
|
||||||
|
Filter generally expects an aggregate expression and a `WHERE` clause.
|
||||||
|
Within group generally expects an aggregate expression and an `ORDER BY` clause.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [:a :b [[:filter :%count.* {:where [:< :x 100]}] :c]
|
||||||
|
[[:within-group [:percentile_disc [:inline 0.25]]
|
||||||
|
{:order-by [:a]}] :inter_max]
|
||||||
|
[[:within-group [:percentile_cont [:inline 0.25]]
|
||||||
|
{:order-by [:a]}] :abs_max]]
|
||||||
|
:from :aa}
|
||||||
|
{:pretty true})
|
||||||
|
;;=> ["
|
||||||
|
SELECT a, b, COUNT(*) FILTER (WHERE x < ?) AS c, PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY a ASC) AS inter_max, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY a ASC) AS abs_max
|
||||||
|
FROM aa
|
||||||
|
"
|
||||||
|
100]
|
||||||
|
```
|
||||||
|
|
||||||
|
There are helpers for both `filter` and `within-group`. Be careful with `filter`
|
||||||
|
since it shadows `clojure.core/filter`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(refer-clojure :exclude '[filter])
|
||||||
|
(require '[honey.sql.helpers :refer [select filter within-group from order-by where]])
|
||||||
|
|
||||||
|
(sql/format (-> (select :a :b [(filter :%count.* (where :< :x 100)) :c]
|
||||||
|
[(within-group [:percentile_disc [:inline 0.25]]
|
||||||
|
(order-by :a)) :inter_max]
|
||||||
|
[(within-group [:percentile_cont [:inline 0.25]]
|
||||||
|
(order-by :a)) :abs_max])
|
||||||
|
(from :aa))
|
||||||
|
{:pretty true})
|
||||||
|
;;=> ["
|
||||||
|
SELECT a, b, COUNT(*) FILTER (WHERE x < ?) AS c, PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY a ASC) AS inter_max, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY a ASC) AS abs_max
|
||||||
|
FROM aa
|
||||||
|
"
|
||||||
|
100]
|
||||||
|
```
|
||||||
|
|
||||||
|
## ignore/respect nulls
|
||||||
|
|
||||||
|
Both of these accept a single argument -- an expression -- and
|
||||||
|
renders that expression followed by `IGNORE NULLS` or `RESPECT NULLS`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format-expr [:array_agg [:ignore-nulls :a]])
|
||||||
|
;;=> ["ARRAY_AGG(a IGNORE NULLS)"]
|
||||||
|
(sql/format-expr [:array_agg [:respect-nulls :a]])
|
||||||
|
;;=> ["ARRAY_AGG(a RESPECT NULLS)"]
|
||||||
|
```
|
||||||
|
|
||||||
## inline
|
## inline
|
||||||
|
|
||||||
Accepts a single argument and tries to render it as a
|
Accepts one or more arguments and tries to render them as a
|
||||||
SQL value directly in the formatted SQL string rather
|
SQL values directly in the formatted SQL string rather
|
||||||
than turning it into a positional parameter:
|
than turning it into a positional parameter:
|
||||||
* `nil` becomes `NULL`
|
* `nil` becomes `NULL`
|
||||||
* keywords and symbols become upper case entities (with `-` replaced by space)
|
* keywords and symbols become upper case entities (with `-` replaced by space)
|
||||||
|
|
@ -76,16 +360,80 @@ than turning it into a positional parameter:
|
||||||
;;=> ["WHERE x = 'foo'"]
|
;;=> ["WHERE x = 'foo'"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If multiple arguments are provided, they are individually formatted as above
|
||||||
|
and joined into a single SQL string with spaces:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:where [:= :x [:inline :DATE "2019-01-01"]]})
|
||||||
|
;;=> ["WHERE x = DATE '2019-01-01'"]
|
||||||
|
```
|
||||||
|
|
||||||
|
This is convenient for rendering DATE/TIME/TIMESTAMP literals in SQL.
|
||||||
|
|
||||||
|
If an argument is an expression, it is formatted as a regular SQL expression
|
||||||
|
except that any parameters are inlined:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:where [:= :x [:inline [:date_add [:now] [:interval 30 :days]]]]})
|
||||||
|
;;=> ["WHERE x = DATE_ADD(NOW(), INTERVAL 30 DAYS)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
In particular, that means that you can use `:inline` to inline a parameter
|
||||||
|
value:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:where [:= :x [:inline :?foo]]} {:params {:foo "bar"}})
|
||||||
|
;;=> ["WHERE x = 'bar'"]
|
||||||
|
(sql/format {:where [:= :x [:inline [:param :foo]]]} {:params {:foo "bar"}})
|
||||||
|
;;=> ["WHERE x = 'bar'"]
|
||||||
|
```
|
||||||
|
|
||||||
## interval
|
## interval
|
||||||
|
|
||||||
Accepts two arguments: an expression and a keyword (or a symbol)
|
Accepts one or two arguments: either a string or an expression and
|
||||||
that represents a time unit. Produces an `INTERVAL` expression:
|
a keyword (or a symbol) that represents a time unit.
|
||||||
|
Produces an `INTERVAL` expression:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format-expr [:date_add [:now] [:interval 30 :days]])
|
(sql/format-expr [:date_add [:now] [:interval 30 :days]])
|
||||||
;;=> ["DATE_ADD(NOW(), INTERVAL ? DAYS)" 30]
|
;;=> ["DATE_ADD(NOW(), INTERVAL ? DAYS)" 30]
|
||||||
|
(sql/format-expr [:date_add [:now] [:interval "24 Hours"]])
|
||||||
|
;;=> ["DATE_ADD(NOW(), INTERVAL '24 Hours')"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: PostgreSQL also has an `INTERVAL` data type which is unrelated to this syntax. In PostgreSQL, the closet equivalent would be `[:cast "30 days" :interval]` which will lift `"30 days"` out as a parameter. In DDL, for PostgreSQL, you can use `:interval` to produce the `INTERVAL` data type (without wrapping it in a vector).
|
||||||
|
|
||||||
|
## join
|
||||||
|
|
||||||
|
Accepts a table name (or expression) followed by one or more join clauses.
|
||||||
|
Produces a nested `JOIN` expression, typically used as the table expression of
|
||||||
|
a `JOIN` clause.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:join [[[:join :tbl1 {:left-join [:tbl2 [:using :id]]}]]]})
|
||||||
|
;;=> ["INNER JOIN (tbl1 LEFT JOIN tbl2 USING (id))"]
|
||||||
|
```
|
||||||
|
|
||||||
|
An alias can be provided:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:join [[[:join [:tbl1 :t] {:left-join [:tbl2 [:using :id]]}]]]})
|
||||||
|
;;=> ["INNER JOIN (tbl1 AS t LEFT JOIN tbl2 USING (id))"]
|
||||||
|
```
|
||||||
|
|
||||||
|
To provide an expression, an extra level of `[...]` is needed:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:join [[[:join [[:make_thing 42] :t] {:left-join [:tbl2 [:using :id]]}]]]})
|
||||||
|
;;=> ["INNER JOIN (MAKE_THING(?) AS t LEFT JOIN tbl2 USING (id))" 42]
|
||||||
|
```
|
||||||
|
|
||||||
|
## lateral
|
||||||
|
|
||||||
|
Accepts a single argument that can be a (`SELECT`) clause or
|
||||||
|
a (function call) expression. Produces a `LATERAL` subquery
|
||||||
|
clause based on the `SELECT` clause or the SQL expression.
|
||||||
|
|
||||||
## lift
|
## lift
|
||||||
|
|
||||||
Used to wrap a Clojure value that should be passed as a
|
Used to wrap a Clojure value that should be passed as a
|
||||||
|
|
@ -112,7 +460,13 @@ level of parentheses around it:
|
||||||
;;=> ["WHERE (x = ?)" 42]
|
;;=> ["WHERE (x = ?)" 42]
|
||||||
```
|
```
|
||||||
|
|
||||||
`nest` is also supported as a SQL clause for the same reason.
|
`:nest` is also supported as a SQL clause for the same reason.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; BigQuery requires UNION clauses be parenthesized:
|
||||||
|
(sql/format {:union-all [{:nest {:select :*}} {:nest {:select :*}}]})
|
||||||
|
;;=> ["(SELECT *) UNION ALL (SELECT *)"]
|
||||||
|
```
|
||||||
|
|
||||||
## not
|
## not
|
||||||
|
|
||||||
|
|
@ -123,9 +477,32 @@ in front of it:
|
||||||
(sql/format-expr [:not nil])
|
(sql/format-expr [:not nil])
|
||||||
;;=> ["NOT NULL"]
|
;;=> ["NOT NULL"]
|
||||||
(sql/format-expr [:not [:= :x 42]])
|
(sql/format-expr [:not [:= :x 42]])
|
||||||
;;=> ["NOT x = ?" 42]
|
;;=> ["NOT (x = ?)" 42]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## order-by
|
||||||
|
|
||||||
|
In addition to the `ORDER BY` clause, HoneySQL also supports `ORDER BY`
|
||||||
|
in an expression (for PostgreSQL). It accepts a SQL expression followed
|
||||||
|
by an ordering specifier, which can be an expression or a pair of expression
|
||||||
|
and direction (`:asc` or `:desc`):
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [[[:array_agg [:order-by :a [:b :desc]]]]] :from :table})
|
||||||
|
;;=> ["SELECT ARRAY_AGG(a ORDER BY b DESC) FROM table"]
|
||||||
|
(sql/format (-> (select [[:array_agg [:order-by :a [:b :desc]]]])
|
||||||
|
(from :table)))
|
||||||
|
;;=> ["SELECT ARRAY_AGG(a ORDER BY b DESC) FROM table"]
|
||||||
|
(sql/format {:select [[[:string_agg :a [:order-by [:inline ","] :a]]]] :from :table})
|
||||||
|
;;=> ["SELECT STRING_AGG(a, ',' ORDER BY a ASC) FROM table"]
|
||||||
|
(sql/format (-> (select [[:string_agg :a [:order-by [:inline ","] :a]]])
|
||||||
|
(from :table)))
|
||||||
|
;;=> ["SELECT STRING_AGG(a, ',' ORDER BY a ASC) FROM table"]
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no helper for the `ORDER BY` special syntax: the `order-by` helper
|
||||||
|
only produces a SQL clause.
|
||||||
|
|
||||||
## over
|
## over
|
||||||
|
|
||||||
This is intended to be used with the `:window` and `:partition-by` clauses.
|
This is intended to be used with the `:window` and `:partition-by` clauses.
|
||||||
|
|
@ -182,9 +559,17 @@ parameters from them:
|
||||||
(sql/format {:select [:a [[:raw ["@var := " [:inline "foo"]]]]]})
|
(sql/format {:select [:a [[:raw ["@var := " [:inline "foo"]]]]]})
|
||||||
;;=> ["SELECT a, @var := 'foo'"]
|
;;=> ["SELECT a, @var := 'foo'"]
|
||||||
(sql/format {:select [:a [[:raw ["@var := " ["foo"]]]]]})
|
(sql/format {:select [:a [[:raw ["@var := " ["foo"]]]]]})
|
||||||
;;=> ["SELECT a, @var := ?" "foo"]
|
;;=> ["SELECT a, @var := (?)" "foo"]
|
||||||
|
;; when multiple expressions are provided, the enclosing
|
||||||
|
;; vector can be omitted:
|
||||||
|
(sql/format {:select [:a [[:raw "@var := " [:inline "foo"]]]]})
|
||||||
|
;;=> ["SELECT a, @var := 'foo'"]
|
||||||
|
(sql/format {:select [:a [[:raw "@var := " ["foo"]]]]})
|
||||||
|
;;=> ["SELECT a, @var := (?)" "foo"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`:raw` is also supported as a SQL clause for the same reason.
|
||||||
|
|
||||||
## Column Descriptors
|
## Column Descriptors
|
||||||
|
|
||||||
There are three types of descriptors that vary
|
There are three types of descriptors that vary
|
||||||
|
|
@ -199,6 +584,7 @@ specifications).
|
||||||
If no arguments are provided, these render as just SQL
|
If no arguments are provided, these render as just SQL
|
||||||
keywords (uppercase):
|
keywords (uppercase):
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[:foreign-key] ;=> FOREIGN KEY
|
[:foreign-key] ;=> FOREIGN KEY
|
||||||
[:primary-key] ;=> PRIMARY KEY
|
[:primary-key] ;=> PRIMARY KEY
|
||||||
|
|
@ -206,12 +592,13 @@ keywords (uppercase):
|
||||||
|
|
||||||
Otherwise, these render as regular function calls:
|
Otherwise, these render as regular function calls:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[:foreign-key :a] ;=> FOREIGN KEY(a)
|
[:foreign-key :a] ;=> FOREIGN KEY(a)
|
||||||
[:primary-key :x :y] ;=> PRIMARY KEY(x, y)
|
[:primary-key :x :y] ;=> PRIMARY KEY(x, y)
|
||||||
```
|
```
|
||||||
|
|
||||||
## constraint, default, references
|
### constraint, default, references
|
||||||
|
|
||||||
Although these are grouped together, they are generally
|
Although these are grouped together, they are generally
|
||||||
used differently. This group renders as SQL keywords if
|
used differently. This group renders as SQL keywords if
|
||||||
|
|
@ -221,6 +608,7 @@ argument. If two or more arguments are provided, this
|
||||||
renders as a SQL keyword followed by the first argument,
|
renders as a SQL keyword followed by the first argument,
|
||||||
followed by the rest as a regular argument list:
|
followed by the rest as a regular argument list:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[:default] ;=> DEFAULT
|
[:default] ;=> DEFAULT
|
||||||
[:default 42] ;=> DEFAULT 42
|
[:default 42] ;=> DEFAULT 42
|
||||||
|
|
@ -229,11 +617,12 @@ followed by the rest as a regular argument list:
|
||||||
[:references :foo :bar] ;=> REFERENCES foo(bar)
|
[:references :foo :bar] ;=> REFERENCES foo(bar)
|
||||||
```
|
```
|
||||||
|
|
||||||
## index, unique
|
### index, unique
|
||||||
|
|
||||||
These behave like the group above except that if the
|
These behave like the group above except that if the
|
||||||
first argument is `nil`, it is omitted:
|
first argument is `nil`, it is omitted:
|
||||||
|
|
||||||
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[:index :foo :bar :quux] ;=> INDEX foo(bar, quux)
|
[:index :foo :bar :quux] ;=> INDEX foo(bar, quux)
|
||||||
[:index nil :bar :quux] ;=> INDEX(bar, quux)
|
[:index nil :bar :quux] ;=> INDEX(bar, quux)
|
||||||
|
|
|
||||||
220
doc/xtdb.md
Normal file
220
doc/xtdb.md
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
# XTDB Support
|
||||||
|
|
||||||
|
As of 2.6.1230, HoneySQL provides support for most of XTDB's SQL
|
||||||
|
extensions, with additional support being added in subsequent releases.
|
||||||
|
|
||||||
|
For the most part, XTDB's SQL is based on
|
||||||
|
[SQL:2011](https://en.wikipedia.org/wiki/SQL:2011), including the
|
||||||
|
bitemporal features, but also includes a number of SQL extensions
|
||||||
|
to support additional XTDB-specific features.
|
||||||
|
|
||||||
|
HoneySQL attempts to support all of these XTDB features in the core
|
||||||
|
ANSI dialect, and this section documents most of those XTDB features.
|
||||||
|
|
||||||
|
For more details, see the XTDB documentation:
|
||||||
|
* [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)
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
The code examples herein assume:
|
||||||
|
```clojure
|
||||||
|
(refer-clojure :exclude '[update set])
|
||||||
|
(require '[honey.sql :as sql]
|
||||||
|
'[honey.sql.helpers :refer [select from where
|
||||||
|
delete-from erase-from
|
||||||
|
insert-into patch-into values
|
||||||
|
records]])
|
||||||
|
```
|
||||||
|
|
||||||
|
Clojure users can opt for the shorter `(require '[honey.sql :as sql] '[honey.sql.helpers :refer :all])` but this syntax is not available to ClojureScript users.
|
||||||
|
|
||||||
|
## `select` Variations
|
||||||
|
|
||||||
|
XTDB allows you to omit `SELECT` in a query. `SELECT *` is assumed if
|
||||||
|
it is omitted. In HoneySQL, you can simply omit the `:select` clause
|
||||||
|
from the DSL to achieve this.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format '{select * from foo where (= status "active")})
|
||||||
|
["SELECT * FROM foo WHERE status = ?" "active"]
|
||||||
|
user=> (sql/format '{from foo where (= status "active")})
|
||||||
|
["FROM foo WHERE status = ?" "active"]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also `SELECT *` and then exclude columns and/or rename columns.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[:* {:exclude :_id :rename [[:title, :name]]}]]})
|
||||||
|
["SELECT * EXCLUDE _id RENAME title AS name"]
|
||||||
|
user=> (sql/format '{select ((a.* {exclude _id})
|
||||||
|
(b.* {rename ((title, name))}))
|
||||||
|
from ((foo a))
|
||||||
|
join ((bar b) (= a._id b.foo_id))})
|
||||||
|
["SELECT a.* EXCLUDE _id, b.* RENAME title AS name FROM foo AS a INNER JOIN bar AS b ON a._id = b.foo_id"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`:exclude` can accept a single column, or a sequence of columns.
|
||||||
|
`:rename` accepts a sequence of pairs (column name, new name).
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[:* {:exclude [:_id :upc]
|
||||||
|
:rename [[:title, :name]
|
||||||
|
[:price, :cost]]}]]})
|
||||||
|
["SELECT * EXCLUDE (_id, upc) RENAME (title AS name, price AS cost)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested Sub-Queries
|
||||||
|
|
||||||
|
XTDB can produce structured results from `SELECT` queries containing
|
||||||
|
sub-queries, using `NEST_ONE` and `NEST_MANY`. In HoneySQL, these are
|
||||||
|
supported as regular function syntax in `:select` clauses.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format '{select (a.*
|
||||||
|
((nest_many {select * from bar where (= foo_id a._id)})
|
||||||
|
b))
|
||||||
|
from ((foo a))})
|
||||||
|
["SELECT a.*, NEST_MANY (SELECT * FROM bar WHERE foo_id = a._id) AS b FROM foo AS a"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember that function calls in `:select` clauses need to be nested three
|
||||||
|
levels of parentheses (brackets):
|
||||||
|
`:select [:col-a [:col-b :alias-b] [[:fn-call :col-c] :alias-c]]`.
|
||||||
|
|
||||||
|
## `records` Clause
|
||||||
|
|
||||||
|
XTDB provides a `RECORDS` clause to specify a list of structured documents,
|
||||||
|
similar to `VALUES` but specifically for documents rather than a collection
|
||||||
|
of column values. HoneySQL supports a `:records` clauses and automatically
|
||||||
|
lifts hash map values to parameters (rather than treating them as DSL fragments).
|
||||||
|
You can inline a hash map to produce XTDB's inline document syntax.
|
||||||
|
See also `insert` and `patch` below.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:records [{:_id 1 :status "active"}]})
|
||||||
|
["RECORDS ?" {:_id 1, :status "active"}]
|
||||||
|
user=> (sql/format {:records [[:inline {:_id 1 :status "active"}]]})
|
||||||
|
["RECORDS {_id: 1, status: 'active'}"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `object` (`record`) Literals
|
||||||
|
|
||||||
|
While `RECORDS` exists in parallel to the `VALUES` clause, XTDB also provides
|
||||||
|
a syntax to construct documents in other contexts in SQL, via the `OBJECT`
|
||||||
|
literal syntax. `RECORD` is a synonym for `OBJECT`. HoneySQL supports both
|
||||||
|
`:object` and `:record` as special syntax:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:object {:_id 1 :status "active"}]]]})
|
||||||
|
["SELECT OBJECT (_id: 1, status: 'active')"]
|
||||||
|
user=> (sql/format {:select [[[:record {:_id 1 :status "active"}]]]})
|
||||||
|
["SELECT RECORD (_id: 1, status: 'active')"]
|
||||||
|
```
|
||||||
|
|
||||||
|
A third option is to use `:inline` with a hash map:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:inline {:_id 1 :status "active"}]]]})
|
||||||
|
["SELECT {_id: 1, status: 'active'}"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object Navigation Expressions
|
||||||
|
|
||||||
|
In order to deal with nested documents, XTDB provides syntax to navigate
|
||||||
|
into them, via field names and/or array indices. HoneySQL supports this
|
||||||
|
via the `:get-in` special syntax, intended to be familiar to Clojure users.
|
||||||
|
|
||||||
|
The first argument to `:get-in` is treated as an expression that produces
|
||||||
|
the document, and subsequent arguments are treated as field names or array
|
||||||
|
indices to navigate into that document.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:get-in :doc :field1 :field2]]]})
|
||||||
|
["SELECT (doc).field1.field2"]
|
||||||
|
user=> (sql/format {:select [[[:get-in :table.col 0 :field]]]})
|
||||||
|
["SELECT (table.col)[0].field"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want an array index to be a parameter, use `:lift`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:get-in :doc [:lift 0] :field]]]})
|
||||||
|
["SELECT (doc)[?].field" 0]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Temporal Queries
|
||||||
|
|
||||||
|
XTDB allows any query to be run in a temporal context via the `SETTING`
|
||||||
|
clause (ahead of the `SELECT` clause). HoneySQL supports this via the
|
||||||
|
`:setting` clause. It accepts a sequence of identifiers and expressions.
|
||||||
|
An identifier ending in `-time` is assumed to be a temporal identifier
|
||||||
|
(e.g., `:system-time` mapping to `SYSTEM_TIME`). Other identifiers are assumed to
|
||||||
|
be regular SQL (so `-` is mapped to a space, e.g., `:as-of` mapping to `AS OF`).
|
||||||
|
A timestamp literal, such as `DATE '2024-11-24'` can be specified in HoneySQL
|
||||||
|
using `[:inline [:DATE "2024-11-24"]]` (note the literal case of `:DATE`
|
||||||
|
to produce `DATE`).
|
||||||
|
|
||||||
|
See [XTDB's Top-level queries documentation](https://docs.xtdb.com/reference/main/sql/queries.html#_top_level_queries) for more details.
|
||||||
|
|
||||||
|
Here's one fairly complex example:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:setting [[:snapshot-time :to [:inline :DATE "2024-11-24"]]
|
||||||
|
[:default :valid-time :to :between [:inline :DATE "2022"] :and [:inline :DATE "2023"]]]})
|
||||||
|
["SETTING SNAPSHOT_TIME TO DATE '2024-11-24', DEFAULT VALID_TIME TO BETWEEN DATE '2022' AND DATE '2023'"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Table references (e.g., in a `FROM` clause) can also have temporal qualifiers.
|
||||||
|
See [HoneySQL's `from` clause documentation](clause-reference.md#from) for
|
||||||
|
examples of that, one of which is reproduced here:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [:username]
|
||||||
|
:from [[:user :for :system-time :as-of [:inline "2019-08-01 15:23:00"]]]
|
||||||
|
:where [:= :id 9]})
|
||||||
|
["SELECT username FROM user FOR SYSTEM_TIME AS OF '2019-08-01 15:23:00' WHERE id = ?" 9]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `delete` and `erase`
|
||||||
|
|
||||||
|
In XTDB, `DELETE` is a temporal deletion -- the data remains in the database
|
||||||
|
but is no longer visible in queries that don't specify a time range prior to
|
||||||
|
the deletion. XTDB provides a similar `ERASE` operation that can permanently
|
||||||
|
delete the data. HoneySQL supports `:erase-from` with the same syntax as
|
||||||
|
`:delete-from`.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:delete-from :foo :where [:= :status "inactive"]})
|
||||||
|
["DELETE FROM foo WHERE status = ?" "inactive"]
|
||||||
|
user=> (sql/format {:erase-from :foo :where [:= :status "inactive"]})
|
||||||
|
["ERASE FROM foo WHERE status = ?" "inactive"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `insert` and `patch`
|
||||||
|
|
||||||
|
XTDB supports `PATCH` as an upsert operation: it will update existing
|
||||||
|
documents (via merging the new data) or insert new documents if they
|
||||||
|
don't already exist. HoneySQL supports `:patch-into` with the same syntax
|
||||||
|
as `:insert-into` with `:records`.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:insert-into :foo
|
||||||
|
:records [{:_id 1 :status "active"}]})
|
||||||
|
["INSERT INTO foo RECORDS ?" {:_id 1, :status "active"}]
|
||||||
|
user=> (sql/format {:patch-into :foo
|
||||||
|
:records [{:_id 1 :status "active"}]})
|
||||||
|
["PATCH INTO foo RECORDS ?" {:_id 1, :status "active"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `assert`
|
||||||
|
|
||||||
|
XTDB supports an `ASSERT` operation that will throw an exception if the
|
||||||
|
asserted predicate is not true:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format '{assert (not-exists {select 1 from users where (= email "james @example.com")})}
|
||||||
|
:inline true)
|
||||||
|
["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
```
|
||||||
57
pom.xml
57
pom.xml
|
|
@ -1,57 +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>
|
|
||||||
<groupId>seancorfield</groupId>
|
|
||||||
<artifactId>honeysql</artifactId>
|
|
||||||
<version>2.0.0-alpha2</version>
|
|
||||||
<name>honeysql</name>
|
|
||||||
<description>SQL as Clojure data structures.</description>
|
|
||||||
<url>https://github.com/seancorfield/honeysql</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>
|
|
||||||
<developer>
|
|
||||||
<name>Justin Kramer</name>
|
|
||||||
</developer>
|
|
||||||
</developers>
|
|
||||||
<scm>
|
|
||||||
<url>https://github.com/seancorfield/honeysql</url>
|
|
||||||
<connection>scm:git:git://github.com/seancorfield/honeysql.git</connection>
|
|
||||||
<developerConnection>scm:git:ssh://git@github.com/seancorfield/honeysql.git</developerConnection>
|
|
||||||
<tag>v2.0.0-alpha2</tag>
|
|
||||||
</scm>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.clojure</groupId>
|
|
||||||
<artifactId>clojure</artifactId>
|
|
||||||
<version>1.10.2</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
<build>
|
|
||||||
<sourceDirectory>src</sourceDirectory>
|
|
||||||
</build>
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>clojars</id>
|
|
||||||
<url>https://repo.clojars.org/</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>sonatype</id>
|
|
||||||
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
<distributionManagement>
|
|
||||||
<repository>
|
|
||||||
<id>clojars</id>
|
|
||||||
<name>Clojars repository</name>
|
|
||||||
<url>https://clojars.org/repo</url>
|
|
||||||
</repository>
|
|
||||||
</distributionManagement>
|
|
||||||
</project>
|
|
||||||
26
run-tests.sh
26
run-tests.sh
|
|
@ -1,26 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo ==== Test README.md ==== && clojure -M:readme && \
|
|
||||||
echo ==== Lint Source ==== && clojure -M:eastwood && \
|
|
||||||
echo ==== Test ClojureScript ==== && clojure -M:test:cljs-runner
|
|
||||||
|
|
||||||
if test $? -eq 0
|
|
||||||
then
|
|
||||||
if test "$1" = "all"
|
|
||||||
then
|
|
||||||
for v in 1.9 1.10 master
|
|
||||||
do
|
|
||||||
echo ==== Test Clojure $v ====
|
|
||||||
clojure -M:test:runner:$v
|
|
||||||
if test $? -ne 0
|
|
||||||
then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo ==== Test Clojure ====
|
|
||||||
clojure -M:test:runner
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
;; copyright (c) 2020-2021 sean corfield, all rights reserved
|
|
||||||
|
|
||||||
(ns honey.specs
|
|
||||||
"Optional namespace containing `clojure.spec` representations of
|
|
||||||
the data format used as the underlying DSL for HoneySQL."
|
|
||||||
(:require [clojure.spec.alpha :as s]))
|
|
||||||
|
|
||||||
(s/def ::sql-expression any?)
|
|
||||||
|
|
||||||
(s/def ::dsl (s/map-of simple-keyword?
|
|
||||||
(s/coll-of ::sql-expression
|
|
||||||
:kind vector?
|
|
||||||
:min-count 1)))
|
|
||||||
2738
src/honey/sql.cljc
2738
src/honey/sql.cljc
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
95
src/honey/sql/pg_ops.cljc
Normal file
95
src/honey/sql/pg_ops.cljc
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
;; copyright (c) 2022-2024 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.sql.pg-ops
|
||||||
|
"Register all the PostgreSQL JSON/JSONB operators
|
||||||
|
and provide convenient Clojure names for those ops.
|
||||||
|
In addition, provide names for the PostgreSQL
|
||||||
|
regex operators as well.
|
||||||
|
|
||||||
|
For the eleven that cannot be written directly as
|
||||||
|
symbols, use mnemonic names: hash for #, at for @,
|
||||||
|
and tilde for ~.
|
||||||
|
|
||||||
|
For the six of those that cannot be written as
|
||||||
|
keywords, invoke the `keyword` function instead.
|
||||||
|
|
||||||
|
Those latter eight (`at>`, `<at`, `at?`, `atat`,
|
||||||
|
`tilde`, `tilde*`, `!tilde`, and `!tilde*`) are
|
||||||
|
the only ones that should really be needed in the
|
||||||
|
DSL. The other names are provided for completeness.
|
||||||
|
|
||||||
|
`regex` and `iregex` are provided as aliases for the
|
||||||
|
regex operators `tilde` and `tilde*` respectively.
|
||||||
|
`!regex` and `!iregex` are provided as aliases for the
|
||||||
|
regex operators `!tilde` and `!tilde*` respectively."
|
||||||
|
(:refer-clojure :exclude [-> ->> -])
|
||||||
|
(:require [honey.sql :as sql]))
|
||||||
|
|
||||||
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
|
;; see https://www.postgresql.org/docs/current/functions-json.html
|
||||||
|
|
||||||
|
(def ->
|
||||||
|
"The -> operator for accessing nested JSON(B) values as JSON(B).
|
||||||
|
Ex.:
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [[[:->> [:-> :my_column \"kids\" [:inline 0]] \"name\"]]]})
|
||||||
|
; => [\"SELECT (my_column -> ? -> 0) ->> ?\" \"kids\" \"name\"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice we need to wrap the keys/indices with :inline if we don't want them to become parameters."
|
||||||
|
:->)
|
||||||
|
(def ->> "The ->> operator - like -> but returns the value as text instead of a JSON object." :->>)
|
||||||
|
(def hash> "The #> operator extracts JSON sub-object at the specified path." :#>)
|
||||||
|
(def hash>> "The #>> operator - like hash> but returns the value as text instead of JSON object." :#>>)
|
||||||
|
(def at> "The @> operator - does the first JSON value contain the second?" (keyword "@>"))
|
||||||
|
(def <at "The <@ operator - is the first JSON value contained in the second?" (keyword "<@"))
|
||||||
|
(def ? "The ? operator - does the text string exist as a top-level key or array element within the JSON value?" :?)
|
||||||
|
(def ?| "The ?| operator - do any of the strings in the text array exist as top-level keys or array elements?" :?|)
|
||||||
|
(def ?& "The ?& operator - do all of the strings in the text array exist as top-level keys or array elements?" :?&)
|
||||||
|
(def || "The || operator - concatenates two jsonb values (arrays or objects; anything else treated as 1-element array)." :||)
|
||||||
|
(def -
|
||||||
|
"The - operator:
|
||||||
|
- text value: deletes a key (and its value) from a JSON object, or matching string value(s) from a JSON array
|
||||||
|
- text[] array value: as above, but for all the provided keys
|
||||||
|
- int value: deletes the array element with specified index (negative integers count from the end)"
|
||||||
|
:-)
|
||||||
|
(def hash- "The #- operator - deletes the field or array element at the specified path, where path elements can be either field keys or array indexes." :#-)
|
||||||
|
(def at? "The @? operator - does JSON path return any item for the specified JSON value?" (keyword "@?"))
|
||||||
|
(def atat
|
||||||
|
"The @@ operator:
|
||||||
|
- returns the result of a JSON path predicate check for the specified JSON value. Only the first item of the result is taken into account.
|
||||||
|
If the result is not Boolean, then NULL is returned.
|
||||||
|
- checks if a text search vector (or a text value implicitly converted to a text search vector) matches a text search query. Returns a Boolean."
|
||||||
|
(keyword "@@"))
|
||||||
|
|
||||||
|
(def tilde "The case-sensitive regex match operator." (keyword "~"))
|
||||||
|
(def tilde* "The case-insensitive regex match operator." (keyword "~*"))
|
||||||
|
(def !tilde "The case-sensitive regex unmatch operator." (keyword "!~"))
|
||||||
|
(def !tilde* "The case-insensitive regex unmatch operator." (keyword "!~*"))
|
||||||
|
;; aliases:
|
||||||
|
(def regex tilde)
|
||||||
|
(def iregex tilde*)
|
||||||
|
(def !regex !tilde)
|
||||||
|
(def !iregex !tilde*)
|
||||||
|
|
||||||
|
(sql/register-op! :->)
|
||||||
|
(sql/register-op! :->>)
|
||||||
|
(sql/register-op! :#>)
|
||||||
|
(sql/register-op! :#>>)
|
||||||
|
(sql/register-op! at>)
|
||||||
|
(sql/register-op! <at)
|
||||||
|
(sql/register-op! :?)
|
||||||
|
(sql/register-op! :?|)
|
||||||
|
(sql/register-op! :?&)
|
||||||
|
;; these are already known operators:
|
||||||
|
;(sql/register-op! :||)
|
||||||
|
;(sql/register-op! :-)
|
||||||
|
(sql/register-op! :#-)
|
||||||
|
(sql/register-op! at?)
|
||||||
|
(sql/register-op! atat)
|
||||||
|
|
||||||
|
(sql/register-op! tilde)
|
||||||
|
(sql/register-op! tilde*)
|
||||||
|
(sql/register-op! !tilde)
|
||||||
|
(sql/register-op! !tilde*)
|
||||||
10
src/honey/sql/protocols.cljc
Normal file
10
src/honey/sql/protocols.cljc
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
;; copyright (c) 2022-2024 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.sql.protocols
|
||||||
|
"InlineValue -- a protocol that defines how to inline
|
||||||
|
values; (sqlize x) produces a SQL string for x.")
|
||||||
|
|
||||||
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
|
(defprotocol InlineValue :extend-via-metadata true
|
||||||
|
(sqlize [this] "Render value inline in a SQL string."))
|
||||||
109
src/honey/sql/util.cljc
Normal file
109
src/honey/sql/util.cljc
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
(ns honey.sql.util
|
||||||
|
"Utility functions for the main honey.sql namespace."
|
||||||
|
(:refer-clojure :exclude [str])
|
||||||
|
(:require clojure.string))
|
||||||
|
|
||||||
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
|
(defn str
|
||||||
|
"More efficient implementation of `clojure.core/str` because it has more
|
||||||
|
non-variadic arities. Optimization is Clojure-only, on other platforms it
|
||||||
|
reverts back to `clojure.core/str`."
|
||||||
|
(^String [] "")
|
||||||
|
(^String [^Object a]
|
||||||
|
#?(:clj (if (nil? a) "" (.toString a))
|
||||||
|
:default (clojure.core/str a)))
|
||||||
|
(^String [^Object a, ^Object b]
|
||||||
|
#?(:clj (if (nil? a)
|
||||||
|
(str b)
|
||||||
|
(if (nil? b)
|
||||||
|
(.toString a)
|
||||||
|
(.concat (.toString a) (.toString b))))
|
||||||
|
:default (clojure.core/str a b)))
|
||||||
|
(^String [a b c]
|
||||||
|
#?(:clj (let [sb (StringBuilder.)]
|
||||||
|
(.append sb (str a))
|
||||||
|
(.append sb (str b))
|
||||||
|
(.append sb (str c))
|
||||||
|
(.toString sb))
|
||||||
|
:default (clojure.core/str a b c)))
|
||||||
|
(^String [a b c d]
|
||||||
|
#?(:clj (let [sb (StringBuilder.)]
|
||||||
|
(.append sb (str a))
|
||||||
|
(.append sb (str b))
|
||||||
|
(.append sb (str c))
|
||||||
|
(.append sb (str d))
|
||||||
|
(.toString sb))
|
||||||
|
:default (clojure.core/str a b c d)))
|
||||||
|
(^String [a b c d e]
|
||||||
|
#?(:clj (let [sb (StringBuilder.)]
|
||||||
|
(.append sb (str a))
|
||||||
|
(.append sb (str b))
|
||||||
|
(.append sb (str c))
|
||||||
|
(.append sb (str d))
|
||||||
|
(.append sb (str e))
|
||||||
|
(.toString sb))
|
||||||
|
:default (clojure.core/str a b c d e)))
|
||||||
|
(^String [a b c d e & more]
|
||||||
|
#?(:clj (let [sb (StringBuilder.)]
|
||||||
|
(.append sb (str a))
|
||||||
|
(.append sb (str b))
|
||||||
|
(.append sb (str c))
|
||||||
|
(.append sb (str d))
|
||||||
|
(.append sb (str e))
|
||||||
|
(run! #(.append sb (str %)) more)
|
||||||
|
(.toString sb))
|
||||||
|
:default (apply clojure.core/str a b c d e more))))
|
||||||
|
|
||||||
|
(defn join
|
||||||
|
"More efficient implementation of `clojure.string/join`. May accept a transducer
|
||||||
|
`xform` to perform operations on each element before combining them together
|
||||||
|
into a string. Clojure-only, delegates to `clojure.string/join` on other
|
||||||
|
platforms."
|
||||||
|
([separator coll] (join separator identity coll))
|
||||||
|
([separator xform coll]
|
||||||
|
#?(:clj
|
||||||
|
(let [sb (StringBuilder.)
|
||||||
|
sep (str separator)]
|
||||||
|
(transduce xform
|
||||||
|
(fn
|
||||||
|
([] false)
|
||||||
|
([_] (.toString sb))
|
||||||
|
([add-sep? x]
|
||||||
|
(when add-sep? (.append sb sep))
|
||||||
|
(.append sb (str x))
|
||||||
|
true))
|
||||||
|
false coll))
|
||||||
|
|
||||||
|
:default
|
||||||
|
(clojure.string/join separator (transduce xform conj [] coll)))))
|
||||||
|
|
||||||
|
(defn split-by-separator
|
||||||
|
"More efficient implementation of `clojure.string/split` for cases when a
|
||||||
|
literal string (not regex) is used as a separator, and for cases where the
|
||||||
|
separator is not present in the haystack at all."
|
||||||
|
[s sep]
|
||||||
|
(loop [start 0, res []]
|
||||||
|
(if-some [sep-idx (clojure.string/index-of s sep start)]
|
||||||
|
(let [sep-idx (long sep-idx)]
|
||||||
|
(recur (inc sep-idx) (conj res (subs s start sep-idx))))
|
||||||
|
(if (= start 0)
|
||||||
|
;; Fastpath - zero separators in s
|
||||||
|
[s]
|
||||||
|
(conj res (subs s start))))))
|
||||||
|
|
||||||
|
(defn into*
|
||||||
|
"An extension of `clojure.core/into` that accepts multiple \"from\" arguments.
|
||||||
|
Doesn't support `xform`."
|
||||||
|
([to from1] (into* to from1 nil nil nil))
|
||||||
|
([to from1 from2] (into* to from1 from2 nil nil))
|
||||||
|
([to from1 from2 from3] (into* to from1 from2 from3 nil))
|
||||||
|
([to from1 from2 from3 from4]
|
||||||
|
(if (or from1 from2 from3 from4)
|
||||||
|
(as-> (transient to) to'
|
||||||
|
(reduce conj! to' from1)
|
||||||
|
(reduce conj! to' from2)
|
||||||
|
(reduce conj! to' from3)
|
||||||
|
(reduce conj! to' from4)
|
||||||
|
(persistent! to'))
|
||||||
|
to)))
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{sql/call honeysql.types/read-sql-call
|
|
||||||
sql/inline honeysql.types/read-sql-inline
|
|
||||||
sql/raw honeysql.types/read-sql-raw
|
|
||||||
sql/param honeysql.types/read-sql-param
|
|
||||||
sql/array honeysql.types/read-sql-array
|
|
||||||
sql/regularize honeysql.format/regularize}
|
|
||||||
54
test/honey/bigquery_test.cljc
Normal file
54
test/honey/bigquery_test.cljc
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
;; copyright (c) 2022-2024 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.bigquery-test
|
||||||
|
(:refer-clojure :exclude [format])
|
||||||
|
(:require [clojure.test :refer [deftest is]]
|
||||||
|
[honey.sql :as sut])
|
||||||
|
#?(:clj (:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
||||||
|
(deftest except-replace-tests
|
||||||
|
(is (= ["SELECT * FROM table WHERE id = ?" 1]
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id 1]})))
|
||||||
|
(is (= ["SELECT * EXCEPT (a, b, c) FROM table WHERE id = ?" 1]
|
||||||
|
(sut/format {:select [[:* :except [:a :b :c]]] :from [:table] :where [:= :id 1]})))
|
||||||
|
(is (= ["SELECT table.* EXCEPT (a, b, c) FROM table WHERE id = ?" 1]
|
||||||
|
(sut/format {:select [[:table.* :except [:a :b :c]]] :from [:table] :where [:= :id 1]})))
|
||||||
|
(is (= ["SELECT * REPLACE (a * 100 AS b, 2 AS c) FROM table WHERE id = ?" 1]
|
||||||
|
(sut/format {:select [[:* :replace [[[:* :a [:inline 100]] :b] [[:inline 2] :c]]]] :from [:table] :where [:= :id 1]})))
|
||||||
|
(is (= ["SELECT * EXCEPT (a, b) REPLACE (2 AS c) FROM table WHERE id = ?" 1]
|
||||||
|
(sut/format {:select [[:* :except [:a :b] :replace [[[:inline 2] :c]]]] :from [:table] :where [:= :id 1]})))
|
||||||
|
(is (= ["SELECT * REPLACE (a * ? AS b, ? AS c) FROM table WHERE id = ?" 100 2 1]
|
||||||
|
(sut/format {:select [[:* :replace [[[:* :a 100] :b] [2 :c]]]] :from [:table] :where [:= :id 1]})))
|
||||||
|
(is (= ["SELECT * EXCEPT (a, b) REPLACE (? AS c) FROM table WHERE id = ?" 2 1]
|
||||||
|
(sut/format {:select [[:* :except [:a :b] :replace [[2 :c]]]] :from [:table] :where [:= :id 1]}))))
|
||||||
|
|
||||||
|
(deftest bad-select-tests
|
||||||
|
(is (thrown? ExceptionInfo
|
||||||
|
(sut/format {:select [[:* :except [:a] :bad]]})))
|
||||||
|
(is (thrown? ExceptionInfo
|
||||||
|
(sut/format {:select [[:* :except]]})))
|
||||||
|
(is (thrown? ExceptionInfo
|
||||||
|
(sut/format {:select [[:foo :bar :quux]]}))))
|
||||||
|
|
||||||
|
(deftest struct-array-tests
|
||||||
|
(is (= ["CREATE TABLE IF NOT EXISTS my_table (name STRING NOT NULL, my_struct STRUCT<name STRING NOT NULL, description STRING>, my_array ARRAY<STRING>)"]
|
||||||
|
(sut/format (-> {:create-table [:my-table :if-not-exists]
|
||||||
|
:with-columns
|
||||||
|
[[:name :string [:not nil]]
|
||||||
|
[:my_struct [:bigquery/struct [:name :string [:not nil]] [:description :string]]]
|
||||||
|
[:my_array [:bigquery/array :string]]]}))))
|
||||||
|
(is (= ["ALTER TABLE my_table ADD COLUMN IF NOT EXISTS name STRING, ADD COLUMN IF NOT EXISTS my_struct STRUCT<name STRING, description STRING>, ADD COLUMN IF NOT EXISTS my_array ARRAY<STRING>"]
|
||||||
|
(sut/format {:alter-table [:my-table
|
||||||
|
{:add-column [:name :string :if-not-exists]}
|
||||||
|
{:add-column [:my_struct [:bigquery/struct [:name :string] [:description :string]] :if-not-exists]}
|
||||||
|
{:add-column [:my_array [:bigquery/array :string] :if-not-exists]}]}))))
|
||||||
|
|
||||||
|
(deftest test-case-expr
|
||||||
|
(is (= ["SELECT CASE foo WHEN ? THEN ? WHEN ? THEN foo / ? ELSE ? END FROM bar"
|
||||||
|
1 -1 2 2 0]
|
||||||
|
(sut/format
|
||||||
|
{:select [[[:case-expr :foo
|
||||||
|
1 -1
|
||||||
|
2 [:/ :foo 2]
|
||||||
|
:else 0]]]
|
||||||
|
:from [:bar]}))))
|
||||||
92
test/honey/cache_test.clj
Normal file
92
test/honey/cache_test.clj
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
;; copyright (c) 2022-2024 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.cache-test
|
||||||
|
(:refer-clojure :exclude [format group-by])
|
||||||
|
(:require [clojure.core.cache.wrapped :as cache]
|
||||||
|
[clojure.test :refer [deftest is]]
|
||||||
|
[honey.sql :as sut]
|
||||||
|
[honey.sql.helpers
|
||||||
|
:refer [select-distinct from join left-join right-join where
|
||||||
|
group-by having order-by limit offset]]))
|
||||||
|
|
||||||
|
(def big-complicated-map
|
||||||
|
(-> (select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
||||||
|
[[:now]] [[:raw "@x := 10"]])
|
||||||
|
(from [:foo :f] [:baz :b])
|
||||||
|
(join :draq [:= :f.b :draq.x]
|
||||||
|
:eldr [:= :f.e :eldr.t])
|
||||||
|
(left-join [:clod :c] [:= :f.a :c.d])
|
||||||
|
(right-join :bock [:= :bock.z :c.e])
|
||||||
|
(where [:or
|
||||||
|
[:and [:= :f.a "bort"] [:not= :b.baz [:param :param1]]]
|
||||||
|
[:and [:< 1 2] [:< 2 3]]
|
||||||
|
[:in :f.e [1 [:param :param2] 3]]
|
||||||
|
[:between :f.e 10 20]])
|
||||||
|
(group-by :f.a :c.e)
|
||||||
|
(having [:< 0 :f.e])
|
||||||
|
(order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
|
||||||
|
(limit 50)
|
||||||
|
(offset 10)))
|
||||||
|
|
||||||
|
(defn- cache-size [cache] (-> cache (deref) (keys) (count)))
|
||||||
|
|
||||||
|
(deftest cache-tests
|
||||||
|
(let [cache (cache/basic-cache-factory {})]
|
||||||
|
(is (zero? (cache-size cache)))
|
||||||
|
(is (= ["SELECT * FROM table WHERE id = ?" 1]
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id 1]}
|
||||||
|
{:cache cache})
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id 1]}
|
||||||
|
{:cache cache})))
|
||||||
|
(is (= 1 (cache-size cache)))
|
||||||
|
(is (= (sut/format {:select [:*] :from [:table] :where [:= :id 2]})
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id 2]}
|
||||||
|
{:cache cache})))
|
||||||
|
(is (= 2 (cache-size cache)))
|
||||||
|
(is (= (sut/format big-complicated-map {:params {:param1 "gabba" :param2 2}})
|
||||||
|
(sut/format big-complicated-map {:cache cache :params {:param1 "gabba" :param2 2}})
|
||||||
|
(sut/format big-complicated-map {:cache cache :params {:param1 "gabba" :param2 2}})))
|
||||||
|
(is (= 3 (cache-size cache)))
|
||||||
|
(is (= (sut/format big-complicated-map {:params {:param1 "foo" :param2 42}})
|
||||||
|
(sut/format big-complicated-map {:cache cache :params {:param1 "foo" :param2 42}})
|
||||||
|
(sut/format big-complicated-map {:cache cache :params {:param1 "foo" :param2 42}})))
|
||||||
|
(is (= 3 (cache-size cache)))
|
||||||
|
(println "Uncached, simple, embedded")
|
||||||
|
(time (dotimes [_ 100000]
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id (rand-int 10)]})))
|
||||||
|
(println "Cached, simple, embedded")
|
||||||
|
(time (dotimes [_ 100000]
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id (rand-int 10)]} {:cache cache})))
|
||||||
|
(is (= 11 (cache-size cache)))
|
||||||
|
(println "Uncached, complex, mixed")
|
||||||
|
(time (dotimes [_ 10000]
|
||||||
|
(sut/format big-complicated-map {:params {:param1 "gabba" :param2 (rand-int 10)}})))
|
||||||
|
(println "Cached, complex, mixed")
|
||||||
|
(time (dotimes [_ 10000]
|
||||||
|
(sut/format big-complicated-map {:cache cache :params {:param1 "gabba" :param2 (rand-int 10)}})))
|
||||||
|
(is (= 11 (cache-size cache))))
|
||||||
|
(let [cache (cache/basic-cache-factory {})]
|
||||||
|
(is (zero? (cache-size cache)))
|
||||||
|
(is (= ["SELECT * FROM table WHERE id = ?" 1]
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id :?id]}
|
||||||
|
{:cache cache :params {:id 1}})
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id :?id]}
|
||||||
|
{:cache cache :params {:id 1}})))
|
||||||
|
(is (= 1 (cache-size cache)))
|
||||||
|
(is (= (sut/format {:select [:*] :from [:table] :where [:= :id :?id]}
|
||||||
|
{:params {:id 2}})
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id :?id]}
|
||||||
|
{:cache cache :params {:id 2}})))
|
||||||
|
(is (= 1 (cache-size cache)))
|
||||||
|
;; different parameter names create different cache entries:
|
||||||
|
(is (= (sut/format {:select [:*] :from [:table] :where [:= :id :?x]}
|
||||||
|
{:cache cache :params {:x 2}})
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:= :id :?y]}
|
||||||
|
{:cache cache :params {:y 2}})))
|
||||||
|
(is (= 3 (cache-size cache)))
|
||||||
|
;; swapping parameter names creates different cache entries:
|
||||||
|
(is (= (sut/format {:select [:*] :from [:table] :where [:and [:= :id :?x] [:= :foo :?y]]}
|
||||||
|
{:cache cache :params {:x 2 :y 3}})
|
||||||
|
(sut/format {:select [:*] :from [:table] :where [:and [:= :id :?y] [:= :foo :?x]]}
|
||||||
|
{:cache cache :params {:x 3 :y 2}})))
|
||||||
|
(is (= 5 (cache-size cache)))))
|
||||||
19
test/honey/ops_test.cljc
Normal file
19
test/honey/ops_test.cljc
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
;; copyright (c) 2023-2025 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.ops-test
|
||||||
|
(:refer-clojure :exclude [format])
|
||||||
|
(:require [clojure.test :refer [deftest is]]
|
||||||
|
[honey.sql :as sut]))
|
||||||
|
|
||||||
|
(deftest issue-454
|
||||||
|
(is (= ["SELECT a - b - c AS x"]
|
||||||
|
(-> {:select [[[:- :a :b :c] :x]]}
|
||||||
|
(sut/format)))))
|
||||||
|
|
||||||
|
(deftest issue-566
|
||||||
|
(is (= ["SELECT * FROM table WHERE a IS DISTINCT FROM b"]
|
||||||
|
(-> {:select :* :from :table :where [:is-distinct-from :a :b]}
|
||||||
|
(sut/format))))
|
||||||
|
(is (= ["SELECT * FROM table WHERE a IS NOT DISTINCT FROM b"]
|
||||||
|
(-> {:select :* :from :table :where [:is-not-distinct-from :a :b]}
|
||||||
|
(sut/format)))))
|
||||||
|
|
@ -1,85 +1,287 @@
|
||||||
;; copyright (c) 2020-2021 sean corfield, all rights reserved
|
;; copyright (c) 2020-2024 sean corfield, all rights reserved
|
||||||
|
|
||||||
(ns honey.sql.helpers-test
|
(ns honey.sql.helpers-test
|
||||||
(:refer-clojure :exclude [update set group-by for partition-by])
|
(:refer-clojure :exclude [filter for group-by partition-by set update])
|
||||||
(:require #?(:clj [clojure.test :refer [deftest is testing]]
|
#_{:clj-kondo/ignore [:unused-namespace]}
|
||||||
:cljs [cljs.test :refer-macros [deftest is testing]])
|
(:require [clojure.core :as c]
|
||||||
|
[clojure.test :refer [deftest is testing]]
|
||||||
[honey.sql :as sql]
|
[honey.sql :as sql]
|
||||||
[honey.sql.helpers
|
[honey.sql.helpers :as h
|
||||||
:refer [add-column add-index alter-table columns create-table create-view
|
:refer [add-column alter-table columns create-table create-table-as create-view
|
||||||
cross-join do-update-set drop-column drop-index drop-table from full-join
|
create-materialized-view
|
||||||
group-by having insert-into
|
create-index
|
||||||
join left-join limit offset on-conflict order-by
|
bulk-collect-into
|
||||||
over partition-by
|
cross-join do-update-set drop-column drop-table
|
||||||
rename-column rename-table returning right-join
|
filter from full-join
|
||||||
select select-distinct values where window with with-columns]]))
|
group-by having insert-into replace-into
|
||||||
|
join-by join left-join limit offset on-conflict
|
||||||
|
on-duplicate-key-update
|
||||||
|
order-by over partition-by refresh-materialized-view
|
||||||
|
returning right-join
|
||||||
|
select select-distinct select-top
|
||||||
|
values where window with with-columns
|
||||||
|
with-data within-group]]))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(deftest helpers-are-complete
|
||||||
|
(let [helpers-ns (find-ns 'honey.sql.helpers)]
|
||||||
|
(testing "all public helpers have docstrings"
|
||||||
|
;; #409 this assert is only valid when :doc metadata is not elided:
|
||||||
|
(when (-> #'h/generic-helper-unary meta :doc)
|
||||||
|
;; ensure #295 stays true (all public functions have docstring):
|
||||||
|
(is (= [] (->> (ns-publics helpers-ns) (vals) (c/filter (comp not :doc meta)))))))
|
||||||
|
(testing "all clauses have public helpers"
|
||||||
|
;; ensure all public functions match clauses:
|
||||||
|
(is (= (c/set (conj @#'honey.sql/default-clause-order
|
||||||
|
:composite :filter :lateral :over :within-group
|
||||||
|
:upsert
|
||||||
|
:generic-helper-variadic :generic-helper-unary))
|
||||||
|
(c/set (conj (map keyword (keys (ns-publics helpers-ns)))
|
||||||
|
:nest :raw))))))))
|
||||||
|
|
||||||
(deftest test-select
|
(deftest test-select
|
||||||
(let [m1 (-> (with [:cte (-> (select :*)
|
(testing "large helper expression"
|
||||||
(from :example)
|
(let [m1 (-> (with [:cte (-> (select :*)
|
||||||
(where [:= :example-column 0]))])
|
(from :example)
|
||||||
(select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
(where [:= :example-column 0]))])
|
||||||
:%now [[:raw "@x := 10"]])
|
(select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
||||||
(from [:foo :f] [:baz :b])
|
:%now [[:raw "@x := 10"]])
|
||||||
(join :draq [:= :f.b :draq.x])
|
(from [:foo :f] [:baz :b])
|
||||||
(left-join [:clod :c] [:= :f.a :c.d])
|
(join :draq [:= :f.b :draq.x])
|
||||||
(right-join :bock [:= :bock.z :c.e])
|
(left-join [:clod :c] [:= :f.a :c.d])
|
||||||
(full-join :beck [:= :beck.x :c.y])
|
(right-join :bock [:= :bock.z :c.e])
|
||||||
(where [:or
|
(full-join :beck [:= :beck.x :c.y])
|
||||||
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
(where [:or
|
||||||
[:and [:< 1 2] [:< 2 3]]
|
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
||||||
[:in :f.e [1 [:param :param2] 3]]
|
[:and [:< 1 2] [:< 2 3]]
|
||||||
[:between :f.e 10 20]])
|
[:in :f.e [1 [:param :param2] 3]]
|
||||||
(group-by :f.a)
|
[:between :f.e 10 20]])
|
||||||
(having [:< 0 :f.e])
|
(group-by :f.a)
|
||||||
(order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
|
(having [:< 0 :f.e])
|
||||||
(limit 50)
|
(order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
|
||||||
(offset 10))
|
(limit 50)
|
||||||
m2 {:with [[:cte {:select [:*]
|
(offset 10))
|
||||||
:from [:example]
|
m2 {:with [[:cte {:select [:*]
|
||||||
:where [:= :example-column 0]}]]
|
:from [:example]
|
||||||
:select-distinct [:f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
:where [:= :example-column 0]}]]
|
||||||
:%now [[:raw "@x := 10"]]]
|
:select-distinct [:f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
||||||
:from [[:foo :f] [:baz :b]]
|
:%now [[:raw "@x := 10"]]]
|
||||||
:join [:draq [:= :f.b :draq.x]]
|
:from [[:foo :f] [:baz :b]]
|
||||||
:left-join [[:clod :c] [:= :f.a :c.d]]
|
:join [:draq [:= :f.b :draq.x]]
|
||||||
:right-join [:bock [:= :bock.z :c.e]]
|
:left-join [[:clod :c] [:= :f.a :c.d]]
|
||||||
:full-join [:beck [:= :beck.x :c.y]]
|
:right-join [:bock [:= :bock.z :c.e]]
|
||||||
:where [:or
|
:full-join [:beck [:= :beck.x :c.y]]
|
||||||
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
:where [:or
|
||||||
[:and [:< 1 2] [:< 2 3]]
|
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
||||||
[:in :f.e [1 [:param :param2] 3]]
|
[:and [:< 1 2] [:< 2 3]]
|
||||||
[:between :f.e 10 20]]
|
[:in :f.e [1 [:param :param2] 3]]
|
||||||
:group-by [:f.a]
|
[:between :f.e 10 20]]
|
||||||
:having [:< 0 :f.e]
|
:group-by [:f.a]
|
||||||
:order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]]
|
:having [:< 0 :f.e]
|
||||||
:limit 50
|
:order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]]
|
||||||
:offset 10}]
|
:limit 50
|
||||||
(testing "Various construction methods are consistent"
|
:offset 10}]
|
||||||
(is (= m1 m2)))
|
(testing "Various construction methods are consistent"
|
||||||
(testing "SQL data formats correctly"
|
(is (= m1 m2)))
|
||||||
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ?"
|
(testing "SQL data formats correctly"
|
||||||
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ?"
|
||||||
(sql/format m1 {:params {:param1 "gabba" :param2 2}}))))
|
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
||||||
#?(:clj (testing "SQL data prints and reads correctly"
|
(sql/format m1 {:params {:param1 "gabba" :param2 2}}))))
|
||||||
(is (= m1 (read-string (pr-str m1))))))
|
#?(:clj (testing "SQL data prints and reads correctly"
|
||||||
#_(testing "SQL data formats correctly with alternate param naming"
|
(is (= m1 (read-string (pr-str m1))))))
|
||||||
(is (= (sql/format m1 {:params {:param1 "gabba" :param2 2}})
|
#_(testing "SQL data formats correctly with alternate param naming"
|
||||||
["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2) AND (b.baz <> $3)) OR (($4 < $5) AND ($6 < $7)) OR (f.e IN ($8, $9, $10)) OR f.e BETWEEN $11 AND $12 GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT $14 OFFSET $15"
|
(is (= (sql/format m1 {:params {:param1 "gabba" :param2 2}})
|
||||||
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10])))
|
["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2) AND (b.baz <> $3)) OR (($4 < $5) AND ($6 < $7)) OR (f.e IN ($8, $9, $10)) OR f.e BETWEEN $11 AND $12 GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT $14 OFFSET $15"
|
||||||
(testing "Locking"
|
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10])))
|
||||||
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS `bla-bla`, NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ? LOCK IN SHARE MODE"
|
(testing "Locking"
|
||||||
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS `bla-bla`, NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ? LOCK IN SHARE MODE"
|
||||||
(sql/format (assoc m1 :lock [:in-share-mode])
|
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
||||||
{:params {:param1 "gabba" :param2 2}
|
(sql/format (assoc m1 :lock [:in-share-mode])
|
||||||
;; to enable :lock
|
{:params {:param1 "gabba" :param2 2}
|
||||||
:dialect :mysql :quoted false}))))))
|
;; to enable :lock
|
||||||
|
:dialect :mysql :quoted false}))))))
|
||||||
|
(testing "large helper expression with simplified where"
|
||||||
|
(let [m1 (-> (with [:cte (-> (select :*)
|
||||||
|
(from :example)
|
||||||
|
(where := :example-column 0))])
|
||||||
|
(select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
||||||
|
:%now [[:raw "@x := 10"]])
|
||||||
|
(from [:foo :f] [:baz :b])
|
||||||
|
(join :draq [:= :f.b :draq.x])
|
||||||
|
(left-join [:clod :c] [:= :f.a :c.d])
|
||||||
|
(right-join :bock [:= :bock.z :c.e])
|
||||||
|
(full-join :beck [:= :beck.x :c.y])
|
||||||
|
(where :or
|
||||||
|
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
||||||
|
[:and [:< 1 2] [:< 2 3]]
|
||||||
|
[:in :f.e [1 [:param :param2] 3]]
|
||||||
|
[:between :f.e 10 20])
|
||||||
|
(group-by :f.a)
|
||||||
|
(having :< 0 :f.e)
|
||||||
|
(order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
|
||||||
|
(limit 50)
|
||||||
|
(offset 10))
|
||||||
|
m2 {:with [[:cte {:select [:*]
|
||||||
|
:from [:example]
|
||||||
|
:where [:= :example-column 0]}]]
|
||||||
|
:select-distinct [:f.* :b.baz :c.quux [:b.bla "bla-bla"]
|
||||||
|
:%now [[:raw "@x := 10"]]]
|
||||||
|
:from [[:foo :f] [:baz :b]]
|
||||||
|
:join [:draq [:= :f.b :draq.x]]
|
||||||
|
:left-join [[:clod :c] [:= :f.a :c.d]]
|
||||||
|
:right-join [:bock [:= :bock.z :c.e]]
|
||||||
|
:full-join [:beck [:= :beck.x :c.y]]
|
||||||
|
:where [:or
|
||||||
|
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
||||||
|
[:and [:< 1 2] [:< 2 3]]
|
||||||
|
[:in :f.e [1 [:param :param2] 3]]
|
||||||
|
[:between :f.e 10 20]]
|
||||||
|
:group-by [:f.a]
|
||||||
|
:having [:< 0 :f.e]
|
||||||
|
:order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]]
|
||||||
|
:limit 50
|
||||||
|
:offset 10}]
|
||||||
|
(testing "Various construction methods are consistent"
|
||||||
|
(is (= m1 m2)))
|
||||||
|
(testing "SQL data formats correctly"
|
||||||
|
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ?"
|
||||||
|
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
||||||
|
(sql/format m1 {:params {:param1 "gabba" :param2 2}}))))
|
||||||
|
#?(:clj (testing "SQL data prints and reads correctly"
|
||||||
|
(is (= m1 (read-string (pr-str m1))))))
|
||||||
|
#_(testing "SQL data formats correctly with alternate param naming"
|
||||||
|
(is (= (sql/format m1 {:params {:param1 "gabba" :param2 2}})
|
||||||
|
["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2) AND (b.baz <> $3)) OR (($4 < $5) AND ($6 < $7)) OR (f.e IN ($8, $9, $10)) OR f.e BETWEEN $11 AND $12 GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT $14 OFFSET $15"
|
||||||
|
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10])))
|
||||||
|
(testing "Locking"
|
||||||
|
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS `bla-bla`, NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ? LOCK IN SHARE MODE"
|
||||||
|
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
||||||
|
(sql/format (assoc m1 :lock [:in-share-mode])
|
||||||
|
{:params {:param1 "gabba" :param2 2}
|
||||||
|
;; to enable :lock
|
||||||
|
:dialect :mysql :quoted false})))))))
|
||||||
|
|
||||||
|
(deftest select-top-tests
|
||||||
|
(testing "Basic TOP syntax"
|
||||||
|
(is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10]
|
||||||
|
(sql/format {:select-top [10 :foo] :from :bar :order-by :quux})))
|
||||||
|
(is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10]
|
||||||
|
(sql/format (-> (select-top 10 :foo)
|
||||||
|
(from :bar)
|
||||||
|
(order-by :quux))))))
|
||||||
|
(testing "Expanded TOP syntax"
|
||||||
|
(is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10]
|
||||||
|
(sql/format {:select-top [[10 :percent :with-ties] :foo :baz] :from :bar :order-by :quux})))
|
||||||
|
(is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10]
|
||||||
|
(sql/format (-> (select-top [10 :percent :with-ties] :foo :baz)
|
||||||
|
(from :bar)
|
||||||
|
(order-by :quux)))))))
|
||||||
|
|
||||||
|
(deftest select-into-tests
|
||||||
|
(testing "SELECT INTO"
|
||||||
|
(is (= ["SELECT * INTO foo FROM bar"]
|
||||||
|
(sql/format {:select :* :into :foo :from :bar})))
|
||||||
|
(is (= ["SELECT * INTO foo IN otherdb FROM bar"]
|
||||||
|
(sql/format {:select :* :into [:foo :otherdb] :from :bar})))
|
||||||
|
(is (= ["SELECT * INTO foo FROM bar"]
|
||||||
|
(sql/format (-> (select '*) (h/into 'foo) (from 'bar)))))
|
||||||
|
(is (= ["SELECT * INTO foo IN otherdb FROM bar"]
|
||||||
|
(sql/format (-> (select :*) (h/into :foo :otherdb) (from :bar))))))
|
||||||
|
(testing "SELECT BULK COLLECT INTO"
|
||||||
|
(is (= ["SELECT * BULK COLLECT INTO foo FROM bar"]
|
||||||
|
(sql/format {:select :* :bulk-collect-into :foo :from :bar})))
|
||||||
|
(is (= ["SELECT * BULK COLLECT INTO foo LIMIT ? FROM bar" 100]
|
||||||
|
(sql/format {:select :* :bulk-collect-into [:foo 100] :from :bar})))
|
||||||
|
(is (= ["SELECT * BULK COLLECT INTO foo FROM bar"]
|
||||||
|
(sql/format (-> (select :*) (bulk-collect-into :foo) (from :bar)))))
|
||||||
|
(is (= ["SELECT * BULK COLLECT INTO foo LIMIT ? FROM bar" 100]
|
||||||
|
(sql/format (-> (select :*) (bulk-collect-into :foo 100) (from :bar)))))))
|
||||||
|
|
||||||
|
(deftest from-expression-tests
|
||||||
|
(testing "FROM can be a function invocation"
|
||||||
|
(is (= ["SELECT foo, bar FROM F(?) AS x" 1]
|
||||||
|
(sql/format {:select [:foo :bar] :from [[[:f 1] :x]]}))))
|
||||||
|
;; these two examples are from https://www.postgresql.org/docs/9.3/queries-table-expressions.html#QUERIES-LATERAL
|
||||||
|
(testing "FROM can be a LATERAL select"
|
||||||
|
(is (= ["SELECT * FROM foo, LATERAL (SELECT * FROM bar WHERE bar.id = foo.bar_id) AS ss"]
|
||||||
|
(sql/format {:select :*
|
||||||
|
:from [:foo
|
||||||
|
[[:lateral {:select :*
|
||||||
|
:from :bar
|
||||||
|
:where [:= :bar.id :foo.bar_id]}] :ss]]}))))
|
||||||
|
(testing "FROM can be a LATERAL expression"
|
||||||
|
(is (= [(str "SELECT p1.id, p2.id, v1, v2"
|
||||||
|
" FROM polygons AS p1, polygons AS p2,"
|
||||||
|
" LATERAL VERTICES(p1.poly) AS v1,"
|
||||||
|
" LATERAL VERTICES(p2.poly) AS v2"
|
||||||
|
" WHERE ((v1 <-> v2) < ?) AND (p1.id <> p2.id)") 10]
|
||||||
|
(sql/format {:select [:p1.id :p2.id :v1 :v2]
|
||||||
|
:from [[:polygons :p1] [:polygons :p2]
|
||||||
|
[[:lateral [:vertices :p1.poly]] :v1]
|
||||||
|
[[:lateral [:vertices :p2.poly]] :v2]]
|
||||||
|
:where [:and [:< [:<-> :v1 :v2] 10] [:!= :p1.id :p2.id]]})))
|
||||||
|
(is (= [(str "SELECT m.name"
|
||||||
|
" FROM manufacturers AS m"
|
||||||
|
" LEFT JOIN LATERAL GET_PRODUCT_NAMES(m.id) AS pname ON TRUE"
|
||||||
|
" WHERE pname IS NULL")]
|
||||||
|
(sql/format {:select :m.name
|
||||||
|
:from [[:manufacturers :m]]
|
||||||
|
:left-join [[[:lateral [:get_product_names :m.id]] :pname] true]
|
||||||
|
:where [:= :pname nil]})))))
|
||||||
|
|
||||||
|
(deftest join-by-test
|
||||||
|
(testing "Natural JOIN orders"
|
||||||
|
(is (= ["SELECT * FROM foo INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y"]
|
||||||
|
(sql/format {:select [:*] :from [:foo]
|
||||||
|
:full-join [:beck [:= :beck.x :c.y]]
|
||||||
|
:right-join [:bock [:= :bock.z :c.e]]
|
||||||
|
:left-join [[:clod :c] [:= :f.a :c.d]]
|
||||||
|
:join [:draq [:= :f.b :draq.x]]}))))
|
||||||
|
(testing "Specific JOIN orders"
|
||||||
|
(is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"]
|
||||||
|
(sql/format {:select [:*] :from [:foo]
|
||||||
|
:join-by [:full [:beck [:= :beck.x :c.y]]
|
||||||
|
:right [:bock [:= :bock.z :c.e]]
|
||||||
|
:left [[:clod :c] [:= :f.a :c.d]]
|
||||||
|
:join [:draq [:= :f.b :draq.x]]]})))
|
||||||
|
(is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"]
|
||||||
|
(-> (select :*)
|
||||||
|
(from :foo)
|
||||||
|
(join-by :full-join [:beck [:= :beck.x :c.y]]
|
||||||
|
:right-join [:bock [:= :bock.z :c.e]]
|
||||||
|
:left-join [[:clod :c] [:= :f.a :c.d]]
|
||||||
|
:inner-join [:draq [:= :f.b :draq.x]])
|
||||||
|
(sql/format)))))
|
||||||
|
(testing "Specific JOIN orders with join clauses"
|
||||||
|
(is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"]
|
||||||
|
(sql/format {:select [:*] :from [:foo]
|
||||||
|
:join-by [{:full-join [:beck [:= :beck.x :c.y]]}
|
||||||
|
{:right-join [:bock [:= :bock.z :c.e]]}
|
||||||
|
{:left-join [[:clod :c] [:= :f.a :c.d]]}
|
||||||
|
{:join [:draq [:= :f.b :draq.x]]}]})))
|
||||||
|
(is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"]
|
||||||
|
(-> (select :*)
|
||||||
|
(from :foo)
|
||||||
|
(join-by (full-join :beck [:= :beck.x :c.y])
|
||||||
|
(right-join :bock [:= :bock.z :c.e])
|
||||||
|
(left-join [:clod :c] [:= :f.a :c.d])
|
||||||
|
(join :draq [:= :f.b :draq.x]))
|
||||||
|
(sql/format))))))
|
||||||
|
|
||||||
(deftest test-cast
|
(deftest test-cast
|
||||||
(is (= ["SELECT foo, CAST(bar AS integer)"]
|
(is (= ["SELECT foo, CAST(bar AS INTEGER)"]
|
||||||
(sql/format {:select [:foo [[:cast :bar :integer]]]})))
|
(sql/format {:select [:foo [[:cast :bar :integer]]]})))
|
||||||
(is (= ["SELECT foo, CAST(bar AS integer)"]
|
(is (= ["SELECT foo, CAST(bar AS INTEGER)"]
|
||||||
(sql/format {:select [:foo [[:cast :bar 'integer]]]}))))
|
(sql/format {:select [:foo [[:cast :bar 'integer]]]})))
|
||||||
|
(is (= ["SELECT foo, CAST(bar AS DOUBLE PRECISION)"] ;; Postgres example
|
||||||
|
(sql/format {:select [:foo [[:cast :bar :double-precision]]]})))
|
||||||
|
(is (= ["SELECT \"foo\", CAST(\"bar\" AS INTEGER)"]
|
||||||
|
(sql/format {:select [:foo [[:cast :bar :integer]]]} {:quoted true})))
|
||||||
|
(is (= ["SELECT `foo`, CAST(`bar` AS INTEGER)"]
|
||||||
|
(sql/format {:select [:foo [[:cast :bar :integer]]]} {:dialect :mysql})))
|
||||||
|
(is (= ["SELECT `foo`, CAST(`bar` AS CHAR(10))"]
|
||||||
|
(sql/format {:select [:foo [[:cast :bar [:char 10]]]]} {:dialect :mysql
|
||||||
|
:inline true}))))
|
||||||
|
|
||||||
(deftest test-value
|
(deftest test-value
|
||||||
(is (= ["INSERT INTO foo (bar) VALUES (?)" {:baz "my-val"}]
|
(is (= ["INSERT INTO foo (bar) VALUES (?)" {:baz "my-val"}]
|
||||||
|
|
@ -140,10 +342,28 @@
|
||||||
(sql/format {:select [:*]
|
(sql/format {:select [:*]
|
||||||
:from [:customers]
|
:from [:customers]
|
||||||
:where [:in :id :?ids]}
|
:where [:in :id :?ids]}
|
||||||
{:params {:ids values}})))))))
|
{:params {:ids values}})))
|
||||||
|
(is (= ["SELECT * FROM customers WHERE id IN ($1, $2)" "1" "2"]
|
||||||
|
(sql/format {:select [:*]
|
||||||
|
:from [:customers]
|
||||||
|
:where [:in :id values]}
|
||||||
|
{:numbered true})))
|
||||||
|
(is (= ["SELECT * FROM customers WHERE id IN ($2, $3)" nil "1" "2"]
|
||||||
|
(sql/format {:select [:*]
|
||||||
|
:from [:customers]
|
||||||
|
:where [:in :id :?ids]}
|
||||||
|
{:params {:ids values} :numbered true})))))))
|
||||||
|
|
||||||
(deftest test-case
|
(deftest test-case
|
||||||
(is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND ((foo MOD ?) = ?) THEN foo / ? ELSE ? END FROM bar"
|
(is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND ((foo % ?) = ?) THEN foo / ? ELSE ? END FROM bar"
|
||||||
|
0 -1 0 2 0 2 0]
|
||||||
|
(sql/format
|
||||||
|
{:select [[[:case
|
||||||
|
[:< :foo 0] -1
|
||||||
|
[:and [:> :foo 0] [:= [:% :foo 2] 0]] [:/ :foo 2]
|
||||||
|
:else 0]]]
|
||||||
|
:from [:bar]})))
|
||||||
|
(is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND (MOD(foo, ?) = ?) THEN foo / ? ELSE ? END FROM bar"
|
||||||
0 -1 0 2 0 2 0]
|
0 -1 0 2 0 2 0]
|
||||||
(sql/format
|
(sql/format
|
||||||
{:select [[[:case
|
{:select [[[:case
|
||||||
|
|
@ -196,6 +416,18 @@
|
||||||
(join :x [:using :id] :y [:using :foo :bar])
|
(join :x [:using :id] :y [:using :foo :bar])
|
||||||
sql/format)))))
|
sql/format)))))
|
||||||
|
|
||||||
|
(defn my-update [& args] (h/generic-helper-unary :update args))
|
||||||
|
(defn my-set [& args] (h/generic-helper-unary :set args))
|
||||||
|
(defn my-where [& args] (h/generic-helper-variadic :where args))
|
||||||
|
|
||||||
|
(deftest custom-helpers-test
|
||||||
|
(testing "nil join"
|
||||||
|
(is (= ["UPDATE foo SET bar = ? WHERE quux = ?" 1 2]
|
||||||
|
(-> (my-update :foo)
|
||||||
|
(my-set {:bar 1})
|
||||||
|
(my-where (sql/map= {:quux 2}))
|
||||||
|
sql/format)))))
|
||||||
|
|
||||||
(deftest inline-test
|
(deftest inline-test
|
||||||
(is (= ["SELECT * FROM foo WHERE id = 5"]
|
(is (= ["SELECT * FROM foo WHERE id = 5"]
|
||||||
(-> (select :*)
|
(-> (select :*)
|
||||||
|
|
@ -320,6 +552,33 @@
|
||||||
" MAX(salary) OVER w AS MaxSalary"
|
" MAX(salary) OVER w AS MaxSalary"
|
||||||
" FROM employee"
|
" FROM employee"
|
||||||
" WINDOW w AS (PARTITION BY department)")]))
|
" WINDOW w AS (PARTITION BY department)")]))
|
||||||
|
;; multiple window tests
|
||||||
|
(is (= (-> (select :id
|
||||||
|
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
|
||||||
|
[[:max :salary] :w :MaxSalary]))
|
||||||
|
(from :employee)
|
||||||
|
(window :w (partition-by :department))
|
||||||
|
(window :x (partition-by :salary))
|
||||||
|
sql/format)
|
||||||
|
[(str "SELECT id,"
|
||||||
|
" AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average,"
|
||||||
|
" MAX(salary) OVER w AS MaxSalary"
|
||||||
|
" FROM employee"
|
||||||
|
" WINDOW w AS (PARTITION BY department)"
|
||||||
|
", x AS (PARTITION BY salary)")]))
|
||||||
|
(is (= (-> (select :id
|
||||||
|
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
|
||||||
|
[[:max :salary] :w :MaxSalary]))
|
||||||
|
(from :employee)
|
||||||
|
(window :w (partition-by :department)
|
||||||
|
:x (partition-by :salary))
|
||||||
|
sql/format)
|
||||||
|
[(str "SELECT id,"
|
||||||
|
" AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average,"
|
||||||
|
" MAX(salary) OVER w AS MaxSalary"
|
||||||
|
" FROM employee"
|
||||||
|
" WINDOW w AS (PARTITION BY department)"
|
||||||
|
", x AS (PARTITION BY salary)")]))
|
||||||
;; test nil / empty window function clause:
|
;; test nil / empty window function clause:
|
||||||
(is (= (-> (select :id
|
(is (= (-> (select :id
|
||||||
(over [[:avg :salary] {} :Average]
|
(over [[:avg :salary] {} :Average]
|
||||||
|
|
@ -343,6 +602,82 @@
|
||||||
(from :cities)
|
(from :cities)
|
||||||
(where [:= :metroflag "y"])))
|
(where [:= :metroflag "y"])))
|
||||||
["CREATE VIEW metro AS SELECT * FROM cities WHERE metroflag = ?" "y"]))
|
["CREATE VIEW metro AS SELECT * FROM cities WHERE metroflag = ?" "y"]))
|
||||||
|
(is (= (sql/format (-> (create-table-as :metro :if-not-exists)
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
["CREATE TABLE IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
|
||||||
|
(is (= (sql/format (-> (create-table-as :metro :or-replace)
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
["CREATE OR REPLACE TABLE metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
|
||||||
|
(is (= (sql/format (-> (create-table-as :temp :metro :if-not-exists)
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
["CREATE TEMP TABLE IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
|
||||||
|
(is (= (sql/format (-> (create-table-as :temp :metro :or-replace)
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
["CREATE OR REPLACE TEMP TABLE metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
|
||||||
|
(is (= (sql/format (-> (create-materialized-view :metro :if-not-exists)
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
["CREATE MATERIALIZED VIEW IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
|
||||||
|
(is (= (sql/format (-> (create-table-as :metro :if-not-exists
|
||||||
|
(columns :foo :bar :baz)
|
||||||
|
[:tablespace [:entity :quux]])
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
[(str "CREATE TABLE IF NOT EXISTS metro"
|
||||||
|
" (foo, bar, baz) TABLESPACE quux"
|
||||||
|
" AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"]))
|
||||||
|
(is (= (sql/format (-> (create-table-as :metro :or-replace
|
||||||
|
(columns :foo :bar :baz)
|
||||||
|
[:tablespace [:entity :quux]])
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
[(str "CREATE OR REPLACE TABLE metro"
|
||||||
|
" (foo, bar, baz) TABLESPACE quux"
|
||||||
|
" AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"]))
|
||||||
|
(is (= (sql/format (-> (create-materialized-view :metro :if-not-exists
|
||||||
|
(columns :foo :bar :baz)
|
||||||
|
[:tablespace [:entity :quux]])
|
||||||
|
(select :*)
|
||||||
|
(from :cities)
|
||||||
|
(where [:= :metroflag "y"])
|
||||||
|
(with-data false)))
|
||||||
|
[(str "CREATE MATERIALIZED VIEW IF NOT EXISTS metro"
|
||||||
|
" (foo, bar, baz) TABLESPACE quux"
|
||||||
|
" AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"]))
|
||||||
|
(is (= (sql/format {:create-materialized-view [:metro :if-not-exists]
|
||||||
|
:select [:*]
|
||||||
|
:from :cities
|
||||||
|
:where [:= :metroflag "y"]
|
||||||
|
:with-data true})
|
||||||
|
["CREATE MATERIALIZED VIEW IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH DATA" "y"]))
|
||||||
|
(is (= (sql/format {:create-materialized-view [:metro :if-not-exists
|
||||||
|
(columns :foo :bar :baz)
|
||||||
|
[:tablespace [:entity :quux]]]
|
||||||
|
:select [:*]
|
||||||
|
:from :cities
|
||||||
|
:where [:= :metroflag "y"]
|
||||||
|
:with-data false})
|
||||||
|
[(str "CREATE MATERIALIZED VIEW IF NOT EXISTS metro"
|
||||||
|
" (foo, bar, baz) TABLESPACE quux"
|
||||||
|
" AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"]))
|
||||||
(is (= (sql/format (-> (create-table :films)
|
(is (= (sql/format (-> (create-table :films)
|
||||||
(with-columns
|
(with-columns
|
||||||
[:id :int :unsigned :auto-increment]
|
[:id :int :unsigned :auto-increment]
|
||||||
|
|
@ -367,29 +702,51 @@
|
||||||
["DROP TABLE foo"]))
|
["DROP TABLE foo"]))
|
||||||
(is (= (sql/format {:drop-table [:if-exists :foo]})
|
(is (= (sql/format {:drop-table [:if-exists :foo]})
|
||||||
["DROP TABLE IF EXISTS foo"]))
|
["DROP TABLE IF EXISTS foo"]))
|
||||||
|
(is (= (sql/format {:drop-view [:if-exists :foo]})
|
||||||
|
["DROP VIEW IF EXISTS foo"]))
|
||||||
|
(is (= (sql/format {:drop-materialized-view [:if-exists :foo]})
|
||||||
|
["DROP MATERIALIZED VIEW IF EXISTS foo"]))
|
||||||
|
(is (= (sql/format {:refresh-materialized-view [:concurrently :foo]
|
||||||
|
:with-data true})
|
||||||
|
["REFRESH MATERIALIZED VIEW CONCURRENTLY foo WITH DATA"]))
|
||||||
(is (= (sql/format '{drop-table (if-exists foo)})
|
(is (= (sql/format '{drop-table (if-exists foo)})
|
||||||
["DROP TABLE IF EXISTS foo"]))
|
["DROP TABLE IF EXISTS foo"]))
|
||||||
(is (= (sql/format {:drop-table [:foo :bar]})
|
(is (= (sql/format {:drop-table [:foo :bar]})
|
||||||
["DROP TABLE foo, bar"]))
|
["DROP TABLE foo, bar"]))
|
||||||
(is (= (sql/format {:drop-table [:if-exists :foo :bar]})
|
(is (= (sql/format {:drop-table [:if-exists :foo :bar]})
|
||||||
["DROP TABLE IF EXISTS foo, bar"]))
|
["DROP TABLE IF EXISTS foo, bar"]))
|
||||||
|
(is (= (sql/format {:drop-table [:if-exists :foo :bar [:cascade]]})
|
||||||
|
["DROP TABLE IF EXISTS foo, bar CASCADE"]))
|
||||||
(is (= (sql/format (drop-table :foo))
|
(is (= (sql/format (drop-table :foo))
|
||||||
["DROP TABLE foo"]))
|
["DROP TABLE foo"]))
|
||||||
(is (= (sql/format (drop-table :if-exists :foo))
|
(is (= (sql/format (drop-table :if-exists :foo))
|
||||||
["DROP TABLE IF EXISTS foo"]))
|
["DROP TABLE IF EXISTS foo"]))
|
||||||
|
(is (= (sql/format (-> (refresh-materialized-view :concurrently :foo)
|
||||||
|
(with-data true)))
|
||||||
|
["REFRESH MATERIALIZED VIEW CONCURRENTLY foo WITH DATA"]))
|
||||||
(is (= (sql/format (drop-table :foo :bar))
|
(is (= (sql/format (drop-table :foo :bar))
|
||||||
["DROP TABLE foo, bar"]))
|
["DROP TABLE foo, bar"]))
|
||||||
(is (= (sql/format (drop-table :if-exists :foo :bar))
|
(is (= (sql/format (drop-table :if-exists :foo :bar [:cascade]))
|
||||||
["DROP TABLE IF EXISTS foo, bar"])))
|
["DROP TABLE IF EXISTS foo, bar CASCADE"])))
|
||||||
|
|
||||||
(deftest issue-293-alter-table
|
(deftest issue-293-alter-table
|
||||||
(is (= (sql/format (-> (alter-table :fruit)
|
(is (= (sql/format (-> (alter-table :fruit)
|
||||||
(add-column :id :int [:not nil])))
|
(add-column :id :int [:not nil])))
|
||||||
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL"]))
|
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL"]))
|
||||||
|
(is (= (sql/format (-> (alter-table :fruit)
|
||||||
|
(add-column :id :int [:not nil])
|
||||||
|
(add-column :a1 :int nil)
|
||||||
|
(add-column :be :text [:not nil])))
|
||||||
|
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, ADD COLUMN a1 INT NULL, ADD COLUMN be TEXT NOT NULL"]))
|
||||||
(is (= (sql/format (alter-table :fruit
|
(is (= (sql/format (alter-table :fruit
|
||||||
(add-column :id :int [:not nil])
|
(add-column :id :int [:not nil])
|
||||||
(drop-column :ident)))
|
(drop-column :ident)
|
||||||
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, DROP COLUMN ident"])))
|
(drop-column :if-exists :another)))
|
||||||
|
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, DROP COLUMN ident, DROP COLUMN IF EXISTS another"]))
|
||||||
|
(is (= (sql/format (alter-table :fruit
|
||||||
|
(drop-column :a :b :if-exists :c :d)
|
||||||
|
(drop-column :if-exists :e)))
|
||||||
|
["ALTER TABLE fruit DROP COLUMN a, DROP COLUMN b, DROP COLUMN IF EXISTS c, DROP COLUMN d, DROP COLUMN IF EXISTS e"])))
|
||||||
|
|
||||||
(deftest issue-293-insert-into-data
|
(deftest issue-293-insert-into-data
|
||||||
;; insert into as (and other tests) based on :insert-into
|
;; insert into as (and other tests) based on :insert-into
|
||||||
|
|
@ -479,4 +836,225 @@
|
||||||
["INSERT INTO transport (id, name) SELECT * FROM cars"]))
|
["INSERT INTO transport (id, name) SELECT * FROM cars"]))
|
||||||
;; three arguments with an alias and columns:
|
;; three arguments with an alias and columns:
|
||||||
(is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)}))
|
(is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)}))
|
||||||
["INSERT INTO transport AS t (id, name) SELECT * FROM cars"])))
|
["INSERT INTO transport AS t (id, name) SELECT * FROM cars"]))
|
||||||
|
;; and again with replace-into:
|
||||||
|
(is (= (sql/format (replace-into '(transport t) '(id, name) '{select (*) from (cars)}))
|
||||||
|
["REPLACE INTO transport AS t (id, name) SELECT * FROM cars"])))
|
||||||
|
|
||||||
|
;; these tests are adapted from Cam Saul's PR #283
|
||||||
|
|
||||||
|
(deftest merge-where-no-params-test
|
||||||
|
(doseq [[k [f merge-f]] {"WHERE" [where where]
|
||||||
|
"HAVING" [having having]}]
|
||||||
|
(testing "merge-where called with just the map as parameter - see #228"
|
||||||
|
(let [sqlmap (-> (select :*)
|
||||||
|
(from :table)
|
||||||
|
(f [:= :foo :bar]))]
|
||||||
|
(is (= [(str "SELECT * FROM table " k " foo = bar")]
|
||||||
|
(sql/format (apply merge-f sqlmap []))))))))
|
||||||
|
|
||||||
|
(deftest merge-where-test
|
||||||
|
(doseq [[k sql-keyword f merge-f] [[:where "WHERE" where where]
|
||||||
|
[:having "HAVING" having having]]]
|
||||||
|
(is (= [(str "SELECT * FROM table " sql-keyword " (foo = bar) AND (quuz = xyzzy)")]
|
||||||
|
(-> (select :*)
|
||||||
|
(from :table)
|
||||||
|
(f [:= :foo :bar] [:= :quuz :xyzzy])
|
||||||
|
sql/format)))
|
||||||
|
(is (= [(str "SELECT * FROM table " sql-keyword " (foo = bar) AND (quuz = xyzzy)")]
|
||||||
|
(-> (select :*)
|
||||||
|
(from :table)
|
||||||
|
(f [:= :foo :bar])
|
||||||
|
(merge-f [:= :quuz :xyzzy])
|
||||||
|
sql/format)))
|
||||||
|
(testing "Should work when first arg isn't a map"
|
||||||
|
(is (= {k [:and [:x] [:y]]}
|
||||||
|
(merge-f [:x] [:y]))))
|
||||||
|
(testing "Shouldn't use conjunction if there is only one clause in the result"
|
||||||
|
(is (= {k [:x]}
|
||||||
|
(merge-f {} [:x]))))
|
||||||
|
(testing "Should be able to specify the conjunction type"
|
||||||
|
(is (= {k [:or [:x] [:y]]}
|
||||||
|
(merge-f {}
|
||||||
|
:or
|
||||||
|
[:x] [:y]))))
|
||||||
|
(testing "Should ignore nil clauses"
|
||||||
|
(is (= {k [:or [:x] [:y]]}
|
||||||
|
(merge-f {}
|
||||||
|
:or
|
||||||
|
[:x] nil [:y]))))))
|
||||||
|
|
||||||
|
(deftest merge-where-combine-clauses-test
|
||||||
|
(doseq [[k f] {:where where
|
||||||
|
:having having}]
|
||||||
|
(testing (str "Combine new " k " clauses into the existing clause when appropriate. (#282)")
|
||||||
|
(testing "No existing clause"
|
||||||
|
(is (= {k [:and [:x] [:y]]}
|
||||||
|
(f {}
|
||||||
|
[:x] [:y]))))
|
||||||
|
(testing "Existing clause is not a conjunction."
|
||||||
|
(is (= {k [:and [:a] [:x] [:y]]}
|
||||||
|
(f {k [:a]}
|
||||||
|
[:x] [:y]))))
|
||||||
|
(testing "Existing clause IS a conjunction."
|
||||||
|
(testing "New clause(s) are not conjunctions"
|
||||||
|
(is (= {k [:and [:a] [:b] [:x] [:y]]}
|
||||||
|
(f {k [:and [:a] [:b]]}
|
||||||
|
[:x] [:y]))))
|
||||||
|
(testing "New clauses(s) ARE conjunction(s)"
|
||||||
|
(is (= {k [:and [:a] [:b] [:x] [:y]]}
|
||||||
|
(f {k [:and [:a] [:b]]}
|
||||||
|
[:and [:x] [:y]])))
|
||||||
|
(is (= {k [:and [:a] [:b] [:x] [:y]]}
|
||||||
|
(f {k [:and [:a] [:b]]}
|
||||||
|
[:and [:x]]
|
||||||
|
[:y])))
|
||||||
|
(is (= {k [:and [:a] [:b] [:x] [:y]]}
|
||||||
|
(f {k [:and [:a] [:b]]}
|
||||||
|
[:and [:x]]
|
||||||
|
[:and [:y]])))))
|
||||||
|
(testing "if existing clause isn't the same conjunction, don't merge into it"
|
||||||
|
(testing "existing conjunction is `:or`"
|
||||||
|
(is (= {k [:and [:or [:a] [:b]] [:x] [:y]]}
|
||||||
|
(f {k [:or [:a] [:b]]}
|
||||||
|
[:x] [:y]))))
|
||||||
|
(testing "pass conjunction type as a param (override default of :and)"
|
||||||
|
(is (= {k [:or [:and [:a] [:b]] [:x] [:y]]}
|
||||||
|
(f {k [:and [:a] [:b]]}
|
||||||
|
:or
|
||||||
|
[:x] [:y]))))))))
|
||||||
|
|
||||||
|
(deftest mysql-on-duplicate-key-update
|
||||||
|
(testing "From https://www.mysqltutorial.org/mysql-insert-or-update-on-duplicate-key-update"
|
||||||
|
(is (= (sql/format (-> (insert-into :device)
|
||||||
|
(columns :name)
|
||||||
|
(values [["Printer"]])
|
||||||
|
(on-duplicate-key-update {:name "Printer"})))
|
||||||
|
["INSERT INTO device (name) VALUES (?) ON DUPLICATE KEY UPDATE name = ?"
|
||||||
|
"Printer" "Printer"]))
|
||||||
|
(is (= (sql/format (-> (insert-into :device)
|
||||||
|
(columns :id :name)
|
||||||
|
(values [[4 "Printer"]])
|
||||||
|
(on-duplicate-key-update {:name "Central Printer"})))
|
||||||
|
["INSERT INTO device (id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = ?"
|
||||||
|
4 "Printer" "Central Printer"]))
|
||||||
|
(is (= (sql/format (-> (insert-into :table)
|
||||||
|
(columns :c1)
|
||||||
|
(values [[42]])
|
||||||
|
(on-duplicate-key-update {:c1 [:+ [:values :c1] 1]})))
|
||||||
|
["INSERT INTO table (c1) VALUES (?) ON DUPLICATE KEY UPDATE c1 = VALUES(c1) + ?"
|
||||||
|
42 1]))))
|
||||||
|
|
||||||
|
(deftest filter-within-order-by-test
|
||||||
|
(testing "PostgreSQL filter, within group, order-by as special syntax"
|
||||||
|
(is (= (sql/format {:select [[[:filter :%count.* {:where [:> :i 5]}] :a]
|
||||||
|
[[:filter ; two pairs -- alias is on last pair
|
||||||
|
[:avg :x [:order-by :y [:a :desc]]] {:where [:< :i 10]}
|
||||||
|
[:sum :q] {:where [:= :x nil]}] :b]
|
||||||
|
[[:within-group [:foo :y] {:order-by :x}]]]})
|
||||||
|
[(str "SELECT COUNT(*) FILTER (WHERE i > ?) AS a,"
|
||||||
|
" AVG(x, y ORDER BY a DESC) FILTER (WHERE i < ?),"
|
||||||
|
" SUM(q) FILTER (WHERE x IS NULL) AS b,"
|
||||||
|
" FOO(y) WITHIN GROUP (ORDER BY x ASC)")
|
||||||
|
5 10])))
|
||||||
|
(testing "PostgreSQL filter, within group, order-by as helpers"
|
||||||
|
(is (= (sql/format (select [(filter :%count.* (where :> :i 5)) :a]
|
||||||
|
[(filter ; two pairs -- alias is on last pair
|
||||||
|
;; order by must remain special syntax here:
|
||||||
|
[:avg :x [:order-by :y [:a :desc]]] (where :< :i 10)
|
||||||
|
[:sum :q] (where := :x nil)) :b]
|
||||||
|
[(within-group [:foo :y] (order-by :x))]))
|
||||||
|
[(str "SELECT COUNT(*) FILTER (WHERE i > ?) AS a,"
|
||||||
|
" AVG(x, y ORDER BY a DESC) FILTER (WHERE i < ?),"
|
||||||
|
" SUM(q) FILTER (WHERE x IS NULL) AS b,"
|
||||||
|
" FOO(y) WITHIN GROUP (ORDER BY x ASC)")
|
||||||
|
5 10]))))
|
||||||
|
|
||||||
|
(deftest issue-322
|
||||||
|
(testing "Combining WHERE clauses with conditions"
|
||||||
|
(is (= {:where [:and [:= :a 1] [:or [:= :b 2] [:= :c 3]]]}
|
||||||
|
(where [:= :a 1] [:or [:= :b 2] [:= :c 3]])))
|
||||||
|
(is (= (-> (where :or [:= :b 2] [:= :c 3]) ; or first
|
||||||
|
(where := :a 1)) ; then implicit and
|
||||||
|
(-> (where := :b 2) ; implicit and
|
||||||
|
(where :or [:= :c 3]) ; then explicit or
|
||||||
|
(where := :a 1)))) ; then implicit and
|
||||||
|
(is (= {:where [:and [:or [:= :b 2] [:= :c 3]] [:= :a 1]]}
|
||||||
|
(where [:or [:= :b 2] [:= :c 3]] [:= :a 1])
|
||||||
|
(-> (where :or [:= :b 2] [:= :c 3]) ; explicit or
|
||||||
|
(where := :a 1)))))) ; then implicit and
|
||||||
|
|
||||||
|
(deftest issue-324
|
||||||
|
(testing "insert-into accepts statement"
|
||||||
|
(is (= (-> (with [:a])
|
||||||
|
(insert-into [:quux [:x :y]]
|
||||||
|
{:select [:id] :from [:table]}))
|
||||||
|
{:with [[:a]],
|
||||||
|
:insert-into [[:quux [:x :y]]
|
||||||
|
{:select [:id], :from [:table]}]}))))
|
||||||
|
|
||||||
|
(deftest issue-431
|
||||||
|
(testing "where false should not be ignored"
|
||||||
|
(is (= {:where false}
|
||||||
|
(where false)))
|
||||||
|
(is (= ["SELECT * FROM table WHERE FALSE"]
|
||||||
|
(sql/format {:select [:*] :from [:table] :where false})))))
|
||||||
|
|
||||||
|
(deftest issue-505
|
||||||
|
(testing "where should merge symbols/keywords correctly"
|
||||||
|
(is (= '{where [:and (= a 1) [:= :b 2]]}
|
||||||
|
(-> '{where (= a 1)}
|
||||||
|
(where [:= :b 2]))))
|
||||||
|
(is (= '{where (= a 1)}
|
||||||
|
(-> '{where (= a 1)}
|
||||||
|
(where))))
|
||||||
|
(is (= '{:where [:and (= a 1) [:= :b 2]]}
|
||||||
|
(-> '{:where (= a 1)}
|
||||||
|
(where [:= :b 2]))))
|
||||||
|
(is (= '{:where (= a 1)}
|
||||||
|
(-> '{:where (= a 1)}
|
||||||
|
(where))))
|
||||||
|
(is (= '{:where [:= :b 2]}
|
||||||
|
(-> '{}
|
||||||
|
(where [:= :b 2]))))
|
||||||
|
(is (= '{}
|
||||||
|
(-> '{}
|
||||||
|
(where))))))
|
||||||
|
|
||||||
|
(deftest test-create-index
|
||||||
|
(testing "create index, commonly supported features"
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table (my_column)"]
|
||||||
|
(sql/format {:create-index [:my-column-idx [:my-table :my-column]]})))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table (my_column)"]
|
||||||
|
(sql/format (create-index :my-column-idx [:my-table :my-column]))))
|
||||||
|
(is (= ["CREATE UNIQUE INDEX my_column_idx ON my_table (my_column)"]
|
||||||
|
(sql/format (create-index [:unique :my-column-idx] [:my-table :my-column]))))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table (my_column, my_other_column)"]
|
||||||
|
(sql/format (create-index :my-column-idx [:my-table :my-column :my-other-column])))))
|
||||||
|
(testing "PostgreSQL extensions (IF NOT EXISTS and expressions)"
|
||||||
|
(is (= ["CREATE INDEX IF NOT EXISTS my_column_idx ON my_table (my_column)"]
|
||||||
|
(sql/format (create-index [:my-column-idx :if-not-exists] [:my-table :my-column]))))
|
||||||
|
(is (= ["CREATE UNIQUE INDEX IF NOT EXISTS my_column_idx ON my_table (my_column)"]
|
||||||
|
(sql/format (create-index [:unique :my-column-idx :if-not-exists] [:my-table :my-column]))))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"]
|
||||||
|
(sql/format (create-index :my-column-idx [:my-table :%lower.my-column])))))
|
||||||
|
(testing "PostgreSQL extensions (USING GIN/HASH)"
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
|
||||||
|
(sql/format {:create-index [:my-column-idx [:my-table :using-gin :my-column]]})))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
|
||||||
|
(sql/format (create-index :my-column-idx [:my-table :using-gin :my-column]))))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING HASH (my_column)"]
|
||||||
|
(sql/format {:create-index [:my-column-idx [:my-table :using-hash :my-column]]})))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING HASH (my_column)"]
|
||||||
|
(sql/format (create-index :my-column-idx [:my-table :using-hash :my-column]))))))
|
||||||
|
|
||||||
|
(deftest join-with-alias
|
||||||
|
(is (= ["SELECT * FROM foo LEFT JOIN (populatons AS pm INNER JOIN customers AS pc ON (pm.id = pc.id) AND (pm.other_id = pc.other_id)) ON foo.fk_id = pm.id"]
|
||||||
|
(sql/format {:select :*
|
||||||
|
:from :foo
|
||||||
|
:left-join [[[:join [:populatons :pm]
|
||||||
|
{:join [[:customers :pc]
|
||||||
|
[:and
|
||||||
|
[:= :pm/id :pc/id]
|
||||||
|
[:= :pm/other-id :pc/other-id]]]}]]
|
||||||
|
[:= :foo/fk-id :pm/id]]}))))
|
||||||
|
|
|
||||||
44
test/honey/sql/pg_ops_test.cljc
Normal file
44
test/honey/sql/pg_ops_test.cljc
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
;; copyright (c) 2022-2024 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.sql.pg-ops-test
|
||||||
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
|
[honey.sql :as sql]
|
||||||
|
[honey.sql.pg-ops :as sut]))
|
||||||
|
|
||||||
|
(deftest pg-op-tests
|
||||||
|
(testing "built-in ops"
|
||||||
|
(is (= ["SELECT a || b AS x"]
|
||||||
|
(sql/format {:select [[[:|| :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a - b AS x"]
|
||||||
|
(sql/format {:select [[[:- :a :b] :x]]}))))
|
||||||
|
(testing "writable ops"
|
||||||
|
(is (= ["SELECT a -> b AS x"]
|
||||||
|
(sql/format {:select [[[:-> :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a ->> b AS x"]
|
||||||
|
(sql/format {:select [[[:->> :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a #> b AS x"]
|
||||||
|
(sql/format {:select [[[:#> :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a #>> b AS x"]
|
||||||
|
(sql/format {:select [[[:#>> :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a ?? b AS x"]
|
||||||
|
(sql/format {:select [[[:? :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a ??| b AS x"]
|
||||||
|
(sql/format {:select [[[:?| :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a ??& b AS x"]
|
||||||
|
(sql/format {:select [[[:?& :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a #- b AS x"]
|
||||||
|
(sql/format {:select [[[:#- :a :b] :x]]}))))
|
||||||
|
(testing "named ops"
|
||||||
|
(is (= ["SELECT a @> b AS x"]
|
||||||
|
(sql/format {:select [[[sut/at> :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a <@ b AS x"]
|
||||||
|
(sql/format {:select [[[sut/<at :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a @?? b AS x"]
|
||||||
|
(sql/format {:select [[[sut/at? :a :b] :x]]})))
|
||||||
|
(is (= ["SELECT a @@ b AS x"]
|
||||||
|
(sql/format {:select [[[sut/atat :a :b] :x]]}))))
|
||||||
|
(testing "variadic ops"
|
||||||
|
(is (= ["SELECT a -> b -> c AS x"]
|
||||||
|
(sql/format {:select [[[:-> :a :b :c] :x]]})))
|
||||||
|
(is (= ["SELECT a || b || c AS x"]
|
||||||
|
(sql/format {:select [[[:|| :a :b :c] :x]]})))))
|
||||||
|
|
@ -1,34 +1,33 @@
|
||||||
;; copied from https://github.com/nilenso/honeysql-postgres
|
;; copied from https://github.com/nilenso/honeysql-postgres
|
||||||
;; on 2021-02-13 to verify the completeness of support for
|
;; on 2021-02-13 to verify the completeness of support for
|
||||||
;; those features within HoneySQL v2
|
;; those features within HoneySQL 2.x
|
||||||
|
|
||||||
;; where there are differences, the original code is kept
|
;; where there are differences, the original code is kept
|
||||||
;; with #_ and the modified code follows it (aside from
|
;; with #_ and the modified code follows it (aside from
|
||||||
;; the ns form which has numerous changes to both match
|
;; the ns form which has numerous changes to both match
|
||||||
;; the structure of HoneySQL v2 and to work with cljs)
|
;; the structure of HoneySQL 2.x and to work with cljs)
|
||||||
|
|
||||||
(ns honey.sql.postgres-test
|
(ns honey.sql.postgres-test
|
||||||
(:refer-clojure :exclude [update partition-by set])
|
(:refer-clojure :exclude [update partition-by set])
|
||||||
(:require #?(:clj [clojure.test :refer [deftest is testing]]
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
:cljs [cljs.test :refer-macros [deftest is testing]])
|
|
||||||
;; pull in all the PostgreSQL helpers that the nilenso
|
;; pull in all the PostgreSQL helpers that the nilenso
|
||||||
;; library provided (as well as the regular HoneySQL ones):
|
;; library provided (as well as the regular HoneySQL ones):
|
||||||
[honey.sql.helpers :as sqlh :refer
|
[honey.sql.helpers :as sqlh :refer
|
||||||
[upsert on-conflict do-nothing on-constraint
|
[upsert on-conflict do-nothing on-constraint
|
||||||
returning do-update-set
|
returning do-update-set
|
||||||
;; not needed because do-update-set can do this directly
|
;; not needed because do-update-set can do this directly
|
||||||
#_do-update-set!
|
#_do-update-set!
|
||||||
alter-table rename-column drop-column
|
alter-table rename-column drop-column
|
||||||
add-column partition-by
|
add-column partition-by
|
||||||
;; not needed because insert-into can do this directly
|
;; not needed because insert-into can do this directly
|
||||||
#_insert-into-as
|
#_insert-into-as
|
||||||
create-table rename-table drop-table
|
create-table rename-table drop-table
|
||||||
window create-view over with-columns
|
window create-view over with-columns
|
||||||
create-extension drop-extension
|
create-extension drop-extension
|
||||||
select-distinct-on
|
select-distinct-on
|
||||||
;; already part of HoneySQL
|
;; already part of HoneySQL
|
||||||
insert-into values where select
|
insert-into values where select
|
||||||
from order-by update set]]
|
from order-by update set]]
|
||||||
[honey.sql :as sql]))
|
[honey.sql :as sql]))
|
||||||
|
|
||||||
(deftest upsert-test
|
(deftest upsert-test
|
||||||
|
|
@ -72,6 +71,20 @@
|
||||||
(on-conflict (on-constraint :distributors_pkey))
|
(on-conflict (on-constraint :distributors_pkey))
|
||||||
do-nothing
|
do-nothing
|
||||||
sql/format)))
|
sql/format)))
|
||||||
|
(is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did) ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"]
|
||||||
|
;; with both name and clause:
|
||||||
|
(-> (insert-into :distributors)
|
||||||
|
(values [{:did 9 :dname "Antwerp Design"}])
|
||||||
|
(on-conflict :did (on-constraint :distributors_pkey))
|
||||||
|
do-nothing
|
||||||
|
sql/format)))
|
||||||
|
(is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did, dname) ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"]
|
||||||
|
;; with multiple names and a clause:
|
||||||
|
(-> (insert-into :distributors)
|
||||||
|
(values [{:did 9 :dname "Antwerp Design"}])
|
||||||
|
(on-conflict :did :dname (on-constraint :distributors_pkey))
|
||||||
|
do-nothing
|
||||||
|
sql/format)))
|
||||||
(is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"]
|
(is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"]
|
||||||
;; almost identical to nilenso version:
|
;; almost identical to nilenso version:
|
||||||
(-> (insert-into :distributors)
|
(-> (insert-into :distributors)
|
||||||
|
|
@ -80,6 +93,20 @@
|
||||||
(upsert (-> (on-conflict (on-constraint :distributors_pkey))
|
(upsert (-> (on-conflict (on-constraint :distributors_pkey))
|
||||||
do-nothing))
|
do-nothing))
|
||||||
sql/format)))
|
sql/format)))
|
||||||
|
(is (= ["INSERT INTO foo (id, data) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET into = ((STATE(?), MODIFIED(NOW()))) WHERE state = ?" 1 42 "enabled" "disabled"]
|
||||||
|
(sql/format (-> (insert-into :foo)
|
||||||
|
(values [{:id 1 :data 42}])
|
||||||
|
(upsert (-> (on-conflict :id)
|
||||||
|
(do-update-set [:state "enabled"]
|
||||||
|
[:modified [:now]])
|
||||||
|
(where [:= :state "disabled"])))))))
|
||||||
|
(is (= ["INSERT INTO foo (id, data) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET state = ?, modified = NOW() WHERE state = ?" 1 42 "enabled" "disabled"]
|
||||||
|
(sql/format (-> (insert-into :foo)
|
||||||
|
(values [{:id 1 :data 42}])
|
||||||
|
(upsert (-> (on-conflict :id)
|
||||||
|
(do-update-set {:state "enabled"
|
||||||
|
:modified [:now]})
|
||||||
|
(where [:= :state "disabled"])))))))
|
||||||
(is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname" 10 "Pinp Design" 11 "Foo Bar Works"]
|
(is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname" 10 "Pinp Design" 11 "Foo Bar Works"]
|
||||||
(sql/format {:insert-into :distributors
|
(sql/format {:insert-into :distributors
|
||||||
:values [{:did 10 :dname "Pinp Design"}
|
:values [{:did 10 :dname "Pinp Design"}
|
||||||
|
|
@ -139,11 +166,11 @@
|
||||||
(sql/format {:delete-from :distributors
|
(sql/format {:delete-from :distributors
|
||||||
:where [:> :did :10]
|
:where [:> :did :10]
|
||||||
:returning [:*]})))
|
:returning [:*]})))
|
||||||
(is (= ["UPDATE distributors SET dname = ? WHERE did = 2 RETURNING did dname" "Foo Bar Designs"]
|
(is (= ["UPDATE distributors SET dname = ? WHERE did = 2 RETURNING did, dname" "Foo Bar Designs"]
|
||||||
(-> (update :distributors)
|
(-> (update :distributors)
|
||||||
(set {:dname "Foo Bar Designs"})
|
(set {:dname "Foo Bar Designs"})
|
||||||
(where [:= :did :2])
|
(where [:= :did :2])
|
||||||
(returning [:did :dname])
|
(returning :did :dname)
|
||||||
sql/format)))))
|
sql/format)))))
|
||||||
|
|
||||||
(deftest create-view-test
|
(deftest create-view-test
|
||||||
|
|
@ -172,7 +199,7 @@
|
||||||
[:location :point]])
|
[:location :point]])
|
||||||
sql/format))))
|
sql/format))))
|
||||||
(testing "create table with foreign key reference"
|
(testing "create table with foreign key reference"
|
||||||
(is (= ["CREATE TABLE weather (city VARCHAR(80) REFERENCES CITIES(CITY), temp_lo INT, temp_hi INT, prcp REAL, date DATE)"]
|
(is (= ["CREATE TABLE weather (city VARCHAR(80) REFERENCES cities(city), temp_lo INT, temp_hi INT, prcp REAL, date DATE)"]
|
||||||
(-> (create-table :weather)
|
(-> (create-table :weather)
|
||||||
(with-columns [[:city [:varchar :80] [:references :cities :city]]
|
(with-columns [[:city [:varchar :80] [:references :cities :city]]
|
||||||
[:temp_lo :int]
|
[:temp_lo :int]
|
||||||
|
|
@ -181,7 +208,7 @@
|
||||||
[:date :date]])
|
[:date :date]])
|
||||||
sql/format))))
|
sql/format))))
|
||||||
(testing "creating table with table level constraint"
|
(testing "creating table with table level constraint"
|
||||||
(is (= ["CREATE TABLE films (code CHAR(5), title VARCHAR(40), did INTEGER, date_prod DATE, kind VARCHAR(10), CONSTRAINT code_title PRIMARY KEY(CODE, TITLE))"]
|
(is (= ["CREATE TABLE films (code CHAR(5), title VARCHAR(40), did INTEGER, date_prod DATE, kind VARCHAR(10), CONSTRAINT code_title PRIMARY KEY(code, title))"]
|
||||||
(-> (create-table :films)
|
(-> (create-table :films)
|
||||||
(with-columns [[:code [:char 5]]
|
(with-columns [[:code [:char 5]]
|
||||||
[:title [:varchar 40]]
|
[:title [:varchar 40]]
|
||||||
|
|
@ -191,7 +218,7 @@
|
||||||
[[:constraint :code_title] [:primary-key :code :title]]])
|
[[:constraint :code_title] [:primary-key :code :title]]])
|
||||||
sql/format))))
|
sql/format))))
|
||||||
(testing "creating table with column level constraint"
|
(testing "creating table with column level constraint"
|
||||||
(is (= ["CREATE TABLE films (code CHAR(5) CONSTRAINT FIRSTKEY PRIMARY KEY, title VARCHAR(40) NOT NULL, did INTEGER NOT NULL, date_prod DATE, kind VARCHAR(10))"]
|
(is (= ["CREATE TABLE films (code CHAR(5) CONSTRAINT firstkey PRIMARY KEY, title VARCHAR(40) NOT NULL, did INTEGER NOT NULL, date_prod DATE, kind VARCHAR(10))"]
|
||||||
(-> (create-table :films)
|
(-> (create-table :films)
|
||||||
(with-columns [[:code [:char 5] [:constraint :firstkey] [:primary-key]]
|
(with-columns [[:code [:char 5] [:constraint :firstkey] [:primary-key]]
|
||||||
[:title [:varchar 40] [:not nil]]
|
[:title [:varchar 40] [:not nil]]
|
||||||
|
|
@ -200,13 +227,13 @@
|
||||||
[:kind [:varchar 10]]])
|
[:kind [:varchar 10]]])
|
||||||
sql/format))))
|
sql/format))))
|
||||||
(testing "creating table with columns with default values"
|
(testing "creating table with columns with default values"
|
||||||
(is (= ["CREATE TABLE distributors (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL)"]
|
(is (= ["CREATE TABLE distributors (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('serial'), name VARCHAR(40) NOT NULL)"]
|
||||||
(-> (create-table :distributors)
|
(-> (create-table :distributors)
|
||||||
(with-columns [[:did :integer [:primary-key] [:default [:nextval "serial"]]]
|
(with-columns [[:did :integer [:primary-key] [:default [:nextval "serial"]]]
|
||||||
[:name [:varchar 40] [:not nil]]])
|
[:name [:varchar 40] [:not nil]]])
|
||||||
sql/format))))
|
sql/format))))
|
||||||
(testing "creating table with column checks"
|
(testing "creating table with column checks"
|
||||||
(is (= ["CREATE TABLE products (product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))"]
|
(is (= ["CREATE TABLE products (product_no INTEGER, name TEXT, price NUMERIC CHECK(price > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))"]
|
||||||
(-> (create-table :products)
|
(-> (create-table :products)
|
||||||
(with-columns [[:product_no :integer]
|
(with-columns [[:product_no :integer]
|
||||||
[:name :text]
|
[:name :text]
|
||||||
|
|
@ -215,7 +242,39 @@
|
||||||
[[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]])
|
[[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]])
|
||||||
sql/format)))))
|
sql/format)))))
|
||||||
|
|
||||||
|
(deftest references-issue-386
|
||||||
|
(is (= ["CREATE TABLE IF NOT EXISTS user (id VARCHAR(255) NOT NULL PRIMARY KEY, company_id INT NOT NULL, name VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY(company_id) REFERENCES company(id))"]
|
||||||
|
(-> {:create-table [:user :if-not-exists]
|
||||||
|
:with-columns
|
||||||
|
[[:id [:varchar 255] [:not nil] [:primary-key]]
|
||||||
|
[:company-id :int [:not nil]]
|
||||||
|
[:name [:varchar 255] [:not nil]]
|
||||||
|
[:password [:varchar 255] [:not nil]]
|
||||||
|
[:created-time :datetime [:default :CURRENT_TIMESTAMP]]
|
||||||
|
[:updated-time :datetime [:default :CURRENT_TIMESTAMP]
|
||||||
|
:on :update :CURRENT_TIMESTAMP]
|
||||||
|
[[:foreign-key :company-id] [:references :company :id]]]}
|
||||||
|
(sql/format)))))
|
||||||
|
|
||||||
|
(deftest create-table-issue-437
|
||||||
|
(is (= ["CREATE TABLE bar (did UUID DEFAULT GEN_RANDOM_UUID(), foo_id VARCHAR NOT NULL, PRIMARY KEY(did, foo_id), FOREIGN KEY(foo_id) REFERENCES foo(id) ON DELETE CASCADE)"]
|
||||||
|
(-> (create-table :bar)
|
||||||
|
(with-columns
|
||||||
|
[[:did :uuid [:default [:gen_random_uuid]]]
|
||||||
|
[:foo-id :varchar [:not nil]]
|
||||||
|
[[:primary-key :did :foo-id]]
|
||||||
|
[[:foreign-key :foo-id]
|
||||||
|
[:references :foo :id]
|
||||||
|
:on-delete :cascade]])
|
||||||
|
(sql/format)))))
|
||||||
|
|
||||||
(deftest over-test
|
(deftest over-test
|
||||||
|
(testing "simple window statement"
|
||||||
|
(is (= ["SELECT AVG(salary) OVER w FROM employee WINDOW w AS (PARTITION BY department ORDER BY salary ASC)"]
|
||||||
|
(sql/format {:select [[[:over [[:avg :salary] :w]]]]
|
||||||
|
:from :employee
|
||||||
|
:window [:w {:partition-by :department
|
||||||
|
:order-by :salary}]}))))
|
||||||
(testing "window function over on select statemt"
|
(testing "window function over on select statemt"
|
||||||
(is (= ["SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary FROM employee WINDOW w AS (PARTITION BY department)"]
|
(is (= ["SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary FROM employee WINDOW w AS (PARTITION BY department)"]
|
||||||
;; honeysql treats over as a function:
|
;; honeysql treats over as a function:
|
||||||
|
|
@ -306,7 +365,7 @@
|
||||||
|
|
||||||
(deftest values-except-select
|
(deftest values-except-select
|
||||||
(testing "select which values are not not present in a table"
|
(testing "select which values are not not present in a table"
|
||||||
(is (= ["(VALUES (?), (?), (?)) EXCEPT (SELECT id FROM images)" 4 5 6]
|
(is (= ["VALUES (?), (?), (?) EXCEPT SELECT id FROM images" 4 5 6]
|
||||||
(sql/format
|
(sql/format
|
||||||
{:except
|
{:except
|
||||||
[{:values [[4] [5] [6]]}
|
[{:values [[4] [5] [6]]}
|
||||||
|
|
@ -314,7 +373,7 @@
|
||||||
|
|
||||||
(deftest select-except-select
|
(deftest select-except-select
|
||||||
(testing "select which rows are not present in another table"
|
(testing "select which rows are not present in another table"
|
||||||
(is (= ["(SELECT ip) EXCEPT (SELECT ip FROM ip_location)"]
|
(is (= ["SELECT ip EXCEPT SELECT ip FROM ip_location"]
|
||||||
(sql/format
|
(sql/format
|
||||||
{:except
|
{:except
|
||||||
[{:select [:ip]}
|
[{:select [:ip]}
|
||||||
|
|
@ -322,7 +381,7 @@
|
||||||
|
|
||||||
(deftest values-except-all-select
|
(deftest values-except-all-select
|
||||||
(testing "select which values are not not present in a table"
|
(testing "select which values are not not present in a table"
|
||||||
(is (= ["(VALUES (?), (?), (?)) EXCEPT ALL (SELECT id FROM images)" 4 5 6]
|
(is (= ["VALUES (?), (?), (?) EXCEPT ALL SELECT id FROM images" 4 5 6]
|
||||||
(sql/format
|
(sql/format
|
||||||
{:except-all
|
{:except-all
|
||||||
[{:values [[4] [5] [6]]}
|
[{:values [[4] [5] [6]]}
|
||||||
|
|
@ -330,7 +389,7 @@
|
||||||
|
|
||||||
(deftest select-except-all-select
|
(deftest select-except-all-select
|
||||||
(testing "select which rows are not present in another table"
|
(testing "select which rows are not present in another table"
|
||||||
(is (= ["(SELECT ip) EXCEPT ALL (SELECT ip FROM ip_location)"]
|
(is (= ["SELECT ip EXCEPT ALL SELECT ip FROM ip_location"]
|
||||||
(sql/format
|
(sql/format
|
||||||
{:except-all
|
{:except-all
|
||||||
[{:select [:ip]}
|
[{:select [:ip]}
|
||||||
|
|
@ -349,6 +408,18 @@
|
||||||
(modifiers :distinct-on :a :b)
|
(modifiers :distinct-on :a :b)
|
||||||
(sql/format :quoting :ansi))))))
|
(sql/format :quoting :ansi))))))
|
||||||
|
|
||||||
|
(deftest select-agg-order-by-test
|
||||||
|
(testing "single expression in order by"
|
||||||
|
(is (= ["SELECT ARRAY_AGG(a ORDER BY x ASC) FROM products"]
|
||||||
|
(sql/format
|
||||||
|
{:select [[[:array_agg [:order-by :a :x]]]]
|
||||||
|
:from :products}))))
|
||||||
|
(testing "multiple expressions in order by"
|
||||||
|
(is (= ["SELECT ARRAY_AGG(a ORDER BY x ASC, y DESC, z ASC) FROM products"]
|
||||||
|
(sql/format
|
||||||
|
{:select [[[:array_agg [:order-by :a [:x :asc] [:y :desc] :z]]]]
|
||||||
|
:from :products})))))
|
||||||
|
|
||||||
(deftest create-extension-test
|
(deftest create-extension-test
|
||||||
;; previously, honeysql required :allow-dashed-names? true
|
;; previously, honeysql required :allow-dashed-names? true
|
||||||
(testing "create extension"
|
(testing "create extension"
|
||||||
|
|
@ -366,3 +437,21 @@
|
||||||
(is (= ["DROP EXTENSION \"uuid-ossp\""]
|
(is (= ["DROP EXTENSION \"uuid-ossp\""]
|
||||||
(-> (drop-extension :uuid-ossp)
|
(-> (drop-extension :uuid-ossp)
|
||||||
(sql/format {:quoted true}))))))
|
(sql/format {:quoted true}))))))
|
||||||
|
|
||||||
|
(deftest issue-453-constraint
|
||||||
|
(testing "standalone constraint"
|
||||||
|
(is (= ["CREATE TABLE bar (a INTEGER, b INTEGER, CONSTRAINT foo_natural_key UNIQUE (a, b))"]
|
||||||
|
(-> {:create-table [:bar]
|
||||||
|
:with-columns
|
||||||
|
[[:a :integer]
|
||||||
|
[:b :integer]
|
||||||
|
[[:constraint :foo_natural_key] :unique [:composite :a :b]]]}
|
||||||
|
(sql/format)))))
|
||||||
|
(testing "inline constraint"
|
||||||
|
(is (= ["CREATE TABLE foo (a INTEGER CONSTRAINT a_pos CHECK(a > 0), b INTEGER, CONSTRAINT a_bigger CHECK(b < a))"]
|
||||||
|
(-> '{create-table foo
|
||||||
|
with-columns
|
||||||
|
((a integer (constraint a_pos) (check (> a 0)))
|
||||||
|
(b integer)
|
||||||
|
((constraint a_bigger) (check (< b a))))}
|
||||||
|
(sql/format))))))
|
||||||
|
|
|
||||||
148
test/honey/sql/xtdb_test.cljc
Normal file
148
test/honey/sql/xtdb_test.cljc
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
;; copyright (c) 2020-2025 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.sql.xtdb-test
|
||||||
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
|
[honey.sql :as sql]
|
||||||
|
[honey.sql.helpers :as h
|
||||||
|
:refer [select exclude rename from]]))
|
||||||
|
|
||||||
|
(deftest select-tests
|
||||||
|
(testing "select, exclude, rename"
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME value AS foo_value FROM foo"]
|
||||||
|
(sql/format (-> (select :*) (exclude :_id) (rename [:value :foo_value])
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE (_id, a) RENAME value AS foo_value FROM foo"]
|
||||||
|
(sql/format (-> (select :*) (exclude :_id :a) (rename [:value :foo_value])
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME (value AS foo_value, a AS b) FROM foo"]
|
||||||
|
(sql/format (-> (select :*) (exclude :_id)
|
||||||
|
(rename [:value :foo_value]
|
||||||
|
[:a :b])
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME value AS foo_value, c.x FROM foo"]
|
||||||
|
(sql/format (-> (select [:* (-> (exclude :_id) (rename [:value :foo_value]))]
|
||||||
|
:c.x)
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE (_id, a) RENAME value AS foo_value, c.x FROM foo"]
|
||||||
|
(sql/format (-> (select [:* (-> (exclude :_id :a) (rename [:value :foo_value]))]
|
||||||
|
:c.x)
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME (value AS foo_value, a AS b), c.x FROM foo"]
|
||||||
|
(sql/format (-> (select [:* (-> (exclude :_id)
|
||||||
|
(rename [:value :foo_value]
|
||||||
|
[:a :b]))]
|
||||||
|
:c.x)
|
||||||
|
(from :foo))))))
|
||||||
|
(testing "select, nest_one, nest_many"
|
||||||
|
(is (= ["SELECT a._id, NEST_ONE (SELECT * FROM foo AS b WHERE b_id = a._id) FROM bar AS a"]
|
||||||
|
(sql/format '{select (a._id,
|
||||||
|
((nest_one {select * from ((foo b)) where (= b_id a._id)})))
|
||||||
|
from ((bar a))})))
|
||||||
|
(is (= ["SELECT a._id, NEST_MANY (SELECT * FROM foo AS b) FROM bar AS a"]
|
||||||
|
(sql/format '{select (a._id,
|
||||||
|
((nest_many {select * from ((foo b))})))
|
||||||
|
from ((bar a))})))))
|
||||||
|
|
||||||
|
(deftest dotted-array-access-tests
|
||||||
|
(is (= ["SELECT (a.b).c"] ; old, partial support:
|
||||||
|
(sql/format '{select (((. (nest :a.b) :c)))})))
|
||||||
|
(is (= ["SELECT (a.b).c"] ; new, complete support:
|
||||||
|
(sql/format '{select (((:get-in :a.b :c)))})))
|
||||||
|
(is (= ["SELECT (a).b.c"] ; the first expression is always parenthesized:
|
||||||
|
(sql/format '{select (((:get-in :a :b :c)))}))))
|
||||||
|
|
||||||
|
(deftest erase-from-test
|
||||||
|
(is (= ["ERASE FROM foo WHERE foo.id = ?" 42]
|
||||||
|
(-> {:erase-from :foo
|
||||||
|
:where [:= :foo.id 42]}
|
||||||
|
(sql/format))))
|
||||||
|
(is (= ["ERASE FROM foo WHERE foo.id = ?" 42]
|
||||||
|
(-> (h/erase-from :foo)
|
||||||
|
(h/where [:= :foo.id 42])
|
||||||
|
(sql/format)))))
|
||||||
|
|
||||||
|
(deftest inline-record-body
|
||||||
|
(is (= ["{_id: 1, name: 'foo', info: {contact: [{loc: 'home', tel: '123'}, {loc: 'work', tel: '456'}]}}"]
|
||||||
|
(sql/format [:inline {:_id 1 :name "foo"
|
||||||
|
:info {:contact [{:loc "home" :tel "123"}
|
||||||
|
{:loc "work" :tel "456"}]}}]))))
|
||||||
|
|
||||||
|
(deftest records-statement
|
||||||
|
(testing "auto-lift maps"
|
||||||
|
(is (= ["RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format {:records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}]}))))
|
||||||
|
(testing "explicit inline"
|
||||||
|
(is (= ["RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]}))))
|
||||||
|
(testing "insert with records"
|
||||||
|
(is (= ["INSERT INTO foo RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:insert-into :foo
|
||||||
|
:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]})))
|
||||||
|
(is (= ["INSERT INTO foo RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:insert-into :foo
|
||||||
|
:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]})))
|
||||||
|
(is (= ["INSERT INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format {:insert-into [:foo ; as a sub-clause
|
||||||
|
{:records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}]}]})))))
|
||||||
|
|
||||||
|
(deftest patch-statement
|
||||||
|
(testing "patch with records"
|
||||||
|
(is (= ["PATCH INTO foo RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:patch-into [:foo]
|
||||||
|
:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]})))
|
||||||
|
(is (= ["PATCH INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format {:patch-into [:foo ; as a sub-clause
|
||||||
|
{:records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}]}]})))
|
||||||
|
(is (= ["PATCH INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format (h/patch-into :foo
|
||||||
|
(h/records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}])))))))
|
||||||
|
|
||||||
|
(deftest object-record-expr
|
||||||
|
(testing "object literal"
|
||||||
|
(is (= ["SELECT OBJECT (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format {:select [[[:object {:_id 1 :name "foo"}]]]})))
|
||||||
|
(is (= ["SELECT OBJECT (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format '{select (((:object {:_id 1 :name "foo"})))}))))
|
||||||
|
(testing "record literal"
|
||||||
|
(is (= ["SELECT RECORD (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format {:select [[[:record {:_id 1 :name "foo"}]]]})))
|
||||||
|
(is (= ["SELECT RECORD (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format '{select (((:record {:_id 1 :name "foo"})))}))))
|
||||||
|
(testing "inline map literal"
|
||||||
|
(is (= ["SELECT {_id: 1, name: 'foo'}"]
|
||||||
|
(sql/format {:select [[[:inline {:_id 1 :name "foo"}]]]})))))
|
||||||
|
|
||||||
|
(deftest navigation-dot-index
|
||||||
|
(is (= ["SELECT (a.b).c[1].d"]
|
||||||
|
(sql/format '{select (((get-in a.b c 1 d)))})))
|
||||||
|
(is (= ["SELECT (a.b).c[?].d" 1]
|
||||||
|
(sql/format '{select (((get-in a.b c (lift 1) d)))})))
|
||||||
|
(is (= ["SELECT (a.b).c[?].d" 1]
|
||||||
|
(sql/format '{select (((get-in (. a b) c (lift 1) d)))})))
|
||||||
|
(is (= ["SELECT (OBJECT (_id: 1, b: 'thing').b).c[?].d" 1]
|
||||||
|
(sql/format '{select (((get-in (. (object {_id 1 b "thing"}) b) c (lift 1) d)))}))))
|
||||||
|
|
||||||
|
(deftest assert-statement
|
||||||
|
(testing "quoted sql"
|
||||||
|
(is (= ["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
(sql/format '{assert (not-exists {select 1 from users where (= email "james @example.com")})}
|
||||||
|
:inline true)))
|
||||||
|
(is (= ["ASSERT TRUE"]
|
||||||
|
(sql/format '{assert true}
|
||||||
|
:inline true))))
|
||||||
|
(testing "helper"
|
||||||
|
(is (= ["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
(-> (h/assert [:not-exists {:select 1 :from :users :where [:= :email "james @example.com"]}])
|
||||||
|
(sql/format {:inline true}))))
|
||||||
|
(is (= ["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
(-> {}
|
||||||
|
(h/assert [:not-exists {:select 1 :from :users :where [:= :email "james @example.com"]}])
|
||||||
|
(sql/format {:inline true}))))))
|
||||||
File diff suppressed because it is too large
Load diff
10
test/honey/unhashable_test.clj
Normal file
10
test/honey/unhashable_test.clj
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
(ns honey.unhashable-test
|
||||||
|
(:require [clojure.test :refer [deftest is]]
|
||||||
|
[honey.sql :as sut]))
|
||||||
|
|
||||||
|
(deftest unhashable-value-509
|
||||||
|
(let [unhashable (reify Object
|
||||||
|
(toString [_] "unhashable")
|
||||||
|
(hashCode [_] (throw (ex-info "Unsupported" {}))))]
|
||||||
|
(is (= ["INSERT INTO table VALUES (?)" unhashable]
|
||||||
|
(sut/format {:insert-into :table :values [[unhashable]]})))))
|
||||||
51
test/honey/union_test.cljc
Normal file
51
test/honey/union_test.cljc
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
;; copyright (c) 2023-2024 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.union-test
|
||||||
|
(:refer-clojure :exclude [format])
|
||||||
|
(:require [clojure.test :refer [deftest is]]
|
||||||
|
[honey.sql :as sut]))
|
||||||
|
|
||||||
|
(deftest issue-451
|
||||||
|
(is (= [(str "SELECT ids.id AS id"
|
||||||
|
" FROM ((SELECT dimension.human_readable_field_id AS id"
|
||||||
|
" FROM dimension AS dimension"
|
||||||
|
" WHERE (dimension.field_id = ?) AND (dimension.human_readable_field_id IS NOT NULL)"
|
||||||
|
" LIMIT ?)"
|
||||||
|
" UNION"
|
||||||
|
" (SELECT dest.id AS id"
|
||||||
|
" FROM field AS source"
|
||||||
|
" LEFT JOIN table AS table ON source.table_id = table.id"
|
||||||
|
" LEFT JOIN field AS dest ON dest.table_id = table.id"
|
||||||
|
" WHERE (source.id = ?) AND (source.semantic_type IN (?)) AND (dest.semantic_type IN (?))"
|
||||||
|
" LIMIT ?)) AS ids"
|
||||||
|
" LIMIT ?")
|
||||||
|
1
|
||||||
|
1
|
||||||
|
1
|
||||||
|
"type/PK"
|
||||||
|
"type/Name"
|
||||||
|
1
|
||||||
|
1]
|
||||||
|
(-> {:select [[:ids.id :id]]
|
||||||
|
:from [[{:union
|
||||||
|
[{:nest
|
||||||
|
{:select [[:dimension.human_readable_field_id :id]]
|
||||||
|
:from [[:dimension :dimension]]
|
||||||
|
:where [:and
|
||||||
|
[:= :dimension.field_id 1]
|
||||||
|
[:not= :dimension.human_readable_field_id nil]]
|
||||||
|
:limit 1}}
|
||||||
|
{:nest
|
||||||
|
{:select [[:dest.id :id]]
|
||||||
|
:from [[:field :source]]
|
||||||
|
:left-join [[:table :table] [:= :source.table_id :table.id] [:field :dest] [:= :dest.table_id :table.id]]
|
||||||
|
:where [:and
|
||||||
|
[:= :source.id 1]
|
||||||
|
[:in :source.semantic_type #{"type/PK"}]
|
||||||
|
[:in :dest.semantic_type #{"type/Name"}]]
|
||||||
|
:limit 1}}]}
|
||||||
|
:ids]]
|
||||||
|
:limit 1}
|
||||||
|
(sut/format))))
|
||||||
|
|
||||||
|
)
|
||||||
62
test/honey/util_test.cljc
Normal file
62
test/honey/util_test.cljc
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
(ns honey.util-test
|
||||||
|
(:refer-clojure :exclude [str])
|
||||||
|
(:require [clojure.test :refer [deftest is are]]
|
||||||
|
[honey.sql.util :as sut]))
|
||||||
|
|
||||||
|
(deftest str-test
|
||||||
|
(are [arg1 result] (= result (sut/str arg1))
|
||||||
|
nil ""
|
||||||
|
1 "1"
|
||||||
|
"foo" "foo"
|
||||||
|
:foo ":foo")
|
||||||
|
(are [arg1 arg2 result] (= result (sut/str arg1 arg2))
|
||||||
|
nil nil ""
|
||||||
|
nil 1 "1"
|
||||||
|
1 nil "1"
|
||||||
|
1 2 "12"
|
||||||
|
:foo "bar" ":foobar")
|
||||||
|
(are [arg1 arg2 arg3 result] (= result (sut/str arg1 arg2 arg3))
|
||||||
|
nil nil nil ""
|
||||||
|
nil 1 nil "1"
|
||||||
|
1 nil nil "1"
|
||||||
|
1 nil 2 "12"
|
||||||
|
:foo "bar" 'baz ":foobarbaz")
|
||||||
|
(are [args result] (= result (apply sut/str args))
|
||||||
|
(range 10) "0123456789"
|
||||||
|
[] ""))
|
||||||
|
|
||||||
|
(deftest join-test
|
||||||
|
(is (= "0123456789" (sut/join "" (range 10))))
|
||||||
|
(is (= "1" (sut/join "" [1])))
|
||||||
|
(is (= "" (sut/join "" [])))
|
||||||
|
(is (= "0, 1, 2, 3, 4, 5, 6, 7, 8, 9" (sut/join ", " (range 10))))
|
||||||
|
(is (= "1" (sut/join ", " [1])))
|
||||||
|
(is (= "" (sut/join ", " [])))
|
||||||
|
|
||||||
|
(is (= "0_0, 1_1, 2_2, 3_3, 4_4, 5_5, 6_6, 7_7, 8_8, 9_9"
|
||||||
|
(sut/join ", " (map #(sut/str % "_" %)) (range 10))))
|
||||||
|
(is (= "1_1"
|
||||||
|
(sut/join ", " (map #(sut/str % "_" %)) [1])))
|
||||||
|
(is (= ""
|
||||||
|
(sut/join ", " (map #(sut/str % "_" %)) [])))
|
||||||
|
|
||||||
|
(is (= "1, 2, 3, 4"
|
||||||
|
(sut/join ", " (remove nil?) [1 nil 2 nil 3 nil nil nil 4])))
|
||||||
|
(is (= "" (sut/join ", " (remove nil?) [nil nil nil nil]))))
|
||||||
|
|
||||||
|
(deftest split-by-separator-test
|
||||||
|
(is (= [""] (sut/split-by-separator "" ".")))
|
||||||
|
(is (= ["" ""] (sut/split-by-separator "." ".")))
|
||||||
|
(is (= ["hello"] (sut/split-by-separator "hello" ".")))
|
||||||
|
(is (= ["h" "e" "l" "l" "o"] (sut/split-by-separator "h.e.l.l.o" ".")))
|
||||||
|
(is (= ["" "h" "e" "" "" "l" "" "l" "o" ""]
|
||||||
|
(sut/split-by-separator ".h.e...l..l.o." "."))))
|
||||||
|
|
||||||
|
(deftest into*-test
|
||||||
|
(is (= [1] (sut/into* [1] nil)))
|
||||||
|
(is (= [1] (sut/into* [1] [])))
|
||||||
|
(is (= [1] (sut/into* [1] nil [] nil [])))
|
||||||
|
(is (= [1 2 3] (sut/into* [1] [2 3])))
|
||||||
|
(is (= [1 2 3 4 5 6] (sut/into* [1] [2 3] [4 5 6])))
|
||||||
|
(is (= [1 2 3 4 5 6 7] (sut/into* [1] [2 3] [4 5 6] [7])))
|
||||||
|
(is (= [1 2 3 4 5 6 7 8 9] (sut/into* [1] [2 3] [4 5 6] [7] [8 9]))))
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
(ns honeysql.core
|
|
||||||
(:refer-clojure :exclude [group-by format])
|
|
||||||
(:require [honeysql.format :as format]
|
|
||||||
[honeysql.types :as types]
|
|
||||||
[honeysql.helpers :refer [build-clause]]
|
|
||||||
#?(:clj [honeysql.util :refer [defalias]])
|
|
||||||
[clojure.string :as string]))
|
|
||||||
|
|
||||||
(#?(:clj defalias :cljs def) call types/call)
|
|
||||||
(#?(:clj defalias :cljs def) raw types/raw)
|
|
||||||
(#?(:clj defalias :cljs def) param types/param)
|
|
||||||
(#?(:clj defalias :cljs def) inline types/inline)
|
|
||||||
(#?(:clj defalias :cljs def) format format/format)
|
|
||||||
(#?(:clj defalias :cljs def) format-predicate format/format-predicate)
|
|
||||||
(#?(:clj defalias :cljs def) quote-identifier format/quote-identifier)
|
|
||||||
|
|
||||||
(defn qualify
|
|
||||||
"Takes one or more keyword or string qualifers and name. Returns
|
|
||||||
a keyword of the concatenated qualifiers and name separated by periods.
|
|
||||||
|
|
||||||
(qualify :foo \"bar\" :baz) => :foo.bar.baz"
|
|
||||||
[& qualifiers+name]
|
|
||||||
(keyword
|
|
||||||
(string/join "."
|
|
||||||
(for [s qualifiers+name
|
|
||||||
:when (not (nil? s))]
|
|
||||||
(if (keyword? s)
|
|
||||||
(name s)
|
|
||||||
(str s))))))
|
|
||||||
|
|
||||||
(defn build
|
|
||||||
"Takes a series of clause+data pairs and returns a SQL map. Example:
|
|
||||||
|
|
||||||
(build :select [:a :b]
|
|
||||||
:from :bar)
|
|
||||||
|
|
||||||
Clauses are defined with the honeysql.helpers/build-clause multimethod.
|
|
||||||
Built-in clauses include:
|
|
||||||
|
|
||||||
:select, :merge-select, :un-select
|
|
||||||
:from, :merge-from
|
|
||||||
:join, :merge-join
|
|
||||||
:left-join, :merge-left-join
|
|
||||||
:right-join, :merge-right-join
|
|
||||||
:full-join, :merge-full-join
|
|
||||||
:cross-join, :merge-cross-join
|
|
||||||
:where, :merge-where
|
|
||||||
:group-by, :merge-group-by
|
|
||||||
:having, :merge-having
|
|
||||||
:limit
|
|
||||||
:offset
|
|
||||||
:modifiers, :merge-modifiers
|
|
||||||
:insert-into
|
|
||||||
:columns, :merge-columns
|
|
||||||
:values, :merge-values
|
|
||||||
:query-values
|
|
||||||
:update
|
|
||||||
:set
|
|
||||||
:delete-from"
|
|
||||||
[& clauses]
|
|
||||||
(let [[base clauses] (if (map? (first clauses))
|
|
||||||
[(first clauses) (rest clauses)]
|
|
||||||
[{} clauses])]
|
|
||||||
(reduce
|
|
||||||
(fn [sql-map [op args]]
|
|
||||||
(build-clause op sql-map args))
|
|
||||||
(if (empty? base)
|
|
||||||
base
|
|
||||||
(apply build (apply concat base)))
|
|
||||||
(partition 2 clauses))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(require '[honeysql.core :as sql])
|
|
||||||
(sql/format {:select [:*] :from [:table] :where [:= :id 1]})
|
|
||||||
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} :quoting :mysql)
|
|
||||||
,)
|
|
||||||
|
|
@ -1,270 +0,0 @@
|
||||||
(ns honeysql.core-test
|
|
||||||
(:refer-clojure :exclude [format update])
|
|
||||||
(:require [#?@(:clj [clojure.test :refer]
|
|
||||||
:cljs [cljs.test :refer-macros]) [deftest testing is]]
|
|
||||||
[honeysql.core :as sql]
|
|
||||||
[honeysql.helpers :refer [select modifiers from join left-join
|
|
||||||
right-join full-join cross-join
|
|
||||||
where group having
|
|
||||||
order-by limit offset values columns
|
|
||||||
insert-into with merge-where]]
|
|
||||||
honeysql.format-test))
|
|
||||||
|
|
||||||
;; TODO: more tests
|
|
||||||
|
|
||||||
(deftest test-select
|
|
||||||
(let [m1 (-> (with [:cte (-> (select :*)
|
|
||||||
(from :example)
|
|
||||||
(where [:= :example-column 0]))])
|
|
||||||
(select :f.* :b.baz :c.quux [:b.bla :bla-bla]
|
|
||||||
:%now (sql/raw "@x := 10"))
|
|
||||||
;;(un-select :c.quux)
|
|
||||||
(modifiers :distinct)
|
|
||||||
(from [:foo :f] [:baz :b])
|
|
||||||
(join :draq [:= :f.b :draq.x])
|
|
||||||
(left-join [:clod :c] [:= :f.a :c.d])
|
|
||||||
(right-join :bock [:= :bock.z :c.e])
|
|
||||||
(full-join :beck [:= :beck.x :c.y])
|
|
||||||
(where [:or
|
|
||||||
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
|
||||||
[:< 1 2 3]
|
|
||||||
[:in :f.e [1 (sql/param :param2) 3]]
|
|
||||||
[:between :f.e 10 20]])
|
|
||||||
;;(merge-where [:not= nil :b.bla])
|
|
||||||
(group :f.a)
|
|
||||||
(having [:< 0 :f.e])
|
|
||||||
(order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
|
|
||||||
(limit 50)
|
|
||||||
(offset 10))
|
|
||||||
m2 {:with [[:cte {:select [:*]
|
|
||||||
:from [:example]
|
|
||||||
:where [:= :example-column 0]}]]
|
|
||||||
:select [:f.* :b.baz :c.quux [:b.bla :bla-bla]
|
|
||||||
:%now (sql/raw "@x := 10")]
|
|
||||||
;;:un-select :c.quux
|
|
||||||
:modifiers :distinct
|
|
||||||
:from [[:foo :f] [:baz :b]]
|
|
||||||
:join [:draq [:= :f.b :draq.x]]
|
|
||||||
:left-join [[:clod :c] [:= :f.a :c.d]]
|
|
||||||
:right-join [:bock [:= :bock.z :c.e]]
|
|
||||||
:full-join [:beck [:= :beck.x :c.y]]
|
|
||||||
:where [:or
|
|
||||||
[:and [:= :f.a "bort"] [:not= :b.baz :?param1]]
|
|
||||||
[:< 1 2 3]
|
|
||||||
[:in :f.e [1 (sql/param :param2) 3]]
|
|
||||||
[:between :f.e 10 20]]
|
|
||||||
;;:merge-where [:not= nil :b.bla]
|
|
||||||
:group-by :f.a
|
|
||||||
:having [:< 0 :f.e]
|
|
||||||
:order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]]
|
|
||||||
:limit 50
|
|
||||||
:offset 10}
|
|
||||||
m3 (sql/build m2)
|
|
||||||
m4 (apply sql/build (apply concat m2))]
|
|
||||||
(testing "Various construction methods are consistent"
|
|
||||||
(is (= m1 m3 m4)))
|
|
||||||
(testing "SQL data formats correctly"
|
|
||||||
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT ? OFFSET ? "
|
|
||||||
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
|
||||||
(sql/format m1 {:param1 "gabba" :param2 2}))))
|
|
||||||
#?(:clj (testing "SQL data prints and reads correctly"
|
|
||||||
(is (= m1 (read-string (pr-str m1))))))
|
|
||||||
(testing "SQL data formats correctly with alternate param naming"
|
|
||||||
(is (= (sql/format m1 :params {:param1 "gabba" :param2 2} :parameterizer :postgresql)
|
|
||||||
["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2 AND b.baz <> $3) OR ($4 < $5 AND $6 < $7) OR (f.e in ($8, $9, $10)) OR f.e BETWEEN $11 AND $12) GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT $14 OFFSET $15 "
|
|
||||||
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10])))
|
|
||||||
(testing "Locking"
|
|
||||||
(is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT ? OFFSET ? FOR UPDATE "
|
|
||||||
0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
|
|
||||||
(sql/format (assoc m1 :lock {:mode :update})
|
|
||||||
{:param1 "gabba" :param2 2}))))))
|
|
||||||
|
|
||||||
(deftest test-cast
|
|
||||||
(is (= ["SELECT foo, CAST(bar AS integer)"]
|
|
||||||
(sql/format {:select [:foo (sql/call :cast :bar :integer)]})))
|
|
||||||
(is (= ["SELECT foo, CAST(bar AS integer)"]
|
|
||||||
(sql/format {:select [:foo (sql/call :cast :bar 'integer)]}))))
|
|
||||||
|
|
||||||
(deftest test-value
|
|
||||||
(is (= ["INSERT INTO foo (bar) VALUES (?)" {:baz "my-val"}]
|
|
||||||
(->
|
|
||||||
(insert-into :foo)
|
|
||||||
(columns :bar)
|
|
||||||
(values [[(honeysql.format/value {:baz "my-val"})]])
|
|
||||||
sql/format)))
|
|
||||||
(is (= ["INSERT INTO foo (a, b, c) VALUES (?, ?, ?), (?, ?, ?)"
|
|
||||||
"a" "b" "c" "a" "b" "c"]
|
|
||||||
(-> (insert-into :foo)
|
|
||||||
(values [(array-map :a "a" :b "b" :c "c")
|
|
||||||
(hash-map :a "a" :b "b" :c "c")])
|
|
||||||
sql/format))))
|
|
||||||
|
|
||||||
(deftest test-operators
|
|
||||||
(testing "="
|
|
||||||
(testing "with nil"
|
|
||||||
(is (= ["SELECT * FROM customers WHERE name IS NULL"]
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:= :name nil]})))
|
|
||||||
(is (= ["SELECT * FROM customers WHERE name = ?" nil]
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:= :name :?name]}
|
|
||||||
{:name nil})))))
|
|
||||||
(testing "in"
|
|
||||||
(doseq [[cname coll] [[:vector []] [:set #{}] [:list '()]]]
|
|
||||||
(testing (str "with values from a " (name cname))
|
|
||||||
(let [values (conj coll 1)]
|
|
||||||
(is (= ["SELECT * FROM customers WHERE (id in (?))" 1]
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:in :id values]})))
|
|
||||||
(is (= ["SELECT * FROM customers WHERE (id in (?))" 1]
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:in :id :?ids]}
|
|
||||||
{:ids values}))))))
|
|
||||||
(testing "with more than one integer"
|
|
||||||
(let [values [1 2]]
|
|
||||||
(is (= ["SELECT * FROM customers WHERE (id in (?, ?))" 1 2]
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:in :id values]})))
|
|
||||||
(is (= ["SELECT * FROM customers WHERE (id in (?, ?))" 1 2]
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:in :id :?ids]}
|
|
||||||
{:ids values})))))
|
|
||||||
(testing "with more than one string"
|
|
||||||
(let [values ["1" "2"]]
|
|
||||||
(is (= ["SELECT * FROM customers WHERE (id in (?, ?))" "1" "2"]
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:in :id values]})
|
|
||||||
(sql/format {:select [:*]
|
|
||||||
:from [:customers]
|
|
||||||
:where [:in :id :?ids]}
|
|
||||||
{:ids values})))))))
|
|
||||||
|
|
||||||
(deftest test-case
|
|
||||||
(is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ? AND (foo mod ?) = ?) THEN (foo / ?) ELSE ? END FROM bar"
|
|
||||||
0 -1 0 2 0 2 0]
|
|
||||||
(sql/format
|
|
||||||
{:select [(sql/call
|
|
||||||
:case
|
|
||||||
[:< :foo 0] -1
|
|
||||||
[:and [:> :foo 0] [:= (sql/call :mod :foo 2) 0]] (sql/call :/ :foo 2)
|
|
||||||
:else 0)]
|
|
||||||
:from [:bar]})))
|
|
||||||
(let [param1 1
|
|
||||||
param2 2
|
|
||||||
param3 "three"]
|
|
||||||
(is (= ["SELECT CASE WHEN foo = ? THEN ? WHEN foo = bar THEN ? WHEN bar = ? THEN (bar * ?) ELSE ? END FROM baz"
|
|
||||||
param1 0 param2 0 param3 "param4"]
|
|
||||||
(sql/format
|
|
||||||
{:select [(sql/call
|
|
||||||
:case
|
|
||||||
[:= :foo :?param1] 0
|
|
||||||
[:= :foo :bar] (sql/param :param2)
|
|
||||||
[:= :bar 0] (sql/call :* :bar :?param3)
|
|
||||||
:else "param4")]
|
|
||||||
:from [:baz]}
|
|
||||||
{:param1 param1
|
|
||||||
:param2 param2
|
|
||||||
:param3 param3})))))
|
|
||||||
|
|
||||||
(deftest test-raw
|
|
||||||
(is (= ["SELECT 1 + 1 FROM foo"]
|
|
||||||
(-> (select (sql/raw "1 + 1"))
|
|
||||||
(from :foo)
|
|
||||||
sql/format))))
|
|
||||||
|
|
||||||
(deftest test-call
|
|
||||||
(is (= ["SELECT min(?) FROM ?" "time" "table"]
|
|
||||||
(-> (select (sql/call :min "time"))
|
|
||||||
(from "table")
|
|
||||||
sql/format))))
|
|
||||||
|
|
||||||
(deftest join-test
|
|
||||||
(testing "nil join"
|
|
||||||
(is (= ["SELECT * FROM foo INNER JOIN x ON foo.id = x.id INNER JOIN y"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :foo)
|
|
||||||
(join :x [:= :foo.id :x.id] :y nil)
|
|
||||||
sql/format)))))
|
|
||||||
|
|
||||||
(deftest join-using-test
|
|
||||||
(testing "nil join"
|
|
||||||
(is (= ["SELECT * FROM foo INNER JOIN x USING (id) INNER JOIN y USING (foo, bar)"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :foo)
|
|
||||||
(join :x [:using :id] :y [:using :foo :bar])
|
|
||||||
sql/format)))))
|
|
||||||
|
|
||||||
(deftest inline-test
|
|
||||||
(is (= ["SELECT * FROM foo WHERE id = 5"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :foo)
|
|
||||||
(where [:= :id (sql/inline 5)])
|
|
||||||
sql/format)))
|
|
||||||
;; testing for = NULL always fails in SQL -- this test is just to show
|
|
||||||
;; that an #inline nil should render as NULL (so make sure you only use
|
|
||||||
;; it in contexts where a literal NULL is acceptable!)
|
|
||||||
(is (= ["SELECT * FROM foo WHERE id = NULL"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :foo)
|
|
||||||
(where [:= :id (sql/inline nil)])
|
|
||||||
sql/format))))
|
|
||||||
|
|
||||||
(deftest merge-where-no-params-test
|
|
||||||
(testing "merge-where called with just the map as parameter - see #228"
|
|
||||||
(let [sqlmap (-> (select :*)
|
|
||||||
(from :table)
|
|
||||||
(where [:= :foo :bar]))]
|
|
||||||
(is (= ["SELECT * FROM table WHERE foo = bar"]
|
|
||||||
(sql/format (apply merge-where sqlmap [])))))))
|
|
||||||
|
|
||||||
(deftest merge-where-test
|
|
||||||
(is (= ["SELECT * FROM table WHERE (foo = bar AND quuz = xyzzy)"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :table)
|
|
||||||
(where [:= :foo :bar] [:= :quuz :xyzzy])
|
|
||||||
sql/format)))
|
|
||||||
(is (= ["SELECT * FROM table WHERE (foo = bar AND quuz = xyzzy)"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :table)
|
|
||||||
(where [:= :foo :bar])
|
|
||||||
(merge-where [:= :quuz :xyzzy])
|
|
||||||
sql/format))))
|
|
||||||
|
|
||||||
(deftest where-nil-params-test
|
|
||||||
(testing "where called with nil parameters - see #246"
|
|
||||||
(is (= ["SELECT * FROM table WHERE (foo = bar AND quuz = xyzzy)"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :table)
|
|
||||||
(where nil [:= :foo :bar] nil [:= :quuz :xyzzy] nil)
|
|
||||||
sql/format)))
|
|
||||||
(is (= ["SELECT * FROM table"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :table)
|
|
||||||
(where)
|
|
||||||
sql/format)))
|
|
||||||
(is (= ["SELECT * FROM table"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :table)
|
|
||||||
(where nil nil nil nil)
|
|
||||||
sql/format)))))
|
|
||||||
|
|
||||||
(deftest cross-join-test
|
|
||||||
(is (= ["SELECT * FROM foo CROSS JOIN bar"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from :foo)
|
|
||||||
(cross-join :bar)
|
|
||||||
sql/format)))
|
|
||||||
(is (= ["SELECT * FROM foo f CROSS JOIN bar b"]
|
|
||||||
(-> (select :*)
|
|
||||||
(from [:foo :f])
|
|
||||||
(cross-join [:bar :b])
|
|
||||||
sql/format))))
|
|
||||||
|
|
||||||
#?(:cljs (cljs.test/run-all-tests))
|
|
||||||
|
|
@ -1,714 +0,0 @@
|
||||||
(ns honeysql.format
|
|
||||||
(:refer-clojure :exclude [format])
|
|
||||||
(:require [honeysql.types :as types
|
|
||||||
:refer [call raw param param-name inline-str
|
|
||||||
#?@(:cljs [SqlCall SqlRaw SqlParam SqlArray SqlInline])]]
|
|
||||||
[clojure.string :as string])
|
|
||||||
#?(:clj (:import [honeysql.types SqlCall SqlRaw SqlParam SqlArray SqlInline])))
|
|
||||||
|
|
||||||
;;(set! *warn-on-reflection* true)
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defn comma-join [s]
|
|
||||||
(string/join ", " s))
|
|
||||||
|
|
||||||
(defn space-join [s]
|
|
||||||
(string/join " " s))
|
|
||||||
|
|
||||||
(defn paren-wrap [x]
|
|
||||||
(str "(" x ")"))
|
|
||||||
|
|
||||||
(def ^:dynamic *clause*
|
|
||||||
"During formatting, *clause* is bound to :select, :from, :where, etc."
|
|
||||||
nil)
|
|
||||||
|
|
||||||
(def ^:dynamic *params*
|
|
||||||
"Will be bound to an atom-vector that accumulates SQL parameters across
|
|
||||||
possibly-recursive function calls"
|
|
||||||
nil)
|
|
||||||
|
|
||||||
(def ^:dynamic *param-names* nil)
|
|
||||||
|
|
||||||
(def ^:dynamic *param-counter* nil)
|
|
||||||
|
|
||||||
(def ^:dynamic *all-param-counter* nil)
|
|
||||||
|
|
||||||
(def ^:dynamic *input-params* nil)
|
|
||||||
|
|
||||||
(def ^:dynamic *fn-context?* false)
|
|
||||||
|
|
||||||
(def ^:dynamic *value-context?* false)
|
|
||||||
|
|
||||||
(def ^:dynamic *subquery?* false)
|
|
||||||
|
|
||||||
(def ^:dynamic *allow-dashed-names?* false)
|
|
||||||
|
|
||||||
(def ^:dynamic *allow-namespaced-names?* false)
|
|
||||||
|
|
||||||
(def ^:dynamic *namespace-as-table?* false)
|
|
||||||
|
|
||||||
(def ^:dynamic *name-transform-fn* nil)
|
|
||||||
|
|
||||||
(def ^:private quote-fns
|
|
||||||
{:ansi #(str \" (string/replace % "\"" "\"\"") \")
|
|
||||||
:mysql #(str \` (string/replace % "`" "``") \`)
|
|
||||||
:sqlserver #(str \[ (string/replace % "]" "]]") \])
|
|
||||||
:oracle #(str \" (string/replace % "\"" "\"\"") \")})
|
|
||||||
|
|
||||||
|
|
||||||
(defmulti parameterize (fn [parameterizer & args] parameterizer))
|
|
||||||
|
|
||||||
(defmethod parameterize :postgresql [_ value pname]
|
|
||||||
(str "$" (swap! *all-param-counter* inc)))
|
|
||||||
|
|
||||||
(defmethod parameterize :jdbc [_ value pname]
|
|
||||||
"?")
|
|
||||||
|
|
||||||
(defmethod parameterize :none [_ value pname]
|
|
||||||
(str (last @*params*)))
|
|
||||||
|
|
||||||
|
|
||||||
(def ^:dynamic *quote-identifier-fn* nil)
|
|
||||||
(def ^:dynamic *parameterizer* nil)
|
|
||||||
|
|
||||||
(defn- undasherize [s]
|
|
||||||
(string/replace s "-" "_"))
|
|
||||||
|
|
||||||
;; String.toUpperCase() or `string/upper-case` for that matter converts the string to uppercase for the DEFAULT
|
|
||||||
;; LOCALE. Normally this does what you'd expect but things like `inner join` get converted to `İNNER JOIN` (dot over
|
|
||||||
;; the I) when user locale is Turkish. This predictably has bad consequences for people who like their SQL queries to
|
|
||||||
;; work. The fix here is to use String.toUpperCase(Locale/US) instead which always converts things the way we'd expect.
|
|
||||||
;;
|
|
||||||
;; Use this function instead of `string/upper-case` as it will always use Locale/US.
|
|
||||||
(def ^:private ^{:arglists '([s])} upper-case
|
|
||||||
;; TODO - not sure if there's a JavaScript equivalent here we should be using as well
|
|
||||||
#?(:clj (fn [^String s] (.. s toString (toUpperCase (java.util.Locale/US))))
|
|
||||||
:cljs string/upper-case))
|
|
||||||
|
|
||||||
(defn quote-identifier [x & {:keys [style split] :or {split true}}]
|
|
||||||
(let [name-transform-fn (cond
|
|
||||||
*name-transform-fn* *name-transform-fn*
|
|
||||||
*allow-dashed-names?* identity
|
|
||||||
:else undasherize)
|
|
||||||
qf (if style
|
|
||||||
(quote-fns style)
|
|
||||||
*quote-identifier-fn*)
|
|
||||||
s (cond
|
|
||||||
(or (keyword? x) (symbol? x))
|
|
||||||
(name-transform-fn
|
|
||||||
(cond *namespace-as-table?*
|
|
||||||
(str (when-let [n (namespace x)]
|
|
||||||
(str n "."))
|
|
||||||
(name x))
|
|
||||||
*allow-namespaced-names?*
|
|
||||||
(str (when-let [n (namespace x)]
|
|
||||||
(str n "/"))
|
|
||||||
(name x))
|
|
||||||
:else
|
|
||||||
(name x)))
|
|
||||||
(string? x) (if qf x (name-transform-fn x))
|
|
||||||
:else (str x))]
|
|
||||||
(if-not qf
|
|
||||||
s
|
|
||||||
(let [qf* #(if (= "*" %) % (qf %))]
|
|
||||||
(if-not split
|
|
||||||
(qf* s)
|
|
||||||
(let [parts (string/split s #"\.")]
|
|
||||||
(string/join "." (map qf* parts))))))))
|
|
||||||
|
|
||||||
(def infix-fns
|
|
||||||
#{"+" "-" "*" "/" "%" "mod" "|" "&" "^"
|
|
||||||
"and" "or" "xor"
|
|
||||||
"in" "not in" "like" "not like" "regexp"})
|
|
||||||
|
|
||||||
(def fn-aliases
|
|
||||||
{"is" "="
|
|
||||||
"is-not" "<>"
|
|
||||||
"not=" "<>"
|
|
||||||
"!=" "<>"
|
|
||||||
"not-in" "not in"
|
|
||||||
"not-like" "not like"
|
|
||||||
"regex" "regexp"})
|
|
||||||
|
|
||||||
(defprotocol ToSql
|
|
||||||
(to-sql [x]))
|
|
||||||
|
|
||||||
(defn to-sql-value [x]
|
|
||||||
(binding [*value-context?* (sequential? x)]
|
|
||||||
(to-sql x)))
|
|
||||||
|
|
||||||
(defmulti fn-handler (fn [op & args] op))
|
|
||||||
|
|
||||||
(defn expand-binary-ops [op & args]
|
|
||||||
(str "("
|
|
||||||
(string/join " AND "
|
|
||||||
(for [[a b] (partition 2 1 args)]
|
|
||||||
(fn-handler op a b)))
|
|
||||||
")"))
|
|
||||||
|
|
||||||
(defmethod fn-handler :default [op & args]
|
|
||||||
(let [args (map to-sql args)]
|
|
||||||
(if (infix-fns op)
|
|
||||||
(paren-wrap (string/join (str " " op " ") args))
|
|
||||||
(str op (paren-wrap (comma-join args))))))
|
|
||||||
|
|
||||||
(defmethod fn-handler "count-distinct" [_ & args]
|
|
||||||
(str "COUNT(DISTINCT " (comma-join (map to-sql args)) ")"))
|
|
||||||
|
|
||||||
(defmethod fn-handler "distinct-on" [_ & args]
|
|
||||||
(str "DISTINCT ON (" (comma-join (map to-sql args)) ")"))
|
|
||||||
|
|
||||||
(defmethod fn-handler "cast" [_ field cast-to-type]
|
|
||||||
(str "CAST" (paren-wrap (str (to-sql field)
|
|
||||||
" AS "
|
|
||||||
(to-sql cast-to-type)))))
|
|
||||||
|
|
||||||
(defmethod fn-handler "=" [_ a b & more]
|
|
||||||
(if (seq more)
|
|
||||||
(apply expand-binary-ops "=" a b more)
|
|
||||||
(cond
|
|
||||||
(nil? a) (str (to-sql-value b) " IS NULL")
|
|
||||||
(nil? b) (str (to-sql-value a) " IS NULL")
|
|
||||||
:else (str (to-sql-value a) " = " (to-sql-value b)))))
|
|
||||||
|
|
||||||
(defmethod fn-handler "<>" [_ a b & more]
|
|
||||||
(if (seq more)
|
|
||||||
(apply expand-binary-ops "<>" a b more)
|
|
||||||
(cond
|
|
||||||
(nil? a) (str (to-sql-value b) " IS NOT NULL")
|
|
||||||
(nil? b) (str (to-sql-value a) " IS NOT NULL")
|
|
||||||
:else (str (to-sql-value a) " <> " (to-sql-value b)))))
|
|
||||||
|
|
||||||
(defmethod fn-handler "<" [_ a b & more]
|
|
||||||
(if (seq more)
|
|
||||||
(apply expand-binary-ops "<" a b more)
|
|
||||||
(str (to-sql-value a) " < " (to-sql-value b))))
|
|
||||||
|
|
||||||
(defmethod fn-handler "<=" [_ a b & more]
|
|
||||||
(if (seq more)
|
|
||||||
(apply expand-binary-ops "<=" a b more)
|
|
||||||
(str (to-sql-value a) " <= " (to-sql-value b))))
|
|
||||||
|
|
||||||
(defmethod fn-handler ">" [_ a b & more]
|
|
||||||
(if (seq more)
|
|
||||||
(apply expand-binary-ops ">" a b more)
|
|
||||||
(str (to-sql-value a) " > " (to-sql-value b))))
|
|
||||||
|
|
||||||
(defmethod fn-handler ">=" [_ a b & more]
|
|
||||||
(if (seq more)
|
|
||||||
(apply expand-binary-ops ">=" a b more)
|
|
||||||
(str (to-sql-value a) " >= " (to-sql-value b))))
|
|
||||||
|
|
||||||
(defmethod fn-handler "between" [_ field lower upper]
|
|
||||||
(str (to-sql-value field) " BETWEEN " (to-sql-value lower) " AND " (to-sql-value upper)))
|
|
||||||
|
|
||||||
;; Handles MySql's MATCH (field) AGAINST (pattern). The third argument
|
|
||||||
;; can be a set containing one or more of :boolean, :natural, or :expand.
|
|
||||||
(defmethod fn-handler "match" [_ fields pattern & [opts]]
|
|
||||||
(str "MATCH ("
|
|
||||||
(comma-join
|
|
||||||
(map to-sql (if (coll? fields) fields [fields])))
|
|
||||||
") AGAINST ("
|
|
||||||
(to-sql-value pattern)
|
|
||||||
(when (seq opts)
|
|
||||||
(str " " (space-join (for [opt opts]
|
|
||||||
(case opt
|
|
||||||
:boolean "IN BOOLEAN MODE"
|
|
||||||
:natural "IN NATURAL LANGUAGE MODE"
|
|
||||||
:expand "WITH QUERY EXPANSION")))))
|
|
||||||
")"))
|
|
||||||
|
|
||||||
(def default-clause-priorities
|
|
||||||
"Determines the order that clauses will be placed within generated SQL"
|
|
||||||
{:with 20
|
|
||||||
:with-recursive 30
|
|
||||||
:intersect 35
|
|
||||||
:union 40
|
|
||||||
:union-all 45
|
|
||||||
:except 47
|
|
||||||
:select 50
|
|
||||||
:insert-into 60
|
|
||||||
:update 70
|
|
||||||
:delete 75
|
|
||||||
:delete-from 80
|
|
||||||
:truncate 85
|
|
||||||
:columns 90
|
|
||||||
:composite 95
|
|
||||||
:set0 100 ; low-priority set clause
|
|
||||||
:from 110
|
|
||||||
:join 120
|
|
||||||
:left-join 130
|
|
||||||
:right-join 140
|
|
||||||
:full-join 150
|
|
||||||
:cross-join 152
|
|
||||||
:set 155
|
|
||||||
:set1 156 ; high-priority set clause (synonym for :set)
|
|
||||||
:where 160
|
|
||||||
:group-by 170
|
|
||||||
:having 180
|
|
||||||
:order-by 190
|
|
||||||
:limit 200
|
|
||||||
:offset 210
|
|
||||||
:lock 215
|
|
||||||
:values 220
|
|
||||||
:query-values 230})
|
|
||||||
|
|
||||||
(def clause-store (atom default-clause-priorities))
|
|
||||||
|
|
||||||
(defn register-clause! [clause-key priority]
|
|
||||||
(swap! clause-store assoc clause-key priority))
|
|
||||||
|
|
||||||
(defn sort-clauses [clauses]
|
|
||||||
(let [m @clause-store]
|
|
||||||
(sort-by
|
|
||||||
(fn [c]
|
|
||||||
(m c #?(:clj Long/MAX_VALUE :cljs js/Number.MAX_VALUE)))
|
|
||||||
clauses)))
|
|
||||||
|
|
||||||
(defn format
|
|
||||||
"Takes a SQL map and optional input parameters and returns a vector
|
|
||||||
of a SQL string and parameters, as expected by `next.jbc` and
|
|
||||||
`clojure.java.jdbc`.
|
|
||||||
|
|
||||||
Input parameters will be filled into designated spots according to
|
|
||||||
name (if a map is provided) or by position (if a sequence is provided).
|
|
||||||
|
|
||||||
Instead of passing parameters, you can use keyword arguments:
|
|
||||||
:params - input parameters
|
|
||||||
:quoting - quote style to use for identifiers; one of :ansi (PostgreSQL),
|
|
||||||
:mysql, :sqlserver, or :oracle. Defaults to no quoting.
|
|
||||||
:parameterizer - style of parameter naming, :postgresql,
|
|
||||||
:jdbc or :none. Defaults to :jdbc.
|
|
||||||
:return-param-names - when true, returns a vector of
|
|
||||||
[sql-str param-values param-names]"
|
|
||||||
[sql-map & params-or-opts]
|
|
||||||
(let [opts (when (keyword? (first params-or-opts))
|
|
||||||
(apply hash-map params-or-opts))
|
|
||||||
params (if (coll? (first params-or-opts))
|
|
||||||
(first params-or-opts)
|
|
||||||
(:params opts))]
|
|
||||||
(binding [*params* (atom [])
|
|
||||||
*param-counter* (atom 0)
|
|
||||||
*all-param-counter* (atom 0)
|
|
||||||
*param-names* (atom [])
|
|
||||||
*input-params* (atom params)
|
|
||||||
*quote-identifier-fn* (quote-fns (:quoting opts))
|
|
||||||
*parameterizer* (or (:parameterizer opts) :jdbc)
|
|
||||||
*allow-dashed-names?* (:allow-dashed-names? opts)
|
|
||||||
*allow-namespaced-names?* (:allow-namespaced-names? opts)
|
|
||||||
*namespace-as-table?* (:namespace-as-table? opts)]
|
|
||||||
(let [sql-str (to-sql sql-map)]
|
|
||||||
(if (and (seq @*params*) (not= :none (:parameterizer opts)))
|
|
||||||
(if (:return-param-names opts)
|
|
||||||
[sql-str @*params* @*param-names*]
|
|
||||||
(into [sql-str] @*params*))
|
|
||||||
[sql-str])))))
|
|
||||||
|
|
||||||
(defprotocol Parameterizable
|
|
||||||
(to-params [value pname]))
|
|
||||||
|
|
||||||
(defn to-params-seq [s pname]
|
|
||||||
(paren-wrap (comma-join (mapv #(to-params % pname) s))))
|
|
||||||
|
|
||||||
(defn to-params-default [value pname]
|
|
||||||
(swap! *params* conj value)
|
|
||||||
(swap! *param-names* conj pname)
|
|
||||||
(parameterize *parameterizer* value pname))
|
|
||||||
|
|
||||||
(extend-protocol Parameterizable
|
|
||||||
#?@(:clj
|
|
||||||
[clojure.lang.Sequential
|
|
||||||
|
|
||||||
(to-params [value pname]
|
|
||||||
(to-params-seq value pname))])
|
|
||||||
#?(:clj clojure.lang.IPersistentSet
|
|
||||||
:cljs cljs.core/PersistentHashSet)
|
|
||||||
(to-params [value pname]
|
|
||||||
(to-params (seq value) pname))
|
|
||||||
nil
|
|
||||||
(to-params [value pname]
|
|
||||||
(swap! *params* conj value)
|
|
||||||
(swap! *param-names* conj pname)
|
|
||||||
(parameterize *parameterizer* value pname))
|
|
||||||
#?(:clj Object :cljs default)
|
|
||||||
(to-params [value pname]
|
|
||||||
#?(:clj
|
|
||||||
(to-params-default value pname)
|
|
||||||
:cljs
|
|
||||||
(if (sequential? value)
|
|
||||||
(to-params-seq value pname)
|
|
||||||
(to-params-default value pname)))))
|
|
||||||
|
|
||||||
(defn add-param [pname pval]
|
|
||||||
(to-params pval pname))
|
|
||||||
|
|
||||||
;; Anonymous param name -- :_1, :_2, etc.
|
|
||||||
(defn add-anon-param [pval]
|
|
||||||
(add-param
|
|
||||||
(keyword (str "_" (swap! *param-counter* inc)))
|
|
||||||
pval))
|
|
||||||
|
|
||||||
(defrecord Value [v]
|
|
||||||
ToSql
|
|
||||||
(to-sql [_]
|
|
||||||
(add-anon-param v)))
|
|
||||||
|
|
||||||
(defn value [x] (Value. x))
|
|
||||||
|
|
||||||
(declare -format-clause)
|
|
||||||
|
|
||||||
(defn map->sql [m]
|
|
||||||
(let [clause-ops (sort-clauses (keys m))
|
|
||||||
sql-str (binding [*subquery?* true
|
|
||||||
*fn-context?* false]
|
|
||||||
(space-join
|
|
||||||
(map (comp #(-format-clause % m) #(find m %))
|
|
||||||
clause-ops)))]
|
|
||||||
(if *subquery?*
|
|
||||||
(paren-wrap sql-str)
|
|
||||||
sql-str)))
|
|
||||||
|
|
||||||
(declare format-predicate*)
|
|
||||||
|
|
||||||
(defn seq->sql [x]
|
|
||||||
(cond
|
|
||||||
*value-context?*
|
|
||||||
;; sequences are operators/functions
|
|
||||||
(format-predicate* x)
|
|
||||||
*fn-context?*
|
|
||||||
;; list argument in fn call
|
|
||||||
(paren-wrap (comma-join (map to-sql x)))
|
|
||||||
:else
|
|
||||||
;; alias
|
|
||||||
(do
|
|
||||||
(assert (= 2 (count x)) (str "Alias should have two parts" x))
|
|
||||||
(let [[target alias] x]
|
|
||||||
(str (to-sql target)
|
|
||||||
; Omit AS in FROM, JOIN, etc. - Oracle doesn't allow it
|
|
||||||
(if (= :select *clause*) " AS " " ")
|
|
||||||
(if (or (string? alias) (keyword? alias) (symbol? alias))
|
|
||||||
(quote-identifier alias :split false)
|
|
||||||
(binding [*subquery?* false]
|
|
||||||
(to-sql alias))))))))
|
|
||||||
|
|
||||||
(extend-protocol types/Inlinable
|
|
||||||
#?(:clj clojure.lang.Keyword
|
|
||||||
:cljs cljs.core/Keyword)
|
|
||||||
(inline-str [x]
|
|
||||||
(name x))
|
|
||||||
nil
|
|
||||||
(inline-str [_]
|
|
||||||
"NULL")
|
|
||||||
#?(:clj Object :cljs default)
|
|
||||||
(inline-str [x]
|
|
||||||
(str x)))
|
|
||||||
|
|
||||||
(extend-protocol ToSql
|
|
||||||
#?(:clj clojure.lang.Keyword
|
|
||||||
:cljs cljs.core/Keyword)
|
|
||||||
(to-sql [x]
|
|
||||||
(let [s (name x)]
|
|
||||||
(case (.charAt s 0)
|
|
||||||
\% (let [call-args (string/split (subs s 1) #"\." 2)]
|
|
||||||
(to-sql (apply call (map keyword call-args))))
|
|
||||||
\? (to-sql (param (keyword (subs s 1))))
|
|
||||||
(quote-identifier x))))
|
|
||||||
#?(:clj clojure.lang.Symbol
|
|
||||||
:cljs cljs.core/Symbol)
|
|
||||||
(to-sql [x] (quote-identifier x))
|
|
||||||
#?(:clj java.lang.Boolean :cljs boolean)
|
|
||||||
(to-sql [x]
|
|
||||||
(if x "TRUE" "FALSE"))
|
|
||||||
#?@(:clj
|
|
||||||
[clojure.lang.Sequential
|
|
||||||
(to-sql [x] (seq->sql x))])
|
|
||||||
SqlCall
|
|
||||||
(to-sql [x]
|
|
||||||
(binding [*fn-context?* true]
|
|
||||||
(let [fn-name (name (.-name x))
|
|
||||||
fn-name (fn-aliases fn-name fn-name)]
|
|
||||||
(apply fn-handler fn-name (.-args x)))))
|
|
||||||
SqlRaw
|
|
||||||
(to-sql [x]
|
|
||||||
(let [s (.-s x)]
|
|
||||||
(if (vector? s)
|
|
||||||
(string/join "" (map (fn [x] (if (string? x) x (to-sql x))) s))
|
|
||||||
s)))
|
|
||||||
#?(:clj clojure.lang.IPersistentMap
|
|
||||||
:cljs cljs.core/PersistentArrayMap)
|
|
||||||
(to-sql [x]
|
|
||||||
(map->sql x))
|
|
||||||
#?(:clj clojure.lang.IPersistentSet
|
|
||||||
:cljs cljs.core/PersistentHashSet)
|
|
||||||
(to-sql [x]
|
|
||||||
(to-sql (seq x)))
|
|
||||||
nil
|
|
||||||
(to-sql [x] "NULL")
|
|
||||||
SqlParam
|
|
||||||
(to-sql [x]
|
|
||||||
(let [pname (param-name x)]
|
|
||||||
(if (map? @*input-params*)
|
|
||||||
(add-param pname (get @*input-params* pname))
|
|
||||||
(let [x (first @*input-params*)]
|
|
||||||
(swap! *input-params* rest)
|
|
||||||
(add-param pname x)))))
|
|
||||||
SqlArray
|
|
||||||
(to-sql [x]
|
|
||||||
(str "ARRAY[" (comma-join (map to-sql (.-values x))) "]"))
|
|
||||||
SqlInline
|
|
||||||
(to-sql [x]
|
|
||||||
(inline-str (.-value x)))
|
|
||||||
#?(:clj Object :cljs default)
|
|
||||||
(to-sql [x]
|
|
||||||
#?(:clj (add-anon-param x)
|
|
||||||
:cljs (if (sequential? x)
|
|
||||||
(seq->sql x)
|
|
||||||
(add-anon-param x))))
|
|
||||||
#?@(:cljs
|
|
||||||
[cljs.core/PersistentHashMap
|
|
||||||
(to-sql [x] (map->sql x))]))
|
|
||||||
|
|
||||||
(defn sqlable? [x]
|
|
||||||
(satisfies? ToSql x))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defn format-predicate* [pred]
|
|
||||||
(if-not (sequential? pred)
|
|
||||||
(to-sql pred)
|
|
||||||
(let [[op & args] pred
|
|
||||||
op-name (name op)]
|
|
||||||
(case op-name
|
|
||||||
"not" (str "NOT " (format-predicate* (first args)))
|
|
||||||
|
|
||||||
("and" "or" "xor")
|
|
||||||
(->> args
|
|
||||||
(remove nil?)
|
|
||||||
(map format-predicate*)
|
|
||||||
(string/join (str " " (upper-case op-name) " "))
|
|
||||||
(paren-wrap))
|
|
||||||
|
|
||||||
"exists"
|
|
||||||
(str "EXISTS " (to-sql (first args)))
|
|
||||||
|
|
||||||
(to-sql (apply call pred))))))
|
|
||||||
|
|
||||||
(defn format-predicate
|
|
||||||
"Formats a predicate (e.g., for WHERE, JOIN, or HAVING) as a string."
|
|
||||||
[pred & {:keys [quoting parameterizer]
|
|
||||||
:or {parameterizer :jdbc}}]
|
|
||||||
(binding [*params* (atom [])
|
|
||||||
*param-counter* (atom 0)
|
|
||||||
*param-names* (atom [])
|
|
||||||
*quote-identifier-fn* (or (quote-fns quoting)
|
|
||||||
*quote-identifier-fn*)
|
|
||||||
*parameterizer* parameterizer]
|
|
||||||
(let [sql-str (format-predicate* pred)]
|
|
||||||
(if (seq @*params*)
|
|
||||||
(into [sql-str] @*params*)
|
|
||||||
[sql-str]))))
|
|
||||||
|
|
||||||
(defmulti format-clause
|
|
||||||
"Takes a map entry representing a clause and returns an SQL string"
|
|
||||||
(fn [clause _] (key clause)))
|
|
||||||
|
|
||||||
(defn- -format-clause
|
|
||||||
[clause _]
|
|
||||||
(binding [*clause* (key clause)]
|
|
||||||
(format-clause clause _)))
|
|
||||||
|
|
||||||
(defmethod format-clause :default [& _]
|
|
||||||
"")
|
|
||||||
|
|
||||||
(defmethod format-clause :exists [[_ table-expr] _]
|
|
||||||
(str "EXISTS " (to-sql table-expr)))
|
|
||||||
|
|
||||||
(defmulti format-modifiers (fn [[op & _]] op))
|
|
||||||
|
|
||||||
(defmethod format-modifiers :distinct [_] "DISTINCT")
|
|
||||||
|
|
||||||
(defmethod format-modifiers :default [coll]
|
|
||||||
(space-join (map (comp upper-case name) coll)))
|
|
||||||
|
|
||||||
(defmethod format-clause :select [[_ fields] sql-map]
|
|
||||||
(str "SELECT "
|
|
||||||
(when (:modifiers sql-map)
|
|
||||||
(str (format-modifiers (:modifiers sql-map)) " "))
|
|
||||||
(comma-join (map to-sql fields))))
|
|
||||||
|
|
||||||
(defmethod format-clause :from [[_ tables] _]
|
|
||||||
(str "FROM " (comma-join (map to-sql tables))))
|
|
||||||
|
|
||||||
(defmethod format-clause :where [[_ pred] _]
|
|
||||||
(str "WHERE " (format-predicate* pred)))
|
|
||||||
|
|
||||||
(defn format-join [type table pred]
|
|
||||||
(str (when type
|
|
||||||
(str (upper-case (name type)) " "))
|
|
||||||
"JOIN " (to-sql table)
|
|
||||||
(when (some? pred)
|
|
||||||
(if (and (sequential? pred) (= :using (first pred)))
|
|
||||||
(str " USING (" (->> pred rest (map quote-identifier) comma-join) ")")
|
|
||||||
(str " ON " (format-predicate* pred))))))
|
|
||||||
|
|
||||||
(defmethod format-clause :join [[_ join-groups] _]
|
|
||||||
(space-join (map #(apply format-join :inner %)
|
|
||||||
(partition 2 join-groups))))
|
|
||||||
|
|
||||||
(defmethod format-clause :left-join [[_ join-groups] _]
|
|
||||||
(space-join (map #(apply format-join :left %)
|
|
||||||
(partition 2 join-groups))))
|
|
||||||
|
|
||||||
(defmethod format-clause :right-join [[_ join-groups] _]
|
|
||||||
(space-join (map #(apply format-join :right %)
|
|
||||||
(partition 2 join-groups))))
|
|
||||||
|
|
||||||
(defmethod format-clause :full-join [[_ join-groups] _]
|
|
||||||
(space-join (map #(apply format-join :full %)
|
|
||||||
(partition 2 join-groups))))
|
|
||||||
|
|
||||||
(defmethod format-clause :cross-join [[_ join-groups] _]
|
|
||||||
(space-join (map #(format-join :cross % nil) join-groups)))
|
|
||||||
|
|
||||||
(defmethod format-clause :group-by [[_ fields] _]
|
|
||||||
(str "GROUP BY " (comma-join (map to-sql fields))))
|
|
||||||
|
|
||||||
(defmethod format-clause :having [[_ pred] _]
|
|
||||||
(str "HAVING " (format-predicate* pred)))
|
|
||||||
|
|
||||||
(defmethod format-clause :order-by [[_ fields] _]
|
|
||||||
(str "ORDER BY "
|
|
||||||
(comma-join (for [field fields]
|
|
||||||
(if (sequential? field)
|
|
||||||
(let [[field & modifiers] field]
|
|
||||||
(string/join " "
|
|
||||||
(cons (to-sql field)
|
|
||||||
(for [modifier modifiers]
|
|
||||||
(case modifier
|
|
||||||
:desc "DESC"
|
|
||||||
:asc "ASC"
|
|
||||||
:nulls-first "NULLS FIRST"
|
|
||||||
:nulls-last "NULLS LAST"
|
|
||||||
"")))))
|
|
||||||
(to-sql field))))))
|
|
||||||
|
|
||||||
(defmethod format-clause :limit [[_ limit] _]
|
|
||||||
(str "LIMIT " (to-sql limit)))
|
|
||||||
|
|
||||||
(defmethod format-clause :offset [[_ offset] _]
|
|
||||||
(str "OFFSET " (to-sql offset)))
|
|
||||||
|
|
||||||
(defmulti format-lock-clause identity)
|
|
||||||
|
|
||||||
(defmethod format-lock-clause :update [_]
|
|
||||||
"FOR UPDATE")
|
|
||||||
|
|
||||||
(defmethod format-lock-clause :mysql-share [_]
|
|
||||||
"LOCK IN SHARE MODE")
|
|
||||||
|
|
||||||
(defmethod format-lock-clause :postgresql-share [_]
|
|
||||||
"FOR SHARE")
|
|
||||||
|
|
||||||
(defmethod format-clause :lock [[_ lock] _]
|
|
||||||
(let [{:keys [mode wait]} lock
|
|
||||||
clause (format-lock-clause mode)]
|
|
||||||
(str clause (when (false? wait) " NOWAIT"))))
|
|
||||||
|
|
||||||
(defmethod format-clause :insert-into [[_ table] _]
|
|
||||||
(if (and (sequential? table) (sequential? (first table)))
|
|
||||||
(str "INSERT INTO "
|
|
||||||
(to-sql (ffirst table))
|
|
||||||
(binding [*namespace-as-table?* false]
|
|
||||||
(str " (" (comma-join (map to-sql (second (first table)))) ") "))
|
|
||||||
(binding [*subquery?* false]
|
|
||||||
(to-sql (second table))))
|
|
||||||
(str "INSERT INTO " (to-sql table))))
|
|
||||||
|
|
||||||
(defmethod format-clause :columns [[_ fields] _]
|
|
||||||
(binding [*namespace-as-table?* false]
|
|
||||||
(str "(" (comma-join (map to-sql fields)) ")")))
|
|
||||||
|
|
||||||
(defmethod format-clause :composite [[_ fields] _]
|
|
||||||
(comma-join (map to-sql fields)))
|
|
||||||
|
|
||||||
(defmethod format-clause :values [[_ values] _]
|
|
||||||
(if (sequential? (first values))
|
|
||||||
(str "VALUES " (comma-join (for [x values]
|
|
||||||
(str "(" (comma-join (map to-sql x)) ")"))))
|
|
||||||
(let [cols (keys (first values))]
|
|
||||||
(str
|
|
||||||
(binding [*namespace-as-table?* false]
|
|
||||||
(str "(" (comma-join (map to-sql cols)) ")"))
|
|
||||||
" VALUES "
|
|
||||||
(comma-join (for [x values]
|
|
||||||
(str "(" (comma-join (map #(to-sql (get x %)) cols)) ")")))))))
|
|
||||||
|
|
||||||
(defmethod format-clause :query-values [[_ query-values] _]
|
|
||||||
(to-sql query-values))
|
|
||||||
|
|
||||||
(defmethod format-clause :update [[_ table] _]
|
|
||||||
(str "UPDATE " (to-sql table)))
|
|
||||||
|
|
||||||
(defmethod format-clause :set [[_ values] _]
|
|
||||||
(str "SET " (comma-join (for [[k v] values]
|
|
||||||
(str (to-sql k) " = " (to-sql v))))))
|
|
||||||
|
|
||||||
(defmethod format-clause :set0 [[_ values] _]
|
|
||||||
(str "SET " (comma-join (for [[k v] values]
|
|
||||||
(str (to-sql k) " = " (to-sql v))))))
|
|
||||||
|
|
||||||
(defmethod format-clause :set1 [[_ values] _]
|
|
||||||
(str "SET " (comma-join (for [[k v] values]
|
|
||||||
(str (to-sql k) " = " (to-sql v))))))
|
|
||||||
|
|
||||||
(defmethod format-clause :delete-from [[_ table] _]
|
|
||||||
(str "DELETE FROM " (to-sql table)))
|
|
||||||
|
|
||||||
(defmethod format-clause :delete [[_ tables] _]
|
|
||||||
(str "DELETE " (comma-join (map to-sql tables))))
|
|
||||||
|
|
||||||
(defmethod format-clause :truncate [[_ table] _]
|
|
||||||
(str "TRUNCATE " (to-sql table)))
|
|
||||||
|
|
||||||
(defn cte->sql
|
|
||||||
[[cte-name query]]
|
|
||||||
(str (binding [*subquery?* false]
|
|
||||||
(to-sql cte-name))
|
|
||||||
" AS "
|
|
||||||
(to-sql query)))
|
|
||||||
|
|
||||||
(defmethod format-clause :with [[_ ctes] _]
|
|
||||||
(str "WITH " (comma-join (map cte->sql ctes))))
|
|
||||||
|
|
||||||
(defmethod format-clause :with-recursive [[_ ctes] _]
|
|
||||||
(str "WITH RECURSIVE " (comma-join (map cte->sql ctes))))
|
|
||||||
|
|
||||||
(defmethod format-clause :union [[_ maps] _]
|
|
||||||
(binding [*subquery?* false]
|
|
||||||
(string/join " UNION " (map to-sql maps))))
|
|
||||||
|
|
||||||
(defmethod format-clause :union-all [[_ maps] _]
|
|
||||||
(binding [*subquery?* false]
|
|
||||||
(string/join " UNION ALL " (map to-sql maps))))
|
|
||||||
|
|
||||||
(defmethod format-clause :intersect [[_ maps] _]
|
|
||||||
(binding [*subquery?* false]
|
|
||||||
(string/join " INTERSECT " (map to-sql maps))))
|
|
||||||
|
|
||||||
(defmethod format-clause :except [[_ maps] _]
|
|
||||||
(binding [*subquery?* false]
|
|
||||||
(string/join " EXCEPT " (map to-sql maps))))
|
|
||||||
|
|
||||||
(defmethod fn-handler "case" [_ & clauses]
|
|
||||||
(str "CASE "
|
|
||||||
(space-join
|
|
||||||
(for [[condition result] (partition 2 clauses)]
|
|
||||||
(if (= :else condition)
|
|
||||||
(str "ELSE " (to-sql result))
|
|
||||||
(let [pred (format-predicate* condition)]
|
|
||||||
(str "WHEN " pred " THEN " (to-sql result))))))
|
|
||||||
" END"))
|
|
||||||
|
|
||||||
(defn regularize [sql-string]
|
|
||||||
(string/replace sql-string #"\s+" " "))
|
|
||||||
|
|
@ -1,322 +0,0 @@
|
||||||
(ns honeysql.format-test
|
|
||||||
(:refer-clojure :exclude [format])
|
|
||||||
(:require [#?@(:clj [clojure.test :refer]
|
|
||||||
:cljs [cljs.test :refer-macros]) [deftest testing is are]]
|
|
||||||
honeysql.core
|
|
||||||
[honeysql.types :as sql]
|
|
||||||
[honeysql.format :refer
|
|
||||||
[*allow-dashed-names?* *allow-namespaced-names?*
|
|
||||||
*namespace-as-table?*
|
|
||||||
quote-identifier format-clause format
|
|
||||||
parameterize]]))
|
|
||||||
|
|
||||||
(deftest test-quote
|
|
||||||
(are
|
|
||||||
[qx res]
|
|
||||||
(= (apply quote-identifier "foo.bar.baz" qx) res)
|
|
||||||
[] "foo.bar.baz"
|
|
||||||
[:style :mysql] "`foo`.`bar`.`baz`"
|
|
||||||
[:style :mysql :split false] "`foo.bar.baz`")
|
|
||||||
(are
|
|
||||||
[x res]
|
|
||||||
(= (quote-identifier x) res)
|
|
||||||
3 "3"
|
|
||||||
'foo "foo"
|
|
||||||
:foo-bar "foo_bar")
|
|
||||||
(is (= (quote-identifier "*" :style :ansi) "*"))
|
|
||||||
(is (= (quote-identifier "foo\"bar" :style :ansi) "\"foo\"\"bar\""))
|
|
||||||
(is (= (quote-identifier "foo\"bar" :style :oracle) "\"foo\"\"bar\""))
|
|
||||||
(is (= (quote-identifier "foo`bar" :style :mysql) "`foo``bar`"))
|
|
||||||
(is (= (quote-identifier "foo]bar" :style :sqlserver) "[foo]]bar]")))
|
|
||||||
|
|
||||||
(deftest test-dashed-quote
|
|
||||||
(binding [*allow-dashed-names?* true]
|
|
||||||
(is (= (quote-identifier :foo-bar) "foo-bar"))
|
|
||||||
(is (= (quote-identifier :foo-bar :style :ansi) "\"foo-bar\""))
|
|
||||||
(is (= (quote-identifier :foo-bar.moo-bar :style :ansi)
|
|
||||||
"\"foo-bar\".\"moo-bar\""))))
|
|
||||||
|
|
||||||
(deftest test-namespaced-identifier
|
|
||||||
(is (= (quote-identifier :foo/bar) "bar"))
|
|
||||||
(is (= (quote-identifier :foo/bar :style :ansi) "\"bar\""))
|
|
||||||
(binding [*namespace-as-table?* true]
|
|
||||||
(is (= (quote-identifier :foo/bar) "foo.bar"))
|
|
||||||
(is (= (quote-identifier :foo/bar :style :ansi) "\"foo\".\"bar\""))
|
|
||||||
(is (= (quote-identifier :foo/bar :style :ansi :split false) "\"foo.bar\"")))
|
|
||||||
(binding [*allow-namespaced-names?* true]
|
|
||||||
(is (= (quote-identifier :foo/bar) "foo/bar"))
|
|
||||||
(is (= (quote-identifier :foo/bar :style :ansi) "\"foo/bar\""))))
|
|
||||||
|
|
||||||
(deftest alias-splitting
|
|
||||||
(is (= ["SELECT `aa`.`c` AS `a.c`, `bb`.`c` AS `b.c`, `cc`.`c` AS `c.c`"]
|
|
||||||
(format {:select [[:aa.c "a.c"]
|
|
||||||
[:bb.c :b.c]
|
|
||||||
[:cc.c 'c.c]]}
|
|
||||||
:quoting :mysql))
|
|
||||||
"aliases containing \".\" are quoted as necessary but not split"))
|
|
||||||
|
|
||||||
(deftest values-alias
|
|
||||||
(is (= ["SELECT vals.a FROM (VALUES (?, ?, ?)) vals (a, b, c)" 1 2 3]
|
|
||||||
(format {:select [:vals.a]
|
|
||||||
:from [[{:values [[1 2 3]]} [:vals {:columns [:a :b :c]}]]]}))))
|
|
||||||
(deftest test-cte
|
|
||||||
(is (= (format-clause
|
|
||||||
(first {:with [[:query {:select [:foo] :from [:bar]}]]}) nil)
|
|
||||||
"WITH query AS SELECT foo FROM bar"))
|
|
||||||
(is (= (format-clause
|
|
||||||
(first {:with-recursive [[:query {:select [:foo] :from [:bar]}]]}) nil)
|
|
||||||
"WITH RECURSIVE query AS SELECT foo FROM bar"))
|
|
||||||
(is (= (format {:with [[[:static {:columns [:a :b :c]}] {:values [[1 2 3] [4 5 6]]}]]})
|
|
||||||
["WITH static (a, b, c) AS (VALUES (?, ?, ?), (?, ?, ?))" 1 2 3 4 5 6]))
|
|
||||||
(is (= (format
|
|
||||||
{:with [[[:static {:columns [:a :b :c]}]
|
|
||||||
{:values [[1 2 3] [4 5 6]]}]]
|
|
||||||
:select [:*]
|
|
||||||
:from [:static]})
|
|
||||||
["WITH static (a, b, c) AS (VALUES (?, ?, ?), (?, ?, ?)) SELECT * FROM static" 1 2 3 4 5 6])))
|
|
||||||
|
|
||||||
(deftest insert-into
|
|
||||||
(is (= (format-clause (first {:insert-into :foo}) nil)
|
|
||||||
"INSERT INTO foo"))
|
|
||||||
(is (= (format-clause (first {:insert-into [:foo {:select [:bar] :from [:baz]}]}) nil)
|
|
||||||
"INSERT INTO foo SELECT bar FROM baz"))
|
|
||||||
(is (= (format-clause (first {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]}) nil)
|
|
||||||
"INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz"))
|
|
||||||
(is (= (format {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]})
|
|
||||||
["INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz"])))
|
|
||||||
|
|
||||||
(deftest insert-into-namespaced
|
|
||||||
;; un-namespaced: works as expected:
|
|
||||||
(is (= (format {:insert-into :foo :values [{:foo/id 1}]})
|
|
||||||
["INSERT INTO foo (id) VALUES (?)" 1]))
|
|
||||||
(is (= (format {:insert-into :foo :columns [:foo/id] :values [[2]]})
|
|
||||||
["INSERT INTO foo (id) VALUES (?)" 2]))
|
|
||||||
(is (= (format {:insert-into :foo :values [{:foo/id 1}]}
|
|
||||||
:namespace-as-table? true)
|
|
||||||
["INSERT INTO foo (id) VALUES (?)" 1]))
|
|
||||||
(is (= (format {:insert-into :foo :columns [:foo/id] :values [[2]]}
|
|
||||||
:namespace-as-table? true)
|
|
||||||
["INSERT INTO foo (id) VALUES (?)" 2])))
|
|
||||||
|
|
||||||
(deftest exists-test
|
|
||||||
(is (= (format {:exists {:select [:a] :from [:foo]}})
|
|
||||||
["EXISTS (SELECT a FROM foo)"]))
|
|
||||||
(is (= (format {:select [:id]
|
|
||||||
:from [:foo]
|
|
||||||
:where [:exists {:select [1]
|
|
||||||
:from [:bar]
|
|
||||||
:where :deleted}]})
|
|
||||||
["SELECT id FROM foo WHERE EXISTS (SELECT ? FROM bar WHERE deleted)" 1])))
|
|
||||||
|
|
||||||
(deftest array-test
|
|
||||||
(is (= (format {:insert-into :foo
|
|
||||||
:columns [:baz]
|
|
||||||
:values [[(sql/array [1 2 3 4])]]})
|
|
||||||
["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?, ?])" 1 2 3 4]))
|
|
||||||
(is (= (format {:insert-into :foo
|
|
||||||
:columns [:baz]
|
|
||||||
:values [[(sql/array ["one" "two" "three"])]]})
|
|
||||||
["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?])" "one" "two" "three"])))
|
|
||||||
|
|
||||||
(deftest union-test
|
|
||||||
;; UNION and INTERSECT subexpressions should not be parenthesized.
|
|
||||||
;; If you need to add more complex expressions, use a subquery like this:
|
|
||||||
;; SELECT foo FROM bar1
|
|
||||||
;; UNION
|
|
||||||
;; SELECT foo FROM (SELECT foo FROM bar2 ORDER BY baz LIMIT 2)
|
|
||||||
;; ORDER BY foo ASC
|
|
||||||
(is (= (format {:union [{:select [:foo] :from [:bar1]}
|
|
||||||
{:select [:foo] :from [:bar2]}]})
|
|
||||||
["SELECT foo FROM bar1 UNION SELECT foo FROM bar2"])))
|
|
||||||
|
|
||||||
(deftest union-all-test
|
|
||||||
(is (= (format {:union-all [{:select [:foo] :from [:bar1]}
|
|
||||||
{:select [:foo] :from [:bar2]}]})
|
|
||||||
["SELECT foo FROM bar1 UNION ALL SELECT foo FROM bar2"])))
|
|
||||||
|
|
||||||
(deftest intersect-test
|
|
||||||
(is (= (format {:intersect [{:select [:foo] :from [:bar1]}
|
|
||||||
{:select [:foo] :from [:bar2]}]})
|
|
||||||
["SELECT foo FROM bar1 INTERSECT SELECT foo FROM bar2"])))
|
|
||||||
|
|
||||||
(deftest except-test
|
|
||||||
(is (= (format {:except [{:select [:foo] :from [:bar1]}
|
|
||||||
{:select [:foo] :from [:bar2]}]})
|
|
||||||
["SELECT foo FROM bar1 EXCEPT SELECT foo FROM bar2"])))
|
|
||||||
|
|
||||||
(deftest inner-parts-test
|
|
||||||
(testing "The correct way to apply ORDER BY to various parts of a UNION"
|
|
||||||
(is (= (format
|
|
||||||
{:union
|
|
||||||
[{:select [:amount :id :created_on]
|
|
||||||
:from [:transactions]}
|
|
||||||
{:select [:amount :id :created_on]
|
|
||||||
:from [{:select [:amount :id :created_on]
|
|
||||||
:from [:other_transactions]
|
|
||||||
:order-by [[:amount :desc]]
|
|
||||||
:limit 5}]}]
|
|
||||||
:order-by [[:amount :asc]]})
|
|
||||||
["SELECT amount, id, created_on FROM transactions UNION SELECT amount, id, created_on FROM (SELECT amount, id, created_on FROM other_transactions ORDER BY amount DESC LIMIT ?) ORDER BY amount ASC" 5]))))
|
|
||||||
|
|
||||||
(deftest compare-expressions-test
|
|
||||||
(testing "Sequences should be fns when in value/comparison spots"
|
|
||||||
(is (= ["SELECT foo FROM bar WHERE (col1 mod ?) = (col2 + ?)" 4 4]
|
|
||||||
(format {:select [:foo]
|
|
||||||
:from [:bar]
|
|
||||||
:where [:= [:mod :col1 4] [:+ :col2 4]]}))))
|
|
||||||
|
|
||||||
(testing "Value context only applies to sequences in value/comparison spots"
|
|
||||||
(let [sub {:select [:%sum.amount]
|
|
||||||
:from [:bar]
|
|
||||||
:where [:in :id ["id-1" "id-2"]]}]
|
|
||||||
(is (= ["SELECT total FROM foo WHERE (SELECT sum(amount) FROM bar WHERE (id in (?, ?))) = total" "id-1" "id-2"]
|
|
||||||
(format {:select [:total]
|
|
||||||
:from [:foo]
|
|
||||||
:where [:= sub :total]})))
|
|
||||||
(is (= ["WITH t AS (SELECT sum(amount) FROM bar WHERE (id in (?, ?))) SELECT total FROM foo WHERE total = t" "id-1" "id-2"]
|
|
||||||
(format {:with [[:t sub]]
|
|
||||||
:select [:total]
|
|
||||||
:from [:foo]
|
|
||||||
:where [:= :total :t]}))))))
|
|
||||||
|
|
||||||
(deftest union-with-cte
|
|
||||||
(is (= (format {:union [{:select [:foo] :from [:bar1]}
|
|
||||||
{:select [:foo] :from [:bar2]}]
|
|
||||||
:with [[[:bar {:columns [:spam :eggs]}]
|
|
||||||
{:values [[1 2] [3 4] [5 6]]}]]})
|
|
||||||
["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) SELECT foo FROM bar1 UNION SELECT foo FROM bar2" 1 2 3 4 5 6])))
|
|
||||||
|
|
||||||
|
|
||||||
(deftest union-all-with-cte
|
|
||||||
(is (= (format {:union-all [{:select [:foo] :from [:bar1]}
|
|
||||||
{:select [:foo] :from [:bar2]}]
|
|
||||||
:with [[[:bar {:columns [:spam :eggs]}]
|
|
||||||
{:values [[1 2] [3 4] [5 6]]}]]})
|
|
||||||
["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) SELECT foo FROM bar1 UNION ALL SELECT foo FROM bar2" 1 2 3 4 5 6])))
|
|
||||||
|
|
||||||
(deftest parameterizer-none
|
|
||||||
(testing "array parameter"
|
|
||||||
(is (= (format {:insert-into :foo
|
|
||||||
:columns [:baz]
|
|
||||||
:values [[(sql/array [1 2 3 4])]]}
|
|
||||||
:parameterizer :none)
|
|
||||||
["INSERT INTO foo (baz) VALUES (ARRAY[1, 2, 3, 4])"])))
|
|
||||||
|
|
||||||
(testing "union complex values"
|
|
||||||
(is (= (format {:union [{:select [:foo] :from [:bar1]}
|
|
||||||
{:select [:foo] :from [:bar2]}]
|
|
||||||
:with [[[:bar {:columns [:spam :eggs]}]
|
|
||||||
{:values [[1 2] [3 4] [5 6]]}]]}
|
|
||||||
:parameterizer :none)
|
|
||||||
["WITH bar (spam, eggs) AS (VALUES (1, 2), (3, 4), (5, 6)) SELECT foo FROM bar1 UNION SELECT foo FROM bar2"]))))
|
|
||||||
|
|
||||||
(deftest where-and
|
|
||||||
(testing "should ignore a nil predicate"
|
|
||||||
(is (= (format {:where [:and [:= :foo "foo"] [:= :bar "bar"] nil]}
|
|
||||||
:parameterizer :postgresql)
|
|
||||||
["WHERE (foo = $1 AND bar = $2)" "foo" "bar"]))))
|
|
||||||
|
|
||||||
|
|
||||||
(defmethod parameterize :single-quote [_ value pname] (str \' value \'))
|
|
||||||
(defmethod parameterize :mysql-fill [_ value pname] "?")
|
|
||||||
|
|
||||||
(deftest customized-parameterizer
|
|
||||||
(testing "should fill param with single quote"
|
|
||||||
(is (= (format {:where [:and [:= :foo "foo"] [:= :bar "bar"] nil]}
|
|
||||||
:parameterizer :single-quote)
|
|
||||||
["WHERE (foo = 'foo' AND bar = 'bar')" "foo" "bar"])))
|
|
||||||
(testing "should fill param with ?"
|
|
||||||
(is (= (format {:where [:and [:= :foo "foo"] [:= :bar "bar"] nil]}
|
|
||||||
:parameterizer :mysql-fill)
|
|
||||||
["WHERE (foo = ? AND bar = ?)" "foo" "bar"]))))
|
|
||||||
|
|
||||||
|
|
||||||
(deftest set-before-from ; issue 235
|
|
||||||
(is (=
|
|
||||||
["UPDATE `films` `f` SET `kind` = `c`.`test` FROM (SELECT `b`.`test` FROM `bar` `b` WHERE `b`.`id` = ?) `c` WHERE `f`.`kind` = ?" 1 "drama"]
|
|
||||||
(->
|
|
||||||
{:update [:films :f]
|
|
||||||
:set0 {:kind :c.test}
|
|
||||||
:from [[{:select [:b.test]
|
|
||||||
:from [[:bar :b]]
|
|
||||||
:where [:= :b.id 1]} :c]]
|
|
||||||
:where [:= :f.kind "drama"]}
|
|
||||||
(format :quoting :mysql)))))
|
|
||||||
|
|
||||||
(deftest set-after-join
|
|
||||||
(is (=
|
|
||||||
["UPDATE `foo` INNER JOIN `bar` ON `bar`.`id` = `foo`.`bar_id` SET `a` = ? WHERE `bar`.`b` = ?" 1 42]
|
|
||||||
(->
|
|
||||||
{:update :foo
|
|
||||||
:join [:bar [:= :bar.id :foo.bar_id]]
|
|
||||||
:set {:a 1}
|
|
||||||
:where [:= :bar.b 42]}
|
|
||||||
(format :quoting :mysql))))
|
|
||||||
(is (=
|
|
||||||
["UPDATE `foo` INNER JOIN `bar` ON `bar`.`id` = `foo`.`bar_id` SET `a` = ? WHERE `bar`.`b` = ?" 1 42]
|
|
||||||
(->
|
|
||||||
{:update :foo
|
|
||||||
:join [:bar [:= :bar.id :foo.bar_id]]
|
|
||||||
:set1 {:a 1}
|
|
||||||
:where [:= :bar.b 42]}
|
|
||||||
(format :quoting :mysql)))))
|
|
||||||
|
|
||||||
(deftest delete-from-test
|
|
||||||
(is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42]
|
|
||||||
(-> {:delete-from :foo
|
|
||||||
:where [:= :foo.id 42]}
|
|
||||||
(format :quoting :mysql)))))
|
|
||||||
|
|
||||||
(deftest delete-test
|
|
||||||
(is (= ["DELETE `t1`, `t2` FROM `table1` `t1` INNER JOIN `table2` `t2` ON `t1`.`fk` = `t2`.`id` WHERE `t1`.`bar` = ?" 42]
|
|
||||||
(-> {:delete [:t1 :t2]
|
|
||||||
:from [[:table1 :t1]]
|
|
||||||
:join [[:table2 :t2] [:= :t1.fk :t2.id]]
|
|
||||||
:where [:= :t1.bar 42]}
|
|
||||||
(format :quoting :mysql)))))
|
|
||||||
|
|
||||||
(deftest truncate-test
|
|
||||||
(is (= ["TRUNCATE `foo`"]
|
|
||||||
(-> {:truncate :foo}
|
|
||||||
(format :quoting :mysql)))))
|
|
||||||
|
|
||||||
(deftest inlined-values-are-stringified-correctly
|
|
||||||
(is (= ["SELECT foo, bar, NULL"]
|
|
||||||
(format {:select [(honeysql.core/inline "foo")
|
|
||||||
(honeysql.core/inline :bar)
|
|
||||||
(honeysql.core/inline nil)]}))))
|
|
||||||
|
|
||||||
;; Make sure if Locale is Turkish we're not generating queries like İNNER JOIN (dot over the I) because
|
|
||||||
;; `string/upper-case` is converting things to upper-case using the default Locale. Generated query should be the same
|
|
||||||
;; regardless of system Locale. See #236
|
|
||||||
#?(:clj
|
|
||||||
(deftest statements-generated-correctly-with-turkish-locale
|
|
||||||
(let [format-with-locale (fn [^String language-tag]
|
|
||||||
(let [original-locale (java.util.Locale/getDefault)]
|
|
||||||
(try
|
|
||||||
(java.util.Locale/setDefault (java.util.Locale/forLanguageTag language-tag))
|
|
||||||
(format {:select [:t2.name]
|
|
||||||
:from [[:table1 :t1]]
|
|
||||||
:join [[:table2 :t2] [:= :t1.fk :t2.id]]
|
|
||||||
:where [:= :t1.id 1]})
|
|
||||||
(finally
|
|
||||||
(java.util.Locale/setDefault original-locale)))))]
|
|
||||||
(is (= (format-with-locale "en")
|
|
||||||
(format-with-locale "tr"))))))
|
|
||||||
|
|
||||||
(deftest join-on-true-253
|
|
||||||
;; used to work on honeysql 0.9.2; broke in 0.9.3
|
|
||||||
(is (= ["SELECT foo FROM bar INNER JOIN table t ON TRUE"]
|
|
||||||
(format {:select [:foo]
|
|
||||||
:from [:bar]
|
|
||||||
:join [[:table :t] true]}))))
|
|
||||||
|
|
||||||
(deftest cross-join-test
|
|
||||||
(is (= ["SELECT * FROM foo CROSS JOIN bar"]
|
|
||||||
(format {:select [:*]
|
|
||||||
:from [:foo]
|
|
||||||
:cross-join [:bar]})))
|
|
||||||
(is (= ["SELECT * FROM foo f CROSS JOIN bar b"]
|
|
||||||
(format {:select [:*]
|
|
||||||
:from [[:foo :f]]
|
|
||||||
:cross-join [[:bar :b]]}))))
|
|
||||||
|
|
@ -1,330 +0,0 @@
|
||||||
(ns honeysql.helpers
|
|
||||||
(:refer-clojure :exclude [update])
|
|
||||||
#?(:cljs (:require-macros [honeysql.helpers :refer [defhelper]])))
|
|
||||||
|
|
||||||
(defmulti build-clause (fn [name & args]
|
|
||||||
name))
|
|
||||||
|
|
||||||
(defmethod build-clause :default [_ m & args]
|
|
||||||
m)
|
|
||||||
|
|
||||||
(defn plain-map? [m]
|
|
||||||
(and
|
|
||||||
(map? m)
|
|
||||||
(not (record? m))))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(defmacro defhelper [helper arglist & more]
|
|
||||||
(when-not (vector? arglist)
|
|
||||||
(throw #?(:clj (IllegalArgumentException. "arglist must be a vector")
|
|
||||||
:cljs (js/Error. "arglist must be a vector"))))
|
|
||||||
(when-not (= (count arglist) 2)
|
|
||||||
(throw #?(:clj (IllegalArgumentException. "arglist must have two entries, map and varargs")
|
|
||||||
:cljs (js/Error. "arglist must have two entries, map and varargs"))))
|
|
||||||
|
|
||||||
(let [kw (keyword (name helper))
|
|
||||||
[m-arg varargs] arglist]
|
|
||||||
`(do
|
|
||||||
(defmethod build-clause ~kw ~['_ m-arg varargs] ~@more)
|
|
||||||
(defn ~helper [& args#]
|
|
||||||
(let [[m# args#] (if (plain-map? (first args#))
|
|
||||||
[(first args#) (rest args#)]
|
|
||||||
[{} args#])]
|
|
||||||
(build-clause ~kw m# args#)))
|
|
||||||
|
|
||||||
;; maintain the original arglist instead of getting
|
|
||||||
;; ([& args__6880__auto__])
|
|
||||||
(alter-meta!
|
|
||||||
(var ~helper)
|
|
||||||
assoc
|
|
||||||
:arglists
|
|
||||||
'(~['& varargs]
|
|
||||||
~[m-arg '& varargs]))))))
|
|
||||||
|
|
||||||
(defn collify [x]
|
|
||||||
(if (coll? x) x [x]))
|
|
||||||
|
|
||||||
(defhelper select [m fields]
|
|
||||||
(assoc m :select (collify fields)))
|
|
||||||
|
|
||||||
(defhelper merge-select [m fields]
|
|
||||||
(update-in m [:select] concat (collify fields)))
|
|
||||||
|
|
||||||
(defhelper un-select [m fields]
|
|
||||||
(update-in m [:select] #(remove (set (collify fields)) %)))
|
|
||||||
|
|
||||||
(defhelper from [m tables]
|
|
||||||
(assoc m :from (collify tables)))
|
|
||||||
|
|
||||||
(defhelper merge-from [m tables]
|
|
||||||
(update-in m [:from] concat (collify tables)))
|
|
||||||
|
|
||||||
(defmethod build-clause :where [_ m pred]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :where pred)))
|
|
||||||
|
|
||||||
(defn- prep-where [args]
|
|
||||||
(let [[m preds] (if (map? (first args))
|
|
||||||
[(first args) (rest args)]
|
|
||||||
[{} args])
|
|
||||||
[logic-op preds] (if (keyword? (first preds))
|
|
||||||
[(first preds) (rest preds)]
|
|
||||||
[:and preds])
|
|
||||||
preds (remove nil? preds)
|
|
||||||
pred (if (>= 1 (count preds))
|
|
||||||
(first preds)
|
|
||||||
(into [logic-op] preds))]
|
|
||||||
[m pred logic-op]))
|
|
||||||
|
|
||||||
(defn where [& args]
|
|
||||||
(let [[m pred] (prep-where args)]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :where pred))))
|
|
||||||
|
|
||||||
(defmethod build-clause :merge-where [_ m pred]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :where (if (not (nil? (:where m)))
|
|
||||||
[:and (:where m) pred]
|
|
||||||
pred))))
|
|
||||||
|
|
||||||
(defn merge-where [& args]
|
|
||||||
(let [[m pred logic-op] (prep-where args)]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :where (if (not (nil? (:where m)))
|
|
||||||
[logic-op (:where m) pred]
|
|
||||||
pred)))))
|
|
||||||
|
|
||||||
(defhelper join [m clauses]
|
|
||||||
(assoc m :join clauses))
|
|
||||||
|
|
||||||
(defhelper merge-join [m clauses]
|
|
||||||
(update-in m [:join] concat clauses))
|
|
||||||
|
|
||||||
(defhelper left-join [m clauses]
|
|
||||||
(assoc m :left-join clauses))
|
|
||||||
|
|
||||||
(defhelper merge-left-join [m clauses]
|
|
||||||
(update-in m [:left-join] concat clauses))
|
|
||||||
|
|
||||||
(defhelper right-join [m clauses]
|
|
||||||
(assoc m :right-join clauses))
|
|
||||||
|
|
||||||
(defhelper merge-right-join [m clauses]
|
|
||||||
(update-in m [:right-join] concat clauses))
|
|
||||||
|
|
||||||
(defhelper full-join [m clauses]
|
|
||||||
(assoc m :full-join clauses))
|
|
||||||
|
|
||||||
(defhelper merge-full-join [m clauses]
|
|
||||||
(update-in m [:full-join] concat clauses))
|
|
||||||
|
|
||||||
(defhelper cross-join [m clauses]
|
|
||||||
(assoc m :cross-join clauses))
|
|
||||||
|
|
||||||
(defhelper merge-cross-join [m clauses]
|
|
||||||
(update-in m [:cross-join] concat clauses))
|
|
||||||
|
|
||||||
(defmethod build-clause :group-by [_ m fields]
|
|
||||||
(assoc m :group-by (collify fields)))
|
|
||||||
|
|
||||||
(defn group [& args]
|
|
||||||
(let [[m fields] (if (map? (first args))
|
|
||||||
[(first args) (rest args)]
|
|
||||||
[{} args])]
|
|
||||||
(build-clause :group-by m fields)))
|
|
||||||
|
|
||||||
(defhelper merge-group-by [m fields]
|
|
||||||
(update-in m [:group-by] concat (collify fields)))
|
|
||||||
|
|
||||||
(defmethod build-clause :having [_ m pred]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :having pred)))
|
|
||||||
|
|
||||||
(defn having [& args]
|
|
||||||
(let [[m pred] (prep-where args)]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :having pred))))
|
|
||||||
|
|
||||||
(defmethod build-clause :merge-having [_ m pred]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :having (if (not (nil? (:having m)))
|
|
||||||
[:and (:having m) pred]
|
|
||||||
pred))))
|
|
||||||
|
|
||||||
(defn merge-having [& args]
|
|
||||||
(let [[m pred logic-op] (prep-where args)]
|
|
||||||
(if (nil? pred)
|
|
||||||
m
|
|
||||||
(assoc m :having (if (not (nil? (:having m)))
|
|
||||||
[logic-op (:having m) pred]
|
|
||||||
pred)))))
|
|
||||||
|
|
||||||
(defhelper order-by [m fields]
|
|
||||||
(assoc m :order-by (collify fields)))
|
|
||||||
|
|
||||||
(defhelper merge-order-by [m fields]
|
|
||||||
(update-in m [:order-by] concat (collify fields)))
|
|
||||||
|
|
||||||
(defhelper limit [m l]
|
|
||||||
(if (nil? l)
|
|
||||||
m
|
|
||||||
(assoc m :limit (if (coll? l) (first l) l))))
|
|
||||||
|
|
||||||
(defhelper offset [m o]
|
|
||||||
(if (nil? o)
|
|
||||||
m
|
|
||||||
(assoc m :offset (if (coll? o) (first o) o))))
|
|
||||||
|
|
||||||
(defhelper lock [m lock]
|
|
||||||
(cond-> m
|
|
||||||
lock
|
|
||||||
(assoc :lock lock)))
|
|
||||||
|
|
||||||
(defhelper modifiers [m ms]
|
|
||||||
(if (nil? ms)
|
|
||||||
m
|
|
||||||
(assoc m :modifiers (collify ms))))
|
|
||||||
|
|
||||||
(defhelper merge-modifiers [m ms]
|
|
||||||
(if (nil? ms)
|
|
||||||
m
|
|
||||||
(update-in m [:modifiers] concat (collify ms))))
|
|
||||||
|
|
||||||
(defmethod build-clause :insert-into [_ m table]
|
|
||||||
(assoc m :insert-into table))
|
|
||||||
|
|
||||||
(defn insert-into
|
|
||||||
([table] (insert-into nil table))
|
|
||||||
([m table] (build-clause :insert-into m table)))
|
|
||||||
|
|
||||||
(defn- check-varargs
|
|
||||||
"Called for helpers that require unrolled arguments to catch the mistake
|
|
||||||
of passing a collection as a single argument."
|
|
||||||
[helper args]
|
|
||||||
(when (and (coll? args) (= 1 (count args)) (coll? (first args)))
|
|
||||||
(let [msg (str (name helper) " takes varargs, not a single collection")]
|
|
||||||
(throw #?(:clj (IllegalArgumentException. msg)
|
|
||||||
:cljs (js/Error. msg))))))
|
|
||||||
|
|
||||||
(defmethod build-clause :columns [_ m fields]
|
|
||||||
(assoc m :columns (collify fields)))
|
|
||||||
|
|
||||||
(defn columns [& args]
|
|
||||||
(let [[m fields] (if (map? (first args))
|
|
||||||
[(first args) (rest args)]
|
|
||||||
[{} args])]
|
|
||||||
(check-varargs :columns fields)
|
|
||||||
(build-clause :columns m fields)))
|
|
||||||
|
|
||||||
(defmethod build-clause :merge-columns [_ m fields]
|
|
||||||
(update-in m [:columns] concat (collify fields)))
|
|
||||||
|
|
||||||
(defn merge-columns [& args]
|
|
||||||
(let [[m fields] (if (map? (first args))
|
|
||||||
[(first args) (rest args)]
|
|
||||||
[{} args])]
|
|
||||||
(check-varargs :merge-columns fields)
|
|
||||||
(build-clause :merge-columns m fields)))
|
|
||||||
|
|
||||||
(defhelper composite [m vs]
|
|
||||||
(if (nil? vs)
|
|
||||||
m
|
|
||||||
(assoc m :composite (collify vs))))
|
|
||||||
|
|
||||||
(defmethod build-clause :values [_ m vs]
|
|
||||||
(assoc m :values vs))
|
|
||||||
|
|
||||||
(defn values
|
|
||||||
([vs] (values nil vs))
|
|
||||||
([m vs] (build-clause :values m vs)))
|
|
||||||
|
|
||||||
(defmethod build-clause :merge-values [_ m vs]
|
|
||||||
(update-in m [:values] concat vs))
|
|
||||||
|
|
||||||
(defn merge-values
|
|
||||||
([vs] (merge-values nil vs))
|
|
||||||
([m vs] (build-clause :merge-values m vs)))
|
|
||||||
|
|
||||||
(defmethod build-clause :query-values [_ m vs]
|
|
||||||
(assoc m :query-values vs))
|
|
||||||
|
|
||||||
(defn query-values
|
|
||||||
([vs] (values nil vs))
|
|
||||||
([m vs] (build-clause :query-values m vs)))
|
|
||||||
|
|
||||||
(defmethod build-clause :update [_ m table]
|
|
||||||
(assoc m :update table))
|
|
||||||
|
|
||||||
(defn update
|
|
||||||
([table] (update nil table))
|
|
||||||
([m table] (build-clause :update m table)))
|
|
||||||
|
|
||||||
(defmethod build-clause :set [_ m values]
|
|
||||||
(assoc m :set values))
|
|
||||||
|
|
||||||
;; short for sql set, to avoid name collision with clojure.core/set
|
|
||||||
(defn sset
|
|
||||||
([vs] (sset nil vs))
|
|
||||||
([m vs] (build-clause :set m vs)))
|
|
||||||
|
|
||||||
(defmethod build-clause :set0 [_ m values]
|
|
||||||
(assoc m :set0 values))
|
|
||||||
|
|
||||||
;; set with lower priority (before from)
|
|
||||||
(defn set0
|
|
||||||
([vs] (set0 nil vs))
|
|
||||||
([m vs] (build-clause :set0 m vs)))
|
|
||||||
|
|
||||||
(defmethod build-clause :set [_ m values]
|
|
||||||
(assoc m :set values))
|
|
||||||
|
|
||||||
;; set with higher priority (after join)
|
|
||||||
(defn set1
|
|
||||||
([vs] (set1 nil vs))
|
|
||||||
([m vs] (build-clause :set1 m vs)))
|
|
||||||
|
|
||||||
(defmethod build-clause :delete-from [_ m table]
|
|
||||||
(assoc m :delete-from table))
|
|
||||||
|
|
||||||
(defn delete-from
|
|
||||||
([table] (delete-from nil table))
|
|
||||||
([m table] (build-clause :delete-from m table)))
|
|
||||||
|
|
||||||
(defmethod build-clause :delete [_ m tables]
|
|
||||||
(assoc m :delete tables))
|
|
||||||
|
|
||||||
(defn delete
|
|
||||||
([tables] (delete nil tables))
|
|
||||||
([m tables] (build-clause :delete m tables)))
|
|
||||||
|
|
||||||
(defmethod build-clause :truncate [_ m table]
|
|
||||||
(assoc m :truncate table))
|
|
||||||
|
|
||||||
(defn truncate
|
|
||||||
([table] (truncate nil table))
|
|
||||||
([m table] (build-clause :truncate m table)))
|
|
||||||
|
|
||||||
(defhelper with [m ctes]
|
|
||||||
(assoc m :with ctes))
|
|
||||||
|
|
||||||
(defhelper with-recursive [m ctes]
|
|
||||||
(assoc m :with-recursive ctes))
|
|
||||||
|
|
||||||
(defmethod build-clause :union [_ m maps]
|
|
||||||
(assoc m :union maps))
|
|
||||||
|
|
||||||
(defmethod build-clause :union-all [_ m maps]
|
|
||||||
(assoc m :union-all maps))
|
|
||||||
|
|
||||||
(defmethod build-clause :intersect [_ m maps]
|
|
||||||
(assoc m :intersect maps))
|
|
||||||
|
|
||||||
(defmethod build-clause :except [_ m maps]
|
|
||||||
(assoc m :except maps))
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
(ns honeysql.types
|
|
||||||
(:refer-clojure :exclude [array]))
|
|
||||||
|
|
||||||
(defrecord SqlCall [name args])
|
|
||||||
|
|
||||||
(defn call
|
|
||||||
"Represents a SQL function call. Name should be a keyword."
|
|
||||||
[name & args]
|
|
||||||
(SqlCall. name args))
|
|
||||||
|
|
||||||
(defn read-sql-call [form]
|
|
||||||
;; late bind so that we get new class on REPL reset
|
|
||||||
(apply #?(:clj (resolve `call) :cljs call) form))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defrecord SqlRaw [s])
|
|
||||||
|
|
||||||
(defn raw
|
|
||||||
"Represents a raw SQL string"
|
|
||||||
[s]
|
|
||||||
(SqlRaw. (if (vector? s) s (str s))))
|
|
||||||
|
|
||||||
(defn read-sql-raw [form]
|
|
||||||
;; late bind, as above
|
|
||||||
(#?(:clj (resolve `raw) :cljs raw) form))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defrecord SqlParam [name])
|
|
||||||
|
|
||||||
(defn param
|
|
||||||
"Represents a SQL parameter which can be filled in later"
|
|
||||||
[name]
|
|
||||||
(SqlParam. name))
|
|
||||||
|
|
||||||
(defn param-name [^SqlParam param]
|
|
||||||
(.-name param))
|
|
||||||
|
|
||||||
(defn read-sql-param [form]
|
|
||||||
;; late bind, as above
|
|
||||||
(#?(:clj (resolve `param) :cljs param) form))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defrecord SqlArray [values])
|
|
||||||
|
|
||||||
(defn array
|
|
||||||
"Represents a SQL array."
|
|
||||||
[values]
|
|
||||||
(SqlArray. values))
|
|
||||||
|
|
||||||
(defn array-vals [^SqlArray a]
|
|
||||||
(.-values a))
|
|
||||||
|
|
||||||
(defn read-sql-array [form]
|
|
||||||
;; late bind, as above
|
|
||||||
(#?(:clj (resolve `array) :cljs array) form))
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defrecord SqlInline [value])
|
|
||||||
|
|
||||||
(defprotocol Inlinable
|
|
||||||
(inline-str [x]))
|
|
||||||
|
|
||||||
(defn inline
|
|
||||||
"Prevents parameterization"
|
|
||||||
[value]
|
|
||||||
(SqlInline. value))
|
|
||||||
|
|
||||||
(defn read-sql-inline [form]
|
|
||||||
(#?(:clj (resolve `inline) :cljs inline) form))
|
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(do
|
|
||||||
(defmethod print-method SqlCall [^SqlCall o ^java.io.Writer w]
|
|
||||||
(.write w (str "#sql/call " (pr-str (into [(.-name o)] (.-args o))))))
|
|
||||||
|
|
||||||
(defmethod print-dup SqlCall [o w]
|
|
||||||
(print-method o w))
|
|
||||||
|
|
||||||
(defmethod print-method SqlRaw [^SqlRaw o ^java.io.Writer w]
|
|
||||||
(.write w (str "#sql/raw " (pr-str (.s o)))))
|
|
||||||
|
|
||||||
(defmethod print-dup SqlRaw [o w]
|
|
||||||
(print-method o w))
|
|
||||||
|
|
||||||
(defmethod print-method SqlParam [^SqlParam o ^java.io.Writer w]
|
|
||||||
(.write w (str "#sql/param " (pr-str (.name o)))))
|
|
||||||
|
|
||||||
(defmethod print-dup SqlParam [o w]
|
|
||||||
(print-method o w))
|
|
||||||
|
|
||||||
(defmethod print-method SqlArray [^SqlArray a ^java.io.Writer w]
|
|
||||||
(.write w (str "#sql/array " (pr-str (.values a)))))
|
|
||||||
|
|
||||||
(defmethod print-dup SqlArray [a w]
|
|
||||||
(print-method a w))
|
|
||||||
|
|
||||||
(defmethod print-method SqlInline [^SqlInline a ^java.io.Writer w]
|
|
||||||
(.write w (str "#sql/inline " (pr-str (.value a)))))
|
|
||||||
|
|
||||||
(defmethod print-dup SqlInline [a w]
|
|
||||||
(print-method a w))))
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
(ns honeysql.util)
|
|
||||||
|
|
||||||
(defmacro defalias [sym var-sym]
|
|
||||||
`(let [v# (var ~var-sym)]
|
|
||||||
(intern *ns* (with-meta (quote ~sym) (meta v#)) @v#)))
|
|
||||||
Loading…
Reference in a new issue