├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── fnsql ├── Cargo.toml ├── crates-io.md └── src │ ├── lib.rs │ ├── postgres.rs │ └── postgres │ ├── cache.rs │ ├── docker-compose.yml │ └── sql_setup.sh ├── macro ├── Cargo.toml ├── README.md ├── crates-io.md └── src │ └── lib.rs ├── scripts ├── README.in.md └── update-docs └── testing ├── Cargo.toml └── src ├── main.rs ├── postgres.rs └── sqlite.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: CI 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-20.04 8 | strategy: 9 | matrix: 10 | rust: 11 | - stable 12 | - 1.58.0 # MSRV 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Check docs 18 | run: | 19 | ./scripts/update-docs 20 | if [[ $(git status --porcelain | wc -l) != 0 ]] ; then 21 | echo "Nede to run ./scripts/update-docs." 22 | exit -1 23 | fi 24 | 25 | - name: Build and test 26 | run: | 27 | make 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | condense_wildcard_suffixes = true 3 | format_strings = true 4 | max_width = 100 5 | newline_style = "Unix" 6 | normalize_comments = true 7 | reorder_impl_items = true 8 | report_fixme = "Always" 9 | report_todo = "Always" 10 | unstable_features = true 11 | use_field_init_shorthand = true 12 | use_try_shorthand = true 13 | wrap_comments = true 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "aho-corasick" 18 | version = "0.7.18" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 21 | dependencies = [ 22 | "memchr", 23 | ] 24 | 25 | [[package]] 26 | name = "arbitrary" 27 | version = "1.1.0" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "c38b6b6b79f671c25e1a3e785b7b82d7562ffc9cd3efdc98627e5668a2472490" 30 | dependencies = [ 31 | "derive_arbitrary", 32 | ] 33 | 34 | [[package]] 35 | name = "async-trait" 36 | version = "0.1.52" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" 39 | dependencies = [ 40 | "proc-macro2", 41 | "quote", 42 | "syn", 43 | ] 44 | 45 | [[package]] 46 | name = "base64" 47 | version = "0.13.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 50 | 51 | [[package]] 52 | name = "bitflags" 53 | version = "1.3.2" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 56 | 57 | [[package]] 58 | name = "block-buffer" 59 | version = "0.10.2" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" 62 | dependencies = [ 63 | "generic-array", 64 | ] 65 | 66 | [[package]] 67 | name = "byteorder" 68 | version = "1.4.3" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 71 | 72 | [[package]] 73 | name = "bytes" 74 | version = "1.1.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 77 | 78 | [[package]] 79 | name = "cfg-if" 80 | version = "1.0.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 83 | 84 | [[package]] 85 | name = "cpufeatures" 86 | version = "0.2.1" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" 89 | dependencies = [ 90 | "libc", 91 | ] 92 | 93 | [[package]] 94 | name = "crypto-common" 95 | version = "0.1.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" 98 | dependencies = [ 99 | "generic-array", 100 | "typenum", 101 | ] 102 | 103 | [[package]] 104 | name = "derive_arbitrary" 105 | version = "1.1.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "98e23c06c035dac87bd802d98f368df73a7f2cb05a66ffbd1f377e821fac4af9" 108 | dependencies = [ 109 | "proc-macro2", 110 | "quote", 111 | "syn", 112 | ] 113 | 114 | [[package]] 115 | name = "digest" 116 | version = "0.10.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" 119 | dependencies = [ 120 | "block-buffer", 121 | "crypto-common", 122 | "subtle", 123 | ] 124 | 125 | [[package]] 126 | name = "fallible-iterator" 127 | version = "0.2.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 130 | 131 | [[package]] 132 | name = "fallible-streaming-iterator" 133 | version = "0.1.9" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 136 | 137 | [[package]] 138 | name = "fnsql" 139 | version = "0.2.7" 140 | dependencies = [ 141 | "fnsql-macro", 142 | "postgres", 143 | "tempdir", 144 | ] 145 | 146 | [[package]] 147 | name = "fnsql-macro" 148 | version = "0.2.7" 149 | dependencies = [ 150 | "arbitrary", 151 | "fnsql", 152 | "lazy_static", 153 | "proc-macro2", 154 | "quote", 155 | "regex", 156 | "rusqlite", 157 | "syn", 158 | ] 159 | 160 | [[package]] 161 | name = "fuchsia-cprng" 162 | version = "0.1.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 165 | 166 | [[package]] 167 | name = "futures" 168 | version = "0.3.21" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" 171 | dependencies = [ 172 | "futures-channel", 173 | "futures-core", 174 | "futures-executor", 175 | "futures-io", 176 | "futures-sink", 177 | "futures-task", 178 | "futures-util", 179 | ] 180 | 181 | [[package]] 182 | name = "futures-channel" 183 | version = "0.3.21" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" 186 | dependencies = [ 187 | "futures-core", 188 | "futures-sink", 189 | ] 190 | 191 | [[package]] 192 | name = "futures-core" 193 | version = "0.3.21" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 196 | 197 | [[package]] 198 | name = "futures-executor" 199 | version = "0.3.21" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" 202 | dependencies = [ 203 | "futures-core", 204 | "futures-task", 205 | "futures-util", 206 | ] 207 | 208 | [[package]] 209 | name = "futures-io" 210 | version = "0.3.21" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" 213 | 214 | [[package]] 215 | name = "futures-macro" 216 | version = "0.3.21" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" 219 | dependencies = [ 220 | "proc-macro2", 221 | "quote", 222 | "syn", 223 | ] 224 | 225 | [[package]] 226 | name = "futures-sink" 227 | version = "0.3.21" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" 230 | 231 | [[package]] 232 | name = "futures-task" 233 | version = "0.3.21" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" 236 | 237 | [[package]] 238 | name = "futures-util" 239 | version = "0.3.21" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" 242 | dependencies = [ 243 | "futures-channel", 244 | "futures-core", 245 | "futures-io", 246 | "futures-macro", 247 | "futures-sink", 248 | "futures-task", 249 | "memchr", 250 | "pin-project-lite", 251 | "pin-utils", 252 | "slab", 253 | ] 254 | 255 | [[package]] 256 | name = "generic-array" 257 | version = "0.14.5" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 260 | dependencies = [ 261 | "typenum", 262 | "version_check", 263 | ] 264 | 265 | [[package]] 266 | name = "getrandom" 267 | version = "0.2.5" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" 270 | dependencies = [ 271 | "cfg-if", 272 | "libc", 273 | "wasi 0.10.2+wasi-snapshot-preview1", 274 | ] 275 | 276 | [[package]] 277 | name = "hashbrown" 278 | version = "0.11.2" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 281 | dependencies = [ 282 | "ahash", 283 | ] 284 | 285 | [[package]] 286 | name = "hashlink" 287 | version = "0.7.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" 290 | dependencies = [ 291 | "hashbrown", 292 | ] 293 | 294 | [[package]] 295 | name = "hmac" 296 | version = "0.12.1" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 299 | dependencies = [ 300 | "digest", 301 | ] 302 | 303 | [[package]] 304 | name = "instant" 305 | version = "0.1.12" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 308 | dependencies = [ 309 | "cfg-if", 310 | ] 311 | 312 | [[package]] 313 | name = "lazy_static" 314 | version = "1.4.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 317 | 318 | [[package]] 319 | name = "libc" 320 | version = "0.2.120" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "ad5c14e80759d0939d013e6ca49930e59fc53dd8e5009132f76240c179380c09" 323 | 324 | [[package]] 325 | name = "libsqlite3-sys" 326 | version = "0.23.2" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" 329 | dependencies = [ 330 | "pkg-config", 331 | "vcpkg", 332 | ] 333 | 334 | [[package]] 335 | name = "lock_api" 336 | version = "0.4.6" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" 339 | dependencies = [ 340 | "scopeguard", 341 | ] 342 | 343 | [[package]] 344 | name = "log" 345 | version = "0.4.14" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 348 | dependencies = [ 349 | "cfg-if", 350 | ] 351 | 352 | [[package]] 353 | name = "md-5" 354 | version = "0.10.1" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582" 357 | dependencies = [ 358 | "digest", 359 | ] 360 | 361 | [[package]] 362 | name = "memchr" 363 | version = "2.4.1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 366 | 367 | [[package]] 368 | name = "mio" 369 | version = "0.8.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "7ba42135c6a5917b9db9cd7b293e5409e1c6b041e6f9825e92e55a894c63b6f8" 372 | dependencies = [ 373 | "libc", 374 | "log", 375 | "miow", 376 | "ntapi", 377 | "wasi 0.11.0+wasi-snapshot-preview1", 378 | "winapi", 379 | ] 380 | 381 | [[package]] 382 | name = "miow" 383 | version = "0.3.7" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 386 | dependencies = [ 387 | "winapi", 388 | ] 389 | 390 | [[package]] 391 | name = "ntapi" 392 | version = "0.3.7" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" 395 | dependencies = [ 396 | "winapi", 397 | ] 398 | 399 | [[package]] 400 | name = "once_cell" 401 | version = "1.10.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" 404 | 405 | [[package]] 406 | name = "parking_lot" 407 | version = "0.11.2" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 410 | dependencies = [ 411 | "instant", 412 | "lock_api", 413 | "parking_lot_core", 414 | ] 415 | 416 | [[package]] 417 | name = "parking_lot_core" 418 | version = "0.8.5" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 421 | dependencies = [ 422 | "cfg-if", 423 | "instant", 424 | "libc", 425 | "redox_syscall", 426 | "smallvec", 427 | "winapi", 428 | ] 429 | 430 | [[package]] 431 | name = "percent-encoding" 432 | version = "2.1.0" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 435 | 436 | [[package]] 437 | name = "phf" 438 | version = "0.10.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" 441 | dependencies = [ 442 | "phf_shared", 443 | ] 444 | 445 | [[package]] 446 | name = "phf_shared" 447 | version = "0.10.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 450 | dependencies = [ 451 | "siphasher", 452 | ] 453 | 454 | [[package]] 455 | name = "pin-project-lite" 456 | version = "0.2.8" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" 459 | 460 | [[package]] 461 | name = "pin-utils" 462 | version = "0.1.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 465 | 466 | [[package]] 467 | name = "pkg-config" 468 | version = "0.3.24" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" 471 | 472 | [[package]] 473 | name = "postgres" 474 | version = "0.19.2" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "eb76d6535496f633fa799bb872ffb4790e9cbdedda9d35564ca0252f930c0dd5" 477 | dependencies = [ 478 | "bytes", 479 | "fallible-iterator", 480 | "futures", 481 | "log", 482 | "tokio", 483 | "tokio-postgres", 484 | ] 485 | 486 | [[package]] 487 | name = "postgres-protocol" 488 | version = "0.6.3" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "79ec03bce71f18b4a27c4c64c6ba2ddf74686d69b91d8714fb32ead3adaed713" 491 | dependencies = [ 492 | "base64", 493 | "byteorder", 494 | "bytes", 495 | "fallible-iterator", 496 | "hmac", 497 | "md-5", 498 | "memchr", 499 | "rand 0.8.5", 500 | "sha2", 501 | "stringprep", 502 | ] 503 | 504 | [[package]] 505 | name = "postgres-types" 506 | version = "0.2.2" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "04619f94ba0cc80999f4fc7073607cb825bc739a883cb6d20900fc5e009d6b0d" 509 | dependencies = [ 510 | "bytes", 511 | "fallible-iterator", 512 | "postgres-protocol", 513 | ] 514 | 515 | [[package]] 516 | name = "ppv-lite86" 517 | version = "0.2.16" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 520 | 521 | [[package]] 522 | name = "proc-macro2" 523 | version = "1.0.36" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 526 | dependencies = [ 527 | "unicode-xid", 528 | ] 529 | 530 | [[package]] 531 | name = "quote" 532 | version = "1.0.15" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 535 | dependencies = [ 536 | "proc-macro2", 537 | ] 538 | 539 | [[package]] 540 | name = "rand" 541 | version = "0.4.6" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 544 | dependencies = [ 545 | "fuchsia-cprng", 546 | "libc", 547 | "rand_core 0.3.1", 548 | "rdrand", 549 | "winapi", 550 | ] 551 | 552 | [[package]] 553 | name = "rand" 554 | version = "0.8.5" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 557 | dependencies = [ 558 | "libc", 559 | "rand_chacha", 560 | "rand_core 0.6.3", 561 | ] 562 | 563 | [[package]] 564 | name = "rand_chacha" 565 | version = "0.3.1" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 568 | dependencies = [ 569 | "ppv-lite86", 570 | "rand_core 0.6.3", 571 | ] 572 | 573 | [[package]] 574 | name = "rand_core" 575 | version = "0.3.1" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 578 | dependencies = [ 579 | "rand_core 0.4.2", 580 | ] 581 | 582 | [[package]] 583 | name = "rand_core" 584 | version = "0.4.2" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 587 | 588 | [[package]] 589 | name = "rand_core" 590 | version = "0.6.3" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 593 | dependencies = [ 594 | "getrandom", 595 | ] 596 | 597 | [[package]] 598 | name = "rdrand" 599 | version = "0.4.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 602 | dependencies = [ 603 | "rand_core 0.3.1", 604 | ] 605 | 606 | [[package]] 607 | name = "redox_syscall" 608 | version = "0.2.11" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" 611 | dependencies = [ 612 | "bitflags", 613 | ] 614 | 615 | [[package]] 616 | name = "regex" 617 | version = "1.5.5" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" 620 | dependencies = [ 621 | "aho-corasick", 622 | "memchr", 623 | "regex-syntax", 624 | ] 625 | 626 | [[package]] 627 | name = "regex-syntax" 628 | version = "0.6.25" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 631 | 632 | [[package]] 633 | name = "remove_dir_all" 634 | version = "0.5.3" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 637 | dependencies = [ 638 | "winapi", 639 | ] 640 | 641 | [[package]] 642 | name = "rusqlite" 643 | version = "0.26.3" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" 646 | dependencies = [ 647 | "bitflags", 648 | "fallible-iterator", 649 | "fallible-streaming-iterator", 650 | "hashlink", 651 | "libsqlite3-sys", 652 | "memchr", 653 | "smallvec", 654 | ] 655 | 656 | [[package]] 657 | name = "scopeguard" 658 | version = "1.1.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 661 | 662 | [[package]] 663 | name = "sha2" 664 | version = "0.10.2" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" 667 | dependencies = [ 668 | "cfg-if", 669 | "cpufeatures", 670 | "digest", 671 | ] 672 | 673 | [[package]] 674 | name = "siphasher" 675 | version = "0.3.10" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" 678 | 679 | [[package]] 680 | name = "slab" 681 | version = "0.4.5" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" 684 | 685 | [[package]] 686 | name = "smallvec" 687 | version = "1.8.0" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 690 | 691 | [[package]] 692 | name = "socket2" 693 | version = "0.4.4" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 696 | dependencies = [ 697 | "libc", 698 | "winapi", 699 | ] 700 | 701 | [[package]] 702 | name = "stringprep" 703 | version = "0.1.2" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" 706 | dependencies = [ 707 | "unicode-bidi", 708 | "unicode-normalization", 709 | ] 710 | 711 | [[package]] 712 | name = "subtle" 713 | version = "2.4.1" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 716 | 717 | [[package]] 718 | name = "syn" 719 | version = "1.0.88" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "ebd69e719f31e88618baa1eaa6ee2de5c9a1c004f1e9ecdb58e8352a13f20a01" 722 | dependencies = [ 723 | "proc-macro2", 724 | "quote", 725 | "unicode-xid", 726 | ] 727 | 728 | [[package]] 729 | name = "tempdir" 730 | version = "0.3.7" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 733 | dependencies = [ 734 | "rand 0.4.6", 735 | "remove_dir_all", 736 | ] 737 | 738 | [[package]] 739 | name = "testing" 740 | version = "0.1.0" 741 | dependencies = [ 742 | "arbitrary", 743 | "fnsql", 744 | "postgres", 745 | "rusqlite", 746 | ] 747 | 748 | [[package]] 749 | name = "tinyvec" 750 | version = "1.5.1" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" 753 | dependencies = [ 754 | "tinyvec_macros", 755 | ] 756 | 757 | [[package]] 758 | name = "tinyvec_macros" 759 | version = "0.1.0" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 762 | 763 | [[package]] 764 | name = "tokio" 765 | version = "1.17.0" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" 768 | dependencies = [ 769 | "bytes", 770 | "libc", 771 | "memchr", 772 | "mio", 773 | "pin-project-lite", 774 | "socket2", 775 | "winapi", 776 | ] 777 | 778 | [[package]] 779 | name = "tokio-postgres" 780 | version = "0.7.5" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "4b6c8b33df661b548dcd8f9bf87debb8c56c05657ed291122e1188698c2ece95" 783 | dependencies = [ 784 | "async-trait", 785 | "byteorder", 786 | "bytes", 787 | "fallible-iterator", 788 | "futures", 789 | "log", 790 | "parking_lot", 791 | "percent-encoding", 792 | "phf", 793 | "pin-project-lite", 794 | "postgres-protocol", 795 | "postgres-types", 796 | "socket2", 797 | "tokio", 798 | "tokio-util", 799 | ] 800 | 801 | [[package]] 802 | name = "tokio-util" 803 | version = "0.6.9" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" 806 | dependencies = [ 807 | "bytes", 808 | "futures-core", 809 | "futures-sink", 810 | "log", 811 | "pin-project-lite", 812 | "tokio", 813 | ] 814 | 815 | [[package]] 816 | name = "typenum" 817 | version = "1.15.0" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 820 | 821 | [[package]] 822 | name = "unicode-bidi" 823 | version = "0.3.7" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" 826 | 827 | [[package]] 828 | name = "unicode-normalization" 829 | version = "0.1.19" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 832 | dependencies = [ 833 | "tinyvec", 834 | ] 835 | 836 | [[package]] 837 | name = "unicode-xid" 838 | version = "0.2.2" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 841 | 842 | [[package]] 843 | name = "vcpkg" 844 | version = "0.2.15" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 847 | 848 | [[package]] 849 | name = "version_check" 850 | version = "0.9.4" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 853 | 854 | [[package]] 855 | name = "wasi" 856 | version = "0.10.2+wasi-snapshot-preview1" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 859 | 860 | [[package]] 861 | name = "wasi" 862 | version = "0.11.0+wasi-snapshot-preview1" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 865 | 866 | [[package]] 867 | name = "winapi" 868 | version = "0.3.9" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 871 | dependencies = [ 872 | "winapi-i686-pc-windows-gnu", 873 | "winapi-x86_64-pc-windows-gnu", 874 | ] 875 | 876 | [[package]] 877 | name = "winapi-i686-pc-windows-gnu" 878 | version = "0.4.0" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 881 | 882 | [[package]] 883 | name = "winapi-x86_64-pc-windows-gnu" 884 | version = "0.4.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 887 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "macro", 5 | "fnsql", 6 | "testing", 7 | ] 8 | 9 | [patch.crates-io] 10 | fnsql-macro = { path = "macro" } 11 | fnsql = { path = "fnsql" } 12 | testing = { path = "testing" } 13 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Dan Aloni 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FNSQL_TEST_POSTGRES_PORT := 5433 2 | 3 | all: 4 | set -e; \ 5 | export FNSQL_TEST_POSTGRES_PORT=$(FNSQL_TEST_POSTGRES_PORT); \ 6 | cargo test -- postgres::tests::docker_up --ignored; \ 7 | sleep 2; \ 8 | cargo test --all-features; \ 9 | cargo run; \ 10 | cargo test -- postgres::tests::docker_down --ignored; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fnsql   [![Build Status]][actions] [![Latest Version]][crates.io] [![Docs badge]][Docs link] [![License badge]][License link] 2 | 3 | [Build Status]: https://github.com/da-x/fnsql/actions/workflows/test.yml/badge.svg 4 | [actions]: https://github.com/da-x/fnsql/actions 5 | [Latest Version]: https://img.shields.io/crates/v/fnsql.svg 6 | [crates.io]: https://crates.io/crates/fnsql 7 | [License badge]: https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg 8 | [License link]: https://travis-ci.org/da-x/fnsql 9 | [Docs badge]: https://docs.rs/fnsql/badge.svg 10 | [Docs link]: https://docs.rs/fnsql 11 | 12 | The `fnsql` crate provides simple type-safe optional wrappers around SQL 13 | queries. Instead of calling type-less `.query()` and `.execute()`, you call to 14 | auto-generated unique wrappers that are strongly typed, `.query_()` and 15 | `.execute_()`. However, you manually specify the input and output types, 16 | but only once, with the query, and in separation with the code that uses the 17 | query. 18 | 19 | It's a very simple implementation that doesn't force any schema or ORM down 20 | your throat, so if you are already using the `rusqlite` or `postgres` crates, 21 | you can gradually replace your type-less queries with the type-ful wrappers, 22 | or migrate from an opinionated ORM. 23 | 24 | The way to generate these wrappers is to specify input and output types for 25 | each one of the queries. For example, consider the following definitions 26 | specified with `fnsql`, based on the `rusqlite` example: 27 | 28 | ```rust 29 | fnsql::fnsql! { 30 | #[rusqlite, test] 31 | create_table_pet() { 32 | "CREATE TABLE pet ( 33 | id INTEGER PRIMARY KEY, 34 | name TEXT NOT NULL, 35 | data BLOB 36 | )" 37 | } 38 | 39 | #[rusqlite, test(with=[create_table_pet])] 40 | insert_new_pet(name: String, data: Option>) { 41 | "INSERT INTO pet (name, data) VALUES (:name, :data)" 42 | } 43 | 44 | #[rusqlite, test(with=[create_table_pet])] 45 | get_pet_id_data(name: Option) -> [(i32, Option>, String)] { 46 | "SELECT id, data, name FROM pet WHERE pet.name = :name" 47 | } 48 | } 49 | ``` 50 | 51 | The definitions can be used as such (commented out is how the previous 52 | type-less interfaces were used): 53 | 54 | ```rust ignore 55 | let mut conn = rusqlite::Connection::open_in_memory()?; 56 | 57 | conn.execute_create_table_pet()?; 58 | // conn.execute( 59 | // "CREATE TABLE pet ( 60 | // id INTEGER PRIMARY KEY, 61 | // name TEXT NOT NULL, 62 | // data BLOB 63 | // )", 64 | // [], 65 | // )?; 66 | 67 | conn.execute_insert_new_pet(&me.name, &me.data)?; 68 | // conn.execute( 69 | // "INSERT INTO pet (name, data) VALUES (?1, ?2)", 70 | // params![me.name, me.data], 71 | // )?; 72 | 73 | let mut stmt = conn.prepare_get_pet_id_data()?; 74 | // let mut stmt = conn.prepare("SELECT id, data, name FROM pet WHERE pet.name = :name")?; 75 | 76 | let pet_iter = stmt.query_map(&Some("Max".to_string()), |id, data, name| { 77 | Ok::<_, rusqlite::Error>(Pet { 78 | id, 79 | data, 80 | name, 81 | }) 82 | })?; 83 | // let pet_iter = stmt.query_map([(":name", "Max".to_string())], |row| { 84 | // Ok(Pet { 85 | // id: row.get(0)?, 86 | // name: row.get(1)?, 87 | // data: row.get(2)?, 88 | // }) 89 | // })?; 90 | ``` 91 | 92 | ## Technical discussion 93 | 94 | The idea with this crate is to allow direct SQL usage but never use inline 95 | queries or have type inference at the call-site. Instead, we declare each query 96 | on top-level, giving each a name and designated accessor methods that derive 97 | from the name. 98 | 99 | - The types of named variables are give in a Rust-like syntax. 100 | - The type of the returned row is also provided. 101 | - `fnsql` does not make an assurances to make sure the types match the query, 102 | you will discover it with `cargo test` and no additional code. 103 | - `fnsql` writes the tests for each of the queries. - `Arbitrary` is used to 104 | generate parameter values. 105 | - If testing one query depend on another, you can specify that with `test(with=[..])`. 106 | 107 | ```text 108 | running 3 tests 109 | test auto_create_table_pet ... ok 110 | test auto_insert_new_pet ... ok 111 | test auto_get_pet_id_data ... ok 112 | ``` 113 | 114 | The following is for allowing generated query tests to compile: 115 | 116 | ```toml 117 | [dev-dependencies] 118 | arbitrary = { version = "1", features = ["derive"] } 119 | ``` 120 | 121 | ## Limitations 122 | 123 | * Though it does provide auto-generated tests for validating queries in `cargo test`, 124 | it does not do any compile-time validation based on the SQL query string. 125 | * It only supports `rusqlite` and `postgres` for now. 126 | 127 | ## License 128 | 129 | `fnsql` is licensed under either of 130 | 131 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 132 | http://www.apache.org/licenses/LICENSE-2.0) 133 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 134 | http://opensource.org/licenses/MIT) 135 | 136 | at your option. 137 | 138 | 139 | ### Contribution 140 | 141 | Unless you explicitly state otherwise, any contribution intentionally submitted 142 | for inclusion in `fnsql` by you, as defined in the Apache-2.0 license, 143 | shall be dual licensed as above, without any additional terms or conditions. 144 | -------------------------------------------------------------------------------- /fnsql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fnsql" 3 | version = "0.2.7" 4 | edition = "2021" 5 | keywords = ["sql", "proc_macro", "procmacro"] 6 | license = "MIT/Apache-2.0" 7 | description = "Type-safe SQL query wrappers" 8 | homepage = "https://github.com/da-x/fnsql" 9 | repository = "https://github.com/da-x/fnsql" 10 | categories = ["database"] 11 | readme = "crates-io.md" 12 | include = ["Cargo.toml", "src/**/*.rs", "src/**/*.yml", "src/**/*.sh", "crates-io.md", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] 13 | 14 | [features] 15 | default = [] 16 | all = ["with-rusqlite", "with-postgres", "prepare-cache"] 17 | with-rusqlite = ["fnsql-macro/with-postgres"] 18 | with-postgres = ["fnsql-macro/with-rusqlite", "postgres", "tempdir"] 19 | prepare-cache = ["fnsql-macro/prepare-cache"] 20 | 21 | [dependencies] 22 | fnsql-macro = "0.2.7" 23 | tempdir = { version = "0.3", optional = true } 24 | postgres = { version = "0.19", optional = true } 25 | -------------------------------------------------------------------------------- /fnsql/crates-io.md: -------------------------------------------------------------------------------- 1 | ../macro/crates-io.md -------------------------------------------------------------------------------- /fnsql/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate fnsql_macro; 2 | 3 | // Re-export macro 4 | pub use fnsql_macro::fnsql; 5 | 6 | #[cfg(feature = "with-postgres")] 7 | pub mod postgres; 8 | -------------------------------------------------------------------------------- /fnsql/src/postgres.rs: -------------------------------------------------------------------------------- 1 | //! Support for PostgreSQL with fnsql. 2 | //! 3 | //! **Dependent on the `with-postgres` manifest feature**. 4 | //! 5 | //! By default arguments subsitution uses $1 and $2 instead of ':name', unless 6 | //! you provide the attribute 'named'. 7 | //! 8 | //! 9 | //! ## Cache for prepared statement 10 | //! 11 | //! **Dependent on the `prepare-cache` manifest feature**. 12 | //! 13 | //! The fnsql 'prepare' method in fnsql for PostgreSQL returns a uniuqe type 14 | //! per query. Behind the scene, there's also a `prepare_cached` that can 15 | //! cache the prepared statements, but it needs to be passed the `Cache` 16 | //! object. 17 | //! 18 | //! 19 | //! ## Auto-genreated tests 20 | //! 21 | //! For the auto-generated tests to work, some PostgreSQL server needs to be 22 | //! available for connectivity. All schemas are done with `pg_temp`, so no 23 | //! actual tables are created. 24 | //! 25 | //! Use the following macro somewhere in your crate: 26 | //! 27 | //! ```ignore 28 | //! fnsql::fnsql_define_postgres_test_handlers!(docker_up, docker_down) 29 | //! ``` 30 | //! 31 | //! This generates two special ignored tests for bring-up/tear-down 32 | //! docker-compose based PostgreSQL setup, to be added to a testing 33 | //! environment as such: 34 | //! 35 | //! ```sh ignore 36 | //! export FNSQL_TEST_POSTGRES_PORT=5433 37 | //! cargo test -- tests::docker_up --ignored 38 | //! sleep 2; 39 | //! cargo test 40 | //! cargo test -- tests::docker_down --ignored 41 | //! ``` 42 | //! 43 | //! This is needed as Rust does not provide provisions for test environment 44 | //! bring-up/tear-down that are external to the process. Doing so on every 45 | //! test would have been quite expensive in run-time. 46 | 47 | #[cfg(feature = "prepare-cache")] 48 | pub mod cache; 49 | #[cfg(feature = "prepare-cache")] 50 | pub use cache::Cache; 51 | use postgres::{NoTls, Client}; 52 | pub use postgres::Error; 53 | 54 | use std::io::{Write}; 55 | use std::fs::File; 56 | use std::path::PathBuf; 57 | 58 | pub fn with_docker_compose(f: F, compose_yaml: &str) -> Result<(), std::io::Error> 59 | where F: FnOnce(PathBuf) -> Result<(), std::io::Error> 60 | { 61 | let tmp_dir = tempdir::TempDir::new("fnsql-postgres-docker")?; 62 | 63 | let file_path = tmp_dir.path().join("docker-compose.yaml"); 64 | let mut tmp_file = File::create(&file_path)?; 65 | writeln!(tmp_file, "{}", compose_yaml)?; 66 | drop(tmp_file); 67 | 68 | let sql_setup = tmp_dir.path().join("sql_setup.sh"); 69 | let mut tmp_file = File::create(&sql_setup)?; 70 | writeln!(tmp_file, "{}", SQL_SETUP)?; 71 | drop(tmp_file); 72 | 73 | let r = f(file_path); 74 | 75 | tmp_dir.close()?; 76 | 77 | r 78 | } 79 | 80 | pub fn testing_client() -> Result { 81 | let port = std::env::var("FNSQL_TEST_POSTGRES_PORT") 82 | .expect("undefined FNSQL_TEST_POSTGRES_PORT"); 83 | let settings = format!("user=postgres host=localhost port={}", port); 84 | let client = Client::connect(&settings, NoTls)?; 85 | Ok(client) 86 | } 87 | 88 | pub fn testing_docker_up(compose_yaml: &str) -> Result<(), std::io::Error> { 89 | with_docker_compose(|path| { 90 | std::process::Command::new("docker-compose") 91 | .arg("-p").arg(module_path!()) 92 | .arg("-f").arg(path) 93 | .arg("up").arg("-d").output()?; 94 | Ok(()) 95 | }, compose_yaml) 96 | } 97 | 98 | pub fn testing_docker_down(compose_yaml: &str) -> Result<(), std::io::Error> { 99 | with_docker_compose(|path| { 100 | std::process::Command::new("docker-compose") 101 | .arg("-p").arg(module_path!()) 102 | .arg("-f").arg(path) 103 | .arg("down").output()?; 104 | Ok(()) 105 | }, compose_yaml) 106 | } 107 | 108 | pub static DOCKER_COMPOSE: &'static str = include_str!("postgres/docker-compose.yml"); 109 | pub static SQL_SETUP: &'static str = include_str!("postgres/sql_setup.sh"); 110 | 111 | #[macro_export] 112 | macro_rules! fnsql_define_postgres_test_handlers { 113 | ($name_up:ident, $name_down:ident, $compose:expr) => { 114 | #[cfg(test)] 115 | mod tests { 116 | #[ignore] 117 | #[test] 118 | fn $name_up() -> Result<(), std::io::Error> { 119 | $crate::postgres::testing_docker_up($compose) 120 | } 121 | 122 | #[ignore] 123 | #[test] 124 | fn $name_down() -> Result<(), std::io::Error> { 125 | $crate::postgres::testing_docker_down($compose) 126 | } 127 | } 128 | }; 129 | ($name_up:ident, $name_down:ident) => { 130 | fnsql_define_postgres_test_handlers!($name_up, $name_down, 131 | $crate::postgres::DOCKER_COMPOSE); 132 | }; 133 | } 134 | 135 | fnsql_define_postgres_test_handlers!(docker_up, docker_down); 136 | -------------------------------------------------------------------------------- /fnsql/src/postgres/cache.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap}; 2 | use postgres::{Statement, types::Type, Error, GenericClient}; 3 | 4 | type Key = (Cow<'static, str>, Cow<'static, [Type]>); 5 | 6 | pub struct Cache { 7 | map: HashMap, 8 | } 9 | 10 | impl Cache { 11 | pub fn new() -> Self { 12 | Self { 13 | map: HashMap::new(), 14 | } 15 | } 16 | 17 | pub fn prepare(&mut self, query: &str, client: &mut impl GenericClient) -> Result { 18 | self.prepare_typed(query, &[], client) 19 | } 20 | 21 | pub fn prepare_typed(&mut self, query: &str, types: &[Type], client: &mut impl GenericClient) -> Result { 22 | let cow_types = Cow::Borrowed(types); 23 | let cow_query = Cow::Borrowed(query); 24 | 25 | match self.map.get(&(cow_query, cow_types)) { 26 | Some(stmt) => return Ok(stmt.clone()), 27 | None => { 28 | let stmt = client.prepare_typed(query, types)?; 29 | self.map.insert((Cow::Owned(query.to_owned()), 30 | Cow::Owned(Vec::from(types))), stmt.clone()); 31 | Ok(stmt) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /fnsql/src/postgres/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | postgres: 4 | image: postgres:14 5 | ports: 6 | - ${FNSQL_TEST_POSTGRES_PORT}:5433 7 | volumes: 8 | - ./sql_setup.sh:/docker-entrypoint-initdb.d/sql_setup.sh 9 | environment: 10 | POSTGRES_PASSWORD: postgres 11 | -------------------------------------------------------------------------------- /fnsql/src/postgres/sql_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cat > "$PGDATA/server.key" <<-EOKEY 5 | -----BEGIN RSA PRIVATE KEY----- 6 | MIIEpAIBAAKCAQEAllItXwrj62MkxKVlz2FimJk42WWc3K82Rn2vAl6z38zQxSCj 7 | t9uWwXWTx5YOdGiUcA+JUAruZxqN7vdfphJoYtTrcrpT4rC/FsCMImBxkj1cxdYT 8 | q94SFn9bQBRZk7RUx4Kolt+/h0d3PpNIb4DbyQ8A0MVvNVxLpRRVwc6yQP+NkRMy 9 | gHR+m3P8fxHEtkHCVy7HORbASvN8fRlREMHDL2hkadX0BNM72DDo+DWhPA8GF6WX 10 | tIl1gU6GP6pSbEeMHD3f+uj7f9iSjvkrHrOt2nLUQ9Qnev2nhmU0/dOIweQ17/Fr 11 | lL9jYDUUFNORyjRnlXXUoP5BO/LdEAAqT2A0pwIDAQABAoIBAQCIXu74XUneHuiZ 12 | Wa+eTqwC4mZXmz6OWonzs0vU65NlgksXuv+r6ZO/2GoD1Bcy9jlL3Fxm+DPF56pB 13 | 07u7TtHSb3VWdMFrU4tYGcBH45TE5dRHSmo4LlPcgxeGb6/ANwX+pYNKtJvuHyCH 14 | 7Vf2iEFcCrdjrumv0BZ0IZmXJGxEV+7mK2Og0bZ/zbmJNaH25muuWj6BKlvLhL0N 15 | S2LlBjKx3HqtppUgUqNFqjLs6IA1u79S5dAomOsxZtnuByaX5WFzpktU2pveZmyF 16 | cl0dwHYZIaxR3ewYeQXGF8ANUmIx3nnxD2JOysPkitaGzeqt6dQZV14tPlDZDKat 17 | Vf0b6BHhAoGBAMWV7rG+7nVXoQ30CIcPGklkST3mVOlrzeBbKP1SeAwoGRbfsdhp 18 | rFMkh5UxTexnOzD4O8HPuJ6NGeWRQfqZT1nnjwHPeJWtiMHT6cnWxlzvxAZ61mio 19 | 0jRfb8flhgFKk+G9+Xa6WaYAAwGWdF062EMe2Ym92oKM9ilTPGFVRk1XAoGBAMLD 20 | ETSQd2UqTF/y7wxMPqF3l6d1KBjwpuNuin2IjkXTOfGkDnAU3mSQlr7K1IPX8NPO 21 | gdyMfJoysfRaBuRcNA/o/0l0wyxW4HWtTtPYI0+pRCFtRLsI1MB997QKeaGKb+me 22 | 3nBXkOksPSr9oa0Cs27z2cSoBOkpq2N/zzBseHExAoGAOyq3rKBZNehEwTHnb9I0 23 | 8+9FA3U6zh9LKjkCIEGW00Uapj/cOMsEIG2a8DEwfW84SWS8OEBkr43fSGBkGo/Y 24 | NDrkFw2ytVee0TQNGTTod6IQ2EPmera7I5XEml5/71kOyZWi40vQVqZAQDR2qgha 25 | BFdzmwywJ1Hg0OUs+pSXlccCgYEAgyOVki80NYolovWQwFcWVOKR2s+oECL6PGlS 26 | FvS714hCm9I7ZnymwlAZMJ6iOaRNJFEIX9i4jZtU95Mm0NzEsXHRc0SLpm9Y8+Oe 27 | EEaYgCsZFOjePpHTr0kiYLgs7fipIkU2wa40hMyk4y2kjzoiV7MaDrCTnevQ205T 28 | 0+c1sgECgYBAXKcwdkh9JVSrLXFamsxiOx3MZ0n6J1d28wpdA3y4Y4AAJm4TGgFt 29 | eG/6qHRy6CHdFtJ7a84EMe1jaVLQJYW/VrOC2bWLftkU7qaOnkXHvr4CAHsXQHcx 30 | JhLfvh4ab3KyoK/iimifvcoS5z9gp7IBFKMyh5IeJ9Y75TgcfJ5HMg== 31 | -----END RSA PRIVATE KEY----- 32 | EOKEY 33 | chmod 0600 "$PGDATA/server.key" 34 | 35 | cat > "$PGDATA/server.crt" <<-EOCERT 36 | -----BEGIN CERTIFICATE----- 37 | MIID9DCCAtygAwIBAgIJAIYfg4EQ2pVAMA0GCSqGSIb3DQEBBQUAMFkxCzAJBgNV 38 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 39 | aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNjA2MjgyMjQw 40 | NDFaFw0yNjA2MjYyMjQwNDFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21l 41 | LVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV 42 | BAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJZS 43 | LV8K4+tjJMSlZc9hYpiZONllnNyvNkZ9rwJes9/M0MUgo7fblsF1k8eWDnRolHAP 44 | iVAK7mcaje73X6YSaGLU63K6U+KwvxbAjCJgcZI9XMXWE6veEhZ/W0AUWZO0VMeC 45 | qJbfv4dHdz6TSG+A28kPANDFbzVcS6UUVcHOskD/jZETMoB0fptz/H8RxLZBwlcu 46 | xzkWwErzfH0ZURDBwy9oZGnV9ATTO9gw6Pg1oTwPBhell7SJdYFOhj+qUmxHjBw9 47 | 3/ro+3/Yko75Kx6zrdpy1EPUJ3r9p4ZlNP3TiMHkNe/xa5S/Y2A1FBTTkco0Z5V1 48 | 1KD+QTvy3RAAKk9gNKcCAwEAAaOBvjCBuzAdBgNVHQ4EFgQUEcuoFxzUZ4VV9VPv 49 | 5frDyIuFA5cwgYsGA1UdIwSBgzCBgIAUEcuoFxzUZ4VV9VPv5frDyIuFA5ehXaRb 50 | MFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJ 51 | bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAIYf 52 | g4EQ2pVAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHwMzmXdtz3R 53 | 83HIdRQic40bJQf9ucSwY5ArkttPhC8ewQGyiGexm1Tvx9YA/qT2rscKPHXCPYcP 54 | IUE+nJTc8lQb8wPnFwGdHUsJfCvurxE4Yv4Oi74+q1enhHBGsvhFdFY5jTYD9unM 55 | zBEn+ZHX3PlKhe3wMub4khBTbPLK+n/laQWuZNsa+kj7BynkAg8W/6RK0Z0cJzzw 56 | aiVP0bSvatAAcSwkEfKEv5xExjWqoewjSlQLEZYIjJhXdtx/8AMnrcyxrFvKALUQ 57 | 9M15FXvlPOB7ez14xIXQBKvvLwXvteHF6kYbzg/Bl1Q2GE9usclPa4UvTpnLv6gq 58 | NmFaAhoxnXA= 59 | -----END CERTIFICATE----- 60 | EOCERT 61 | 62 | cat >> "$PGDATA/postgresql.conf" <<-EOCONF 63 | port = 5433 64 | ssl = on 65 | ssl_cert_file = 'server.crt' 66 | ssl_key_file = 'server.key' 67 | EOCONF 68 | 69 | cat > "$PGDATA/pg_hba.conf" <<-EOCONF 70 | # TYPE DATABASE USER ADDRESS METHOD 71 | host all pass_user 0.0.0.0/0 password 72 | host all md5_user 0.0.0.0/0 md5 73 | host all scram_user 0.0.0.0/0 scram-sha-256 74 | host all pass_user ::0/0 password 75 | host all md5_user ::0/0 md5 76 | host all scram_user ::0/0 scram-sha-256 77 | 78 | hostssl all ssl_user 0.0.0.0/0 trust 79 | hostssl all ssl_user ::0/0 trust 80 | host all ssl_user 0.0.0.0/0 reject 81 | host all ssl_user ::0/0 reject 82 | 83 | # IPv4 local connections: 84 | host all postgres 0.0.0.0/0 trust 85 | # IPv6 local connections: 86 | host all postgres ::0/0 trust 87 | # Unix socket connections: 88 | local all postgres trust 89 | EOCONF 90 | 91 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 92 | CREATE ROLE pass_user PASSWORD 'password' LOGIN; 93 | CREATE ROLE md5_user PASSWORD 'password' LOGIN; 94 | SET password_encryption TO 'scram-sha-256'; 95 | CREATE ROLE scram_user PASSWORD 'password' LOGIN; 96 | CREATE ROLE ssl_user LOGIN; 97 | CREATE EXTENSION hstore; 98 | CREATE EXTENSION citext; 99 | EOSQL 100 | -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fnsql-macro" 3 | version = "0.2.7" 4 | edition = "2021" 5 | keywords = ["sql", "proc_macro", "procmacro"] 6 | license = "MIT/Apache-2.0" 7 | description = "Type-safe SQL query wrappers" 8 | homepage = "https://github.com/da-x/fnsql" 9 | repository = "https://github.com/da-x/fnsql" 10 | categories = ["database"] 11 | readme = "crates-io.md" 12 | include = ["Cargo.toml", "src/**/*.rs", "crates-io.md", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [features] 18 | default = [] 19 | all = ["with-rusqlite", "with-postgres", "prepare-cache"] 20 | with-rusqlite = [] 21 | with-postgres = [] 22 | prepare-cache = [] 23 | 24 | [dependencies] 25 | quote = "1" 26 | proc-macro2 = "1.0" 27 | lazy_static = "1.4" 28 | syn = "1.0" 29 | regex = "1.5" 30 | 31 | [dev-dependencies] 32 | rusqlite = "0.26.3" 33 | fnsql = "0.2.7" 34 | arbitrary = { version = "1", features = ["derive"] } 35 | -------------------------------------------------------------------------------- /macro/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /macro/crates-io.md: -------------------------------------------------------------------------------- 1 | The `fnsql` crate provides simple type-safe optional wrappers around SQL 2 | queries. Instead of calling type-less `.query()` and `.execute()`, you call to 3 | auto-generated unique wrappers that are strongly typed, `.query_()` and 4 | `.execute_()`. However, you manually specify the input and output types, 5 | but only once, with the query, and in separation with the code that uses the 6 | query. 7 | 8 | It's a very simple implementation that doesn't force any schema or ORM down 9 | your throat, so if you are already using the `rusqlite` or `postgres` crates, 10 | you can gradually replace your type-less queries with the type-ful wrappers, 11 | or migrate from an opinionated ORM. 12 | 13 | The way to generate these wrappers is to specify input and output types for 14 | each one of the queries. For example, consider the following definitions 15 | specified with `fnsql`, based on the `rusqlite` example: 16 | 17 | ```rust 18 | fnsql::fnsql! { 19 | #[rusqlite, test] 20 | create_table_pet() { 21 | "CREATE TABLE pet ( 22 | id INTEGER PRIMARY KEY, 23 | name TEXT NOT NULL, 24 | data BLOB 25 | )" 26 | } 27 | 28 | #[rusqlite, test(with=[create_table_pet])] 29 | insert_new_pet(name: String, data: Option>) { 30 | "INSERT INTO pet (name, data) VALUES (:name, :data)" 31 | } 32 | 33 | #[rusqlite, test(with=[create_table_pet])] 34 | get_pet_id_data(name: Option) -> [(i32, Option>, String)] { 35 | "SELECT id, data, name FROM pet WHERE pet.name = :name" 36 | } 37 | } 38 | ``` 39 | 40 | The definitions can be used as such (commented out is how the previous 41 | type-less interfaces were used): 42 | 43 | ```rust ignore 44 | let mut conn = rusqlite::Connection::open_in_memory()?; 45 | 46 | conn.execute_create_table_pet()?; 47 | // conn.execute( 48 | // "CREATE TABLE pet ( 49 | // id INTEGER PRIMARY KEY, 50 | // name TEXT NOT NULL, 51 | // data BLOB 52 | // )", 53 | // [], 54 | // )?; 55 | 56 | conn.execute_insert_new_pet(&me.name, &me.data)?; 57 | // conn.execute( 58 | // "INSERT INTO pet (name, data) VALUES (?1, ?2)", 59 | // params![me.name, me.data], 60 | // )?; 61 | 62 | let mut stmt = conn.prepare_get_pet_id_data()?; 63 | // let mut stmt = conn.prepare("SELECT id, data, name FROM pet WHERE pet.name = :name")?; 64 | 65 | let pet_iter = stmt.query_map(&Some("Max".to_string()), |id, data, name| { 66 | Ok::<_, rusqlite::Error>(Pet { 67 | id, 68 | data, 69 | name, 70 | }) 71 | })?; 72 | // let pet_iter = stmt.query_map([(":name", "Max".to_string())], |row| { 73 | // Ok(Pet { 74 | // id: row.get(0)?, 75 | // name: row.get(1)?, 76 | // data: row.get(2)?, 77 | // }) 78 | // })?; 79 | ``` 80 | 81 | ## Technical discussion 82 | 83 | The idea with this crate is to allow direct SQL usage but never use inline 84 | queries or have type inference at the call-site. Instead, we declare each query 85 | on top-level, giving each a name and designated accessor methods that derive 86 | from the name. 87 | 88 | - The types of named variables are give in a Rust-like syntax. 89 | - The type of the returned row is also provided. 90 | - `fnsql` does not make an assurances to make sure the types match the query, 91 | you will discover it with `cargo test` and no additional code. 92 | - `fnsql` writes the tests for each of the queries. - `Arbitrary` is used to 93 | generate parameter values. 94 | - If testing one query depend on another, you can specify that with `test(with=[..])`. 95 | 96 | ```text 97 | running 3 tests 98 | test auto_create_table_pet ... ok 99 | test auto_insert_new_pet ... ok 100 | test auto_get_pet_id_data ... ok 101 | ``` 102 | 103 | The following is for allowing generated query tests to compile: 104 | 105 | ```toml 106 | [dev-dependencies] 107 | arbitrary = { version = "1", features = ["derive"] } 108 | ``` 109 | 110 | ## Limitations 111 | 112 | * Though it does provide auto-generated tests for validating queries in `cargo test`, 113 | it does not do any compile-time validation based on the SQL query string. 114 | * It only supports `rusqlite` and `postgres` for now. -------------------------------------------------------------------------------- /macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The `fnsql` crate provides simple type-safe optional wrappers around SQL 2 | //! queries. Instead of calling type-less `.query()` and `.execute()`, you call to 3 | //! auto-generated unique wrappers that are strongly typed, `.query_()` and 4 | //! `.execute_()`. However, you manually specify the input and output types, 5 | //! but only once, with the query, and in separation with the code that uses the 6 | //! query. 7 | //! 8 | //! It's a very simple implementation that doesn't force any schema or ORM down 9 | //! your throat, so if you are already using the `rusqlite` or `postgres` crates, 10 | //! you can gradually replace your type-less queries with the type-ful wrappers, 11 | //! or migrate from an opinionated ORM. 12 | //! 13 | //! The way to generate these wrappers is to specify input and output types for 14 | //! each one of the queries. For example, consider the following definitions 15 | //! specified with `fnsql`, based on the `rusqlite` example: 16 | //! 17 | //! ```rust 18 | //! fnsql::fnsql! { 19 | //! #[rusqlite, test] 20 | //! create_table_pet() { 21 | //! "CREATE TABLE pet ( 22 | //! id INTEGER PRIMARY KEY, 23 | //! name TEXT NOT NULL, 24 | //! data BLOB 25 | //! )" 26 | //! } 27 | //! 28 | //! #[rusqlite, test(with=[create_table_pet])] 29 | //! insert_new_pet(name: String, data: Option>) { 30 | //! "INSERT INTO pet (name, data) VALUES (:name, :data)" 31 | //! } 32 | //! 33 | //! #[rusqlite, test(with=[create_table_pet])] 34 | //! get_pet_id_data(name: Option) -> [(i32, Option>, String)] { 35 | //! "SELECT id, data, name FROM pet WHERE pet.name = :name" 36 | //! } 37 | //! } 38 | //! ``` 39 | //! 40 | //! The definitions can be used as such (commented out is how the previous 41 | //! type-less interfaces were used): 42 | //! 43 | //! ```rust ignore 44 | //! let mut conn = rusqlite::Connection::open_in_memory()?; 45 | //! 46 | //! conn.execute_create_table_pet()?; 47 | //! // conn.execute( 48 | //! // "CREATE TABLE pet ( 49 | //! // id INTEGER PRIMARY KEY, 50 | //! // name TEXT NOT NULL, 51 | //! // data BLOB 52 | //! // )", 53 | //! // [], 54 | //! // )?; 55 | //! 56 | //! conn.execute_insert_new_pet(&me.name, &me.data)?; 57 | //! // conn.execute( 58 | //! // "INSERT INTO pet (name, data) VALUES (?1, ?2)", 59 | //! // params![me.name, me.data], 60 | //! // )?; 61 | //! 62 | //! let mut stmt = conn.prepare_get_pet_id_data()?; 63 | //! // let mut stmt = conn.prepare("SELECT id, data, name FROM pet WHERE pet.name = :name")?; 64 | //! 65 | //! let pet_iter = stmt.query_map(&Some("Max".to_string()), |id, data, name| { 66 | //! Ok::<_, rusqlite::Error>(Pet { 67 | //! id, 68 | //! data, 69 | //! name, 70 | //! }) 71 | //! })?; 72 | //! // let pet_iter = stmt.query_map([(":name", "Max".to_string())], |row| { 73 | //! // Ok(Pet { 74 | //! // id: row.get(0)?, 75 | //! // name: row.get(1)?, 76 | //! // data: row.get(2)?, 77 | //! // }) 78 | //! // })?; 79 | //! ``` 80 | //! 81 | //! ## Technical discussion 82 | //! 83 | //! The idea with this crate is to allow direct SQL usage but never use inline 84 | //! queries or have type inference at the call-site. Instead, we declare each query 85 | //! on top-level, giving each a name and designated accessor methods that derive 86 | //! from the name. 87 | //! 88 | //! - The types of named variables are give in a Rust-like syntax. 89 | //! - The type of the returned row is also provided. 90 | //! - `fnsql` does not make an assurances to make sure the types match the query, 91 | //! you will discover it with `cargo test` and no additional code. 92 | //! - `fnsql` writes the tests for each of the queries. - `Arbitrary` is used to 93 | //! generate parameter values. 94 | //! - If testing one query depend on another, you can specify that with `test(with=[..])`. 95 | //! 96 | //! ```text 97 | //! running 3 tests 98 | //! test auto_create_table_pet ... ok 99 | //! test auto_insert_new_pet ... ok 100 | //! test auto_get_pet_id_data ... ok 101 | //! ``` 102 | //! 103 | //! The following is for allowing generated query tests to compile: 104 | //! 105 | //! ```toml 106 | //! [dev-dependencies] 107 | //! arbitrary = { version = "1", features = ["derive"] } 108 | //! ``` 109 | //! 110 | //! ## Limitations 111 | //! 112 | //! * Though it does provide auto-generated tests for validating queries in `cargo test`, 113 | //! it does not do any compile-time validation based on the SQL query string. 114 | //! * It only supports `rusqlite` and `postgres` for now. 115 | 116 | extern crate proc_macro; 117 | 118 | use std::collections::HashMap; 119 | 120 | use proc_macro::TokenStream; 121 | use proc_macro2::TokenStream as Tokens; 122 | use quote::{quote, ToTokens}; 123 | use regex::{Regex, Captures}; 124 | use syn::{ 125 | braced, bracketed, parenthesized, 126 | parse::{Parse, ParseStream}, 127 | parse_macro_input, 128 | punctuated::Punctuated, 129 | token, Ident, Token, LitStr, 130 | }; 131 | 132 | struct Queries { 133 | list: Vec, 134 | } 135 | 136 | impl Parse for Queries { 137 | fn parse(input: ParseStream) -> syn::Result { 138 | let mut list = vec![]; 139 | while !input.is_empty() { 140 | list.push(input.parse()?) 141 | } 142 | 143 | Ok(Queries { list }) 144 | } 145 | } 146 | 147 | enum Kind { 148 | Rusqlite, 149 | PostgreSQL, 150 | } 151 | 152 | struct Query { 153 | name: Ident, 154 | params: Vec, 155 | outputs: Vec, 156 | query: syn::LitStr, 157 | kind: Kind, 158 | test: Option>, 159 | named: bool, 160 | } 161 | 162 | impl Parse for Query { 163 | fn parse(input: ParseStream) -> syn::Result { 164 | let mut kind = None; 165 | let mut test = None; 166 | let mut named = false; 167 | 168 | if input.peek(Token![#]) { 169 | let _: Token![#] = input.parse()?; 170 | let content; 171 | let _ = bracketed!(content in input); 172 | let list: Punctuated = content.parse_terminated(Parse::parse)?; 173 | 174 | for attr in list { 175 | match attr { 176 | Attr::Kind(attr_kind) => { 177 | kind = Some(attr_kind); 178 | } 179 | Attr::Test(test_attrs) => { 180 | if test.is_none() { 181 | test = Some(vec![]); 182 | } 183 | for test_attr in test_attrs { 184 | match test_attr { 185 | TestAttr::With(v) => { 186 | test.as_mut().unwrap().extend(v); 187 | } 188 | } 189 | } 190 | } 191 | Attr::Named => { 192 | named = true; 193 | }, 194 | } 195 | } 196 | }; 197 | 198 | let name = input.parse()?; 199 | let kind = match kind { 200 | None => panic!("unknown SQL type. Supported: rusqlite"), 201 | Some(kind) => kind, 202 | }; 203 | let content; 204 | let _ = parenthesized!(content in input); 205 | let list: Punctuated<_, Token![,]> = content.parse_terminated(Parse::parse)?; 206 | let params = list.into_iter().collect(); 207 | 208 | let outputs = if input.peek(Token![->]) { 209 | let _: Token![->] = input.parse()?; 210 | 211 | let content; 212 | let _ = bracketed!(content in input); 213 | { 214 | let sub_content; 215 | let _ = parenthesized!(sub_content in content); 216 | let list: Punctuated<_, Token![,]> = sub_content.parse_terminated(Parse::parse)?; 217 | list.into_iter().collect() 218 | } 219 | } else { 220 | vec![] 221 | }; 222 | 223 | let content; 224 | let _ = braced!(content in input); 225 | let query = content.parse::()?; 226 | 227 | Ok(Query { 228 | name, 229 | params, 230 | outputs, 231 | query, 232 | kind, 233 | test, 234 | named, 235 | }) 236 | } 237 | } 238 | 239 | impl Query { 240 | fn prepend_name(&self, prefix: &'static str) -> Ident { 241 | Ident::new(&format!("{}{}", prefix, &self.name), self.name.span()) 242 | } 243 | 244 | fn params_declr(&self) -> Tokens { 245 | let list: Vec<_> = self.params.iter().map(|x| x.expand_declr()).collect(); 246 | quote! { #(, #list)* } 247 | } 248 | 249 | fn outputs_declr(&self) -> Tokens { 250 | let list: Vec<_> = self.outputs.iter().map(|x| x.expand_declr()).collect(); 251 | quote! { #(#list),* } 252 | } 253 | 254 | fn outputs_row_get_numbered(&self) -> Tokens { 255 | let list: Vec<_> = self 256 | .outputs 257 | .iter() 258 | .enumerate() 259 | .map(|(i, _)| { 260 | let i = syn::LitInt::new(&format!("{}", i), self.name.span()); 261 | quote! {row.get(#i)?} 262 | }) 263 | .collect(); 264 | 265 | quote! { #(#list),* } 266 | } 267 | 268 | fn outputs_row_try_get_numbered(&self) -> Tokens { 269 | let list: Vec<_> = self 270 | .outputs 271 | .iter() 272 | .enumerate() 273 | .map(|(i, _)| { 274 | let i = syn::LitInt::new(&format!("{}", i), self.name.span()); 275 | quote! {row.try_get(#i)?} 276 | }) 277 | .collect(); 278 | 279 | quote! { #(#list),* } 280 | } 281 | 282 | fn outputs_mapped_row_closure(&self) -> Tokens { 283 | let list = self.outputs_row_get_numbered(); 284 | quote! { Ok(map(#list)) } 285 | } 286 | 287 | fn params_arbitrary(&self) -> (Tokens, Tokens) { 288 | let mut gen_lets = vec![]; 289 | let mut params = vec![]; 290 | 291 | let _ = self 292 | .params 293 | .iter() 294 | .enumerate() 295 | .map(|(idx, param)| { 296 | let ttype = ¶m.ttype; 297 | let owned_ttype = if ttype.to_token_stream().to_string() == "str" { 298 | quote! {String} 299 | } else if ttype.to_token_stream().to_string() == "[u8]" { 300 | quote! {Vec} 301 | } else { 302 | quote! {#ttype} 303 | }; 304 | let ident = Ident::new(&format!("i_{}", idx), self.name.span()); 305 | 306 | gen_lets.push(quote! { 307 | let #ident: #owned_ttype = arbitrary::Arbitrary::arbitrary(uns).unwrap(); 308 | }); 309 | params.push(quote! {&#ident}); 310 | }) 311 | .collect::>(); 312 | 313 | (quote! { #(#gen_lets);* }, quote! { #(#params),* }) 314 | } 315 | 316 | fn params_query(&self) -> Tokens { 317 | let list: Vec<_> = self.params.iter().map(|x| x.expand_query(self)).collect(); 318 | if list.len() == 0 { 319 | quote! { [] } 320 | } else { 321 | quote! { &[#(#list),*] } 322 | } 323 | } 324 | 325 | fn params_query_ref(&self) -> Tokens { 326 | let list: Vec<_> = self.params.iter().map(|x| x.expand_query(self)).collect(); 327 | if list.len() == 0 { 328 | quote! { &[] } 329 | } else { 330 | quote! { &[#(#list),*] } 331 | } 332 | } 333 | 334 | fn params_relay(&self) -> Tokens { 335 | let list: Vec<_> = self 336 | .params 337 | .iter() 338 | .map(|x| { 339 | let name = &x.name; 340 | quote! { #name } 341 | }) 342 | .collect(); 343 | if list.len() == 0 { 344 | quote! {} 345 | } else { 346 | quote! { #(#list),*, } 347 | } 348 | } 349 | 350 | fn expand(&self) -> Tokens { 351 | match self.kind { 352 | Kind::Rusqlite => self.sqlite_expand(), 353 | Kind::PostgreSQL => self.postgres_expand(), 354 | } 355 | } 356 | 357 | fn postgres_expand(&self) -> Tokens { 358 | #[allow(non_snake_case)] 359 | let Client = self.prepend_name("Client_"); 360 | #[allow(non_snake_case)] 361 | let Statement = self.prepend_name("Statement_"); 362 | let execute_name = self.prepend_name("execute_"); 363 | let execute_prepared_name = self.prepend_name("execute_prepared_"); 364 | let prepare_name = self.prepend_name("prepare_"); 365 | let prepare_cached_name = self.prepend_name("prepare_cached_"); 366 | let convert_row = self.prepend_name("convert_row_"); 367 | let query_name = self.prepend_name("query_"); 368 | let query_prepared_name = self.prepend_name("query_prepared_"); 369 | let query_one_name = self.prepend_name("query_one_"); 370 | let query_one_prepared_name = self.prepend_name("query_one_prepared_"); 371 | let query_opt_name = self.prepend_name("query_opt_"); 372 | let query_opt_prepared_name = self.prepend_name("query_opt_prepared_"); 373 | let params_declr = self.params_declr(); 374 | let params_query_ref = self.params_query_ref(); 375 | let outputs_declr = self.outputs_declr(); 376 | let row_try_get_numbered = self.outputs_row_try_get_numbered(); 377 | 378 | let query; 379 | if self.named { 380 | lazy_static::lazy_static! { 381 | static ref RE: Regex = Regex::new(":([A-Za-z_][_A-Za-z0-9]*)($|[^_A-Za-z0-9])").unwrap(); 382 | } 383 | 384 | let params: HashMap<_, _> = self 385 | .params 386 | .iter() 387 | .enumerate() 388 | .map(|(idx, param)| { 389 | (format!("{}", param.name), idx) 390 | }).collect(); 391 | 392 | query = String::from(RE.replace_all(&self.query.value(), |captures: &Captures| { 393 | let c1 = captures.get(1).unwrap().as_str(); 394 | let c2 = captures.get(2).unwrap().as_str(); 395 | match params.get(c1) { 396 | Some(idx) => format!("${}{}", idx + 1, c2), 397 | None => format!("{}{}", c1, c2), 398 | } 399 | })); 400 | } else { 401 | query = self.query.value(); 402 | }; 403 | let query = LitStr::new(query.as_str(), self.query.span()); 404 | 405 | #[cfg(feature = "prepare-cache")] 406 | let (prepare_cached_decl, prepare_cached_impl) = { 407 | let prepare_cached_decl = quote! { 408 | fn #prepare_cached_name(&mut self, cache: &mut fnsql::postgres::Cache) -> Result<#Statement, postgres::Error>; 409 | }; 410 | 411 | let prepare_cached_impl = quote! { 412 | fn #prepare_cached_name(&mut self, cache: &mut fnsql::postgres::Cache) -> Result<#Statement, postgres::Error> { 413 | Ok(#Statement(cache.prepare(#query, self)?)) 414 | } 415 | }; 416 | 417 | (prepare_cached_decl, prepare_cached_impl) 418 | }; 419 | 420 | #[cfg(not(feature = "prepare-cache"))] 421 | let (prepare_cached_decl, prepare_cached_impl) = { 422 | (quote!{}, quote!{}) 423 | }; 424 | 425 | let defs = quote! { 426 | #[allow(non_camel_case_types)] 427 | pub struct #Statement(pub postgres::Statement); 428 | 429 | #[allow(non_camel_case_types)] 430 | pub trait #Client { 431 | fn #prepare_name(&mut self) -> Result<#Statement, postgres::Error>; 432 | #prepare_cached_decl 433 | fn #execute_name(&mut self #params_declr) -> Result; 434 | fn #execute_prepared_name(&mut self, stmt: &#Statement #params_declr) 435 | -> Result; 436 | fn #query_name(&mut self #params_declr) -> Result, postgres::Error>; 437 | fn #query_prepared_name(&mut self, stmt: &#Statement #params_declr) -> Result, postgres::Error>; 438 | fn #query_one_name(&mut self #params_declr) -> Result<(#outputs_declr), postgres::Error>; 439 | fn #query_one_prepared_name(&mut self, stmt: &#Statement #params_declr) -> Result<(#outputs_declr), postgres::Error>; 440 | fn #query_opt_name(&mut self #params_declr) -> Result, postgres::Error>; 441 | fn #query_opt_prepared_name(&mut self, stmt: &#Statement #params_declr) -> Result, postgres::Error>; 442 | } 443 | 444 | pub fn #convert_row(row: postgres::Row) -> Result<(#outputs_declr), postgres::Error> { 445 | Ok((#row_try_get_numbered)) 446 | } 447 | }; 448 | 449 | let timpl = quote! { 450 | fn #prepare_name(&mut self) -> Result<#Statement, postgres::Error> { 451 | self.prepare(#query).map(#Statement) 452 | } 453 | 454 | #prepare_cached_impl 455 | 456 | fn #execute_name(&mut self #params_declr) -> Result { 457 | self.execute(#query, #params_query_ref) 458 | } 459 | 460 | fn #execute_prepared_name(&mut self, stmt: &#Statement #params_declr) 461 | -> Result 462 | { 463 | self.execute(&stmt.0, #params_query_ref) 464 | } 465 | 466 | fn #query_name(&mut self #params_declr) -> Result, postgres::Error> { 467 | let result: Result, postgres::Error> = 468 | self.query(#query, #params_query_ref)?.into_iter().map(#convert_row).collect(); 469 | result 470 | } 471 | 472 | fn #query_one_name(&mut self #params_declr) -> Result<(#outputs_declr), postgres::Error> { 473 | Ok(#convert_row(self.query_one(#query, #params_query_ref)?)?) 474 | } 475 | 476 | fn #query_prepared_name(&mut self, stmt: &#Statement #params_declr) -> Result, postgres::Error> { 477 | let result: Result, postgres::Error> = 478 | self.query(&stmt.0, #params_query_ref)?.into_iter().map(#convert_row).collect(); 479 | result 480 | } 481 | 482 | fn #query_one_prepared_name(&mut self, stmt: &#Statement #params_declr) -> Result<(#outputs_declr), postgres::Error> { 483 | Ok(#convert_row(self.query_one(&stmt.0, #params_query_ref)?)?) 484 | } 485 | 486 | fn #query_opt_name(&mut self #params_declr) -> Result, postgres::Error> { 487 | match self.query_opt(#query, #params_query_ref)? { 488 | None => Ok(None), 489 | Some(x) => Ok(Some(#convert_row(x)?)), 490 | } 491 | } 492 | 493 | fn #query_opt_prepared_name(&mut self, stmt: &#Statement #params_declr) -> Result, postgres::Error> { 494 | match self.query_opt(&stmt.0, #params_query_ref)? { 495 | None => Ok(None), 496 | Some(x) => Ok(Some(#convert_row(x)?)), 497 | } 498 | } 499 | }; 500 | 501 | let test_code = self.test_code(); 502 | 503 | quote! { 504 | #defs 505 | 506 | impl #Client for postgres::Client { 507 | #timpl 508 | } 509 | 510 | impl<'a> #Client for postgres::Transaction<'a> { 511 | #timpl 512 | } 513 | 514 | #test_code 515 | } 516 | } 517 | 518 | fn sqlite_expand(&self) -> Tokens { 519 | let conn_trait_name = self.prepend_name("Connection_"); 520 | #[allow(non_snake_case)] 521 | let StatementType = self.prepend_name("Statement_"); 522 | #[allow(non_snake_case)] 523 | let CachedStatementType = self.prepend_name("CachedStatement_"); 524 | #[allow(non_snake_case)] 525 | let MappedRows = self.prepend_name("MappedRows_"); 526 | #[allow(non_snake_case)] 527 | let Rows = self.prepend_name("Rows_"); 528 | let prepare_name = self.prepend_name("prepare_"); 529 | let prepare_cached_name = self.prepend_name("prepare_cached_"); 530 | let execute_name = self.prepend_name("execute_"); 531 | let query_row_name = self.prepend_name("query_row_"); 532 | let params_declr = self.params_declr(); 533 | let outputs_declr = self.outputs_declr(); 534 | let row_closure = self.outputs_row_get_numbered(); 535 | let mapped_row_closure = self.outputs_mapped_row_closure(); 536 | let params_query = self.params_query(); 537 | let params_relay = self.params_relay(); 538 | let query = &self.query; 539 | 540 | let test_code = self.test_code(); 541 | 542 | quote! { 543 | #[allow(non_camel_case_types)] 544 | pub trait #conn_trait_name { 545 | fn #prepare_name(&self) -> rusqlite::Result<#StatementType<'_>>; 546 | fn #prepare_cached_name(&self) -> rusqlite::Result<#CachedStatementType<'_>>; 547 | fn #execute_name(&self #params_declr) -> rusqlite::Result; 548 | fn #query_row_name(&mut self #params_declr, f: F) -> rusqlite::Result 549 | where 550 | F: FnMut(#outputs_declr) -> T; 551 | } 552 | 553 | impl #conn_trait_name for rusqlite::Connection { 554 | fn #prepare_name(&self) -> rusqlite::Result<#StatementType<'_>> { 555 | self.prepare(#query).map(#StatementType) 556 | } 557 | 558 | fn #prepare_cached_name(&self) -> rusqlite::Result<#CachedStatementType<'_>> { 559 | self.prepare_cached(#query).map(#CachedStatementType) 560 | } 561 | 562 | fn #execute_name(&self #params_declr) -> rusqlite::Result { 563 | self.execute(#query, #params_query) 564 | } 565 | 566 | fn #query_row_name(&mut self #params_declr, f: F) -> rusqlite::Result 567 | where 568 | F: FnMut(#outputs_declr) -> T, 569 | { 570 | let mut stmt = self.#prepare_name()?; 571 | stmt.query_row(#params_relay f) 572 | } 573 | } 574 | 575 | #[allow(non_camel_case_types)] 576 | pub struct #MappedRows<'stmt, F> { 577 | rows: rusqlite::Rows<'stmt>, 578 | map: F, 579 | } 580 | 581 | impl<'stmt, T, F> #MappedRows<'stmt, F> 582 | where 583 | F: FnMut(#outputs_declr) -> T 584 | { 585 | pub(crate) fn new(rows: rusqlite::Rows<'stmt>, f: F) -> Self { 586 | Self { rows, map: f } 587 | } 588 | } 589 | 590 | impl<'stmt, T, F> Iterator for #MappedRows<'stmt, F> 591 | where 592 | F: FnMut(#outputs_declr) -> T 593 | { 594 | type Item = rusqlite::Result; 595 | 596 | fn next(&mut self) -> Option> { 597 | let map = &mut self.map; 598 | self.rows 599 | .next() 600 | .transpose() 601 | .map(|row_result| { 602 | row_result.and_then(|row| { 603 | #mapped_row_closure 604 | }) 605 | }) 606 | } 607 | } 608 | 609 | #[allow(non_camel_case_types)] 610 | pub struct #Rows<'stmt> { 611 | rows: rusqlite::Rows<'stmt>, 612 | } 613 | 614 | impl<'stmt> #Rows<'stmt> { 615 | pub(crate) fn new(rows: rusqlite::Rows<'stmt>) -> Self { 616 | Self { rows } 617 | } 618 | } 619 | 620 | impl<'stmt> Iterator for #Rows<'stmt> { 621 | type Item = rusqlite::Result<(#outputs_declr)>; 622 | 623 | fn next(&mut self) -> Option { 624 | self.rows 625 | .next() 626 | .transpose() 627 | .map(|row_result| { 628 | row_result.and_then(|row| { 629 | Ok((#row_closure)) 630 | }) 631 | }) 632 | } 633 | } 634 | 635 | #[allow(non_camel_case_types)] 636 | pub struct #StatementType<'a>(pub rusqlite::Statement<'a>); 637 | 638 | impl<'a> #StatementType<'a> { 639 | fn query_map(&mut self #params_declr, f: F) -> rusqlite::Result<#MappedRows<'_, F>> 640 | where 641 | F: FnMut(#outputs_declr) -> T, 642 | { 643 | let rows = self.0.query(#params_query)?; 644 | Ok(#MappedRows::new(rows, f)) 645 | } 646 | 647 | fn query_row(&mut self #params_declr, f: F) -> rusqlite::Result 648 | where 649 | F: FnMut(#outputs_declr) -> T, 650 | { 651 | let rows = self.query_map(#params_relay f)?; 652 | for item in rows { 653 | return Ok(item?); 654 | } 655 | Err(rusqlite::Error::QueryReturnedNoRows) 656 | } 657 | 658 | fn query(&mut self #params_declr) -> rusqlite::Result<#Rows<'_>> { 659 | let rows = self.0.query(#params_query)?; 660 | Ok(#Rows::new(rows)) 661 | } 662 | 663 | fn execute(&mut self #params_declr) -> rusqlite::Result<()> { 664 | self.0.execute(#params_query)?; 665 | Ok(()) 666 | } 667 | } 668 | 669 | #[allow(non_camel_case_types)] 670 | pub struct #CachedStatementType<'a>(pub rusqlite::CachedStatement<'a>); 671 | 672 | impl<'a> #CachedStatementType<'a> { 673 | fn query_map(&mut self #params_declr, f: F) -> rusqlite::Result<#MappedRows<'_, F>> 674 | where 675 | F: FnMut(#outputs_declr) -> T, 676 | { 677 | let rows = self.0.query(#params_query)?; 678 | Ok(#MappedRows::new(rows, f)) 679 | } 680 | 681 | fn query_row(&mut self #params_declr, f: F) -> rusqlite::Result 682 | where 683 | F: FnMut(#outputs_declr) -> T, 684 | { 685 | let rows = self.query_map(#params_relay f)?; 686 | for item in rows { 687 | return Ok(item?); 688 | } 689 | Err(rusqlite::Error::QueryReturnedNoRows) 690 | } 691 | 692 | fn query(&mut self #params_declr) -> rusqlite::Result<#Rows<'_>> { 693 | let rows = self.0.query(#params_query)?; 694 | Ok(#Rows::new(rows)) 695 | } 696 | 697 | fn execute(&mut self #params_declr) -> rusqlite::Result<()> { 698 | self.0.execute(#params_query)?; 699 | Ok(()) 700 | } 701 | } 702 | 703 | #test_code 704 | } 705 | } 706 | 707 | fn test_code(&self) -> Tokens { 708 | let test_name = self.prepend_name("auto_"); 709 | let testsetup_name = self.prepend_name("testsetup_"); 710 | let (params_arbit_prep, params_arbit) = self.params_arbitrary(); 711 | let execute_name = self.prepend_name("execute_"); 712 | let name = syn::LitStr::new(&self.name.to_string(), self.name.span()); 713 | 714 | let client_type = match self.kind { 715 | Kind::Rusqlite => quote!{rusqlite::Connection}, 716 | Kind::PostgreSQL => quote!{postgres::Client}, 717 | }; 718 | let client_ref_type = match self.kind { 719 | Kind::Rusqlite => quote!{&}, 720 | Kind::PostgreSQL => quote!{&mut}, 721 | }; 722 | let ignore_error = match self.kind { 723 | Kind::Rusqlite => quote!{Err(rusqlite::Error::ExecuteReturnedResults) => {}}, 724 | Kind::PostgreSQL => quote!{}, 725 | }; 726 | let error_type = match self.kind { 727 | Kind::Rusqlite => quote!{rusqlite::Error}, 728 | Kind::PostgreSQL => quote!{postgres::Error}, 729 | }; 730 | let open_client = match self.kind { 731 | Kind::Rusqlite => quote!{ 732 | let conn = #client_type::open_in_memory()?; 733 | }, 734 | Kind::PostgreSQL => quote!{let mut conn = { 735 | let mut conn = fnsql::postgres::testing_client().expect("unable to connect testing client"); 736 | conn.execute("SET search_path TO pg_temp", &[]).unwrap(); 737 | conn 738 | }; }, 739 | }; 740 | 741 | let test = if let Some(depends) = &self.test { 742 | let depends = depends.iter().map(|name| { 743 | let parent_testsetup_name = 744 | Ident::new(&format!("testsetup_{}", name), self.name.span()); 745 | quote! { 746 | #parent_testsetup_name(uns, deps, conn)?; 747 | } 748 | }); 749 | quote! { 750 | #[cfg(test)] 751 | fn #testsetup_name( 752 | uns: &mut arbitrary::Unstructured, 753 | deps: &mut std::collections::HashSet<&'static str>, 754 | conn: #client_ref_type #client_type) -> Result<(), #error_type> 755 | { 756 | if !deps.insert(#name) { 757 | return Ok(()); 758 | } 759 | 760 | #(#depends);* 761 | 762 | #params_arbit_prep; 763 | let r = conn.#execute_name(#params_arbit); 764 | match r { 765 | Ok(_) => {} 766 | #ignore_error 767 | Err(err) => { 768 | eprintln!("{:?}", err); 769 | Err(err)?; 770 | }, 771 | } 772 | Ok(()) 773 | } 774 | 775 | #[test] 776 | fn #test_name() -> Result<(), #error_type> { 777 | #open_client; 778 | let mut deps = std::collections::HashSet::new(); 779 | let raw_data: &[u8] = &[1, 2, 3]; 780 | let mut unstructured = arbitrary::Unstructured::new(raw_data); 781 | 782 | #testsetup_name(&mut unstructured, &mut deps, #client_ref_type conn)?; 783 | Ok(()) 784 | } 785 | } 786 | } else { 787 | quote! {} 788 | }; 789 | test 790 | } 791 | } 792 | 793 | struct Output { 794 | ttype: syn::Type, 795 | } 796 | 797 | impl Parse for Output { 798 | fn parse(input: ParseStream) -> syn::Result { 799 | let ttype = input.parse()?; 800 | 801 | Ok(Self { ttype }) 802 | } 803 | } 804 | 805 | impl Output { 806 | fn expand_declr(&self) -> Tokens { 807 | let ttype = &self.ttype; 808 | 809 | quote! { #ttype } 810 | } 811 | } 812 | 813 | struct Param { 814 | name: Ident, 815 | ttype: syn::Type, 816 | } 817 | 818 | impl Parse for Param { 819 | fn parse(input: ParseStream) -> syn::Result { 820 | let name = input.parse()?; 821 | let _: Token![:] = input.parse()?; 822 | let ttype = input.parse()?; 823 | 824 | Ok(Self { name, ttype }) 825 | } 826 | } 827 | 828 | impl Param { 829 | fn expand_declr(&self) -> Tokens { 830 | let name = &self.name; 831 | let ttype = &self.ttype; 832 | 833 | quote! { #name: &#ttype } 834 | } 835 | 836 | fn expand_query(&self, query: &Query) -> Tokens { 837 | let name = &self.name; 838 | let specifier = syn::LitStr::new(&format!(":{}", name), name.span()); 839 | 840 | match query.kind { 841 | Kind::Rusqlite => quote! { (#specifier, &#name as &dyn rusqlite::ToSql) }, 842 | Kind::PostgreSQL => quote! { &#name as &(dyn postgres::types::ToSql + Sync) } 843 | } 844 | } 845 | } 846 | 847 | enum Attr { 848 | Kind(Kind), 849 | Test(Vec), 850 | Named, 851 | } 852 | 853 | impl Parse for Attr { 854 | fn parse(input: ParseStream) -> syn::Result { 855 | let ident: Ident = input.parse()?; 856 | if ident == "rusqlite" { 857 | return Ok(Attr::Kind(Kind::Rusqlite)); 858 | } 859 | if ident == "postgres" { 860 | return Ok(Attr::Kind(Kind::PostgreSQL)); 861 | } 862 | if ident == "named" { 863 | return Ok(Attr::Named); 864 | } 865 | if ident == "test" { 866 | let mut v = vec![]; 867 | 868 | if input.peek(token::Paren) { 869 | let content; 870 | let _ = parenthesized!(content in input); 871 | let list: Punctuated = 872 | content.parse_terminated(Parse::parse)?; 873 | v = list.into_iter().collect(); 874 | }; 875 | 876 | return Ok(Attr::Test(v)); 877 | } 878 | panic!("unknown attribute {}", ident); 879 | } 880 | } 881 | 882 | enum TestAttr { 883 | With(Vec), 884 | } 885 | 886 | impl Parse for TestAttr { 887 | fn parse(input: ParseStream) -> syn::Result { 888 | let ident: Ident = input.parse()?; 889 | if ident == "with" { 890 | let mut v = vec![]; 891 | 892 | let _: Token![=] = input.parse()?; 893 | let content; 894 | let _ = bracketed!(content in input); 895 | let list: Punctuated = content.parse_terminated(Parse::parse)?; 896 | for item in list { 897 | v.push(item.to_string()); 898 | } 899 | 900 | return Ok(TestAttr::With(v)); 901 | } 902 | 903 | panic!("unknown test attribute {}", ident); 904 | } 905 | } 906 | 907 | /// The general structure of the input to the `fnsql` macro is the following: 908 | /// 909 | /// ```ignore 910 | /// fnsql! { 911 | /// #[, [OPTIONAL: test(with=[other-function-a, other-function-b...])]] 912 | /// (param1: type, param2: type...) 913 | /// [OPTIONAL: -> [(col a type, col b type, ...)]] 914 | /// { 915 | /// "SQL QUERY STRING" 916 | /// } 917 | /// 918 | /// ... 919 | /// } 920 | /// ``` 921 | /// 922 | /// **For examples see the root doc of the `fnsql` crate.** 923 | /// 924 | /// - Return type is optional, and only meaningful for SQL operations that return row data. 925 | /// - sql-engine-type: supported backends: `rusqlite` and `postgres`. 926 | /// - Testing is optional - you have to specific the `test` attribute for it. 927 | /// - With `test(with=[...])`, you specify the quries that need execution for this 928 | /// query to work. 929 | /// - The `named` attribute allows using named arguments, e.g. ':name' with `postgres` in additon to the default position-based arguments of '$1' '$2', etc. 930 | 931 | #[proc_macro] 932 | pub fn fnsql(input: TokenStream) -> TokenStream { 933 | let queries: Queries = parse_macro_input!(input); 934 | let queries: Vec<_> = queries.list.iter().map(|x| x.expand()).collect(); 935 | 936 | quote! { #(#queries)* }.into() 937 | } 938 | -------------------------------------------------------------------------------- /scripts/README.in.md: -------------------------------------------------------------------------------- 1 | # fnsql   [![Build Status]][actions] [![Latest Version]][crates.io] [![Docs badge]][Docs link] [![License badge]][License link] 2 | 3 | [Build Status]: https://github.com/da-x/fnsql/actions/workflows/test.yml/badge.svg 4 | [actions]: https://github.com/da-x/fnsql/actions 5 | [Latest Version]: https://img.shields.io/crates/v/fnsql.svg 6 | [crates.io]: https://crates.io/crates/fnsql 7 | [License badge]: https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg 8 | [License link]: https://travis-ci.org/da-x/fnsql 9 | [Docs badge]: https://docs.rs/fnsql/badge.svg 10 | [Docs link]: https://docs.rs/fnsql 11 | 12 | @@MAIN_DOC@@ 13 | 14 | ## License 15 | 16 | `fnsql` is licensed under either of 17 | 18 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 19 | http://www.apache.org/licenses/LICENSE-2.0) 20 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 21 | http://opensource.org/licenses/MIT) 22 | 23 | at your option. 24 | 25 | 26 | ### Contribution 27 | 28 | Unless you explicitly state otherwise, any contribution intentionally submitted 29 | for inclusion in `fnsql` by you, as defined in the Apache-2.0 license, 30 | shall be dual licensed as above, without any additional terms or conditions. 31 | -------------------------------------------------------------------------------- /scripts/update-docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | main_doc = [] 4 | for line in open("macro/src/lib.rs").readlines(): 5 | line = line[:-1] 6 | if line.startswith("//!"): 7 | if line.strip() == "": 8 | line = "" 9 | main_doc.append(line[4:]) 10 | else: 11 | break 12 | main_doc = '\n'.join(main_doc) 13 | 14 | readme_in_template = open("./scripts/README.in.md").read() 15 | readme_in_template = readme_in_template.replace("@@MAIN_DOC@@", main_doc) 16 | open("README.md", "w").write(readme_in_template) 17 | open("macro/crates-io.md", "w").write(main_doc) 18 | -------------------------------------------------------------------------------- /testing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testing" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fnsql = { version = "*", features = ["all"] } 8 | rusqlite = "0.26" 9 | postgres = "0.19" 10 | 11 | [dev-dependencies] 12 | arbitrary = { version = "1", features = ["derive"] } 13 | -------------------------------------------------------------------------------- /testing/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate postgres as crate_postgres; 2 | 3 | mod sqlite; 4 | mod postgres; 5 | 6 | fn main() { 7 | sqlite::main().unwrap(); 8 | postgres::main().unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /testing/src/postgres.rs: -------------------------------------------------------------------------------- 1 | fnsql::fnsql! { 2 | #[postgres, test] 3 | create_table_pet() { 4 | "CREATE TABLE pet ( 5 | id INTEGER PRIMARY KEY, 6 | name TEXT NOT NULL, 7 | data BYTEA 8 | )" 9 | } 10 | 11 | #[postgres, test(with=[create_table_pet])] 12 | get_pet_id_data(name: Option) -> [(i32, Option>)] { 13 | "SELECT id, data FROM pet WHERE pet.name = $1" 14 | } 15 | 16 | #[postgres, named, test(with=[create_table_pet])] 17 | insert_new_pet(id: i32, name: String, data: Option>) { 18 | "INSERT INTO pet (id, name, data) VALUES (:id, :name, :data)" 19 | } 20 | 21 | #[postgres, test(with=[create_table_pet])] 22 | insert_new_pet_str(id: i32, name: str, data: Option>) { 23 | "INSERT INTO pet (id, name, data) VALUES ($1, $2, $3)" 24 | } 25 | 26 | #[postgres, test(with=[create_table_pet])] 27 | update_pet_data(name: str, data: [u8]) { 28 | "UPDATE pet SET data = $2 WHERE name = $1" 29 | } 30 | 31 | #[postgres, test(with=[create_table_pet])] 32 | get_pet_count(pet_id: i32) -> [(i32)] {r#" 33 | SELECT count(*) 34 | FROM pet 35 | WHERE id = $1 36 | "#} 37 | } 38 | 39 | 40 | #[derive(Debug)] 41 | struct Pet { 42 | id: i32, 43 | name: String, 44 | data: Option>, 45 | } 46 | 47 | pub fn main() -> Result<(), postgres::Error> { 48 | let mut conn = fnsql::postgres::testing_client()?; 49 | conn.execute("SET search_path TO pg_temp", &[]).unwrap(); 50 | conn.execute("CREATE TYPE foo AS ENUM ('Bar', 'Baz')", &[]).unwrap(); 51 | 52 | conn.execute_create_table_pet()?; 53 | 54 | let mut me = Pet { 55 | id: 0, 56 | name: "Max".to_string(), 57 | data: None, 58 | }; 59 | 60 | conn.execute_insert_new_pet(&me.id, &me.name, &me.data)?; 61 | 62 | me.id += 1; 63 | let prep = conn.prepare_insert_new_pet()?; 64 | conn.execute_prepared_insert_new_pet(&prep, &me.id, &me.name, &me.data)?; 65 | 66 | me.id += 1; 67 | let mut cache = fnsql::postgres::Cache::new(); 68 | let prep = conn.prepare_cached_insert_new_pet(&mut cache)?; 69 | conn.execute_prepared_insert_new_pet(&prep, &me.id, &me.name, &me.data)?; 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /testing/src/sqlite.rs: -------------------------------------------------------------------------------- 1 | fnsql::fnsql! { 2 | #[rusqlite, test] 3 | create_table_pet() { 4 | "CREATE TABLE pet ( 5 | id INTEGER PRIMARY KEY, 6 | name TEXT NOT NULL, 7 | data BLOB 8 | )" 9 | } 10 | 11 | #[rusqlite, test(with=[create_table_pet])] 12 | get_pet_id_data(name: Option) -> [(i32, Option>)] { 13 | "SELECT id, data FROM pet WHERE pet.name = :name" 14 | } 15 | 16 | #[rusqlite, test(with=[create_table_pet])] 17 | insert_new_pet(name: String, data: Option>) { 18 | "INSERT INTO pet (name, data) VALUES (:name, :data)" 19 | } 20 | 21 | #[rusqlite, test(with=[create_table_pet])] 22 | insert_new_pet_str(name: str, data: Option>) { 23 | "INSERT INTO pet (name, data) VALUES (:name, :data)" 24 | } 25 | 26 | #[rusqlite, test(with=[create_table_pet])] 27 | update_pet_data(name: str, data: [u8]) { 28 | "UPDATE pet SET data = :data WHERE name = :name" 29 | } 30 | 31 | #[rusqlite, test(with=[create_table_pet])] 32 | get_pet_count(pet_id: i64) -> [(i64)] {r#" 33 | SELECT count(*) 34 | FROM pet 35 | WHERE id = :pet_id 36 | "#} 37 | } 38 | 39 | #[derive(Debug)] 40 | struct Pet { 41 | _id: i32, 42 | name: String, 43 | data: Option>, 44 | } 45 | 46 | pub fn main() -> rusqlite::Result<()> { 47 | let mut conn = rusqlite::Connection::open_in_memory()?; 48 | 49 | { 50 | conn.execute_create_table_pet()?; 51 | let me = Pet { 52 | _id: 0, 53 | name: "Max".to_string(), 54 | data: None, 55 | }; 56 | conn.execute_insert_new_pet(&me.name, &me.data)?; 57 | { 58 | let mut stmt = conn.prepare_get_pet_id_data()?; 59 | let pet_iter = stmt.query_map(&Some("Max".to_string()), |_id, data| { 60 | Ok::<_, rusqlite::Error>(Pet { 61 | _id, 62 | data, 63 | name: "Max".to_string(), 64 | }) 65 | })?; 66 | 67 | for pet in pet_iter { 68 | println!("Found pet {:?}", pet.unwrap()); 69 | } 70 | 71 | for pet in stmt.query(&Some("Max".to_string()))? { 72 | let pet = pet?; 73 | println!("Found pet {:?}", pet); 74 | } 75 | 76 | { 77 | let mut rows = stmt.query(&Some("Max".to_string()))?; 78 | while let Some(Ok(pet)) = rows.next() { 79 | println!("Found pet {:?}", pet); 80 | } 81 | } 82 | } 83 | { 84 | let mut stmt = conn.prepare_cached_get_pet_id_data()?; 85 | let _pet_iter = stmt.query_map(&Some("Max".to_string()), |_id, data| { 86 | Ok::<_, rusqlite::Error>(Pet { 87 | _id, 88 | data, 89 | name: "Max".to_string(), 90 | }) 91 | })?; 92 | } 93 | conn.execute_insert_new_pet_str(&me.name, &me.data)?; 94 | 95 | { 96 | let mut stmt = conn.prepare_insert_new_pet_str()?; 97 | stmt.execute(&me.name, &me.data)?; 98 | } 99 | 100 | { 101 | let mut stmt = conn.prepare_cached_insert_new_pet_str()?; 102 | stmt.execute(&me.name, &me.data)?; 103 | } 104 | 105 | let _pet: Pet = 106 | conn.query_row_get_pet_id_data(&Some("Max".to_string()), |_id, data| Pet { 107 | _id, 108 | data, 109 | name: "Max".to_string(), 110 | })?; 111 | } 112 | 113 | { 114 | conn.execute_update_pet_data("x", "asd".as_bytes())?; 115 | } 116 | 117 | let tx = conn.transaction()?; 118 | 119 | { 120 | let mut stmt = tx.prepare_cached_get_pet_id_data()?; 121 | { 122 | let _pet_iter = stmt.query_map(&Some("Max".to_string()), |_id, data| { 123 | Ok::<_, rusqlite::Error>(Pet { 124 | _id, 125 | data, 126 | name: "Max".to_string(), 127 | }) 128 | })?; 129 | } 130 | let _pet: Pet = stmt.query_row(&Some("Max".to_string()), |_id, data| Pet { 131 | _id, 132 | data, 133 | name: "Max".to_string(), 134 | })?; 135 | } 136 | 137 | tx.commit()?; 138 | 139 | Ok(()) 140 | } 141 | --------------------------------------------------------------------------------