1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366
|
(defpackage :s-sql-tests
(:use :common-lisp :fiveam :s-sql :cl-postgres :cl-postgres-error :cl-postgres-tests :postmodern))
(in-package :s-sql-tests)
;; Adjust the above to some db/user/pass/host/[port] combination that
;; refers to a valid postgresql database, then after loading the file,
;; run the tests with (fiveam:run! :cl-postgres)
(fiveam:def-suite :s-sql
:description "Master suite for s-sql")
(fiveam:in-suite :s-sql)
(defmacro with-test-connection (&body body)
`(postmodern:with-connection *test-connection* ,@body))
(defmacro protect (&body body)
`(unwind-protect (progn ,@(butlast body)) ,(car (last body))))
(test connect-sanity
(with-test-connection
(is (not (null *database*)))))
(test sql-error)
(test strcat
"Testing strcat. Concatenate a list of strings into a single one."
(is (equal (s-sql::strcat '("a" "b")) "ab"))
(is (equal (s-sql::strcat '("a " "b")) "a b")))
(test implode
"Testing implode. Reduce a list of strings to a single string, inserting a separator
between them."
(is (equal (s-sql::implode "/" '("aa" "bb" " " "cc")) "aa/bb/ /cc")))
(test split-on-keywords%
"Testing split-on-keywords%. Helper function for split-on-keywords. Extracts the values
associated with the keywords from an argument list, and checks for errors."
(is (equal (s-sql::split-on-keywords% '((owner ?)) '(:owner "Sabra"))
'((OWNER "Sabra"))))
(is (equal (s-sql::split-on-keywords% '((interval ? *)) '(:interval 5 hours))
'((INTERVAL 5 HOURS))))
(is (equal (s-sql::split-on-keywords% '((a1 * ?) (b2 ?) (c3 ? *)) '(:a1 "Alpha1 " :b2 "Beta2 " :c3 "Ceta3 "))
'((A1 "Alpha1 ") (B2 "Beta2 ") (C3 "Ceta3 "))))
(is (equal (s-sql::split-on-keywords% '((a1 * ?) (c3 ? *)) '(:a1 "Alpha1 " :b2 "Beta2" :c3 "Ceta3 "))
'((A1 "Alpha1 " :B2 "Beta2") (C3 "Ceta3 "))))
(signals sql-error (s-sql::split-on-keywords% '((a1 * ?) (b2 ?) (c3 ? *)) '(:a1 "Alpha1 " :c3 "Ceta3 ")))
(signals sql-error (s-sql::split-on-keywords% '((a1 * ?) (b2 -) (c3 ? *)) '(:a1 "Alpha1 " :b2 "Beta2" :c3 "Ceta3 ")))
(signals sql-error (s-sql::split-on-keywords% '((a1 * ?) (b2 ) (c3 ? *)) '(:a1 "Alpha1 " :c3 "Ceta3 ")))
(signals sql-error (s-sql::split-on-keywords% '((owner ?)) '(:owner "Sabra" :tourist "Geoffrey")))
(signals sql-error (s-sql::split-on-keywords% '((a1 * ?) (c3 ? )) '(:a1 "Alpha1 " :c3 "Ceta3 " "Ceta3.5")))
)
(test split-on-keywords
"Testing split-on-keywords. Used to handle arguments to some complex SQL operations.
Arguments are divided by keywords, which are interned with the name of the
non-keyword symbols in words, and bound to these symbols. After the
naming symbols, a ? can be used to indicate this argument group is
optional, an * to indicate it can consist of more than one element,
and a - to indicate it does not take any elements."
(is (equal (s-sql::split-on-keywords ((a1 * ?) (b2 ?) (c3 ? *)) '(:a1 "Alpha1 " :b2 "Beta2 " :c3 "Ceta3 ")
`("Results " ,@ (when a1 a1) ,@ (when c3 c3) ,@(when b2 b2)))
'("Results " "Alpha1 " "Ceta3 " "Beta2 ")))
(signals sql-error (s-sql::split-on-keywords ((a1 * ?) (b2 ?) (c3 ? *)) '(:a1 "Alpha1 " :c3 "Ceta3 ")
`("Results " ,@ (when a1 a1) ,@ (when c3 c3) ,@(when b2 b2)))
'("Results " "Alpha1 " "Ceta3 "))
(is (equal (s-sql::split-on-keywords ((a1 * ?) (c3 ? *)) '(:a1 "Alpha1 " :b2 "Beta2" :c3 "Ceta3 ")
`("Results " ,@ (when a1 a1) ,@ (when c3 c3)))
'("Results " "Alpha1 " :B2 "Beta2" "Ceta3 ")))
;; Keyword does not take any arguments
(signals sql-error (s-sql::split-on-keywords ((a1 * ?) (b2 -) (c3 ? *)) '(:a1 "Alpha1 " :b2 "Beta2" :c3 "Ceta3 ")
`("Results " ,@ (when a1 a1) ,@ (when c3 c3) ,@(when b2 b2))))
;; Required keyword missing
(signals sql-error (s-sql::split-on-keywords ((a1 * ?) (b2 ) (c3 ? *)) '(:a1 "Alpha1 " :c3 "Ceta3 ")
`("Results " ,@ (when a1 a1) ,@ (when c3 c3) ,@(when b2 b2))))
(is (equal (s-sql::split-on-keywords ((a1 * ?) (c3 ? *)) '(:a1 "Alpha1 " :b2 "Beta2" :c3 "Ceta3 ")
`("Results " ,@ (when a1 a1) ,@ (when c3 c3)))
'("Results " "Alpha1 " :B2 "Beta2" "Ceta3 ")))
;;too many elements for a keyword
(signals sql-error (s-sql::split-on-keywords ((a1 * ?) (c3 ? )) '(:a1 "Alpha1 " :c3 "Ceta3 " "Ceta3.5")
`("Results " ,@ (when a1 a1) ,@ (when c3 c3)))))
(test to-sql-name
"Testing to-sql-name. Convert a symbol or string into a name that can be an sql table,
column, or operation name. Add quotes when escape-p is true, or
escape-p is :auto and the name contains reserved words."
(is (equal (s-sql::to-sql-name 'George-Harrison)
"george_harrison"))
(is (equal (s-sql::to-sql-name "George-Harrison")
"george_harrison"))
(is (equal (s-sql::to-sql-name "George/Harriet")
"george_harriet"))
(is (equal (s-sql::to-sql-name "George Harriet")
"george_harriet"))
(is (equal (s-sql::to-sql-name "George \"Harriet\"" nil)
"george__harriet_"))
(is (equal (s-sql::to-sql-name "George.Harriet")
"george.harriet"))
(is (equal (s-sql::to-sql-name "current-schema" :auto)
"\"current_schema\""))
(is (equal (s-sql::to-sql-name "current-schema" nil)
"current_schema"))
(is (equal (s-sql::to-sql-name "create" :auto)
"\"create\""))
(is (equal (s-sql::to-sql-name "create" nil)
"create"))
(is (equal (s-sql::to-sql-name 'George-Harrison :literal)
"\"GEORGE-HARRISON\""))
(is (equal (s-sql::to-sql-name "George-Harrison" :literal)
"\"George-Harrison\""))
(is (equal (s-sql::to-sql-name "George/Harrison" :literal)
"\"George/Harrison\""))
(is (equal (s-sql::to-sql-name "George/Harrison" :auto)
"george_harrison"))
(is (equal (s-sql::to-sql-name "George_Harrison" :literal)
"\"George_Harrison\""))
(is (equal (s-sql::to-sql-name "George\\Harrison" :literal)
"\"George\\Harrison\""))
(is (equal (s-sql::to-sql-name "George-Harrison" :literal)
"\"George-Harrison\"")))
(test from-sql-name
"Testing from-sql-name. Convert a string to something that might have been its original
lisp name \(does not work if this name contained non-alphanumeric
characters other than #\-)"
(is (equal (s-sql::from-sql-name "create_all")
:CREATE-ALL))
(is (equal (s-sql::from-sql-name "region/los_angeles")
:REGION/LOS-ANGELES)))
(test sql-type-name
"Testing sql-type-name. Transform a lisp type into a string containing
something SQL understands. Default is to just use the type symbol's
name."
(is (equal (sql-type-name 'string "5")
"CHAR(5)"))
(is (equal (sql-type-name 'string)
"TEXT"))
(is (equal (sql-type-name 'double-float)
"DOUBLE PRECISION"))
(is (equal (sql-type-name 'ratio)
"RATIO"))
(is (equal (sql-type-name 'float)
"REAL"))
(signals error (sql-type-name 'array))
(is (equal (sql-type-name 'array 'integer)
"INTEGER[]"))
(signals error (sql-type-name 'array "boulder")))
(test to-type-name
"Testing to-type-name. Turn a Lisp type expression into an SQL typename."
(is (equal (s-sql::to-type-name 'float)
"REAL"))
(is (equal (s-sql::to-type-name '(string "5"))
"CHAR(5)")))
(test s-sql:sql-escape-string
"Testing sql-escape-string. Escape string data so it can be used in a query."
(is (equal (sql-escape-string "Puss in 'Boots'")
"E'Puss in ''Boots'''")))
(test sql-escape
"Testing sql-escape. Get the representation of a Lisp value so that it
can be used in a query."
(is (equal (sql-escape (/ 1 13))
"0.0769230769230769230769230769230769230"))
(is (equal (sql-escape #("Baden-Wurttemberg" "Bavaria" "Berlin" "Brandenburg"))
"ARRAY[E'Baden-Wurttemberg', E'Bavaria', E'Berlin', E'Brandenburg']")))
(test sql-expand
"Testing sql-expand. Compile-time expansion of forms into lists of stuff that evaluates
to strings \(which will form an SQL query when concatenated)."
(is (equal (s-sql::sql-expand (/ 1 13))
'("0.0769230769230769230769230769230769230")))
(is (equal (s-sql::sql-expand '("george" "paul" "john" "ringo"))
'((SQL-ESCAPE ("george" "paul" "john" "ringo")))))
(is (equal (s-sql::sql-expand '(george "paul"))
'((SQL-ESCAPE (GEORGE "paul")))))
(is (equal (s-sql::sql-expand '(:george "paul" "Beatles"))
'("george" "(" "E'paul'" ", " "E'Beatles'" ")")))
(is (equal (s-sql::sql-expand '(:george "rhythm" :group "Beatles"))
'("george" "(" "E'rhythm'" ", " "\"group\"" ", " "E'Beatles'" ")")))
(is (equal (s-sql::sql-expand '(george "rhythm" :group "Beatles"))
'((SQL-ESCAPE (GEORGE "rhythm" :GROUP "Beatles")))))
(is (equal (s-sql::sql-expand '(:= 'facs.facid 1))
'("(" "facs.facid" " = " "1" ")"))))
(test sql-expand-list
"Testing sql-expand-list. Expand a list of elements, adding a separator in between them."
(is (equal (s-sql::sql-expand-list '(george paul john "ringo" "mary-ann" carol-anne))
'((SQL-ESCAPE GEORGE) ", " (SQL-ESCAPE PAUL) ", " (SQL-ESCAPE JOHN) ", "
"E'ringo'" ", " "E'mary-ann'" ", " (SQL-ESCAPE CAROL-ANNE))))
(is (equal (s-sql::sql-expand-list '((:desc 'today) 'tomorrow 'yesterday))
'("today" " DESC" ", " "tomorrow" ", " "yesterday")))
(is (equal (s-sql::sql-expand-list (remove nil '(george paul john "ringo" "mary-ann" nil carol-anne nil)))
'((SQL-ESCAPE GEORGE) ", " (SQL-ESCAPE PAUL) ", " (SQL-ESCAPE JOHN) ", "
"E'ringo'" ", " "E'mary-ann'" ", " (SQL-ESCAPE CAROL-ANNE)))))
(test sql-expand-names
"Testing sql-expand-names"
(is (equal (s-sql::sql-expand-names '("george" "paul" "john" "ringo" "mary-ann"))
'("george" ", " "paul" ", " "john" ", " "ringo" ", " "mary_ann")))
(is (equal (s-sql::sql-expand-names '((george. "paul") "john" "ringo" "mary-ann"))
'("george." "(" "E'paul'" ")" ", " "john" ", " "ringo" ", " "mary_ann")))
(is (equal (s-sql::sql-expand-names '((george . paul) "john" "ringo" "mary-ann"))
'("george" "(" ")" ", " "john" ", " "ringo" ", " "mary_ann")))
(is (equal (s-sql::sql-expand-names '((george "paul") "john" "ringo" "mary-ann"))
'("george" "(" "E'paul'" ")" ", " "john" ", " "ringo" ", " "mary_ann"))))
(test reduce-strings
"Testing reduce-strings. Join adjacent strings in a list, leave other values intact."
(is (equal (s-sql::reduce-strings '("john" 7 "paul" "ringo" "george"))
'("john" 7 "paulringogeorge")))
(is (equal (s-sql::reduce-strings '("john" 7 "paul" "ringo" george))
'("john" 7 "paulringo" GEORGE))))
(test sql-macro
"Testing sql-macro. Compile form to an sql expression as far as possible."
(is (equal (sql (:select 'name :from 'items :where (:= 'id 1)))
"(SELECT name FROM items WHERE (id = 1))")))
(test sql-compile
"Testing sql-compile"
(is (equal (sql-compile '(:select 'name :from 'items :where (:= 'id 1)))
"(SELECT name FROM items WHERE (id = 1))")))
(test sql-template
"Testing sql-template"
(is (functionp (sql-template '(:select 'name :from 'items :where (:= 'id 1))))))
(test expand-sql-op
"Testing expand-sql-op"
(is (equal (s-sql::expand-sql-op :max '(1 2 3))
'("max" "(" "1" ", " "2" ", " "3" ")"))))
(test make-expander
"Testing make-expander"
(is (equal (funcall (s-sql::make-expander :unary "unary1") '("like"))
'("(" "unary1" " " "E'like'" ")")))
(is (equal (funcall (s-sql::make-expander :unary-postfix "unary2") '("like"))
'("(" "E'like'" " " "unary2" ")")))
(is (equal (funcall (s-sql::make-expander :n-ary "unary3") '("like" "a" "b"))
'("(" "E'like'" " unary3 " "E'a'" " unary3 " "E'b'" ")")))
(is (equal (funcall (s-sql::make-expander :2+-ary "unary4") '("like" "a" "b"))
'("(" "E'like'" " unary4 " "E'a'" " unary4 " "E'b'" ")")))
(is (equal (funcall (s-sql::make-expander :2+-ary "unary4") '("like" "a"))
'("(" "E'like'" " unary4 " "E'a'" ")")))
(signals sql-error (funcall (s-sql::make-expander :2+-ary "unary4") '("like")))
(is (equal (funcall (s-sql::make-expander :n-or-unary "unary5") '("like" "a" "b"))
'("(" "E'like'" " unary5 " "E'a'" " unary5 " "E'b'" ")")))
(is (equal (funcall (s-sql::make-expander :n-or-unary "unary5") '("like"))
'("(" "unary5" " " "E'like'" ")")))
(is (equal (funcall (s-sql::make-expander :n-or-unary "unary5") '("like" "a"))
'("(" "E'like'" " unary5 " "E'a'" ")"))))
(test select-simple
"Testing select modifiers"
(is (equal (sql (:select 'item :from 'item-table))
"(SELECT item FROM item_table)"))
(is (equal (sql (:select 'item :from 'item-table :where (:= 'id 2)))
"(SELECT item FROM item_table WHERE (id = 2))"))
(is (equal (sql (:select 'item :distinct :from 'item-table :where (:= 'col1 "Albania")))
"(SELECT DISTINCT item FROM item_table WHERE (col1 = E'Albania'))"))
(is (equal (sql (:select 'item 'groups :from 'item-table 'item-groups :where (:= 'item-table.group-id 'item-groups.id)))
"(SELECT item, groups FROM item_table, item_groups WHERE (item_table.group_id = item_groups.id))"))
(is (equal (sql (:select (:over (:sum 'salary) 'w)
(:over (:avg 'salary) 'w)
:from 'empsalary :window
(:as 'w (:partition-by 'depname :order-by (:desc 'salary)))))
"(SELECT (sum(salary) OVER w), (avg(salary) OVER w) FROM empsalary WINDOW w AS (PARTITION BY depname ORDER BY salary DESC))"))
(is (equal (let ((param-latitude nil) (param-longitude t))
(sql (:select 'id 'name (when param-latitude '0)
(when param-longitude 'longitude)
:from 'countries
:where (:= 'id 20))))
"(SELECT id, name, false, longitude FROM countries WHERE (id = 20))"))
(is (equal (sql (:with (:as 'upd
(:parens
(:update 'employees :set 'sales-count (:= 'sales-count 1)
:where (:= 'id
(:select 'sales-person
:from 'accounts
:where (:= 'name "Acme Corporation")))
:returning '*)))
(:insert-into 'employees-log
(:select '* 'current-timestamp :from
'upd))))
"WITH upd AS (UPDATE employees SET sales_count = (sales_count = 1) WHERE (id = (SELECT sales_person FROM accounts WHERE (name = E'Acme Corporation'))) RETURNING *) INSERT INTO employees_log (SELECT *, \"current_timestamp\" FROM upd)"))
(is (equal (sql "(SELECT countries.id, (countries.name || '-' || regions.name)
FROM countries, regions
WHERE ((regions.id = countries.region_id) and (countries.name = 'US')))")
"E'(SELECT countries.id, (countries.name || ''-'' || regions.name)
FROM countries, regions
WHERE ((regions.id = countries.region_id) and (countries.name = ''US'')))'"))
(is (equal (sql (:select (:+ 'id 12) 'name :from 'regions :where (:= 'name "South America")))
"(SELECT (id + 12), name FROM regions WHERE (name = E'South America'))")))
(test select-distinct
"Testing select with distinct. From https://www.pgexercises.com/questions/basic/unique.html"
(is (equal (sql (:limit (:order-by (:select 'surname :distinct :from 'cd.members) 'surname) 10))
"(((SELECT DISTINCT surname FROM cd.members) ORDER BY surname) LIMIT 10)")))
(test select-distinct-on
"Testing select with distinct on. https://www.postgresql.org/docs/current/static/sql-select.html.
SELECT DISTINCT ON ( expression [, ...] ) keeps only the first row of each set of rows where the given expressions evaluate to equal. The DISTINCT ON expressions are interpreted using the same rules as for ORDER BY (see above). Note that the “first row” of each set is unpredictable unless ORDER BY is used to ensure that the desired row appears first. "
(is (equal (sql (:order-by (:select 'location 'time 'report
:distinct-on 'location
:from 'weather-reports)
'location (:desc 'time)))
"((SELECT DISTINCT ON (location) location, time, report FROM weather_reports) ORDER BY location, time DESC)")))
(test select-join-1
"Testing basic join. Note full use of as. https://www.postgresql.org/docs/current/static/sql-select.html
To join the table films with the table distributors:"
(is (equal (sql (:select 'f.title 'f.did 'd.name 'f.date-prod 'f.kind
:from (:as 'distributors 'd) (:as 'films 'f)
:where (:= 'f.did 'd.did)))
"(SELECT f.title, f.did, d.name, f.date_prod, f.kind FROM distributors AS d, films AS f WHERE (f.did = d.did))"))
;; Cross Join
(is (equal (sql (:select '* :from 't1 :cross-join 't2))
"(SELECT * FROM t1 CROSS JOIN t2)"))
(is (equal (sql (:select '* :from 't1 't2))
"(SELECT * FROM t1, t2)"))
;; Examples from https://www.postgresql.org/docs/current/static/queries-table-expressions.html#QUERIES-WINDOW
;; Inner Join
(is (equal (sql (:select '* :from 't1 :inner-join 't2 :on (:= 't1.num 't2.num)))
"(SELECT * FROM t1 INNER JOIN t2 ON (t1.num = t2.num))"))
;; From https://www.pgexercises.com/questions/joins/simplejoin.html
(is (equal (sql (:select 'bks.starttime
:from (:as 'cd.bookings 'bks)
:inner-join (:as 'cd.members 'mems)
:on (:= 'mems.memid 'bks.memid)
:where (:and (:= 'mems.firstname "David")
(:= 'mems.surname "Farrell"))))
"(SELECT bks.starttime FROM cd.bookings AS bks INNER JOIN cd.members AS mems ON (mems.memid = bks.memid) WHERE ((mems.firstname = E'David') and (mems.surname = E'Farrell')))"))
;; From https://www.pgexercises.com/questions/joins/self.html
(is (equal (sql (:order-by (:select (:as 'recs.firstname 'firstname)
(:as 'recs.surname 'surname)
:distinct
:from (:as 'cd.members 'mems)
:inner-join (:as 'cd.members 'recs)
:on (:= 'recs.memid 'mems.recommendedby))
'surname 'firstname))
"((SELECT DISTINCT recs.firstname AS firstname, recs.surname AS surname FROM cd.members AS mems INNER JOIN cd.members AS recs ON (recs.memid = mems.recommendedby)) ORDER BY surname, firstname)"))
;; inner join with min from
(is (equal (sql (:order-by
(:select 'mems.surname 'mems.firstname 'mems.memid (:as (:min 'bks.starttime) 'starttime)
:from (:as 'cd.bookings 'bks)
:inner-join (:as 'cd.members 'mems)
:on (:= 'mems.memid 'bks.memid)
:where (:>= 'starttime "2012-09-01")
:group-by 'mems.surname 'mems.firstname 'mems.memid)
'mems.memid))
"((SELECT mems.surname, mems.firstname, mems.memid, min(bks.starttime) AS starttime FROM cd.bookings AS bks INNER JOIN cd.members AS mems ON (mems.memid = bks.memid) WHERE (starttime >= E'2012-09-01') GROUP BY mems.surname, mems.firstname, mems.memid) ORDER BY mems.memid)"))
;; Inner Join with using
(is (equal (sql (:select '* :from 't1 :inner-join 't2 :using ('num)))
"(SELECT * FROM t1 INNER JOIN t2 USING (num))"))
;; inner join with case from https://www.pgexercises.com/questions/joins/threejoin2.html
(is (equal (sql (:order-by (:select (:as (:|| 'mems.firstname " " 'mems.surname) 'member) (:as 'facs.name 'facility)
(:as (:case ((:= 'mems.memid 0 ) (:* 'bks.slots 'facs.guestcost))
(:else (:* 'bks.slots 'facs.membercost)))
'cost)
:from (:as 'cd.members 'mems)
:inner-join (:as 'cd.bookings 'bks)
:on (:= 'mems.memid 'bks.memid)
:inner-join (:as 'cd.facilities 'facs)
:on (:= 'bks.facid 'facs.facid)
:where
(:and (:>= 'bks.starttime "2012-09-14")
(:<= 'bks.starttime "2012-09-15")
(:or (:and (:= 'mems.memid 0)
(:> (:* 'bks.slots 'facs.guestcost) 30))
(:and (:not (:= 'mems.memid 0))
(:> (:* 'bks.slots 'facs.membercost) 30)))))
(:desc 'cost)))
"((SELECT (mems.firstname || E' ' || mems.surname) AS member, facs.name AS facility, CASE WHEN (mems.memid = 0) THEN (bks.slots * facs.guestcost) ELSE (bks.slots * facs.membercost) END AS cost FROM cd.members AS mems INNER JOIN cd.bookings AS bks ON (mems.memid = bks.memid) INNER JOIN cd.facilities AS facs ON (bks.facid = facs.facid) WHERE ((bks.starttime >= E'2012-09-14') and (bks.starttime <= E'2012-09-15') and (((mems.memid = 0) and ((bks.slots * facs.guestcost) > 30)) or ((not (mems.memid = 0)) and ((bks.slots * facs.membercost) > 30))))) ORDER BY cost DESC)"))
;; Natural Inner Join
(is (equal (sql (:select '* :from 't1 :natural :inner-join 't2))
"(SELECT * FROM t1 NATURAL INNER JOIN t2)"))
;; Left Join (also known as left outer join
(is (equal (sql (:select '* :from 't1 :left-join 't2 :on (:= 't1.num 't2.num)))
"(SELECT * FROM t1 LEFT JOIN t2 ON (t1.num = t2.num))"))
;; from https://www.pgexercises.com/questions/joins/self2.html
(is (equal (sql (:order-by (:select (:as 'mems.firstname 'memfname)
(:as 'mems.surname 'memsname)
(:as 'recs.firstname 'recfname)
(:as 'recs.surname 'recsname)
:from (:as 'cd.members 'mems)
:left-join (:as 'cd.members 'recs)
:on (:= 'recs.memid 'mems.recommendedby))
'memsname 'memfname))
"((SELECT mems.firstname AS memfname, mems.surname AS memsname, recs.firstname AS recfname, recs.surname AS recsname FROM cd.members AS mems LEFT JOIN cd.members AS recs ON (recs.memid = mems.recommendedby)) ORDER BY memsname, memfname)"))
;; multiple inner join with column concatenate from https://www.pgexercises.com/questions/joins/threejoin.html
(is (equal (sql (:order-by (:select (:as (:|| 'mems.firstname " " 'mems.surname) 'member)
(:as 'facs.name 'facility)
:distinct
:from (:as 'cd.members 'mems)
:inner-join (:as 'cd.bookings 'bks) :on (:= 'mems.memid 'bks.memid)
:inner-join (:as 'cd.facilities 'facs) :on (:= 'bks.facid 'facs.facid)
:where (:in 'bks.facid (:set 0 1)))
'member))
"((SELECT DISTINCT (mems.firstname || E' ' || mems.surname) AS member, facs.name AS facility FROM cd.members AS mems INNER JOIN cd.bookings AS bks ON (mems.memid = bks.memid) INNER JOIN cd.facilities AS facs ON (bks.facid = facs.facid) WHERE (bks.facid IN (0, 1))) ORDER BY member)"))
;; Right Join
(is (equal (sql (:select '* :from 't1 :right-join 't2 :on (:= 't1.num 't2.num)))
"(SELECT * FROM t1 RIGHT JOIN t2 ON (t1.num = t2.num))"))
;; Full Outer Join
(is (equal (sql (:select '* :from 't1 :outer-join 't2 :on (:= 't1.num 't2.num)))
"(SELECT * FROM t1 FULL OUTER JOIN t2 ON (t1.num = t2.num))")))
(test subselects
"Testing subselects"
;; From https://www.pgexercises.com/questions/joins/sub.html
(is (equal (sql (:order-by (:select (:as (:|| 'mems.firstname " " 'mems.surname) 'member)
(:select (:as (:|| 'recs.firstname " " 'recs.surname) 'recommender)
:from (:as 'cd.members 'recs)
:where (:= 'recs.memid 'mems.recommendedby)) :distinct
:from (:as 'cd.members 'mems))
'member))
"((SELECT DISTINCT (mems.firstname || E' ' || mems.surname) AS member, (SELECT (recs.firstname || E' ' || recs.surname) AS recommender FROM cd.members AS recs WHERE (recs.memid = mems.recommendedby)) FROM cd.members AS mems) ORDER BY member)"))
(is (equal (sql (:order-by (:select 'member 'facility 'cost
:from
(:as (:select (:as (:|| 'mems.firstname " " 'mems.surname) 'member)
(:as 'facs.name 'facility)
(:as (:case ((:= 'mems.memid 0 ) (:* 'bks.slots 'facs.guestcost))
(:else (:* 'bks.slots 'facs.membercost)))
'cost)
:from (:as 'cd.members 'mems)
:inner-join (:as 'cd.bookings 'bks)
:on (:= 'mems.memid 'bks.memid)
:inner-join (:as 'cd.facilities 'facs)
:on (:= 'bks.facid 'facs.facid)
:where
(:and (:>= 'bks.starttime "2012-09-14")
(:<= 'bks.starttime "2012-09-15")))
'bookings)
:where (:> 'cost 30))
(:desc 'cost)))
"((SELECT member, facility, cost FROM (SELECT (mems.firstname || E' ' || mems.surname) AS member, facs.name AS facility, CASE WHEN (mems.memid = 0) THEN (bks.slots * facs.guestcost) ELSE (bks.slots * facs.membercost) END AS cost FROM cd.members AS mems INNER JOIN cd.bookings AS bks ON (mems.memid = bks.memid) INNER JOIN cd.facilities AS facs ON (bks.facid = facs.facid) WHERE ((bks.starttime >= E'2012-09-14') and (bks.starttime <= E'2012-09-15'))) AS bookings WHERE (cost > 30)) ORDER BY cost DESC)")))
(test select-case
"Testing case statements from https://www.pgexercises.com/questions/basic/classify.html"
(is (equal (sql (:select 'name (:as (:case ((:> 'monthlymaintenance 100) "expensive")
(:else "cheap")) 'cost)
:from 'cd.facilities))
"(SELECT name, CASE WHEN (monthlymaintenance > 100) THEN E'expensive' ELSE E'cheap' END AS cost FROM cd.facilities)")))
(test select-dates
"Testing date selection from https://www.pgexercises.com/questions/basic/date.html"
(is (equal (sql (:select 'memid 'surname 'firstname 'joindate :from 'cd.members :where (:>= 'joindate "2012-09-01")))
"(SELECT memid, surname, firstname, joindate FROM cd.members WHERE (joindate >= E'2012-09-01'))")))
(test select-group-by
"https://www.postgresql.org/docs/current/static/sql-select.html
To sum the column len of all films and group the results by kind:"
(is (equal (sql (:select 'kind (:as (:sum 'len) 'total) :from 'films :group-by 'kind))
"(SELECT kind, sum(len) AS total FROM films GROUP BY kind)")))
(test select-sum-group-interval
"CAUTION: DOES NOT WORK. VALIDATE THAT THIS ACTUALLY WORKS WITH THE ESCAPED INTERVAL. To sum the column len of all films, group the results by kind and show those group totals that are less than 5 hours:"
;;; SELECT kind, sum(len) AS total
;;; FROM films
;;; GROUP BY kind
;;; HAVING sum(len) < interval '5 hours';
(is (equal (sql (:select 'kind (:as (:sum 'len) 'total)
:from 'films
:group-by 'kind
:having (:< (:sum 'len) 'interval "5 hours")))
"(SELECT kind, sum(len) AS total FROM films GROUP BY kind HAVING (sum(len) < interval < E'5 hours'))")))
(test select-except
"Testing the use of except in two select statements. Except removes all matches. Except all is slightly different. If the first select statement has two rows that match a single row in the second select statement, only one is removed."
(is (equal (sql (:except (:select 'id 'name
:from 'countries
:where (:like 'name "%New%"))
(:select 'id 'name
:from 'countries
:where (:like 'name "%Zealand%"))))
"((SELECT id, name FROM countries WHERE (name like E'%New%')) except (SELECT id, name FROM countries WHERE (name like E'%Zealand%')))"))
(is (equal (sql (:except-all (:select '* :from 'clients) (:select '* :from 'vips)))
"((SELECT * FROM clients) except all (SELECT * FROM vips))")))
(test select-intersect
"Testing the intersect in two select clauses. Note that intersect removes duplicates and intersect-all does not remove duplicates."
(is (equal (sql (:intersect-all (:select '* :from 'clients) (:select '* :from 'vips)))
"((SELECT * FROM clients) intersect all (SELECT * FROM vips))"))
(is (equal (sql (:intersect (:select '* :from 'clients) (:select '* :from 'vips)))
"((SELECT * FROM clients) intersect (SELECT * FROM vips))")))
(test select-aggregation
"Testing aggregation functions."
(is (equal (sql (:select (:as (:max 'joindate) 'latest) :from 'cd.members))
"(SELECT max(joindate) AS latest FROM cd.members)"))
(is (equal (sql (:select 'firstname 'surname 'joindate
:from 'cd.members
:where (:= 'joindate (:select (:max 'joindate)
:from 'cd.members))))
"(SELECT firstname, surname, joindate FROM cd.members WHERE (joindate = (SELECT max(joindate) FROM cd.members)))")))
(test aggregation-1
"Testing Aggregation"
;; From https://www.pgexercises.com/questions/aggregates/count.html
(is (equal (sql (:select (:count '*) :from 'cd.facilities))
"(SELECT COUNT(*) FROM cd.facilities)"))
(is (equal (sql (:select 'facid (:select (:count '*) :from 'cd.facilities) :from 'cd.facilities))
"(SELECT facid, (SELECT COUNT(*) FROM cd.facilities) FROM cd.facilities)"))
;; From https://www.pgexercises.com/questions/aggregates/count2.html
(is (equal (sql (:select (:count '*) :from 'cd.facilities :where (:>= 'guestcost 10)))
"(SELECT COUNT(*) FROM cd.facilities WHERE (guestcost >= 10))"))
;; From https://www.pgexercises.com/questions/aggregates/count3.html
(is (equal (sql (:order-by (:select 'recommendedby (:count '*)
:from 'cd.members
:where (:not (:is-null 'recommendedby))
:group-by 'recommendedby)
'recommendedby))
"((SELECT recommendedby, COUNT(*) FROM cd.members WHERE (not (recommendedby IS NULL)) GROUP BY recommendedby) ORDER BY recommendedby)"))
;; From https://www.pgexercises.com/questions/aggregates/fachours.html
(is (equal (sql (:order-by (:select 'facid (:as (:sum 'slots) 'total-slots)
:from 'cd.bookings
:group-by 'facid)
'facid))
"((SELECT facid, sum(slots) AS total_slots FROM cd.bookings GROUP BY facid) ORDER BY facid)"))
;; From https://www.pgexercises.com/questions/aggregates/fachoursbymonth.html
(is (equal (sql (:order-by (:select 'facid (:as (:sum 'slots) 'total-slots)
:from 'cd.bookings
:where (:and (:>= 'starttime "2012-09-01")
(:< 'starttime "2012-10-01"))
:group-by 'facid)
(:sum 'slots)))
"((SELECT facid, sum(slots) AS total_slots FROM cd.bookings WHERE ((starttime >= E'2012-09-01') and (starttime < E'2012-10-01')) GROUP BY facid) ORDER BY sum(slots))"))
;; From https://www.pgexercises.com/questions/aggregates/fachoursbymonth2.html
(is (equal (sql (:order-by (:select 'facid (:as (:extract 'month 'starttime) 'month)
(:as (:sum 'slots) 'total-slots)
:from 'cd.bookings
:where (:and (:>= 'starttime "2012-01-01")
(:< 'starttime "2013-01-01"))
:group-by 'facid 'month)
'facid 'month))
"((SELECT facid, EXTRACT(month FROM starttime) AS month, sum(slots) AS total_slots FROM cd.bookings WHERE ((starttime >= E'2012-01-01') and (starttime < E'2013-01-01')) GROUP BY facid, month) ORDER BY facid, month)"))
;; From https://www.pgexercises.com/questions/aggregates/members1.html
(is (equal (sql (:select (:count 'memid :distinct) :from 'cd.bookings))
"(SELECT COUNT(DISTINCT memid) FROM cd.bookings)"))
;; From https://www.pgexercises.com/questions/aggregates/fachours1a.html
(is (equal (sql (:order-by (:select 'facid (:as (:sum 'slots) 'total-slots)
:from 'cd.bookings
:group-by 'facid
:having (:> (:sum 'slots)
1000))
'facid))
"((SELECT facid, sum(slots) AS total_slots FROM cd.bookings GROUP BY facid HAVING (sum(slots) > 1000)) ORDER BY facid)"))
;; From https://www.pgexercises.com/questions/aggregates/facrev.html
(is (equal (sql (:order-by (:select 'facs.name (:as (:sum (:* 'slots
(:case ((:= 'memid 0) 'facs.guestcost)
(:else 'facs.membercost))))
'revenue)
:from (:as 'cd.bookings 'bks)
:inner-join (:as 'cd.facilities 'facs)
:on (:= 'bks.facid 'facs.facid)
:group-by 'facs.name)
'revenue))
"((SELECT facs.name, sum((slots * CASE WHEN (memid = 0) THEN facs.guestcost ELSE facs.membercost END)) AS revenue FROM cd.bookings AS bks INNER JOIN cd.facilities AS facs ON (bks.facid = facs.facid) GROUP BY facs.name) ORDER BY revenue)"))
;; From https://www.pgexercises.com/questions/aggregates/facrev2.html
;; V1
(is (equal (sql (:order-by
(:select 'name 'revenue
:from (:as (:select 'facs.name
(:as (:sum (:case ((:= 'memid 0) (:* 'slots 'facs.guestcost))
(:else (:* 'slots 'membercost))))
'revenue)
:from (:as 'cd.bookings 'bks)
:inner-join (:as 'cd.facilities 'facs)
:on (:= 'bks.facid 'facs.facid)
:group-by 'facs.name)
'agg)
:where (:< 'revenue 1000))
'revenue))
"((SELECT name, revenue FROM (SELECT facs.name, sum(CASE WHEN (memid = 0) THEN (slots * facs.guestcost) ELSE (slots * membercost) END) AS revenue FROM cd.bookings AS bks INNER JOIN cd.facilities AS facs ON (bks.facid = facs.facid) GROUP BY facs.name) AS agg WHERE (revenue < 1000)) ORDER BY revenue)"))
;; V2
(is (equal (sql (:order-by
(:select 'facs.name (:as (:sum (:case ((:= 'memid 0) (:* 'slots 'facs.guestcost))
(:else (:* 'slots 'membercost))))
'revenue)
:from (:as 'cd.bookings 'bks)
:inner-join (:as 'cd.facilities 'facs)
:on (:= 'bks.facid 'facs.facid)
:group-by 'facs.name
:having (:< (:sum (:case ((:= 'memid 0) (:* 'slots 'facs.guestcost))
(:else (:* 'slots 'membercost))))
1000))
'revenue))
"((SELECT facs.name, sum(CASE WHEN (memid = 0) THEN (slots * facs.guestcost) ELSE (slots * membercost) END) AS revenue FROM cd.bookings AS bks INNER JOIN cd.facilities AS facs ON (bks.facid = facs.facid) GROUP BY facs.name HAVING (sum(CASE WHEN (memid = 0) THEN (slots * facs.guestcost) ELSE (slots * membercost) END) < 1000)) ORDER BY revenue)"))
;; From https://www.pgexercises.com/questions/aggregates/fachours2.html
;; V1
(is (equal (sql (:limit (:order-by (:select 'facid (:as (:sum 'slots) 'total-slots)
:from 'cd.bookings
:group-by 'facid)
(:desc (:sum 'slots)))
1))
"(((SELECT facid, sum(slots) AS total_slots FROM cd.bookings GROUP BY facid) ORDER BY sum(slots) DESC) LIMIT 1)"))
;; V2
(is (equal (sql (:select 'facid (:as (:sum 'slots) 'totalslots)
:from 'cd.bookings
:group-by 'facid
:having (:= (:sum 'slots) (:select (:max 'sum2.totalslots)
:from (:as (:select (:as (:sum 'slots ) 'totalslots)
:from 'cd.bookings
:group-by 'facid)
'sum2)))))
"(SELECT facid, sum(slots) AS totalslots FROM cd.bookings GROUP BY facid HAVING (sum(slots) = (SELECT max(sum2.totalslots) FROM (SELECT sum(slots) AS totalslots FROM cd.bookings GROUP BY facid) AS sum2)))"))
;; From https://www.pgexercises.com/questions/aggregates/fachoursbymonth3.html
;; V1
(is (equal (sql (:order-by (:select 'facid (:as (:extract 'month 'starttime) 'month) (:as (:sum 'slots) 'slots)
:from 'cd.bookings
:where (:and (:>= 'starttime "2012-01-01")
(:< 'starttime "2013-01-01"))
:group-by (:rollup 'facid 'month))
'facid 'month))
"((SELECT facid, EXTRACT(month FROM starttime) AS month, sum(slots) AS slots FROM cd.bookings WHERE ((starttime >= E'2012-01-01') and (starttime < E'2013-01-01')) GROUP BY rollup(facid, month)) ORDER BY facid, month)"))
;; V2
(is (equal (sql (:order-by (:union-all (:select 'facid (:as (:extract 'month 'starttime) 'month) (:as (:sum 'slots) 'slots)
:from 'cd.bookings
:where (:and (:>= 'starttime "2012-01-01")
(:< 'starttime "2013-01-01"))
:group-by 'facid 'month)
(:select 'facid :null (:as (:sum 'slots) 'slots)
:from 'cd.bookings
:where (:and (:>= 'starttime "2012-01-01")
(:< 'starttime "2013-01-01"))
:group-by 'facid)
(:select :null :null (:as (:sum 'slots) 'slots)
:from 'cd.bookings
:where (:and (:>= 'starttime "2012-01-01")
(:< 'starttime "2013-01-01")))) 'facid 'month))
"(((SELECT facid, EXTRACT(month FROM starttime) AS month, sum(slots) AS slots FROM cd.bookings WHERE ((starttime >= E'2012-01-01') and (starttime < E'2013-01-01')) GROUP BY facid, month) union all (SELECT facid, NULL, sum(slots) AS slots FROM cd.bookings WHERE ((starttime >= E'2012-01-01') and (starttime < E'2013-01-01')) GROUP BY facid) union all (SELECT NULL, NULL, sum(slots) AS slots FROM cd.bookings WHERE ((starttime >= E'2012-01-01') and (starttime < E'2013-01-01')))) ORDER BY facid, month)"))
)
(test select-union
"testing basic union."
;;; https://www.postgresql.org/docs/current/static/typeconv-union-case.html
(is (equal (sql (:union (:select (:as "a" 'text) ) (:select "b")))
"((SELECT E'a' AS text) union (SELECT E'b'))"))
(is (equal (sql (:union (:select (:as 1.2 "numeric")) (:select 1)))
"((SELECT 1.2 AS E'numeric') union (SELECT 1))"))
;;; This shows how to obtain the union of the tables distributors and actors, restricting the results to those that begin with the letter W in each table. Only distinct rows are wanted, so the key word ALL is omitted. https://www.postgresql.org/docs/current/static/sql-select.html
(is (equal (sql (:union (:select 'distributors.name
:from 'distributors
:where (:like 'distributors.name "W%"))
(:select 'actors.name
:from 'actors
:where (:like 'actors.name "W%"))))
"((SELECT distributors.name FROM distributors WHERE (distributors.name like E'W%')) union (SELECT actors.name FROM actors WHERE (actors.name like E'W%')))"))
;;; Union-all with a simple with clause https://www.postgresql.org/docs/current/static/sql-select.html
(is (equal (sql (:union-all
(:with (:as 't1 (:select (:as (:random) 'x)
:from (:generate-series 1 3)))
(:select '* :from 't1)) (:select '* :from 't1)))
"(WITH t1 AS (SELECT random() AS x FROM generate_series(1, 3))(SELECT * FROM t1) union all (SELECT * FROM t1))")))
(test select-with
"Testing select having a CTE with function From https://www.pgexercises.com/questions/aggregates/fachours2.html"
(is (equal (sql (:with (:as 'sum (:select 'facid (:as (:sum 'slots) 'totalslots)
:from 'cd.bookings
:group-by 'facid))
(:select 'facid 'totalslots
:from 'sum
:where (:= 'totalslots (:select (:max 'totalslots) :from 'sum)))))
"WITH sum AS (SELECT facid, sum(slots) AS totalslots FROM cd.bookings GROUP BY facid)(SELECT facid, totalslots FROM sum WHERE (totalslots = (SELECT max(totalslots) FROM sum)))"))
;; From https://www.pgexercises.com/questions/aggregates/fachoursbymonth3.html
(is (equal (sql (:order-by (:with (:as 'bookings (:select 'facid (:as (:extract 'month 'starttime) 'month) 'slots
:from 'cd.bookings
:where (:and (:>= 'starttime "2012-01-01")
(:< 'starttime "2013-01-01"))))
(:union-all (:select 'facid 'month (:sum 'slots) :from 'bookings :group-by 'facid 'month)
(:select 'facid :null (:sum 'slots) :from 'bookings :group-by 'facid)
(:select :null :null (:sum 'slots) :from 'bookings))) 'facid 'month))
"(WITH bookings AS (SELECT facid, EXTRACT(month FROM starttime) AS month, slots FROM cd.bookings WHERE ((starttime >= E'2012-01-01') and (starttime < E'2013-01-01')))((SELECT facid, month, sum(slots) FROM bookings GROUP BY facid, month) union all (SELECT facid, NULL, sum(slots) FROM bookings GROUP BY facid) union all (SELECT NULL, NULL, sum(slots) FROM bookings)) ORDER BY facid, month)"))
(is (equal (sql (:order-by (:select 'facs.facid 'facs.name
(:as (:trim (:to-char (:/ (:sum 'bks.slots) 2.0) "9999999999999999D99")) 'total-hours)
:from (:as 'cd.bookings 'bks)
:inner-join (:as 'cd.facilities 'facs)
:on (:= 'facs.facid 'bks.facid)
:group-by 'facs.facid 'facs.name)
'facs.facid))
"((SELECT facs.facid, facs.name, trim(to_char((sum(bks.slots) / 2.0), E'9999999999999999D99')) AS total_hours FROM cd.bookings AS bks INNER JOIN cd.facilities AS facs ON (facs.facid = bks.facid) GROUP BY facs.facid, facs.name) ORDER BY facs.facid)"))
)
(test select-over
"Testing with over and partition by. From https://www.pgexercises.com/questions/aggregates/countmembers.html"
(is (equal (sql (:order-by
(:select (:over (:count '*)) 'firstname 'surname
:from 'cd.members)
'joindate))
"((SELECT (COUNT(*) OVER ()) , firstname, surname FROM cd.members) ORDER BY joindate)"))
(is (equal (sql (:order-by (:select (:over (:count '*)
(:partition-by (:date-trunc "month" 'joindate)))
'firstname 'surname
:from 'cd.members )
'joindate))
"((SELECT (COUNT(*) OVER (PARTITION BY date_trunc(E'month', joindate))), firstname, surname FROM cd.members) ORDER BY joindate)"))
;; From https://www.pgexercises.com/questions/aggregates/nummembers.html
(is (equal (sql (:order-by
(:select (:over (:row-number) (:order-by 'joindate)) 'firstname 'surname
:from 'cd.members)
'joindate))
"((SELECT (row_number() OVER ( ORDER BY joindate)), firstname, surname FROM cd.members) ORDER BY joindate)"))
;;From https://www.pgexercises.com/questions/aggregates/fachours4.html
(is (equal (sql (:select 'facid 'total
:from (:as (:select 'facid (:as (:sum 'slots) 'total)
(:as (:over (:rank) (:order-by (:desc (:sum 'slots))))
'rank)
:from 'cd.bookings
:group-by 'facid)
'ranked)
:where (:= 'rank 1)))
"(SELECT facid, total FROM (SELECT facid, sum(slots) AS total, (rank() OVER ( ORDER BY sum(slots) DESC)) AS rank FROM cd.bookings GROUP BY facid) AS ranked WHERE (rank = 1))"))
(is (equal (sql (:select 'facid 'total
:from (:as (:select 'facid 'total (:as (:over (:rank) (:order-by (:desc 'total)))
'rank)
:from (:as (:select 'facid (:as (:sum 'slots) 'total)
:from 'cd.bookings
:group-by 'facid) 'sumslots))
'ranked)
:where (:= 'rank 1)))
"(SELECT facid, total FROM (SELECT facid, total, (rank() OVER ( ORDER BY total DESC)) AS rank FROM (SELECT facid, sum(slots) AS total FROM cd.bookings GROUP BY facid) AS sumslots) AS ranked WHERE (rank = 1))"))
;; from https://www.pgexercises.com/questions/aggregates/classify.html
(is (equal (sql (:order-by (:select 'name (:as (:case ((:= 'class 1) "high")
((:= 'class 2) "average")
(:else "low"))
'revenue)
:from (:as
(:select (:as 'facs.name 'name)
(:as (:over (:ntile 3)
(:order-by
(:desc
(:sum (:case
((:= 'memid 0) (:* 'slots 'facs.guestcost))
(:else (:* 'slots 'membercost)))))))
'class)
:from (:as 'cd.bookings 'bks)
:inner-join (:as 'cd.facilities 'facs)
:on (:= 'bks.facid 'facs.facid)
:group-by 'facs.name)
'subq))
'class 'name))
"((SELECT name, CASE WHEN (class = 1) THEN E'high' WHEN (class = 2) THEN E'average' ELSE E'low' END AS revenue FROM (SELECT facs.name AS name, (ntile(3) OVER ( ORDER BY sum(CASE WHEN (memid = 0) THEN (slots * facs.guestcost) ELSE (slots * membercost) END) DESC)) AS class FROM cd.bookings AS bks INNER JOIN cd.facilities AS facs ON (bks.facid = facs.facid) GROUP BY facs.name) AS subq) ORDER BY class, name)")))
(test select-with-recursive
"Testing with recursive. When working with recursive queries it is important to be sure that the recursive part of the query will eventually return no tuples, or else the query will loop indefinitely. Sometimes, using UNION instead of UNION ALL can accomplish this by discarding rows that duplicate previous output rows. However, often a cycle does not involve output rows that are completely duplicate: it may be necessary to check just one or a few fields to see if the same point has been reached before. The standard method for handling such situations is to compute an array of the already-visited values."
(is (equal (sql
(:with-recursive
(:as (:t1 'n)
(:union-all (:values 1)
(:select (:n 1)
:from 't1
:where (:< 'n 100))))
(:select (:sum 'n) :from 't1)))
"WITH RECURSIVE t1(n) AS (values(1) union all (SELECT n(1) FROM t1 WHERE (n < 100)))(SELECT sum(n) FROM t1)"))
;; the following query that searches a table graph using a link field:
(is (equal (sql
(:with-recursive
(:as (:search-graph 'id 'link 'data 'depth)
(:union-all (:select 'g.id 'g.link 'g.data 1
:from (:as 'graph 'g))
(:select 'g.id 'g.link 'g.data (:= 'sg.depth 1)
:from (:as 'graph 'g) (:as 'search-graph 'sg)
:where (:= 'g.id 'sg.link))))
(:select '* :from 'search-graph)))
"WITH RECURSIVE search_graph(id, link, data, depth) AS ((SELECT g.id, g.link, g.data, 1 FROM graph AS g) union all (SELECT g.id, g.link, g.data, (sg.depth = 1) FROM graph AS g, search_graph AS sg WHERE (g.id = sg.link)))(SELECT * FROM search_graph)"))
;; Recursive queries are typically used to deal with hierarchical or tree-structured data. A useful example is this query to find all the direct and indirect sub-parts of a product, given only a table that shows immediate inclusions:
(is (equal (sql
(:with-recursive
(:as (:included-parts 'sub-part 'part 'quantity)
(:union-all
(:select 'sub-part 'part 'quantity
:from 'parts
:where (:= 'part "our-product"))
(:select 'p.sub-part 'p.part 'p.quantity
:from (:as 'included-parts 'pr)
(:as 'parts 'p)
:where (:= 'p.part 'pr.sub-part))))
(:select 'sub-part (:as (:sum 'quantity) 'total-quantity)
:from 'included-parts
:group-by 'sub-part)))
"WITH RECURSIVE included_parts(sub_part, part, quantity) AS ((SELECT sub_part, part, quantity FROM parts WHERE (part = E'our-product')) union all (SELECT p.sub_part, p.part, p.quantity FROM included_parts AS pr, parts AS p WHERE (p.part = pr.sub_part)))(SELECT sub_part, sum(quantity) AS total_quantity FROM included_parts GROUP BY sub_part)"))
;; This query will loop if the link relationships contain cycles. Because we require a “depth” output, just changing UNION ALL to UNION would not eliminate the looping. Instead we need to recognize whether we have reached the same row again while following a particular path of links. We add two columns path and cycle to the loop-prone query:
;; In the general case where more than one field needs to be checked to recognize a cycle, use an array of rows. For example, if we needed to compare fields f1 and f2:
(is (equal
(sql
(:with-recursive
(:as (:search-graph 'id 'link 'data'depth 'path 'cycle)
(:union-all
(:select 'g.id 'g.link 'g.data 1
(:[] 'g.f1 'g.f2) nil
:from (:as 'graph 'g))
(:select 'g.id 'g.link 'g.data (:= 'sg.depth 1)
(:|| 'path (:row 'g.f1 'g.f2))
(:= (:row 'g.f1 'g.f2)
(:any* 'path))
:from (:as 'graph 'g)
(:as 'search-graph 'sg)
:where (:and (:= 'g.id 'sg.link)
(:not 'cycle)))))
(:select '* :from 'search-graph)))
"WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS ((SELECT g.id, g.link, g.data, 1, (g.f1)[g.f2], false FROM graph AS g) union all (SELECT g.id, g.link, g.data, (sg.depth = 1), (path || row(g.f1, g.f2)), (row(g.f1, g.f2) = ANY(path)) FROM graph AS g, search_graph AS sg WHERE ((g.id = sg.link) and (not cycle))))(SELECT * FROM search_graph)"))
;; Aside from preventing cycles, the array value is often useful in its own right as representing the “path” taken to reach any particular row.
(is (equal (sql
(:with (:as 'regional-sales (:select 'region (:as (:sum 'amount) 'total-sales)
:from 'orders :group-by 'region))
(:as 'top-regions (:select 'region
:from 'regional-sales
:where (:> 'total-sales (:select (:/ (:sum 'total-sales) 10)
:from 'regional-sales))))
(:select 'region 'product (:as (:sum 'quantity) 'product-units)
(:as (:sum 'amount) 'product-sales)
:from 'orders
:where (:in 'region (:select 'region :from 'top-regions))
:group-by 'region 'product)))
"WITH regional_sales AS (SELECT region, sum(amount) AS total_sales FROM orders GROUP BY region), top_regions AS (SELECT region FROM regional_sales WHERE (total_sales > (SELECT (sum(total_sales) / 10) FROM regional_sales)))(SELECT region, product, sum(quantity) AS product_units, sum(amount) AS product_sales FROM orders WHERE (region IN (SELECT region FROM top_regions)) GROUP BY region, product)"))
(is (equal (sql
(:with-recursive
(:as 'children
(:union
(:select 'depended-on :from 'dependencies :where (:= 'depends-on '$1))
(:select 'a.depended-on :from (:as 'dependencies 'a)
:inner-join (:as 'children 'b)
:on (:= 'a.depends-on 'b.depended-on))))
(:select '* :from 'children)))
"WITH RECURSIVE children AS ((SELECT depended_on FROM dependencies WHERE (depends_on = $1)) union (SELECT a.depended_on FROM dependencies AS a INNER JOIN children AS b ON (a.depends_on = b.depended_on)))(SELECT * FROM children)"))
;; https://www.postgresql.org/docs/current/static/sql-select.html
;; This example uses WITH RECURSIVE to find all subordinates (direct or indirect) of the employee Mary, and their level of indirectness, from a table that shows only direct subordinates:
(is (equal (sql (:with-recursive
(:as (:employee-recursive 'distance 'employee-name 'manager-name)
(:union-all
(:select 1 'employee-name 'manager-name :from 'employee :where (:= 'manager-name "Mary"))
(:select (:+ 'er.distance 1) 'e.employee-name 'e.manager-name
:from (:as 'employee-recursive 'er) (:as 'employee 'e)
:where (:= 'er.employee-name 'e.manager-name))))
(:select 'distance 'employee-name :from 'employee-recursive)))
"WITH RECURSIVE employee_recursive(distance, employee_name, manager_name) AS ((SELECT 1, employee_name, manager_name FROM employee WHERE (manager_name = E'Mary')) union all (SELECT (er.distance + 1), e.employee_name, e.manager_name FROM employee_recursive AS er, employee AS e WHERE (er.employee_name = e.manager_name)))(SELECT distance, employee_name FROM employee_recursive)")))
(test insert-into
"Testing insert into"
(is (equal (sql (:insert-into 'cd.facilities :set 'facid 9 'name "Spa" 'membercost 20 'guestcost 30
'initialoutlay 100000 'monthlymaintenance 800))
"INSERT INTO cd.facilities (facid, name, membercost, guestcost, initialoutlay, monthlymaintenance) VALUES (9, E'Spa', 20, 30, 100000, 800)"))
(is (equal (sql (:insert-into 'test :set 'id 15 'number-string "12" 'numeric-item 12.45
'ratio-item (/ 1 13) 'created-at "2018-02-01"))
"INSERT INTO test (id, number_string, numeric_item, ratio_item, created_at) VALUES (15, E'12', 12.45, 0.0769230769230769230769230769230769230, E'2018-02-01')"))
;; From https://www.pgexercises.com/questions/updates/insert2.html
(is (equal (sql (:insert-rows-into 'cd.facilities
:columns 'facid 'name 'membercost 'guestcost 'initialoutlay 'monthlymaintenance
:values '((9 "Spa" 20 30 100000 800) (10 "Squash Court 2" 3.5 17.5 5000 80))))
"INSERT INTO cd.facilities (facid, name, membercost, guestcost, initialoutlay, monthlymaintenance) VALUES (9, E'Spa', 20, 30, 100000, 800), (10, E'Squash Court 2', 3.5, 17.5, 5000, 80)"))
;; From https://www.pgexercises.com/questions/updates/insert3.html
(is (equal (sql (:insert-into 'cd.facilities
:set 'facid (:select (:+ (:select (:max 'facid) :from 'cd.facilities) 1))
'name "Spa" 'membercost 20 'guestcost 30 'initialoutlay 100000 'monthlymaintenance 800))
"INSERT INTO cd.facilities (facid, name, membercost, guestcost, initialoutlay, monthlymaintenance) VALUES ((SELECT ((SELECT max(facid) FROM cd.facilities) + 1)), E'Spa', 20, 30, 100000, 800)"))
;; Now using rows https://www.pgexercises.com/questions/updates/insert3.html
(is (equal (sql (:insert-rows-into 'cd.facilities
:columns 'facid 'name 'membercost 'guestcost 'initialoutlay 'monthlymaintenance
:values '(((:select (:+ (:select (:max 'facid) :from 'cd.facilities) 1)) "Spa" 20 30 100000 800 ))))
"INSERT INTO cd.facilities (facid, name, membercost, guestcost, initialoutlay, monthlymaintenance) VALUES ((SELECT ((SELECT max(facid) FROM cd.facilities) + 1)), E'Spa', 20, 30, 100000, 800)")))
(test update
"Testing updates"
;; From https://www.pgexercises.com/questions/updates/update.html
(is (equal (sql (:update 'cd.facilities :set 'initialoutlay 10000 :where (:= 'facid 1)))
"UPDATE cd.facilities SET initialoutlay = 10000 WHERE (facid = 1)"))
;; From https://www.pgexercises.com/questions/updates/updatemultiple.html
(is (equal (sql (:update 'cd.facilities :set 'membercost 6 'guestcost 30 :where (:in 'facid (:set 0 1))))
"UPDATE cd.facilities SET membercost = 6, guestcost = 30 WHERE (facid IN (0, 1))"))
;; From https://www.pgexercises.com/questions/updates/updatecalculated.html
(is (equal (sql (:update (:as 'cd.facilities 'facs)
:set 'membercost (:select (:* 'membercost 1.1)
:from 'cd.facilities
:where (:= 'facid 0))
'guestcost (:select (:* 'guestcost 1.1)
:from 'cd.facilities
:where (:= 'facid 0))
:where (:= 'facs.facid 1)))
"UPDATE cd.facilities AS facs SET membercost = (SELECT (membercost * 1.1) FROM cd.facilities WHERE (facid = 0)), guestcost = (SELECT (guestcost * 1.1) FROM cd.facilities WHERE (facid = 0)) WHERE (facs.facid = 1)"))
;; Version 2
(is (equal (sql (:update (:as 'cd.facilities 'facs)
:set 'membercost (:* 'facs2.membercost 1.1)
'guestcost (:* 'facs2.guestcost 1.1)
:from (:as (:select '*
:from 'cd.facilities
:where (:= 'facid 0))
'facs2)
:where (:= 'facs.facid 1)))
"UPDATE cd.facilities AS facs SET membercost = (facs2.membercost * 1.1), guestcost = (facs2.guestcost * 1.1) FROM (SELECT * FROM cd.facilities WHERE (facid = 0)) AS facs2 WHERE (facs.facid = 1)")))
(test delete
"Testing deletes"
(is (equal (sql (:delete-from 'cd.bookings :where (:= 'id 5)))
"DELETE FROM cd.bookings WHERE (id = 5)"))
;; From https://www.pgexercises.com/questions/updates/deletewh2.html
(is (equal (sql (:delete-from 'cd.members
:where (:not (:in 'memid (:select 'memid :from 'cd.bookings)))))
"DELETE FROM cd.members WHERE (not (memid IN (SELECT memid FROM cd.bookings)))"))
(is (equal (sql (:delete-from (:as 'cd.members 'mems)
:where (:not (:exists (:select 1
:from 'cd.bookings
:where (:= 'memid 'mems.memid))))))
"DELETE FROM cd.members AS mems WHERE (not (EXISTS (SELECT 1 FROM cd.bookings WHERE (memid = mems.memid))))")))
(test arrays
"Testing arrays"
(is (equal (sql (:create-table array-provinces ((name :type text) (provinces :type text[]))))
"CREATE TABLE array_provinces (name TEXT NOT NULL, provinces TEXT[] NOT NULL)"))
(is (equal (sql (:insert-rows-into 'array-provinces
:columns 'name 'provinces
:values '(("Germany" #("Baden-Wurttemberg" "Bavaria" "Berlin" "Brandenburg"))
("Switzerland" #("Aargau" "Appenzell Ausserrhoden" "Basel-Landschaft" "Fribourg")))))
"INSERT INTO array_provinces (name, provinces) VALUES (E'Germany', ARRAY[E'Baden-Wurttemberg', E'Bavaria', E'Berlin', E'Brandenburg']), (E'Switzerland', ARRAY[E'Aargau', E'Appenzell Ausserrhoden', E'Basel-Landschaft', E'Fribourg'])"))
(is (equal (sql (:insert-into 'array-provinces :set 'name "Canada" 'provinces #("Alberta" "British Columbia" "Manitoba" "Ontario")))
"INSERT INTO array_provinces (name, provinces) VALUES (E'Canada', ARRAY[E'Alberta', E'British Columbia', E'Manitoba', E'Ontario'])"))
(is (equal (sql (:select (:[] 'provinces 2) :from 'array-provinces))
"(SELECT (provinces)[2] FROM array_provinces)"))
(is (equal (sql (:select (:[] 'provinces 1) :from 'array-provinces :where (:= 'name "Germany")))
"(SELECT (provinces)[1] FROM array_provinces WHERE (name = E'Germany'))"))
(is (equal (sql (:select (:[] 'provinces 2) :from 'array-provinces :where (:= (:[] 'provinces 2) "Bavaria")))
"(SELECT (provinces)[2] FROM array_provinces WHERE ((provinces)[2] = E'Bavaria'))"))
(is (equal (sql (:select '* :from 'array-provinces :where (:= (:[] 'provinces 2) "Bavaria")))
"(SELECT * FROM array_provinces WHERE ((provinces)[2] = E'Bavaria'))"))
)
(test multi-dimension-arrays
"Testing multi-dimensional arrays"
(is (equal (sql (:create-table sal-emp ((name :type text) (pay-by-quarter :type integer[]) (schedule :type text[][]))))
"CREATE TABLE sal_emp (name TEXT NOT NULL, pay_by_quarter INTEGER[] NOT NULL, schedule TEXT[][] NOT NULL)"))
(is (equal (sql (:insert-into 'sal-emp :set 'name "Bill" 'pay-by-quarter #(10000 10000 10000 10000) 'schedule #(#( "meeting" "lunch") #("training" "presentation"))))
"INSERT INTO sal_emp (name, pay_by_quarter, schedule) VALUES (E'Bill', ARRAY[10000, 10000, 10000, 10000], ARRAY[ARRAY[E'meeting', E'lunch'], ARRAY[E'training', E'presentation']])"))
(is (equal (sql (:select 'name :from 'sal-emp :where (:<> (:[] 'pay-by-quarter 1) (:[] 'pay-by-quarter 2))))
"(SELECT name FROM sal_emp WHERE ((pay_by_quarter)[1] <> (pay_by_quarter)[2]))"))
(is (equal (sql (:update 'sal-emp :set 'schedule #(#( "breakfast" "consulting") #("meeting" "lunch"))
:where (:= 'name "Carol")))
"UPDATE sal_emp SET schedule = ARRAY[ARRAY[E'breakfast', E'consulting'], ARRAY[E'meeting', E'lunch']] WHERE (name = E'Carol')")))
#|
https://www.postgresql.org/docs/current/static/sql-select.html
This example shows how to use a function in the FROM clause, both with and without a column definition list:
CREATE FUNCTION distributors(int) RETURNS SETOF distributors AS $$
SELECT * FROM distributors WHERE did = $1;
$$ LANGUAGE SQL;
SELECT * FROM distributors(111);
CREATE FUNCTION distributors_2(int) RETURNS SETOF record AS $$
SELECT * FROM distributors WHERE did = $1;
$$ LANGUAGE SQL;
SELECT * FROM distributors_2(111) AS (f1 int, f2 text);
Here is an example of a function with an ordinality column added:
SELECT * FROM unnest(ARRAY['a','b','c','d','e','f']) WITH ORDINALITY;
https://www.postgresql.org/docs/current/static/sql-select.html
This example uses LATERAL to apply a set-returning function get_product_names() for each row of the manufacturers table:
SELECT m.name AS mname, pname
FROM manufacturers m, LATERAL get_product_names(m.id) pname;
Manufacturers not currently having any products would not appear in the result, since it is an inner join. If we wished to include the names of such manufacturers in the result, we could do:
SELECT m.name AS mname, pname
FROM manufacturers m LEFT JOIN LATERAL get_product_names(m.id) pname ON true;
|#
(test dissect-type-0
"Testing dissect-type"
(multiple-value-bind (type null?)
(s-sql::dissect-type 'char)
(is (eq type 'char))
(is (not null?))))
(test dissect-type-1
"Testing dissect-type"
(multiple-value-bind (type null?)
(s-sql::dissect-type '(or char db-null))
(is (eq type 'char))
(is (equal null? t))))
(test dissect-type-2
"Testing dissect-type"
(multiple-value-bind (type null?)
(s-sql::dissect-type '(or "char(5)" db-null))
(is (equal type "char(5)"))
(is (eq null? t))))
(test expand-table-column
"Testing expand-table-column"
(is (equal (s-sql::expand-table-column 'code '(:type varchar :primary-key 't))
'("code" " " "VARCHAR" " NOT NULL" " PRIMARY KEY ")))
(is (equal (s-sql::expand-table-column 'code '(:type (or char db-null) :primary-key 't))
'("code" " " "CHAR" " PRIMARY KEY ")))
(is (equal (s-sql::expand-table-column 'code '(:type (or (string 5) db-null) :primary-key 't))
'("code" " " "CHAR(5)" " PRIMARY KEY "))))
;;; CREATE TABLE TESTS
(test create-table-1
"Testing Create Table. First example from https://www.postgresql.org/docs/10/static/sql-createtable.html
Right now we do not have the intervals fixed."
(is (equal (s-sql:sql (:create-table films
((code :type (or (string 5) db-null) :constraint 'firstkey :primary-key 't)
(title :type (varchar 40))
(did :type integer)
(date-prod :type (or date db-null))
(kind :type (or (varchar 10) db-null))
(len :type (or interval db-null)))))
"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), len INTERVAL)"))) ; Create table films and table distributors:
(test create-table-2
"Second example from https://www.postgresql.org/docs/10/static/sql-createtable.html"
(is (equal (s-sql:sql (:create-table distributors ((did :type (or integer db-null)
:primary-key "generated by default as identity")
(name :type (varchar 40) :check (:<> 'name "")))))
"CREATE TABLE distributors (did INTEGER PRIMARY KEY generated by default as identity, name VARCHAR(40) NOT NULL CHECK (name <> E''))")))
(test create-table-with-a-2-dimensional-array
"Create a table with a 2-dimensional array"
(is (equal (sql (:create-table array_int ((vector :type (or int[][] db-null)))))
"CREATE TABLE array_int (vector INT[][])")))
(test create-table-full-1
"Test :create-table with extended table constraints."
(is (equal (s-sql:sql (:create-table-full faa.d_airports
((AirportID :type integer)
(Name :type text)
(City :type text)
(Country :type text)
(airport_code :type text)
(ICOA_code :type text)
(Latitude :type float8)
(Longitude :type float8)
(Altitude :type float8)
(TimeZoneOffset :type float)
(DST_Flag :type text)
(TZ :type text))
()
((:distributed-by (airport_code)))))
"CREATE TABLE faa.d_airports (airportid INTEGER NOT NULL, name TEXT NOT NULL, city TEXT NOT NULL, country TEXT NOT NULL, airport_code TEXT NOT NULL, icoa_code TEXT NOT NULL, latitude FLOAT8 NOT NULL, longitude FLOAT8 NOT NULL, altitude FLOAT8 NOT NULL, timezoneoffset REAL NOT NULL, dst_flag TEXT NOT NULL, tz TEXT NOT NULL) DISTRIBUTED BY (airport_code) ")))
(test sequence-tests
"sequence testing"
(with-test-connection
(when (pomo:sequence-exists-p :knobo-seq)
(pomo:query (:drop-sequence :knobo-seq)))
;; Setup new sequence
(is (eq
(pomo:query (:create-sequence :knobo-seq) :single)
nil))
;; Test that we can set increment
(is (equal (sql (:alter-sequence :knobo-seq :increment 1))
"ALTER SEQUENCE knobo_seq INCREMENT BY 1"))
(is (eq (pomo:query (:alter-sequence :knobo-seq :increment 1))
nil))
;; Test that currval is not yet set
(signals error (pomo:query (:select (:currval :knobo-seq)) :single))
;; Test next value
(is (equal (pomo:query (:select (:nextval :knobo-seq)) :single)
1))
;; Test currval
(is (eq (pomo:query (:select (:currval :knobo-seq)) :single) 1))
;; Test that we can set restart at 2
;; TODO Test that when we restart, we get 2.
(is (equal (sql (:alter-sequence :knobo-seq :start 2))
"ALTER SEQUENCE knobo_seq START 2"))
(is (eq (pomo:query (:alter-sequence :knobo-seq :start 2))
nil))
;; Testing that we can set max value
(is (equal (sql (:alter-sequence :knobo-seq :max-value 5))
"ALTER SEQUENCE knobo_seq MAXVALUE 5"))
(is (eq (pomo:query (:alter-sequence :knobo-seq :max-value 5))
nil))
;; TODO: chech here that we don't can do next past max-value
(is (equal (pomo:query (:select (:nextval :knobo-seq)) :single) 2))
(is (equal (sql (:alter-sequence :knobo-seq :start 3))
"ALTER SEQUENCE knobo_seq START 3"))
(is (eq (pomo:query (:alter-sequence :knobo-seq :start 3))
nil))
;; Test set min value
(is (equal (sql (:alter-sequence :knobo-seq :min-value 2))
"ALTER SEQUENCE knobo_seq MINVALUE 2"))
(signals error (pomo:query (:alter-sequence :knobo-seq :min-value 3)))
(is (equal (pomo:query (:alter-sequence :knobo-seq :min-value 2)) nil))
;; test remove max value
(is (equal (sql (:alter-sequence :knobo-seq :no-max))
"ALTER SEQUENCE knobo_seq NO MAXVALUE"))
(is (eq (pomo:query (:alter-sequence :knobo-seq :no-max))
nil))
;; test remove min value
(is (equal (sql (:alter-sequence :knobo-seq :no-min))
"ALTER SEQUENCE knobo_seq NO MINVALUE"))
(is (eq (pomo:query (:alter-sequence :knobo-seq :no-min))
nil))
(is (eq (pomo:query (:alter-sequence :knobo-seq :cycle))
nil))
(is (eq (pomo:query (:alter-sequence :knobo-seq :no-cycle))
nil))
(is (eq (pomo:query (:alter-sequence :knobo-seq :cache 1))
nil))
(unless (pomo:table-exists-p "seq-test")
(pomo:query (:create-table "seq-test" ((:id :type :int)))))
(is (eq (pomo:query (:alter-sequence :knobo-seq :owned-by :seq-test.id))
nil))
;; cleanup
(pomo:query (:drop-sequence :knobo-seq))))
#|
(test create-table-with-constraint
"Create a table with constraint"
(is (equal )))
Define a unique table constraint for the table films. Unique table constraints can be defined on one or more columns of the table:
CREATE TABLE films (
code char(5),
title varchar(40),
did integer,
date_prod date,
kind varchar(10),
len interval hour to minute,
CONSTRAINT production UNIQUE(date_prod)
);
Define a check column constraint:
CREATE TABLE distributors (
did integer CHECK (did > 100),
name varchar(40)
);
Define a check table constraint:
CREATE TABLE distributors (
did integer,
name varchar(40)
CONSTRAINT con1 CHECK (did > 100 AND name <> '')
);
Define a primary key table constraint for the table films:
CREATE TABLE films (
code char(5),
title varchar(40),
did integer,
date_prod date,
kind varchar(10),
len interval hour to minute,
CONSTRAINT code_title PRIMARY KEY(code,title)
);
Define a primary key constraint for table distributors. The following two examples are equivalent, the first using the table constraint syntax, the second the column constraint syntax:
CREATE TABLE distributors (
did integer,
name varchar(40),
PRIMARY KEY(did)
);
CREATE TABLE distributors (
did integer PRIMARY KEY,
name varchar(40)
);
Assign a literal constant default value for the column name, arrange for the default value of column did to be generated by selecting the next value of a sequence object, and make the default value of modtime be the time at which the row is inserted:
CREATE TABLE distributors (
name varchar(40) DEFAULT 'Luso Films',
did integer DEFAULT nextval('distributors_serial'),
modtime timestamp DEFAULT current_timestamp
);
(test create-table-two-column-constraints
"Define two NOT NULL column constraints on the table distributors, one of which is explicitly given a name.
Note the need for quoting the no-null constraint in the column. Is there any way to fix this?"
(is (equal (s-sql:sql (:create-table distributors ((did :type integer :constraint 'no-null) (name :type (varchar 40 )))))
"CREATE TABLE distributors (did INTEGER NOT NULL CONSTRAINT no_null, name VARCHAR(40) NOT NULL)")))
Define a unique constraint for the name column:
CREATE TABLE distributors (
did integer,
name varchar(40) UNIQUE
);
The same, specified as a table constraint:
CREATE TABLE distributors (
did integer,
name varchar(40),
UNIQUE(name)
);
Create the same table, specifying 70% fill factor for both the table and its unique index:
CREATE TABLE distributors (
did integer,
name varchar(40),
UNIQUE(name) WITH (fillfactor=70)
)
WITH (fillfactor=70);
Create table circles with an exclusion constraint that prevents any two circles from overlapping:
CREATE TABLE circles (
c circle,
EXCLUDE USING gist (c WITH &&)
);
Create table cinemas in tablespace diskvol1:
CREATE TABLE cinemas (
id serial,
name text,
location text
) TABLESPACE diskvol1;
Create a composite type and a typed table:
CREATE TYPE employee_type AS (name text, salary numeric);
CREATE TABLE employees OF employee_type (
PRIMARY KEY (name),
salary WITH OPTIONS DEFAULT 1000
);
Create a range partitioned table:
CREATE TABLE measurement (
logdate date not null,
peaktemp int,
unitsales int
) PARTITION BY RANGE (logdate);
Create a range partitioned table with multiple columns in the partition key:
CREATE TABLE measurement_year_month (
logdate date not null,
peaktemp int,
unitsales int
) PARTITION BY RANGE (EXTRACT(YEAR FROM logdate), EXTRACT(MONTH FROM logdate));
Create a list partitioned table:
CREATE TABLE cities (
city_id bigserial not null,
name text not null,
population bigint
) PARTITION BY LIST (left(lower(name), 1));
Create partition of a range partitioned table:
CREATE TABLE measurement_y2016m07
PARTITION OF measurement (
unitsales DEFAULT 0
) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');
Create a few partitions of a range partitioned table with multiple columns in the partition key:
CREATE TABLE measurement_ym_older
PARTITION OF measurement_year_month
FOR VALUES FROM (MINVALUE, MINVALUE) TO (2016, 11);
CREATE TABLE measurement_ym_y2016m11
PARTITION OF measurement_year_month
FOR VALUES FROM (2016, 11) TO (2016, 12);
CREATE TABLE measurement_ym_y2016m12
PARTITION OF measurement_year_month
FOR VALUES FROM (2016, 12) TO (2017, 01);
CREATE TABLE measurement_ym_y2017m01
PARTITION OF measurement_year_month
FOR VALUES FROM (2017, 01) TO (2017, 02);
Create partition of a list partitioned table:
CREATE TABLE cities_ab
PARTITION OF cities (
CONSTRAINT city_id_nonzero CHECK (city_id != 0)
) FOR VALUES IN ('a', 'b');
Create partition of a list partitioned table that is itself further partitioned and then add a partition to it:
CREATE TABLE cities_ab
PARTITION OF cities (
CONSTRAINT city_id_nonzero CHECK (city_id != 0)
) FOR VALUES IN ('a', 'b') PARTITION BY RANGE (population);
CREATE TABLE cities_ab_10000_to_100000
PARTITION OF cities_ab FOR VALUES FROM (10000) TO (100000);
|#
|